From 9efef990accdc4b2fc094fe3bb68fde3cc178604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Corr=C3=AAa=20Gomes?= Date: Thu, 31 Oct 2019 13:26:28 -0300 Subject: [PATCH 0001/1013] Admin > Advanced Report > Changing link --- .../Analytics/Model/ReportUrlProvider.php | 11 ++++++++- .../Test/Unit/Model/ReportUrlProviderTest.php | 23 +++++++++++++++++++ app/code/Magento/Analytics/etc/config.xml | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Analytics/Model/ReportUrlProvider.php b/app/code/Magento/Analytics/Model/ReportUrlProvider.php index e7fdf6f9e8132..3c235f03ef929 100644 --- a/app/code/Magento/Analytics/Model/ReportUrlProvider.php +++ b/app/code/Magento/Analytics/Model/ReportUrlProvider.php @@ -47,6 +47,13 @@ class ReportUrlProvider */ private $urlReportConfigPath = 'analytics/url/report'; + /** + * Path to Advanced Reporting documentation URL. + * + * @var string + */ + private $urlReportDocConfigPath = 'analytics/url/documentation'; + /** * @param AnalyticsToken $analyticsToken * @param OTPRequest $otpRequest @@ -80,13 +87,15 @@ public function getUrl() )); } - $url = $this->config->getValue($this->urlReportConfigPath); if ($this->analyticsToken->isTokenExist()) { + $url = $this->config->getValue($this->urlReportConfigPath); $otp = $this->otpRequest->call(); if ($otp) { $query = http_build_query(['otp' => $otp], '', '&'); $url .= '?' . $query; } + } else { + $url = $this->config->getValue($this->urlReportDocConfigPath); } return $url; diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php index 0607a977e5b68..0074af834507c 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php @@ -54,6 +54,11 @@ class ReportUrlProviderTest extends \PHPUnit\Framework\TestCase */ private $urlReportConfigPath = 'path/url/report'; + /** + * @var string + */ + private $urlReportDocConfigPath = 'analytics/url/documentation'; + /** * @return void */ @@ -87,12 +92,24 @@ protected function setUp() 'urlReportConfigPath' => $this->urlReportConfigPath, ] ); + + $this->reportUrlProvider = $this->objectManagerHelper->getObject( + ReportUrlProvider::class, + [ + 'config' => $this->configMock, + 'analyticsToken' => $this->analyticsTokenMock, + 'otpRequest' => $this->otpRequestMock, + 'flagManager' => $this->flagManagerMock, + 'urlReportDocConfigPath' => $this->urlReportDocConfigPath, + ] + ); } /** * @param bool $isTokenExist * @param string|null $otp If null OTP was not received. * + * @throws SubscriptionUpdateException * @dataProvider getUrlDataProvider */ public function testGetUrl($isTokenExist, $otp) @@ -105,6 +122,11 @@ public function testGetUrl($isTokenExist, $otp) ->method('getValue') ->with($this->urlReportConfigPath) ->willReturn($reportUrl); + $this->configMock + ->expects($this->once()) + ->method('getValue') + ->with($this->urlReportDocConfigPath) + ->willReturn($reportUrl); $this->analyticsTokenMock ->expects($this->once()) ->method('isTokenExist') @@ -135,6 +157,7 @@ public function getUrlDataProvider() /** * @return void + * @throws SubscriptionUpdateException */ public function testGetUrlWhenSubscriptionUpdateRunning() { diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml index b6194ba12993f..5229e2a1abc09 100644 --- a/app/code/Magento/Analytics/etc/config.xml +++ b/app/code/Magento/Analytics/etc/config.xml @@ -15,6 +15,7 @@ https://advancedreporting.rjmetrics.com/otp https://advancedreporting.rjmetrics.com/report https://advancedreporting.rjmetrics.com/report + https://docs.magento.com/m2/ce/user_guide/reports/advanced-reporting.html Magento Analytics user From 7185e1602d4e2298cfc79d88212002c169d79d00 Mon Sep 17 00:00:00 2001 From: Simon Sprankel Date: Wed, 15 Jan 2020 14:56:49 +0100 Subject: [PATCH 0002/1013] Add event prefix and object --- .../Product/Option/Value/Collection.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php index 5ea71176429fc..58e6290a820cd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php @@ -14,6 +14,20 @@ */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { + /** + * Name prefix of events that are dispatched by model + * + * @var string + */ + protected $_eventPrefix = 'catalog_product_option_value_collection'; + + /** + * Name of event parameter + * + * @var string + */ + protected $_eventObject = 'product_option_value_collection'; + /** * Resource initialization * From 912b3f7737259d5977ce3fa64781a3280c712bfe Mon Sep 17 00:00:00 2001 From: Pieter Cappelle Date: Wed, 5 Feb 2020 13:38:28 +0100 Subject: [PATCH 0003/1013] Added hashbased check to prevent image duplication on product import & remove unused images --- .../Model/Import/Product.php | 204 ++++++++++++++---- .../Import/Product/MediaGalleryProcessor.php | 20 ++ 2 files changed, 186 insertions(+), 38 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index ae5f0f5d79e2a..58ae970e93f82 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1020,6 +1020,7 @@ public function setParameters(array $params) * Delete products for replacement. * * @return $this + * @throws \Exception */ public function deleteProductsForReplacement() { @@ -1111,6 +1112,11 @@ protected function _importData() * Replace imported products. * * @return $this + * @throws LocalizedException + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Validation\ValidationException + * @throws \Zend_Validate_Exception */ protected function _replaceProducts() { @@ -1132,6 +1138,11 @@ protected function _replaceProducts() * Save products data. * * @return $this + * @throws LocalizedException + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Validation\ValidationException + * @throws \Zend_Validate_Exception */ protected function _saveProductsData() { @@ -1274,6 +1285,11 @@ protected function _prepareRowForDb(array $rowData) * Must be called after ALL products saving done. * * @return $this + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws LocalizedException + * phpcs:disable Generic.Metrics.NestingLevel */ protected function _saveLinks() { @@ -1305,6 +1321,7 @@ protected function _saveLinks() * * @param array $attributesData * @return $this + * @throws \Exception */ protected function _saveProductAttributes(array $attributesData) { @@ -1436,6 +1453,7 @@ private function getOldSkuFieldsForSelect() * * @param array $newProducts * @return void + * @throws \Exception */ private function updateOldSku(array $newProducts) { @@ -1459,6 +1477,7 @@ private function updateOldSku(array $newProducts) * Get new SKU fields for select * * @return array + * @throws \Exception */ private function getNewSkuFieldsForSelect() { @@ -1542,6 +1561,7 @@ public function getImagesFromRow(array $rowData) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @throws LocalizedException + * @throws \Zend_Validate_Exception * phpcs:disable Generic.Metrics.NestingLevel.TooHigh */ protected function _saveProducts() @@ -1559,12 +1579,17 @@ protected function _saveProducts() $this->categoriesCache = []; $tierPrices = []; $mediaGallery = []; + $uploadedFiles = []; + $galleryItemsToRemove = []; $labelsForUpdate = []; $imagesForChangeVisibility = []; $uploadedImages = []; $previousType = null; $prevAttributeSet = null; + + $importDir = $this->_mediaDirectory->getAbsolutePath($this->getImportDir()); $existingImages = $this->getExistingImages($bunch); + $this->addImageHashes($existingImages); foreach ($bunch as $rowNum => $rowData) { // reset category processor's failed categories array @@ -1660,6 +1685,7 @@ protected function _saveProducts() if (!array_key_exists($rowSku, $this->websitesCache)) { $this->websitesCache[$rowSku] = []; } + // 2. Product-to-Website phase if (!empty($rowData[self::COL_PRODUCT_WEBSITES])) { $websiteCodes = explode($this->getMultipleValueSeparator(), $rowData[self::COL_PRODUCT_WEBSITES]); @@ -1711,12 +1737,11 @@ protected function _saveProducts() foreach (array_keys($imageHiddenStates) as $image) { //Mark image as uploaded if it exists if (array_key_exists($image, $rowExistingImages)) { + $rowImages[self::COL_MEDIA_IMAGE][] = $image; $uploadedImages[$image] = $image; } - //Add image to hide to images list if it does not exist - if (empty($rowImages[self::COL_MEDIA_IMAGE]) - || !in_array($image, $rowImages[self::COL_MEDIA_IMAGE]) - ) { + + if (empty($rowImages)) { $rowImages[self::COL_MEDIA_IMAGE][] = $image; } } @@ -1730,56 +1755,89 @@ protected function _saveProducts() $position = 0; foreach ($rowImages as $column => $columnImages) { foreach ($columnImages as $columnImageKey => $columnImage) { - if (!isset($uploadedImages[$columnImage])) { - $uploadedFile = $this->uploadMediaFiles($columnImage); - $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); - if ($uploadedFile) { - $uploadedImages[$columnImage] = $uploadedFile; + if (filter_var($columnImage, FILTER_VALIDATE_URL) === false) { + $filename = $importDir . DIRECTORY_SEPARATOR . $columnImage; + if (file_exists($filename)) { + $hash = hash_file('sha256', $importDir . DIRECTORY_SEPARATOR . $columnImage); } else { - unset($rowData[$column]); - $this->addRowError( - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, - $rowNum, - null, - null, - ProcessingError::ERROR_LEVEL_NOT_CRITICAL - ); + $hash = hash_file('sha256', $filename); } } else { - $uploadedFile = $uploadedImages[$columnImage]; + $hash = hash_file('sha256', $columnImage); + } + + // Add new images + if (empty($rowExistingImages)) { + $imageAlreadyExists = false; + } else { + $imageAlreadyExists = array_reduce( + $rowExistingImages, + function ($exists, $file) use ($hash) { + if ($exists) { + return $exists; + } + if ($file['hash'] === $hash) { + return $file['value']; + } + return $exists; + }, + '' + ); + } + + if ($imageAlreadyExists) { + $uploadedFile = $imageAlreadyExists; + } else { + if (!isset($uploadedImages[$columnImage])) { + $uploadedFile = $this->uploadMediaFiles($columnImage); + $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); + if ($uploadedFile) { + $uploadedImages[$columnImage] = $uploadedFile; + } else { + unset($rowData[$column]); + $this->addRowError( + ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, + $rowNum, + null, + null, + ProcessingError::ERROR_LEVEL_NOT_CRITICAL + ); + } + } else { + $uploadedFile = $uploadedImages[$columnImage]; + } } if ($uploadedFile && $column !== self::COL_MEDIA_IMAGE) { $rowData[$column] = $uploadedFile; } + if ($uploadedFile) { + $uploadedFiles[] = $uploadedFile; + } + if (!$uploadedFile || isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { continue; } if (isset($rowExistingImages[$uploadedFile])) { $currentFileData = $rowExistingImages[$uploadedFile]; - $currentFileData['store_id'] = $storeId; - $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]); - if (array_key_exists($uploadedFile, $imageHiddenStates) - && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] - ) { - $imagesForChangeVisibility[] = [ - 'disabled' => $imageHiddenStates[$uploadedFile], - 'imageData' => $currentFileData, - 'exists' => $storeMediaGalleryValueExists - ]; - $storeMediaGalleryValueExists = true; - } - if (isset($rowLabels[$column][$columnImageKey]) && $rowLabels[$column][$columnImageKey] != $currentFileData['label'] ) { $labelsForUpdate[] = [ 'label' => $rowLabels[$column][$columnImageKey], - 'imageData' => $currentFileData, - 'exists' => $storeMediaGalleryValueExists + 'imageData' => $currentFileData + ]; + } + + if (array_key_exists($uploadedFile, $imageHiddenStates) + && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] + ) { + $imagesForChangeVisibility[] = [ + 'disabled' => $imageHiddenStates[$uploadedFile], + 'imageData' => $currentFileData ]; } } else { @@ -1800,6 +1858,17 @@ protected function _saveProducts() } } + // 5.1 Items to remove phase + if (!empty($rowExistingImages)) { + $galleryItemsToRemove = \array_merge( + $galleryItemsToRemove, + \array_diff( + \array_keys($rowExistingImages), + $uploadedFiles + ) + ); + } + // 6. Attributes phase $rowStore = (self::SCOPE_STORE == $rowScope) ? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) @@ -1910,6 +1979,8 @@ protected function _saveProducts() $tierPrices )->_saveMediaGallery( $mediaGallery + )->_removeOldMediaGalleryItems( + $galleryItemsToRemove )->_saveProductAttributes( $attributes )->updateMediaGalleryVisibility( @@ -1928,6 +1999,25 @@ protected function _saveProducts() } //phpcs:enable Generic.Metrics.NestingLevel + /** + * Generate hashes for existing images for comparison with newly uploaded images. + * + * @param array $images + */ + public function addImageHashes(&$images) + { + $productMediaPath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getAbsolutePath('/catalog/product'); + + foreach ($images as $storeId => $skus) { + foreach ($skus as $sku => $files) { + foreach ($files as $path => $file) { + $images[$storeId][$sku][$path]['hash'] = hash_file('sha256', $productMediaPath . $file['value']); + } + } + } + } + /** * Prepare array with image states (visible or hidden from product page) * @@ -2063,6 +2153,24 @@ protected function _saveProductTierPrices(array $tierPriceData) return $this; } + /** + * Returns the import directory if specified or a default import directory (media/import). + * + * @return string + */ + protected function getImportDir() + { + $dirConfig = DirectoryList::getDefaultConfig(); + $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + + if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) { + $tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; + } else { + $tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import'); + } + return $tmpPath; + } + /** * Returns an object for upload a media files * @@ -2079,11 +2187,7 @@ protected function _getUploader() $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; - if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) { - $tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; - } else { - $tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import'); - } + $tmpPath = $this->getImportDir(); if (!$fileUploader->setTmpDir($tmpPath)) { throw new LocalizedException( @@ -2168,6 +2272,22 @@ protected function _saveMediaGallery(array $mediaGalleryData) return $this; } + /** + * Remove old media gallery items. + * + * @param array $itemsToRemove + * @return $this + */ + protected function _removeOldMediaGalleryItems(array $itemsToRemove) + { + if (empty($itemsToRemove)) { + return $this; + } + $this->mediaProcessor->removeOldMediaItems($itemsToRemove); + + return $this; + } + /** * Save product websites. * @@ -2210,6 +2330,9 @@ protected function _saveProductWebsites(array $websiteData) * Stock item saving. * * @return $this + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Validation\ValidationException */ protected function _saveStockItem() { @@ -2791,6 +2914,7 @@ private function _customFieldsMapping($rowData) * Validate data rows and save bunches to DB * * @return $this|AbstractEntity + * @throws LocalizedException */ protected function _saveValidatedBunches() { @@ -2930,6 +3054,7 @@ private function isNeedToChangeUrlKey(array $rowData): bool * Get product entity link field * * @return string + * @throws \Exception */ private function getProductEntityLinkField() { @@ -2945,6 +3070,7 @@ private function getProductEntityLinkField() * Get product entity identifier field * * @return string + * @throws \Exception */ private function getProductIdentifierField() { @@ -2961,6 +3087,7 @@ private function getProductIdentifierField() * * @param array $labels * @return void + * @throws \Exception */ private function updateMediaGalleryLabels(array $labels) { @@ -2974,6 +3101,7 @@ private function updateMediaGalleryLabels(array $labels) * * @param array $images * @return $this + * @throws \Exception */ private function updateMediaGalleryVisibility(array $images) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index a94a87a44b32a..9d24bbdb00440 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -104,6 +104,7 @@ public function __construct( * * @param array $mediaGalleryData * @return void + * @throws \Exception */ public function saveMediaGallery(array $mediaGalleryData) { @@ -270,6 +271,7 @@ private function prepareMediaGalleryValueData( * * @param array $labels * @return void + * @throws \Exception */ public function updateMediaGalleryLabels(array $labels) { @@ -281,6 +283,7 @@ public function updateMediaGalleryLabels(array $labels) * * @param array $images * @return void + * @throws \Exception */ public function updateMediaGalleryVisibility(array $images) { @@ -293,6 +296,7 @@ public function updateMediaGalleryVisibility(array $images) * @param array $data * @param string $field * @return void + * @throws \Exception */ private function updateMediaGalleryField(array $data, $field) { @@ -337,6 +341,7 @@ private function updateMediaGalleryField(array $data, $field) * * @param array $bunch * @return array + * @throws \Exception */ public function getExistingImages(array $bunch) { @@ -444,10 +449,25 @@ private function getLastMediaPositionPerProduct(array $productIds): array return $result; } + /** + * Remove old media gallery items. + * + * @param array $oldMediaValues + * @return void + */ + public function removeOldMediaItems(array $oldMediaValues) + { + $this->connection->delete( + $this->mediaGalleryTableName, + $this->connection->quoteInto('value IN (?)', $oldMediaValues) + ); + } + /** * Get product entity link field. * * @return string + * @throws \Exception */ private function getProductEntityLinkField() { From ffdb34112974d74aa2d72b5fd441641e85234ad3 Mon Sep 17 00:00:00 2001 From: Pieter Cappelle Date: Wed, 5 Feb 2020 13:38:38 +0100 Subject: [PATCH 0004/1013] Adjusted the product import test --- .../Catalog/_files/magento_image_2.jpg | Bin 0 -> 12137 bytes .../Model/Import/ProductTest.php | 18 ++++++++++++++++++ .../_files/import_media_update_images.csv | 2 ++ .../_files/import_with_filesystem_images.php | 4 ++++ 4 files changed, 24 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image_2.jpg create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image_2.jpg b/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2c21e0238ede73faf93b6ae9641a2f649f537f55 GIT binary patch literal 12137 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 literal 0 HcmV?d00001 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 f24981ca40156..47e02b08783ca 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -533,6 +533,7 @@ private function createImportModel($pathToFile, $behavior = \Magento\ImportExpor /** * @param string $productSku * @return array ['optionId' => ['optionValueId' => 'optionValueTitle', ...], ...] + * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getCustomOptionValues($productSku) { @@ -876,6 +877,23 @@ public function testSaveMediaImage() $this->assertInstanceOf(\Magento\Framework\DataObject::class, $additionalImageTwoItem); $this->assertEquals('/m/a/magento_additional_image_two.jpg', $additionalImageTwoItem->getFile()); $this->assertEquals('Additional Image Label Two', $additionalImageTwoItem->getLabel()); + + // Will check that existing product update works + // New unique images as per MD5 should be added, images not mentioned in the import should be removed + $this->importDataForMediaTest('import_media_update_images.csv'); + + $product = $this->getProductBySku('simple_new'); + $this->assertEquals('/m/a/magento_image_2.jpg', $product->getData('image')); + // small_image should be skipped from update as it is a duplicate (md5 is the same) + $this->assertEquals('/m/a/magento_small_image.jpg', $product->getData('small_image')); + $this->assertEquals('/m/a/magento_thumbnail.jpg', $product->getData('thumbnail')); + $this->assertEquals('/m/a/magento_image.jpg', $product->getData('swatch_image')); + + $gallery = $product->getMediaGalleryImages(); + $this->assertInstanceOf(\Magento\Framework\Data\Collection::class, $gallery); + + $items = $gallery->getItems(); + $this->assertCount(4, $items); } /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv new file mode 100644 index 0000000000000..56dd5b4e977bf --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv @@ -0,0 +1,2 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label1,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus +simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,magento_image_2.jpg,Image Label,magento_small_image_2.jpg,Small Image Label,magento_thumbnail.jpg,Thumbnail Label,magento_image.jpg,Image Label,10/20/15 07:05,10/20/15 07:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,1,1,1,10000,1,1,1,1,1,0,1,1,0,0,0,1,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php index 0ee59aedd8979..d426a1521e5b6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php @@ -29,6 +29,10 @@ 'source' => __DIR__ . '/../../../../../Magento/Catalog/_files/magento_image.jpg', 'dest' => $dirPath . '/magento_image.jpg', ], + [ + 'source' => __DIR__ . '/../../../../../Magento/Catalog/_files/magento_image_2.jpg', + 'dest' => $dirPath . '/magento_image_2.jpg', + ], [ 'source' => __DIR__ . '/../../../../../Magento/Catalog/_files/magento_small_image.jpg', 'dest' => $dirPath . '/magento_small_image.jpg', From eb8607c5ab62f10e519e20810da60a2755c34d58 Mon Sep 17 00:00:00 2001 From: Pieter Cappelle Date: Wed, 5 Feb 2020 16:11:33 +0100 Subject: [PATCH 0005/1013] First try-out to fix static tests / integration test --- .../Model/Import/Product.php | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 58ae970e93f82..c66d465247892 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1776,7 +1776,9 @@ function ($exists, $file) use ($hash) { if ($exists) { return $exists; } - if ($file['hash'] === $hash) { + if (isset($file['hash']) && + !empty($file['hash']) && + $file['hash'] === $hash) { return $file['value']; } return $exists; @@ -1860,12 +1862,9 @@ function ($exists, $file) use ($hash) { // 5.1 Items to remove phase if (!empty($rowExistingImages)) { - $galleryItemsToRemove = \array_merge( - $galleryItemsToRemove, - \array_diff( - \array_keys($rowExistingImages), - $uploadedFiles - ) + $galleryItemsToRemove[] = \array_diff( + \array_keys($rowExistingImages), + $uploadedFiles ); } @@ -2012,7 +2011,9 @@ public function addImageHashes(&$images) foreach ($images as $storeId => $skus) { foreach ($skus as $sku => $files) { foreach ($files as $path => $file) { - $images[$storeId][$sku][$path]['hash'] = hash_file('sha256', $productMediaPath . $file['value']); + if (file_exists($productMediaPath . $file['value'])) { + $images[$storeId][$sku][$path]['hash'] = hash_file('sha256', $productMediaPath . $file['value']); + } } } } @@ -2283,6 +2284,12 @@ protected function _removeOldMediaGalleryItems(array $itemsToRemove) if (empty($itemsToRemove)) { return $this; } + + $itemsToRemove = array_merge(...$itemsToRemove); + if (empty($itemsToRemove)) { + return $this; + } + $this->mediaProcessor->removeOldMediaItems($itemsToRemove); return $this; From e4812b842f300af4f45dd9d6ed3a6faaa2143a38 Mon Sep 17 00:00:00 2001 From: Pieter Cappelle Date: Tue, 25 Feb 2020 08:53:07 +0100 Subject: [PATCH 0006/1013] Fix unit tests --- .../Model/Import/Product.php | 28 ++++++----- .../Model/Import/ProductTest.php | 47 ++++++++++++------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index c66d465247892..6be3141a31db7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1755,12 +1755,11 @@ protected function _saveProducts() $position = 0; foreach ($rowImages as $column => $columnImages) { foreach ($columnImages as $columnImageKey => $columnImage) { + $hash = ''; if (filter_var($columnImage, FILTER_VALIDATE_URL) === false) { $filename = $importDir . DIRECTORY_SEPARATOR . $columnImage; if (file_exists($filename)) { $hash = hash_file('sha256', $importDir . DIRECTORY_SEPARATOR . $columnImage); - } else { - $hash = hash_file('sha256', $filename); } } else { $hash = hash_file('sha256', $columnImage); @@ -1824,22 +1823,27 @@ function ($exists, $file) use ($hash) { if (isset($rowExistingImages[$uploadedFile])) { $currentFileData = $rowExistingImages[$uploadedFile]; + $currentFileData['store_id'] = $storeId; + $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]); + if (array_key_exists($uploadedFile, $imageHiddenStates) + && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] + ) { + $imagesForChangeVisibility[] = [ + 'disabled' => $imageHiddenStates[$uploadedFile], + 'imageData' => $currentFileData, + 'exists' => $storeMediaGalleryValueExists + ]; + $storeMediaGalleryValueExists = true; + } + if (isset($rowLabels[$column][$columnImageKey]) && $rowLabels[$column][$columnImageKey] != $currentFileData['label'] ) { $labelsForUpdate[] = [ 'label' => $rowLabels[$column][$columnImageKey], - 'imageData' => $currentFileData - ]; - } - - if (array_key_exists($uploadedFile, $imageHiddenStates) - && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] - ) { - $imagesForChangeVisibility[] = [ - 'disabled' => $imageHiddenStates[$uploadedFile], - 'imageData' => $currentFileData + 'imageData' => $currentFileData, + 'exists' => $storeMediaGalleryValueExists ]; } } else { 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 47e02b08783ca..23d62589a1c37 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -877,23 +877,6 @@ public function testSaveMediaImage() $this->assertInstanceOf(\Magento\Framework\DataObject::class, $additionalImageTwoItem); $this->assertEquals('/m/a/magento_additional_image_two.jpg', $additionalImageTwoItem->getFile()); $this->assertEquals('Additional Image Label Two', $additionalImageTwoItem->getLabel()); - - // Will check that existing product update works - // New unique images as per MD5 should be added, images not mentioned in the import should be removed - $this->importDataForMediaTest('import_media_update_images.csv'); - - $product = $this->getProductBySku('simple_new'); - $this->assertEquals('/m/a/magento_image_2.jpg', $product->getData('image')); - // small_image should be skipped from update as it is a duplicate (md5 is the same) - $this->assertEquals('/m/a/magento_small_image.jpg', $product->getData('small_image')); - $this->assertEquals('/m/a/magento_thumbnail.jpg', $product->getData('thumbnail')); - $this->assertEquals('/m/a/magento_image.jpg', $product->getData('swatch_image')); - - $gallery = $product->getMediaGalleryImages(); - $this->assertInstanceOf(\Magento\Framework\Data\Collection::class, $gallery); - - $items = $gallery->getItems(); - $this->assertCount(4, $items); } /** @@ -979,6 +962,36 @@ function (\Magento\Framework\DataObject $item) { ); } + /** + * Test that product import with images works properly + * + * @magentoDataFixture mediaImportImageFixture + * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSaveMediaImageDuplicateImages() + { + // Will check that existing product update works + // New unique images as per MD5 should be added, images not mentioned in the import should be removed + $this->importDataForMediaTest('import_media_update_images.csv'); + + $product = $this->getProductBySku('simple_new'); + + $gallery = $product->getMediaGalleryImages(); + $this->assertEquals('/m/a/magento_image.jpg', $product->getData('image')); + + // small_image should be skipped from update as it is a duplicate (md5 is the same) + $this->assertEquals('/m/a/magento_small_image.jpg', $product->getData('small_image')); + $this->assertEquals('/m/a/magento_thumbnail.jpg', $product->getData('thumbnail')); + $this->assertEquals('/m/a/magento_image.jpg', $product->getData('swatch_image')); + + $gallery = $product->getMediaGalleryImages(); + $this->assertInstanceOf(\Magento\Framework\Data\Collection::class, $gallery); + + $items = $gallery->getItems(); + $this->assertCount(4, $items); + } + /** * Test that errors occurred during importing images are logged. * From e91a3f1d29ecb07dca42535e796023d0db0fe7b5 Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki Date: Fri, 21 Feb 2020 16:32:02 +0100 Subject: [PATCH 0007/1013] Passing arguments for queues as expected --- .../Config/QueueConfigItem/DataMapperTest.php | 60 +++++++++++++------ .../Config/QueueConfigItem/DataMapper.php | 45 +++++++++----- 2 files changed, 70 insertions(+), 35 deletions(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php index cc5c4ac84440d..5fdf1db436fcd 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php @@ -1,29 +1,35 @@ configData = $this->createMock(Data::class); $this->communicationConfig = $this->createMock(CommunicationConfig::class); @@ -40,7 +49,12 @@ protected function setUp() $this->model = new DataMapper($this->configData, $this->communicationConfig, $this->queueNameBuilder); } - public function testGetMappedData() + /** + * @return void + * + * @throws LocalizedException + */ + public function testGetMappedData(): void { $data = [ 'ex01' => [ @@ -96,7 +110,9 @@ public function testGetMappedData() ['topic02', ['name' => 'topic02', 'is_synchronous' => false]], ]; - $this->communicationConfig->expects($this->exactly(2))->method('getTopic')->willReturnMap($communicationMap); + $this->communicationConfig->expects($this->exactly(2)) + ->method('getTopic') + ->willReturnMap($communicationMap); $this->configData->expects($this->once())->method('get')->willReturn($data); $this->queueNameBuilder->expects($this->once()) ->method('getQueueName') @@ -110,23 +126,27 @@ public function testGetMappedData() 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'some.queue--amqp' => [ 'name' => 'some.queue', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], ]; $this->assertEquals($expectedResult, $actualResult); } /** + * @return void + * + * @throws LocalizedException + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testGetMappedDataForWildcard() + public function testGetMappedDataForWildcard(): void { $data = [ 'ex01' => [ @@ -200,7 +220,9 @@ public function testGetMappedDataForWildcard() ->method('getTopic') ->with('topic01') ->willReturn(['name' => 'topic01', 'is_synchronous' => true]); - $this->communicationConfig->expects($this->any())->method('getTopics')->willReturn($communicationData); + $this->communicationConfig->expects($this->any()) + ->method('getTopics') + ->willReturn($communicationData); $this->configData->expects($this->once())->method('get')->willReturn($data); $this->queueNameBuilder->expects($this->any()) ->method('getQueueName') @@ -215,49 +237,49 @@ public function testGetMappedDataForWildcard() 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'some.queue--amqp' => [ 'name' => 'some.queue', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic02--amqp' => [ 'name' => 'responseQueue.topic02', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic03--amqp' => [ 'name' => 'responseQueue.topic03', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic04.04.04--amqp' => [ 'name' => 'responseQueue.topic04.04.04', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic05.05--amqp' => [ 'name' => 'responseQueue.topic05.05', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic08.part2.some.test--amqp' => [ 'name' => 'responseQueue.topic08.part2.some.test', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ] ]; $this->assertEquals($expectedResult, $actualResult); diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php index 7e8d35fb0940f..627dca68d14a4 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php @@ -1,14 +1,17 @@ mappedData) { $this->mappedData = []; @@ -68,12 +72,18 @@ public function getMappedData() $connection = $exchange['connection']; foreach ($exchange['bindings'] as $binding) { if ($binding['destinationType'] === 'queue') { - $queueItems = $this->createQueueItems($binding['destination'], $binding['topic'], $connection); - $this->mappedData = array_merge($this->mappedData, $queueItems); + $queueItems = $this->createQueueItems( + (string) $binding['destination'], + (string) $binding['topic'], + (array) $binding['arguments'], + (string) $connection + ); + $this->mappedData += $queueItems; } } } } + return $this->mappedData; } @@ -82,10 +92,12 @@ public function getMappedData() * * @param string $name * @param string $topic + * @param array $arguments * @param string $connection * @return array + * @throws LocalizedException */ - private function createQueueItems($name, $topic, $connection) + private function createQueueItems(string $name, string $topic, array $arguments, string $connection): array { $output = []; $synchronousTopics = []; @@ -103,7 +115,7 @@ private function createQueueItems($name, $topic, $connection) 'connection' => $connection, 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => $arguments, ]; } @@ -112,8 +124,9 @@ private function createQueueItems($name, $topic, $connection) 'connection' => $connection, 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => $arguments, ]; + return $output; } @@ -124,15 +137,14 @@ private function createQueueItems($name, $topic, $connection) * @return bool * @throws LocalizedException */ - private function isSynchronousTopic($topicName) + private function isSynchronousTopic(string $topicName): bool { try { $topic = $this->communicationConfig->getTopic($topicName); - $isSync = (bool)$topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; - } catch (LocalizedException $e) { + return (bool) $topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + } catch (LocalizedException $exception) { throw new LocalizedException(new Phrase('Error while checking if topic is synchronous')); } - return $isSync; } /** @@ -141,22 +153,24 @@ private function isSynchronousTopic($topicName) * @param string $wildcard * @return array */ - private function matchSynchronousTopics($wildcard) + private function matchSynchronousTopics(string $wildcard): array { $topicDefinitions = array_filter( $this->communicationConfig->getTopics(), function ($item) { - return (bool)$item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + return (bool) $item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; } ); $topics = []; $pattern = $this->buildWildcardPattern($wildcard); + foreach (array_keys($topicDefinitions) as $topicName) { if (preg_match($pattern, $topicName)) { $topics[$topicName] = $topicName; } } + return $topics; } @@ -166,11 +180,10 @@ function ($item) { * @param string $wildcardKey * @return string */ - private function buildWildcardPattern($wildcardKey) + private function buildWildcardPattern(string $wildcardKey): string { $pattern = '/^' . str_replace('.', '\.', $wildcardKey); - $pattern = str_replace('#', '.+', $pattern); - $pattern = str_replace('*', '[^\.]+', $pattern); + $pattern = str_replace(['#', '*'], ['.+', '[^\.]+'], $pattern); $pattern .= strpos($wildcardKey, '#') === strlen($wildcardKey) ? '/' : '$/'; return $pattern; } From 1000822c16ddb32a483b3966bd5e01e7cfcad536 Mon Sep 17 00:00:00 2001 From: Rafael Kassner Date: Thu, 26 Mar 2020 12:10:12 +0100 Subject: [PATCH 0008/1013] Add missing order_data array to EmailSender classes --- .../Sales/Model/Order/Creditmemo/Sender/EmailSender.php | 6 ++++++ .../Sales/Model/Order/Invoice/Sender/EmailSender.php | 6 ++++++ .../Sales/Model/Order/Shipment/Sender/EmailSender.php | 8 +++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php index 93c8ed00f9daa..a92a1480bd023 100644 --- a/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Creditmemo/Sender/EmailSender.php @@ -111,6 +111,12 @@ public function send( 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php index 004f36c277028..44b4df17619d8 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Sender/EmailSender.php @@ -111,6 +111,12 @@ public function send( 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index 1d4418c50047d..288cbd40b1e5b 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -110,7 +110,13 @@ public function send( 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); From 56f8bf8dcdfd47d656fc563c8d77c80997268923 Mon Sep 17 00:00:00 2001 From: Rafael Kassner Date: Thu, 26 Mar 2020 14:46:10 +0100 Subject: [PATCH 0009/1013] Implement unit tests --- .../Model/Order/Creditmemo/Sender/EmailSenderTest.php | 11 +++++++++++ .../Model/Order/Invoice/Sender/EmailSenderTest.php | 11 +++++++++++ .../Model/Order/Shipment/Sender/EmailSenderTest.php | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php index 13ed0739348b2..b97db473687fb 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Creditmemo/Sender/EmailSenderTest.php @@ -250,6 +250,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -260,6 +265,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new \Magento\Framework\DataObject($transport); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php index 6db1ec0392e0e..0ab413229c703 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Sender/EmailSenderTest.php @@ -249,6 +249,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -259,6 +264,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new \Magento\Framework\DataObject($transport); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 2262fbf03c1a1..6a892e4af7972 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -251,6 +251,11 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending ->method('setSendEmail') ->with($emailSendingResult); + $this->orderMock->method('getCustomerName')->willReturn('Customer name'); + $this->orderMock->method('getIsNotVirtual')->willReturn(true); + $this->orderMock->method('getEmailCustomerNote')->willReturn(null); + $this->orderMock->method('getFrontendStatusLabel')->willReturn('Pending'); + if (!$configValue || $forceSyncMode) { $transport = [ 'order' => $this->orderMock, @@ -261,6 +266,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => 'Customer name', + 'is_not_virtual' => true, + 'email_customer_note' => null, + 'frontend_status_label' => 'Pending', + ], ]; $transport = new \Magento\Framework\DataObject($transport); From 59dd0db5300a4a4f580f51e2bd8d21db74ca05ed Mon Sep 17 00:00:00 2001 From: Eden Date: Sat, 4 Apr 2020 19:44:50 +0700 Subject: [PATCH 0010/1013] Fix wrong position of button list when load "New Attribute" page --- .../Adminhtml/Product/Attribute/Edit.php | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php index 6ab039aa27849..7c680a108adf8 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php @@ -42,6 +42,8 @@ public function __construct( } /** + * Construct block + * * @return void */ protected function _construct() @@ -51,6 +53,14 @@ protected function _construct() parent::_construct(); + $this->buttonList->update('save', 'label', __('Save Attribute')); + $this->buttonList->update('save', 'class', 'save primary'); + $this->buttonList->update( + 'save', + 'data_attribute', + ['mage-init' => ['button' => ['event' => 'save', 'target' => '#edit_form']]] + ); + if ($this->getRequest()->getParam('popup')) { $this->buttonList->remove('back'); if ($this->getRequest()->getParam('product_tab') != 'variations') { @@ -64,6 +74,8 @@ protected function _construct() 100 ); } + $this->buttonList->update('reset', 'level', 10); + $this->buttonList->update('save', 'class', 'save action-secondary'); } else { $this->addButton( 'save_and_edit_button', @@ -79,14 +91,6 @@ protected function _construct() ); } - $this->buttonList->update('save', 'label', __('Save Attribute')); - $this->buttonList->update('save', 'class', 'save primary'); - $this->buttonList->update( - 'save', - 'data_attribute', - ['mage-init' => ['button' => ['event' => 'save', 'target' => '#edit_form']]] - ); - $entityAttribute = $this->_coreRegistry->registry('entity_attribute'); if (!$entityAttribute || !$entityAttribute->getIsUserDefined()) { $this->buttonList->remove('delete'); @@ -96,7 +100,7 @@ protected function _construct() } /** - * {@inheritdoc} + * @inheritdoc */ public function addButton($buttonId, $data, $level = 0, $sortOrder = 0, $region = 'toolbar') { From 1dd4e1bbd58525b83373da00f691bd2daf4ede60 Mon Sep 17 00:00:00 2001 From: Eden Date: Sat, 4 Apr 2020 21:16:10 +0700 Subject: [PATCH 0011/1013] Fix static test --- .../Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php index 7c680a108adf8..efb7d6dbbeff3 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Attribute/Edit.php @@ -107,7 +107,7 @@ public function addButton($buttonId, $data, $level = 0, $sortOrder = 0, $region if ($this->getRequest()->getParam('popup')) { $region = 'header'; } - parent::addButton($buttonId, $data, $level, $sortOrder, $region); + return parent::addButton($buttonId, $data, $level, $sortOrder, $region); } /** From eb8cd708869546a203b17b922c2ce573a6c7f35d Mon Sep 17 00:00:00 2001 From: Nikolay Sumrak Date: Thu, 9 Apr 2020 13:32:19 +0300 Subject: [PATCH 0012/1013] Fixed creating shipping labels in part-shipment --- .../view/adminhtml/templates/order/packaging/popup.phtml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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..592babecdbfd6 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 @@ -44,7 +44,11 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : } }); packaging.setItemQtyCallback(function(itemId){ - var item = $$('[name="shipment[items]['+itemId+']"]')[0]; + var item = $$('[name="shipment[items]['+itemId+']"]')[0], + itemTitle = $('order_item_' + itemId + '_title'); + if (!itemTitle && !item) { + return 0; + } if (item && !isNaN(item.value)) { return item.value; } From 73106d48473c3f78f965df22212df68fbf4da301 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk Date: Thu, 9 Apr 2020 15:06:49 +0300 Subject: [PATCH 0013/1013] Focus search field when pressing '/' --- .../adminhtml/Magento/backend/web/js/theme.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index 05d73ac20fcbd..996f6c05935f2 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -345,6 +345,25 @@ define('globalSearch', [ this.input.on('focus.activateGlobalSearchForm', function () { self.field.addClass(self.options.fieldActiveClass); }); + + $(document).keydown(function (e) { + var inputs = [ + 'input', + 'select', + 'textarea' + ]; + + if (e.which !== 191 || // forward slash - '/' + inputs.indexOf(e.target.tagName.toLowerCase()) !== -1 || + e.target.isContentEditable + ) { + return; + } + + e.preventDefault(); + + self.input.focus(); + }); } }); From 158582ac38e7749771a5acf3997a6e97ee258e7a Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Wed, 22 Apr 2020 10:25:53 +0100 Subject: [PATCH 0014/1013] Use helper method that doesn't throw when no model When a payment configuration node exists in XML but there is no defined, getList() will throw an UnexpectedValueException. One use case for creating such a set-up is a module that provides default configuration overrides for payment methods which are not installed on the current website. For example: 0 0 production In this case, the payment method 'not_installed_here' does not have a node defined, as the payment method module is not installed on the website. --- .../Magento/Payment/Model/PaymentMethodList.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/app/code/Magento/Payment/Model/PaymentMethodList.php b/app/code/Magento/Payment/Model/PaymentMethodList.php index 4e400dbf0c906..d1d5fec2a88e6 100644 --- a/app/code/Magento/Payment/Model/PaymentMethodList.php +++ b/app/code/Magento/Payment/Model/PaymentMethodList.php @@ -39,26 +39,12 @@ public function __construct( */ public function getList($storeId) { - $methodsCodes = array_keys($this->helper->getPaymentMethods()); - - $methodsInstances = array_map( - function ($code) { - return $this->helper->getMethodInstance($code); - }, - $methodsCodes - ); + $methodsInstances = $this->helper->getStoreMethods($storeId); $methodsInstances = array_filter($methodsInstances, function (MethodInterface $method) { return !($method instanceof \Magento\Payment\Model\Method\Substitution); }); - @uasort( - $methodsInstances, - function (MethodInterface $a, MethodInterface $b) use ($storeId) { - return (int)$a->getConfigData('sort_order', $storeId) - (int)$b->getConfigData('sort_order', $storeId); - } - ); - $methodList = array_map( function (MethodInterface $methodInstance) use ($storeId) { From 29621ba467e4c6e145558bba9b378e29b9192d56 Mon Sep 17 00:00:00 2001 From: Matei Purcaru Date: Fri, 24 Apr 2020 16:15:56 +0300 Subject: [PATCH 0015/1013] ISSUE-27954: Forgot password save user only one column --- .../ConfirmCustomerByToken.php | 16 +++++++--------- .../ForgotPasswordToken/GetCustomerByToken.php | 2 +- .../Customer/Model/ResourceModel/Customer.php | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php index 6aadc814a4b9b..e8e9ac9764c3b 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php @@ -7,7 +7,7 @@ namespace Magento\Customer\Model\ForgotPasswordToken; -use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; /** * Confirm customer by reset password token @@ -20,22 +20,22 @@ class ConfirmCustomerByToken private $getByToken; /** - * @var CustomerRepositoryInterface + * @var CustomerResource */ - private $customerRepository; + private $customerResource; /** * ConfirmByToken constructor. * * @param GetCustomerByToken $getByToken - * @param CustomerRepositoryInterface $customerRepository + * @param CustomerResource $customerResource */ public function __construct( GetCustomerByToken $getByToken, - CustomerRepositoryInterface $customerRepository + CustomerResource $customerResource ) { $this->getByToken = $getByToken; - $this->customerRepository = $customerRepository; + $this->customerResource = $customerResource; } /** @@ -50,9 +50,7 @@ public function execute(string $resetPasswordToken): void { $customer = $this->getByToken->execute($resetPasswordToken); if ($customer->getConfirmation()) { - $this->customerRepository->save( - $customer->setConfirmation(null) - ); + $this->customerResource->updateColumn($customer->getId(), 'confirmation', null); } } } diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php index 09af4e296bd92..7ea4031cb5512 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php @@ -54,7 +54,7 @@ public function __construct( * @throws NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException */ - public function execute(string $resetPasswordToken):CustomerInterface + public function execute(string $resetPasswordToken): CustomerInterface { $this->searchCriteriaBuilder->addFilter( 'rp_token', diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index 1477287f79f4b..e0a79822ebeb8 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -403,4 +403,20 @@ public function changeResetPasswordLinkToken(\Magento\Customer\Model\Customer $c } return $this; } + + /** + * @param int $customerId + * @param string $column + * @param string $value + */ + public function updateColumn($customerId, $column, $value) + { + $this->getConnection()->update( + $this->getTable('customer_entity'), + [$column => $value], + [$this->getEntityIdField() . ' = ?' => $customerId] + ); + + return $this; + } } From 86d8b9b3cb4565868f4d9023a903a87d978a2d0d Mon Sep 17 00:00:00 2001 From: Navarr Barnier Date: Fri, 28 Feb 2020 11:54:47 -0500 Subject: [PATCH 0016/1013] Remove documentation that duplicates information in method signature --- .../Framework/App/Cache/FlushCacheByTags.php | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php index 8f8dfd3baf1b6..d9deedbcc4684 100644 --- a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -37,12 +37,6 @@ class FlushCacheByTags */ private $tagResolver; - /** - * @param FrontendPool $cachePool - * @param StateInterface $cacheState - * @param string[] $cacheList - * @param Resolver $tagResolver - */ public function __construct( FrontendPool $cachePool, StateInterface $cacheState, @@ -56,12 +50,8 @@ public function __construct( } /** - * Clean cache on save object + * Clean cache when object is saved * - * @param AbstractResource $subject - * @param \Closure $proceed - * @param AbstractModel $object - * @return AbstractResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function aroundSave(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource @@ -74,12 +64,8 @@ public function aroundSave(AbstractResource $subject, \Closure $proceed, Abstrac } /** - * Clean cache on delete object + * Clean cache when object is deleted * - * @param AbstractResource $subject - * @param \Closure $proceed - * @param AbstractModel $object - * @return AbstractResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function aroundDelete(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource @@ -95,7 +81,6 @@ public function aroundDelete(AbstractResource $subject, \Closure $proceed, Abstr * Clean cache by tags * * @param string[] $tags - * @return void */ private function cleanCacheByTags(array $tags): void { From 46c09c64c78dc3b26b64e3e0bd16ff9e569a4533 Mon Sep 17 00:00:00 2001 From: Navarr Barnier Date: Fri, 28 Feb 2020 13:57:50 -0500 Subject: [PATCH 0017/1013] Reduce the amount of times unique tags need be calculated Performs a micro-optimization in cleanCacheByTags to ensure that we only need to calculate the unique tags once, as opposed to once for each cache type. --- lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php index d9deedbcc4684..f03a4ac23ab8f 100644 --- a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -87,11 +87,12 @@ private function cleanCacheByTags(array $tags): void if (!$tags) { return; } + $uniqueTags = null; foreach ($this->cacheList as $cacheType) { if ($this->cacheState->isEnabled($cacheType)) { $this->cachePool->get($cacheType)->clean( \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, - \array_unique($tags) + $uniqueTags = $uniqueTags ?? \array_unique($tags) ); } } From 2ef99f941d082bf1446a1aca7e8eabbedc0130ea Mon Sep 17 00:00:00 2001 From: Navarr Barnier Date: Fri, 28 Feb 2020 14:37:34 -0500 Subject: [PATCH 0018/1013] Convert around plugins to after plugins We don't need to increase the callstack here with an around plugin, as the parameters we need to be able to access are provided to after plugins. --- .../Framework/App/Cache/FlushCacheByTags.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php index f03a4ac23ab8f..a2925fb3cc246 100644 --- a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -54,9 +54,11 @@ public function __construct( * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundSave(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource - { - $result = $proceed($object); + public function afterSave( + AbstractResource $subject, + AbstractResource $result, + AbstractModel $object + ): AbstractResource { $tags = $this->tagResolver->getTags($object); $this->cleanCacheByTags($tags); @@ -68,10 +70,12 @@ public function aroundSave(AbstractResource $subject, \Closure $proceed, Abstrac * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundDelete(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource - { + public function afterDelete( + AbstractResource $subject, + AbstractResource $result, + AbstractModel $object + ): AbstractResource { $tags = $this->tagResolver->getTags($object); - $result = $proceed($object); $this->cleanCacheByTags($tags); return $result; From e8c63a70e806ca5ef13e897d99d216fbd8ae211f Mon Sep 17 00:00:00 2001 From: Navarr Barnier Date: Fri, 28 Feb 2020 15:33:02 -0500 Subject: [PATCH 0019/1013] Update unit tests for FlushCacheByTags changes --- .../App/Test/Unit/Cache/FlushCacheByTagsTest.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php index 60dba582177eb..f16802f88049e 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php @@ -59,7 +59,7 @@ protected function setUp() /** * @return void */ - public function testAroundSave(): void + public function testAfterSave(): void { $resource = $this->getMockBuilder(AbstractResource::class) ->disableOriginalConstructor() @@ -69,11 +69,9 @@ public function testAroundSave(): void ->getMockForAbstractClass(); $this->tagResolver->expects($this->atLeastOnce())->method('getTags')->with($model)->willReturn([]); - $result = $this->plugin->aroundSave( + $result = $this->plugin->afterSave( + $resource, $resource, - function () use ($resource) { - return $resource; - }, $model ); @@ -83,7 +81,7 @@ function () use ($resource) { /** * @return void */ - public function testAroundDelete(): void + public function testAfterDelete(): void { $resource = $this->getMockBuilder(AbstractResource::class) ->disableOriginalConstructor() @@ -93,11 +91,9 @@ public function testAroundDelete(): void ->getMockForAbstractClass(); $this->tagResolver->expects($this->atLeastOnce())->method('getTags')->with($model)->willReturn([]); - $result = $this->plugin->aroundDelete( + $result = $this->plugin->afterDelete( + $resource, $resource, - function () use ($resource) { - return $resource; - }, $model ); From b7c9c19a3a8dfea3fc85865f555e17752dcf403b Mon Sep 17 00:00:00 2001 From: Navarr Barnier Date: Wed, 22 Apr 2020 15:01:55 -0400 Subject: [PATCH 0020/1013] Re-add phpdoc --- .../Framework/App/Cache/FlushCacheByTags.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php index a2925fb3cc246..363c9740b38aa 100644 --- a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -37,6 +37,12 @@ class FlushCacheByTags */ private $tagResolver; + /** + * @param FrontendPool $cachePool + * @param StateInterface $cacheState + * @param string[] $cacheList + * @param Resolver $tagResolver + */ public function __construct( FrontendPool $cachePool, StateInterface $cacheState, @@ -52,6 +58,10 @@ public function __construct( /** * Clean cache when object is saved * + * @param AbstractResource $subject + * @param AbstractResource $result + * @param AbstractModel $object + * @return AbstractResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterSave( @@ -68,6 +78,10 @@ public function afterSave( /** * Clean cache when object is deleted * + * @param AbstractResource $subject + * @param AbstractResource $result + * @param AbstractModel $object + * @return AbstractResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function afterDelete( @@ -85,6 +99,7 @@ public function afterDelete( * Clean cache by tags * * @param string[] $tags + * @return void */ private function cleanCacheByTags(array $tags): void { From 1c876482f83d0dff21d706ea926206f553c5ee8c Mon Sep 17 00:00:00 2001 From: Alex Taranovsky Date: Wed, 22 Apr 2020 16:21:19 +0300 Subject: [PATCH 0021/1013] magento/magento2#: Remove a redundant getMappedNums from a loop --- .../Framework/GraphQl/Query/EnumLookup.php | 23 +-- .../Test/Unit/Query/EnumLookupTest.php | 172 ++++++++++++++++++ 2 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php diff --git a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php index faddd54e5f180..fb4d9d40b05fa 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php @@ -10,7 +10,6 @@ use Magento\Framework\GraphQl\Config\Element\Enum; use Magento\Framework\GraphQl\ConfigInterface; use Magento\Framework\GraphQl\Schema\Type\Enum\DataMapperInterface; -use Magento\Framework\Phrase; /** * Processor that looks up definition data of an enum to lookup and convert data as it's specified in the schema. @@ -28,6 +27,8 @@ class EnumLookup private $enumDataMapper; /** + * EnumLookup constructor. + * * @param ConfigInterface $typeConfig * @param DataMapperInterface $enumDataMapper */ @@ -43,23 +44,19 @@ public function __construct(ConfigInterface $typeConfig, DataMapperInterface $en * @param string $enumName * @param string $fieldValue * @return string - * @throws \Magento\Framework\Exception\RuntimeException */ public function getEnumValueFromField(string $enumName, string $fieldValue) : string { - $priceViewEnum = $this->typeConfig->getConfigElement($enumName); - if ($priceViewEnum instanceof Enum) { - foreach ($priceViewEnum->getValues() as $enumItem) { - $mappedValues = $this->enumDataMapper->getMappedEnums($enumName); - if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] == $fieldValue) { - return $enumItem->getValue(); - } + /** @var Enum $enumObject */ + $enumObject = $this->typeConfig->getConfigElement($enumName); + $mappedValues = $this->enumDataMapper->getMappedEnums($enumName); + + foreach ($enumObject->getValues() as $enumItem) { + if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] == $fieldValue) { + return $enumItem->getValue(); } - } else { - throw new \Magento\Framework\Exception\RuntimeException( - new Phrase('Enum type "%1" not defined', [$enumName]) - ); } + return ''; } } diff --git a/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php new file mode 100644 index 0000000000000..2d60518806000 --- /dev/null +++ b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php @@ -0,0 +1,172 @@ +objectManager = new ObjectManager($this); + + $this->map = [ + self::ENUM_NAME => [ + 'subscribed' => '1', + 'not_active' => '2', + 'unsubscribed' => '3', + 'unconfirmed' => '4', + ] + ]; + + $this->values = [ + 'NOT_ACTIVE' => new EnumValue('not_active', 'NOT_ACTIVE'), + 'SUBSCRIBED' => new EnumValue('subscribed', 'SUBSCRIBED'), + 'UNSUBSCRIBED' => new EnumValue('unsubscribed', 'UNSUBSCRIBED'), + 'UNCONFIRMED' => new EnumValue('unconfirmed', 'UNCONFIRMED'), + ]; + + $this->enumMock = $this->getMockBuilder(Enum::class) + ->setConstructorArgs( + [ + self::ENUM_NAME, + $this->values, + 'Subscription statuses', + ] + ) + ->getMock(); + + $this->enumDataMapperMock = $this->getMockBuilder(DataMapperInterface::class) + ->setConstructorArgs($this->map) + ->getMock(); + + $this->configDataMock = $this->getMockBuilder(DataInterface::class) + ->getMock(); + $this->configElementFactoryMock = $this->getMockBuilder(ConfigElementFactoryInterface::class) + ->getMock(); + $this->queryFieldsMock = $this->getMockBuilder(QueryFields::class) + ->getMock(); + + $this->typeConfigMock = $this->getMockBuilder(ConfigInterface::class) + ->setConstructorArgs( + [ + $this->configDataMock, + $this->configElementFactoryMock, + $this->queryFieldsMock, + ] + ) + ->getMock(); + + $this->enumLookup = $this->objectManager->getObject( + EnumLookup::class, + [ + 'typeConfig' => $this->typeConfigMock, + 'enumDataMapper' => $this->enumDataMapperMock, + ] + ); + } + + public function testGetEnumValueFromField() + { + $enumName = self::ENUM_NAME; + $fieldValue = '1'; + + $this->enumDataMapperMock + ->expects($this->once()) + ->method('getMappedEnums') + ->willReturn($this->map[$enumName]); + + $this->typeConfigMock + ->expects($this->once()) + ->method('getConfigElement') + ->willReturn($this->enumMock); + + $this->enumMock + ->expects($this->once()) + ->method('getValues') + ->willReturn($this->values); + + $this->assertEquals( + 'SUBSCRIBED', + $this->enumLookup->getEnumValueFromField($enumName, $fieldValue) + ); + } +} From a100d245f0feeed948ffe322bbd2cd26b9909855 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 02:43:42 +0200 Subject: [PATCH 0022/1013] Introduce separate BlockByIdentifier class to get Layout Block based on CMS Block Identifier --- app/code/Magento/Cms/Block/Block.php | 2 + .../Magento/Cms/Block/BlockByIdentifier.php | 145 ++++++++++++++ .../Test/Unit/Block/BlockByIdentifierTest.php | 183 ++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100644 app/code/Magento/Cms/Block/BlockByIdentifier.php create mode 100644 app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php index 86cf059525e1e..afc95d369f67d 100644 --- a/app/code/Magento/Cms/Block/Block.php +++ b/app/code/Magento/Cms/Block/Block.php @@ -10,6 +10,8 @@ /** * Cms block content block + * @deprecated This class introduces caching issues and should no longer be used + * @see \Magento\Cms\Block\BlockByIdentifier */ class Block extends AbstractBlock implements \Magento\Framework\DataObject\IdentityInterface { diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php new file mode 100644 index 0000000000000..11dbf03642289 --- /dev/null +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -0,0 +1,145 @@ +` definition + */ +class BlockByIdentifier extends AbstractBlock implements IdentityInterface +{ + const CACHE_KEY_PREFIX = 'CMS_BLOCK'; + + /** + * @var GetBlockByIdentifierInterface + */ + private $blockByIdentifier; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var FilterProvider + */ + private $filterProvider; + + /** + * @var BlockInterface + */ + private $cmsBlock; + + public function __construct( + GetBlockByIdentifierInterface $blockByIdentifier, + StoreManagerInterface $storeManager, + FilterProvider $filterProvider, + Context $context, + array $data = [] + ) { + parent::__construct($context, $data); + $this->blockByIdentifier = $blockByIdentifier; + $this->storeManager = $storeManager; + $this->filterProvider = $filterProvider; + } + + /** + * @inheritDoc + */ + protected function _toHtml(): string + { + try { + return $this->filterOutput( + $this->getCmsBlock()->getContent() + ); + } catch (NoSuchEntityException $e) { + return ''; + } + } + + /** + * Filters the Content + * + * @param string $content + * @return string + * @throws NoSuchEntityException + */ + private function filterOutput(string $content): string + { + return $this->filterProvider->getBlockFilter() + ->setStoreId($this->getCurrentStore()->getId()) + ->filter($content); + } + + /** + * Loads the CMS block by `identifier` provided as an argument + * + * @return BlockInterface + * @throws NoSuchEntityException + */ + private function getCmsBlock(): BlockInterface + { + if (!$this->getIdentifier()) { + throw new NoSuchEntityException( + __('Expected value of `identifier` was not provided') + ); + } + + if (null === $this->cmsBlock) { + $this->cmsBlock = $this->blockByIdentifier->execute( + (string)$this->getIdentifier(), + (int)$this->getCurrentStore()->getId() + ); + } + + return $this->cmsBlock; + } + + /** + * Returns the StoreInterface of currently opened Store scope + * + * @return StoreInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getCurrentStore(): StoreInterface + { + return $this->storeManager->getStore(); + } + + /** + * Returns array of Block Identifiers used to determine Cache Tags + * + * This implementation supports different CMS blocks caching having the same identifier, + * resolving the bug introduced in scope of \Magento\Cms\Block\Block + * + * @return string[] + */ + public function getIdentities(): array + { + try { + return [ + self::CACHE_KEY_PREFIX . '_' . $this->getCmsBlock()->getId(), + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStore()->getId() + ]; + } catch (NoSuchEntityException $e) { + // If CMS Block does not exist, it should not be cached + return []; + } + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php new file mode 100644 index 0000000000000..b3e582be61446 --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -0,0 +1,183 @@ +storeMock = $this->createMock(StoreInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->storeManagerMock->method('getStore')->willReturn($this->storeMock); + + $this->getBlockByIdentifierMock = $this->createMock(GetBlockByIdentifierInterface::class); + + $this->filterProviderMock = $this->createMock(FilterProvider::class); + $this->filterProviderMock->method('getBlockFilter')->willReturn($this->getPassthroughFilterMock()); + } + + public function testBlockReturnsEmptyStringWhenNoIdentifierProvided(): void + { + // Given + $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null); + + // Expect + $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); + $this->assertSame(self::ASSERT_NO_CACHE_IDENTITIES, $missingIdentifierBlock->getIdentities()); + } + + public function testBlockReturnsCmsContentsWhenIdentifierFound(): void + { + // Given + $cmsBlockMock = $this->getCmsBlockMock( + self::STUB_CMS_BLOCK_ID, + self::STUB_EXISTING_IDENTIFIER, + self::STUB_CONTENT + ); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + $this->getBlockByIdentifierMock->method('execute') + ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) + ->willReturn($cmsBlockMock); + $block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER); + + // Expect + $this->assertSame(self::ASSERT_CONTENT_HTML, $block->toHtml()); + } + + public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void + { + // Given + $cmsBlockMock = $this->getCmsBlockMock( + self::STUB_CMS_BLOCK_ID, + self::STUB_EXISTING_IDENTIFIER, + self::STUB_CONTENT + ); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + $this->getBlockByIdentifierMock->method('execute') + ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) + ->willReturn($cmsBlockMock); + $block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER); + + // When + $identities = $block->getIdentities(); + + // Then + $this->assertContains($this->getCacheKeyStubById(self::STUB_CMS_BLOCK_ID), $identities); + $this->assertContains( + $this->getCacheKeyStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), + $identities + ); + } + + /** + * Initializes the tested block with injecting the references required by parent classes. + * + * @param string|null $identifier + * @return BlockByIdentifier + */ + private function getTestedBlockUsingIdentifier(?string $identifier): BlockByIdentifier + { + $eventManagerMock = $this->createMock(ManagerInterface::class); + $scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $scopeConfigMock->method('getValue')->willReturn(self::STUB_MODULE_OUTPUT_DISABLED); + + $contextMock = $this->createMock(Context::class); + $contextMock->method('getEventManager')->willReturn($eventManagerMock); + $contextMock->method('getScopeConfig')->willReturn($scopeConfigMock); + + return new BlockByIdentifier( + $this->getBlockByIdentifierMock, + $this->storeManagerMock, + $this->filterProviderMock, + $contextMock, + ['identifier' => $identifier] + ); + } + + /** + * Mocks the CMS Block object for further play + * + * @param int $entityId + * @param string $identifier + * @param string $content + * @return MockObject|BlockInterface + */ + private function getCmsBlockMock(int $entityId, string $identifier, string $content): BlockInterface + { + $cmsBlock = $this->createMock(BlockInterface::class); + + $cmsBlock->method('getId')->willReturn($entityId); + $cmsBlock->method('getIdentifier')->willReturn($identifier); + $cmsBlock->method('getContent')->willReturn($content); + + return $cmsBlock; + } + + /** + * Creates mock of the Filter that actually is doing nothing + * + * @return MockObject|Template + */ + private function getPassthroughFilterMock(): Template + { + $filterMock = $this->getMockBuilder(Template::class) + ->disableOriginalConstructor() + ->setMethods(['setStoreId', 'filter']) + ->getMock(); + $filterMock->method('setStoreId')->willReturnSelf(); + $filterMock->method('filter')->willReturnArgument(0); + + return $filterMock; + } + + private function getCacheKeyStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string + { + return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $identifier . '_' . $storeId; + } + + private function getCacheKeyStubById(int $cmsBlockId): string + { + return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $cmsBlockId; + } +} From 450127126cb2c555c8bd2391e13bafdb7cf67922 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 02:55:44 +0200 Subject: [PATCH 0023/1013] Add support for save Block in the caches for all scopes that block is assigned to --- .../Magento/Cms/Block/BlockByIdentifier.php | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 11dbf03642289..e362806433404 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -14,7 +14,6 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Element\Context; -use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -83,7 +82,7 @@ protected function _toHtml(): string private function filterOutput(string $content): string { return $this->filterProvider->getBlockFilter() - ->setStoreId($this->getCurrentStore()->getId()) + ->setStoreId($this->getCurrentStoreId()) ->filter($content); } @@ -104,7 +103,7 @@ private function getCmsBlock(): BlockInterface if (null === $this->cmsBlock) { $this->cmsBlock = $this->blockByIdentifier->execute( (string)$this->getIdentifier(), - (int)$this->getCurrentStore()->getId() + $this->getCurrentStoreId() ); } @@ -112,14 +111,14 @@ private function getCmsBlock(): BlockInterface } /** - * Returns the StoreInterface of currently opened Store scope + * Returns the current Store ID * - * @return StoreInterface + * @return int * @throws \Magento\Framework\Exception\NoSuchEntityException */ - private function getCurrentStore(): StoreInterface + private function getCurrentStoreId(): int { - return $this->storeManager->getStore(); + return (int)$this->storeManager->getStore()->getId(); } /** @@ -133,10 +132,19 @@ private function getCurrentStore(): StoreInterface public function getIdentities(): array { try { - return [ - self::CACHE_KEY_PREFIX . '_' . $this->getCmsBlock()->getId(), - self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStore()->getId() - ]; + $cmsBlock = $this->getCmsBlock(); + + $identities = [self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId()]; + + if (method_exists($this->getCmsBlock(), 'getStores')) { + foreach ($cmsBlock->getStores() as $store) { + $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $store; + } + } + + $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStoreId(); + + return $identities; } catch (NoSuchEntityException $e) { // If CMS Block does not exist, it should not be cached return []; From ae11a0c6908794a487129dc5b08e03d62025f2a9 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 03:21:51 +0200 Subject: [PATCH 0024/1013] Improve the logic behind getIdentifiers and Unit Tests coverage --- .../Magento/Cms/Block/BlockByIdentifier.php | 23 +++++++++++------- .../Test/Unit/Block/BlockByIdentifierTest.php | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index e362806433404..2f1ae518552fc 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -131,23 +131,28 @@ private function getCurrentStoreId(): int */ public function getIdentities(): array { + if (!$this->getIdentifier()) { + return []; + } + + $identities = [ + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier(), + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStoreId() + ]; + try { $cmsBlock = $this->getCmsBlock(); - $identities = [self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId()]; + $identities[] = self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId(); if (method_exists($this->getCmsBlock(), 'getStores')) { - foreach ($cmsBlock->getStores() as $store) { - $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $store; + foreach ($cmsBlock->getStores() as $storeId) { + $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $storeId; } } - - $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStoreId(); - - return $identities; + // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { - // If CMS Block does not exist, it should not be cached - return []; } + return $identities; } } diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index b3e582be61446..3ff782fe728c9 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -14,6 +14,7 @@ use Magento\Cms\Model\Template\FilterProvider; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filter\Template; use Magento\Framework\View\Element\Context; use Magento\Store\Api\Data\StoreInterface; @@ -25,6 +26,7 @@ class BlockByIdentifierTest extends TestCase { private const STUB_MODULE_OUTPUT_DISABLED = false; private const STUB_EXISTING_IDENTIFIER = 'existingOne'; + private const STUB_UNAVAILABLE_IDENTIFIER = 'notExists'; private const STUB_DEFAULT_STORE = 1; private const STUB_CMS_BLOCK_ID = 1; private const STUB_CONTENT = 'Content'; @@ -32,6 +34,10 @@ class BlockByIdentifierTest extends TestCase private const ASSERT_EMPTY_BLOCK_HTML = ''; private const ASSERT_CONTENT_HTML = self::STUB_CONTENT; private const ASSERT_NO_CACHE_IDENTITIES = []; + private const ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES = [ + BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER, + BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER . '_' . self::STUB_DEFAULT_STORE + ]; /** @var MockObject|GetBlockByIdentifierInterface */ private $getBlockByIdentifierMock; @@ -61,12 +67,30 @@ public function testBlockReturnsEmptyStringWhenNoIdentifierProvided(): void { // Given $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); // Expect $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); $this->assertSame(self::ASSERT_NO_CACHE_IDENTITIES, $missingIdentifierBlock->getIdentities()); } + public function testBlockReturnsEmptyStringWhenIdentifierProvidedNotFound(): void + { + // Given + $this->getBlockByIdentifierMock->method('execute')->willThrowException( + new NoSuchEntityException(__('NoSuchEntityException')) + ); + $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(self::STUB_UNAVAILABLE_IDENTIFIER); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + + // Expect + $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); + $this->assertSame( + self::ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES, + $missingIdentifierBlock->getIdentities() + ); + } + public function testBlockReturnsCmsContentsWhenIdentifierFound(): void { // Given From 74b3e6228f65787dc7f514fbd8d49d9a672ebb8e Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 03:30:12 +0200 Subject: [PATCH 0025/1013] Rename stub methods and add missing doc blocks. --- .../Test/Unit/Block/BlockByIdentifierTest.php | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 3ff782fe728c9..2b0005c012f09 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -127,9 +127,9 @@ public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void $identities = $block->getIdentities(); // Then - $this->assertContains($this->getCacheKeyStubById(self::STUB_CMS_BLOCK_ID), $identities); + $this->assertContains($this->getIdentityStubById(self::STUB_CMS_BLOCK_ID), $identities); $this->assertContains( - $this->getCacheKeyStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), + $this->getIdentityStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), $identities ); } @@ -195,12 +195,25 @@ private function getPassthroughFilterMock(): Template return $filterMock; } - private function getCacheKeyStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string + /** + * Returns stub of Identity based on `$identifier` and `$storeId` + * + * @param string $identifier + * @param int $storeId + * @return string + */ + private function getIdentityStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string { return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $identifier . '_' . $storeId; } - private function getCacheKeyStubById(int $cmsBlockId): string + /** + * Returns stub of Identity based on `$cmsBlockId` + * + * @param int $cmsBlockId + * @return string + */ + private function getIdentityStubById(int $cmsBlockId): string { return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $cmsBlockId; } From 4656d9eaf44720445e3c40ba68acb782bbf6b809 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 03:31:49 +0200 Subject: [PATCH 0026/1013] Redundant empty line --- app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 2b0005c012f09..d6fb9e9ad1a44 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -1,5 +1,4 @@ Date: Fri, 8 May 2020 12:22:00 +0200 Subject: [PATCH 0027/1013] Fix #24091 - Selected configurable product attribute options are not displaying in wishlist page. --- .../Magento/Wishlist/Block/AddToWishlist.php | 38 ++++++++------- .../frontend/layout/catalog_category_view.xml | 10 +++- .../layout/catalogsearch_result_index.xml | 12 +++++ .../view/frontend/web/js/add-to-wishlist.js | 48 +++++++++++++++++-- 4 files changed, 84 insertions(+), 24 deletions(-) diff --git a/app/code/Magento/Wishlist/Block/AddToWishlist.php b/app/code/Magento/Wishlist/Block/AddToWishlist.php index 3ba350af94176..dffd8cb027e74 100644 --- a/app/code/Magento/Wishlist/Block/AddToWishlist.php +++ b/app/code/Magento/Wishlist/Block/AddToWishlist.php @@ -6,13 +6,15 @@ namespace Magento\Wishlist\Block; +use Magento\Framework\View\Element\Template; + /** * Wishlist js plugin initialization block * * @api * @since 100.1.0 */ -class AddToWishlist extends \Magento\Framework\View\Element\Template +class AddToWishlist extends Template { /** * Product types @@ -21,20 +23,6 @@ class AddToWishlist extends \Magento\Framework\View\Element\Template */ private $productTypes; - /** - * @param \Magento\Framework\View\Element\Template\Context $context - * @param array $data - */ - public function __construct( - \Magento\Framework\View\Element\Template\Context $context, - array $data = [] - ) { - parent::__construct( - $context, - $data - ); - } - /** * Returns wishlist widget options * @@ -43,7 +31,10 @@ public function __construct( */ public function getWishlistOptions() { - return ['productType' => $this->getProductTypes()]; + return [ + 'productType' => $this->getProductTypes(), + 'isProductList' => (bool)$this->getData('is_product_list') + ]; } /** @@ -56,7 +47,7 @@ private function getProductTypes() { if ($this->productTypes === null) { $this->productTypes = []; - $block = $this->getLayout()->getBlock('category.products.list'); + $block = $this->getLayout()->getBlock($this->getProductListBlockName()); if ($block) { $productCollection = $block->getLoadedProductCollection(); $productTypes = []; @@ -71,7 +62,18 @@ private function getProductTypes() } /** - * {@inheritdoc} + * Get product list block name in layout + * + * @return string + */ + private function getProductListBlockName(): string + { + return $this->getData('product_list_block') ?: 'category.products.list'; + } + + /** + * @inheritDoc + * * @since 100.1.0 */ protected function _toHtml() diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml index a4860ace166d8..8b784cfd31783 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml @@ -21,7 +21,15 @@ template="Magento_Wishlist::catalog/product/list/addto/wishlist.phtml"/> - + + + true + + diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml index c293175ccceac..1f597a9ce1e3a 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml @@ -14,5 +14,17 @@ template="Magento_Wishlist::catalog/product/list/addto/wishlist.phtml"/> + + + + true + search_result_list + + + diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 55cd77b196be5..1cdad4953b3c2 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -17,7 +17,10 @@ define([ downloadableInfo: '#downloadable-links-list input', customOptionsInfo: '.product-custom-option', qtyInfo: '#qty', - actionElement: '[data-action="add-to-wishlist"]' + actionElement: '[data-action="add-to-wishlist"]', + productListItem: '.item.product-item', + productListPriceBox: '.price-box', + isProductList: false }, /** @inheritdoc */ @@ -65,6 +68,7 @@ define([ _updateWishlistData: function (event) { var dataToAdd = {}, isFileUploaded = false, + productId = null, self = this; if (event.handleObj.selector == this.options.qtyInfo) { //eslint-disable-line eqeqeq @@ -83,7 +87,19 @@ define([ $(element).is('textarea') || $('#' + element.id + ' option:selected').length ) { - if ($(element).data('selector') || $(element).attr('name')) { + if (!($(element).data('selector') || $(element).attr('name'))) { + return; + } + + if (self.options.isProductList) { + productId = self.retrieveListProductId(this); + + dataToAdd[productId] = $.extend( + {}, + dataToAdd[productId] ? dataToAdd[productId] : {}, + self._getElementData(element) + ); + } else { dataToAdd = $.extend({}, dataToAdd, self._getElementData(element)); } @@ -107,10 +123,17 @@ define([ * @private */ _updateAddToWishlistButton: function (dataToAdd) { - var self = this; + var productId = null, + self = this; $('[data-action="add-to-wishlist"]').each(function (index, element) { - var params = $(element).data('post'); + var params = $(element).data('post'), + dataToAddObj = dataToAdd; + + if (self.options.isProductList) { + productId = self.retrieveListProductId(element); + dataToAddObj = typeof dataToAdd[productId] !== 'undefined' ? dataToAdd[productId] : {}; + } if (!params) { params = { @@ -118,7 +141,7 @@ define([ }; } - params.data = $.extend({}, params.data, dataToAdd, { + params.data = $.extend({}, params.data, dataToAddObj, { 'qty': $(self.options.qtyInfo).val() }); $(element).data('post', params); @@ -241,6 +264,21 @@ define([ return; } + }, + + /** + * Retrieve product id from element on products list + * + * @param {jQuery.Object} element + * @private + */ + retrieveListProductId: function (element) { + return parseInt( + $(element).closest(this.options.productListItem) + .find(this.options.productListPriceBox) + .data('product-id'), + 10 + ); } }); From 3e6bb4b7e1a9887b4fb7d30e38162b315488b524 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 16:05:49 +0200 Subject: [PATCH 0028/1013] Final adjustments to the implementation --- .../Magento/Cms/Block/BlockByIdentifier.php | 20 ++++++++------ .../Test/Unit/Block/BlockByIdentifierTest.php | 27 ++++++++++++------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 2f1ae518552fc..eb14399741c41 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -9,6 +9,7 @@ use Magento\Cms\Api\Data\BlockInterface; use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Cms\Model\Block as BlockModel; use Magento\Cms\Model\Template\FilterProvider; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Exception\NoSuchEntityException; @@ -45,6 +46,13 @@ class BlockByIdentifier extends AbstractBlock implements IdentityInterface */ private $cmsBlock; + /** + * @param GetBlockByIdentifierInterface $blockByIdentifier + * @param StoreManagerInterface $storeManager + * @param FilterProvider $filterProvider + * @param Context $context + * @param array $data + */ public function __construct( GetBlockByIdentifierInterface $blockByIdentifier, StoreManagerInterface $storeManager, @@ -89,7 +97,7 @@ private function filterOutput(string $content): string /** * Loads the CMS block by `identifier` provided as an argument * - * @return BlockInterface + * @return BlockInterface|BlockModel * @throws NoSuchEntityException */ private function getCmsBlock(): BlockInterface @@ -142,17 +150,13 @@ public function getIdentities(): array try { $cmsBlock = $this->getCmsBlock(); - - $identities[] = self::CACHE_KEY_PREFIX . '_' . $cmsBlock->getId(); - - if (method_exists($this->getCmsBlock(), 'getStores')) { - foreach ($cmsBlock->getStores() as $storeId) { - $identities[] = self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $storeId; - } + if ($cmsBlock instanceof IdentityInterface) { + $identities = array_merge($identities, $cmsBlock->getIdentities()); } // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } + return $identities; } } diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index d6fb9e9ad1a44..3a55666b2867b 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -10,6 +10,7 @@ use Magento\Cms\Api\Data\BlockInterface; use Magento\Cms\Api\GetBlockByIdentifierInterface; use Magento\Cms\Block\BlockByIdentifier; +use Magento\Cms\Model\Block; use Magento\Cms\Model\Template\FilterProvider; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event\ManagerInterface; @@ -21,6 +22,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class BlockByIdentifierTest extends TestCase { private const STUB_MODULE_OUTPUT_DISABLED = false; @@ -37,6 +41,8 @@ class BlockByIdentifierTest extends TestCase BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER, BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER . '_' . self::STUB_DEFAULT_STORE ]; + private const STUB_CMS_BLOCK_IDENTITY_BY_ID = 'CMS_BLOCK_' . self::STUB_CMS_BLOCK_ID; + private const STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER = 'CMS_BLOCK_' . self::STUB_EXISTING_IDENTIFIER; /** @var MockObject|GetBlockByIdentifierInterface */ private $getBlockByIdentifierMock; @@ -108,14 +114,19 @@ public function testBlockReturnsCmsContentsWhenIdentifierFound(): void $this->assertSame(self::ASSERT_CONTENT_HTML, $block->toHtml()); } - public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void + public function testBlockCacheIdentitiesContainCmsBlockIdentities(): void { // Given - $cmsBlockMock = $this->getCmsBlockMock( - self::STUB_CMS_BLOCK_ID, - self::STUB_EXISTING_IDENTIFIER, - self::STUB_CONTENT + $cmsBlockMock = $this->createMock(Block::class); + $cmsBlockMock->method('getId')->willReturn(self::STUB_CMS_BLOCK_ID); + $cmsBlockMock->method('getIdentifier')->willReturn(self::STUB_EXISTING_IDENTIFIER); + $cmsBlockMock->method('getIdentities')->willReturn( + [ + self::STUB_CMS_BLOCK_IDENTITY_BY_ID, + self::STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER + ] ); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); $this->getBlockByIdentifierMock->method('execute') ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) @@ -127,10 +138,8 @@ public function testBlockCacheIdentitiesContainExplicitScopeInformation(): void // Then $this->assertContains($this->getIdentityStubById(self::STUB_CMS_BLOCK_ID), $identities); - $this->assertContains( - $this->getIdentityStubByIdentifier(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE), - $identities - ); + $this->assertContains(self::STUB_CMS_BLOCK_IDENTITY_BY_ID, $identities); + $this->assertContains(self::STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER, $identities); } /** From ab2be56ec292f0feee21c8f689603b8f28ea527a Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 16:56:52 +0200 Subject: [PATCH 0029/1013] Introduce private method to correctly support strict types --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index eb14399741c41..9bfad5c68c11a 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -19,8 +19,6 @@ /** * This class is replacement of \Magento\Cms\Block\Block, that accepts only `string` identifier of CMS Block - * - * @method getIdentifier(): int Returns the value of `identifier` injected in `` definition */ class BlockByIdentifier extends AbstractBlock implements IdentityInterface { @@ -80,6 +78,16 @@ protected function _toHtml(): string } } + /** + * Returns the value of `identifier` injected in `` definition + * + * @return string|null + */ + private function getIdentifier(): ?string + { + return $this->getdata('identifier') ?: null; + } + /** * Filters the Content * From ac5ef0772142af4a66ef1a7a4456d483d128483e Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 16:59:18 +0200 Subject: [PATCH 0030/1013] Cleaner way to handle ignoring the PHPCS warning --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 9bfad5c68c11a..690de727e8c05 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -161,7 +161,7 @@ public function getIdentities(): array if ($cmsBlock instanceof IdentityInterface) { $identities = array_merge($identities, $cmsBlock->getIdentities()); } - // phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (NoSuchEntityException $e) { } From cdcad5c888cf752c1b8efb142d3f4716ebc772bd Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Fri, 8 May 2020 23:23:39 +0200 Subject: [PATCH 0031/1013] Static check fixes --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- .../Cms/Test/Unit/Block/BlockByIdentifierTest.php | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 690de727e8c05..d579b3b222c53 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -22,7 +22,7 @@ */ class BlockByIdentifier extends AbstractBlock implements IdentityInterface { - const CACHE_KEY_PREFIX = 'CMS_BLOCK'; + public const CACHE_KEY_PREFIX = 'CMS_BLOCK'; /** * @var GetBlockByIdentifierInterface diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 3a55666b2867b..e9e94ffd966d0 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -203,18 +203,6 @@ private function getPassthroughFilterMock(): Template return $filterMock; } - /** - * Returns stub of Identity based on `$identifier` and `$storeId` - * - * @param string $identifier - * @param int $storeId - * @return string - */ - private function getIdentityStubByIdentifier(string $identifier, int $storeId = self::STUB_DEFAULT_STORE): string - { - return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $identifier . '_' . $storeId; - } - /** * Returns stub of Identity based on `$cmsBlockId` * From 4fd92e7edc9a62c3e09f7ed0461da4f36aba4d27 Mon Sep 17 00:00:00 2001 From: Per Date: Sat, 9 May 2020 17:09:57 +0200 Subject: [PATCH 0032/1013] Issue #27925, moved the submit button to the inside of the
Placing the submit button inside the `
` makes implicit submission possible --- .../template/payment/purchaseorder-form.html | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html b/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html index 89d16bd732e7c..3a42a84b620b8 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html +++ b/app/code/Magento/OfflinePayments/view/frontend/web/template/payment/purchaseorder-form.html @@ -42,27 +42,29 @@ - -
- - - -
-
-
- + +
+ + +
-
+ +
+
+ +
+
+
- + From e9511e8db117b06516c4b8cc541376e5657a264d Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Wed, 13 May 2020 21:24:10 +0100 Subject: [PATCH 0033/1013] Replace hard-coded list of category attributes --- .../Catalog/Model/Category/DataProvider.php | 75 +++++++------------ app/code/Magento/Catalog/etc/di.xml | 3 + 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index d8c79c485e3e5..d9e655fff2cef 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -22,6 +22,7 @@ use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Config\DataInterfaceFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; @@ -153,6 +154,11 @@ class DataProvider extends ModifierPoolDataProvider */ private $categoryFactory; + /** + * @var DataInterfaceFactory + */ + protected $uiConfigFactory; + /** * @var ScopeOverriddenValue */ @@ -193,6 +199,7 @@ class DataProvider extends ModifierPoolDataProvider * @param Config $eavConfig * @param RequestInterface $request * @param CategoryFactory $categoryFactory + * @param DataInterfaceFactory $uiConfigFactory * @param array $meta * @param array $data * @param PoolInterface|null $pool @@ -215,6 +222,7 @@ public function __construct( Config $eavConfig, RequestInterface $request, CategoryFactory $categoryFactory, + DataInterfaceFactory $uiConfigFactory, array $meta = [], array $data = [], PoolInterface $pool = null, @@ -233,6 +241,7 @@ public function __construct( $this->storeManager = $storeManager; $this->request = $request; $this->categoryFactory = $categoryFactory; + $this->uiConfigFactory = $uiConfigFactory; $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); $this->arrayUtils = $arrayUtils ?? ObjectManager::getInstance()->get(ArrayUtils::class); $this->scopeOverriddenValue = $scopeOverriddenValue ?: @@ -645,56 +654,22 @@ public function getDefaultMetaData($result) */ protected function getFieldsMap() { - return [ - 'general' => [ - 'parent', - 'path', - 'is_active', - 'include_in_menu', - 'name', - ], - 'content' => [ - 'image', - 'description', - 'landing_page', - ], - 'display_settings' => [ - 'display_mode', - 'is_anchor', - 'available_sort_by', - 'use_config.available_sort_by', - 'default_sort_by', - 'use_config.default_sort_by', - 'filter_price_range', - 'use_config.filter_price_range', - ], - 'search_engine_optimization' => [ - 'url_key', - 'url_key_create_redirect', - 'url_key_group', - 'meta_title', - 'meta_keywords', - 'meta_description', - ], - 'assign_products' => [ - ], - 'design' => [ - 'custom_use_parent_settings', - 'custom_apply_to_products', - 'custom_design', - 'page_layout', - 'custom_layout_update', - 'custom_layout_update_file' - ], - 'schedule_design_update' => [ - 'custom_design_from', - 'custom_design_to', - ], - 'category_view_optimization' => [ - ], - 'category_permissions' => [ - ], - ]; + $referenceName = 'category_form'; + $config = $this->uiConfigFactory + ->create(['componentName' => $referenceName]) + ->get($referenceName); + + $fieldsMap = []; + + if (isset($config['children']) && !empty($config['children'])) { + foreach ($config['children'] as $name => $child) { + if (isset($child['children']) && !empty($child['children'])) { + $fieldsMap[$name] = array_keys($child['children']); + } + } + } + + return $fieldsMap; } /** diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 5a7a3135b4bfe..b012219ee8f44 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -79,6 +79,9 @@ + + uiComponentConfigFactory + From 5f3a2eb96bddda13363cf59284855259cc8aed71 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz Date: Sat, 16 May 2020 14:52:19 +0200 Subject: [PATCH 0034/1013] Fix invalid Unit Test declaration --- app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index e9e94ffd966d0..612d077110d9f 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -56,7 +56,7 @@ class BlockByIdentifierTest extends TestCase /** @var MockObject|StoreInterface */ private $storeMock; - protected function setUp() + protected function setUp(): void { $this->storeMock = $this->createMock(StoreInterface::class); $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); From cf5d73b89648c31fa0832fc6a1957f13b338a512 Mon Sep 17 00:00:00 2001 From: Alexander Steshuk Date: Mon, 18 May 2020 12:40:15 +0300 Subject: [PATCH 0035/1013] #28172: MFTF tests --- .../Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml index 16fd373d3ae4d..bacf8ac4b9fb5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutPaymentSection.xml @@ -30,6 +30,7 @@ + From 9173df72586e897d4a624a59cc7465b5fbfb23bd Mon Sep 17 00:00:00 2001 From: Alexander Steshuk Date: Mon, 18 May 2020 12:41:18 +0300 Subject: [PATCH 0036/1013] #28172: MFTF tests --- ...thPurchaseOrderNumberPressKeyEnterTest.xml | 68 +++++++++++++++++++ ...ontCheckoutWithPurchaseOrderNumberTest.xml | 67 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberTest.xml diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml new file mode 100644 index 0000000000000..0959962d50d81 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml @@ -0,0 +1,68 @@ + + + + + + + + + + <description value="Create Checkout with purchase order payment method. Press key Enter on field Purchase Order Number for create Order."/> + <severity value="MAJOR"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="SimpleTwo" stepKey="createSimpleProduct"/> + + <!-- Enable payment method --> + <magentoCLI command="config:set {{PurchaseOrderEnableConfigData.path}} {{PurchaseOrderEnableConfigData.value}}" stepKey="enablePaymentMethod"/> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!-- Disable payment method --> + <magentoCLI command="config:set {{PurchaseOrderDisabledConfigData.path}} {{PurchaseOrderDisabledConfigData.value}}" stepKey="disablePaymentMethod"/> + </after> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + + <!-- Checkout select Purchase Order payment --> + <actionGroup ref="CheckoutSelectPurchaseOrderPaymentActionGroup" stepKey="selectPurchaseOrderPayment"> + <argument name="purchaseOrderNumber" value="12345"/> + </actionGroup> + + <!--Press Key ENTER--> + <pressKey selector="{{StorefrontCheckoutPaymentMethodSection.purchaseOrderNumber}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressKeyEnter"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!--See success messages--> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order # is: " stepKey="seeOrderNumber"/> + + </test> + +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberTest.xml new file mode 100644 index 0000000000000..0b46bbdb7db65 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberTest.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="StorefrontCheckoutWithPurchaseOrderNumberTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout with Purchase Order Payment"/> + <title value="Create Checkout with purchase order payment method test"/> + <description value="Create Checkout with purchase order payment method"/> + <severity value="MAJOR"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="SimpleTwo" stepKey="createSimpleProduct"/> + + <!-- Enable payment method --> + <magentoCLI command="config:set {{PurchaseOrderEnableConfigData.path}} {{PurchaseOrderEnableConfigData.value}}" stepKey="enablePaymentMethod"/> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!-- Disable payment method --> + <magentoCLI command="config:set {{PurchaseOrderDisabledConfigData.path}} {{PurchaseOrderDisabledConfigData.value}}" stepKey="disablePaymentMethod"/> + </after> + + <!--Go to product page--> + <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForCatalogPageLoad"/> + + <!--Add Product to Shopping Cart--> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!--Go to Checkout--> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="guestCheckoutFillingShippingSection"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + + <!-- Checkout select Purchase Order payment --> + <actionGroup ref="CheckoutSelectPurchaseOrderPaymentActionGroup" stepKey="selectPurchaseOrderPayment"> + <argument name="purchaseOrderNumber" value="12345"/> + </actionGroup> + + <!--Click Place Order button--> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!--See success messages--> + <see selector="{{CheckoutSuccessMainSection.successTitle}}" userInput="Thank you for your purchase!" stepKey="seeSuccessTitle"/> + <see selector="{{CheckoutSuccessMainSection.orderNumberText}}" userInput="Your order # is: " stepKey="seeOrderNumber"/> + + </test> + +</tests> From 2dee5655c8acce78a3fd146e34b2581215786324 Mon Sep 17 00:00:00 2001 From: Alexander Steshuk <grp-engcom-vendorworker-Kilo@adobe.com> Date: Mon, 18 May 2020 13:55:17 +0300 Subject: [PATCH 0037/1013] #28172: MFTF tests --- ...tSelectPurchaseOrderPaymentActionGroup.xml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutSelectPurchaseOrderPaymentActionGroup.xml diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutSelectPurchaseOrderPaymentActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutSelectPurchaseOrderPaymentActionGroup.xml new file mode 100644 index 0000000000000..dbc9739a9247f --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/CheckoutSelectPurchaseOrderPaymentActionGroup.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="CheckoutSelectPurchaseOrderPaymentActionGroup"> + <annotations> + <description>Selects the 'Purchase Order' Payment Method on the Storefront Checkout page.</description> + </annotations> + + <arguments> + <argument name="purchaseOrderNumber" type="string"/> + </arguments> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <remove keyForRemoval="checkCheckMoneyOption"/> + <conditionalClick selector="{{CheckoutPaymentSection.purchaseOrderPayment}}" dependentSelector="{{CheckoutPaymentSection.purchaseOrderPayment}}" visible="true" stepKey="checkPurchaseOrderOption"/> + <fillField selector="{{StorefrontCheckoutPaymentMethodSection.purchaseOrderNumber}}" userInput="{{purchaseOrderNumber}}" stepKey="fillPurchaseOrderNumber"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterPaymentMethodSelection"/> + </actionGroup> +</actionGroups> From c3d14d1525ec7681a9f02ebcbd013cf8d646b381 Mon Sep 17 00:00:00 2001 From: David Manners <dmanners87@gmail.com> Date: Sat, 4 Apr 2020 16:14:04 +0200 Subject: [PATCH 0038/1013] Create a new class for getting new names of files plus a test class in preperation for refactoring --- lib/internal/Magento/Framework/File/Name.php | 64 +++++++++++++++++++ .../Framework/Test/Unit/File/NameTest.php | 50 +++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 lib/internal/Magento/Framework/File/Name.php create mode 100644 lib/internal/Magento/Framework/Test/Unit/File/NameTest.php diff --git a/lib/internal/Magento/Framework/File/Name.php b/lib/internal/Magento/Framework/File/Name.php new file mode 100644 index 0000000000000..8c58dabb94c84 --- /dev/null +++ b/lib/internal/Magento/Framework/File/Name.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\File; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Validation\ValidationException; + +/** + * Class Name + * + * @package Magento\Framework\File + */ +class Name +{ + /** + * Get new file name if the given name is in use + * + * @param string $destinationFile + * @return string + */ + public function getNewFileName(string $destinationFile) + { + $fileInfo = $this->getPathInfo($destinationFile); + if ($this->fileExist($destinationFile)) { + $index = 1; + $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension']; + while (file_exists($fileInfo['dirname'] . '/' . $baseName)) { + $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension']; + $index++; + } + $destFileName = $baseName; + } else { + return $fileInfo['basename']; + } + + return $destFileName; + } + + /** + * Get the path information from a given file + * + * @param string $destinationFile + * @return string|string[] + */ + private function getPathInfo(string $destinationFile) + { + return pathinfo($destinationFile); + } + + /** + * Check to see if a given file exists + * + * @param string $destinationFile + * @return bool + */ + private function fileExist(string $destinationFile) + { + return file_exists($destinationFile); + } +} diff --git a/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php new file mode 100644 index 0000000000000..65c37524cbe73 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Test\Unit\File; + +use Magento\Framework\File\Name; + +class NameTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var string + */ + private $existingFilePath; + + /** + * @var string + */ + private $nonExistingFilePath; + + /** + * @var Name + */ + private $name; + + protected function setUp() + { + $this->name = new Name(); + $this->existingFilePath = __DIR__ . '/../_files/source.txt'; + $this->nonExistingFilePath = __DIR__ . '/../_files/file.txt'; + } + + /** + * @test + */ + public function testGetNewFileNameWhenFileExists() + { + $this->assertEquals('source_1.txt', $this->name->getNewFileName($this->existingFilePath)); + } + + /** + * @test + */ + public function testGetNewFileNameWhenFileDoesNotExist() + { + $this->assertEquals('file.txt', $this->name->getNewFileName($this->nonExistingFilePath)); + } +} From f2f88a4000505753ace90718928ee34e948b6ae5 Mon Sep 17 00:00:00 2001 From: David Manners <dmanners87@gmail.com> Date: Sat, 4 Apr 2020 16:16:36 +0200 Subject: [PATCH 0039/1013] Update the test cases to cover the case when multiple files with the same name are in the directory --- lib/internal/Magento/Framework/File/Name.php | 2 +- .../Framework/Test/Unit/File/NameTest.php | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/File/Name.php b/lib/internal/Magento/Framework/File/Name.php index 8c58dabb94c84..20ce5515a33d7 100644 --- a/lib/internal/Magento/Framework/File/Name.php +++ b/lib/internal/Magento/Framework/File/Name.php @@ -28,7 +28,7 @@ public function getNewFileName(string $destinationFile) if ($this->fileExist($destinationFile)) { $index = 1; $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension']; - while (file_exists($fileInfo['dirname'] . '/' . $baseName)) { + while ($this->fileExist($fileInfo['dirname'] . '/' . $baseName)) { $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension']; $index++; } diff --git a/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php index 65c37524cbe73..819996f159143 100644 --- a/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php @@ -20,6 +20,11 @@ class NameTest extends \PHPUnit\Framework\TestCase */ private $nonExistingFilePath; + /** + * @var string + */ + private $multipleExistingFilePath; + /** * @var Name */ @@ -29,17 +34,26 @@ protected function setUp() { $this->name = new Name(); $this->existingFilePath = __DIR__ . '/../_files/source.txt'; + $this->multipleExistingFilePath = __DIR__ . '/../_files/name.txt'; $this->nonExistingFilePath = __DIR__ . '/../_files/file.txt'; } /** * @test */ - public function testGetNewFileNameWhenFileExists() + public function testGetNewFileNameWhenOneFileExists() { $this->assertEquals('source_1.txt', $this->name->getNewFileName($this->existingFilePath)); } + /** + * @test + */ + public function testGetNewFileNameWhenTwoFileExists() + { + $this->assertEquals('name_2.txt', $this->name->getNewFileName($this->multipleExistingFilePath)); + } + /** * @test */ From 25a9bdf0bd3dc426132f20e6df5efcad21e5db34 Mon Sep 17 00:00:00 2001 From: David Manners <dmanners87@gmail.com> Date: Sat, 4 Apr 2020 16:24:42 +0200 Subject: [PATCH 0040/1013] Update the Catalog ImageUploader to use our new nameing class --- .../Magento/Catalog/Model/ImageUploader.php | 24 +++++++++++++------ .../Framework/Test/Unit/_files/name.txt | 1 + .../Framework/Test/Unit/_files/name_1.txt | 1 + 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 lib/internal/Magento/Framework/Test/Unit/_files/name.txt create mode 100644 lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index b0c8d56057431..102e23b9f6a6a 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -6,6 +6,8 @@ namespace Magento\Catalog\Model; use Magento\Framework\File\Uploader; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\Name; /** * Catalog image uploader @@ -74,17 +76,23 @@ class ImageUploader private $allowedMimeTypes; /** - * ImageUploader constructor + * @var \Magento\Framework\File\Name + */ + private $fileNameLookup; + + /** + * ImageUploader constructor. * * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Psr\Log\LoggerInterface $logger - * @param string $baseTmpPath - * @param string $basePath - * @param string[] $allowedExtensions - * @param string[] $allowedMimeTypes + * @param $baseTmpPath + * @param $basePath + * @param $allowedExtensions + * @param array $allowedMimeTypes + * @param Name|null $fileNameLookup */ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase, @@ -95,7 +103,8 @@ public function __construct( $baseTmpPath, $basePath, $allowedExtensions, - $allowedMimeTypes = [] + $allowedMimeTypes = [], + Name $fileNameLookup = null ) { $this->coreFileStorageDatabase = $coreFileStorageDatabase; $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); @@ -106,6 +115,7 @@ public function __construct( $this->basePath = $basePath; $this->allowedExtensions = $allowedExtensions; $this->allowedMimeTypes = $allowedMimeTypes; + $this->fileNameLookup = $fileNameLookup ?? ObjectManager::getInstance()->get(Name::class); } /** @@ -203,7 +213,7 @@ public function moveFileFromTmp($imageName, $returnRelativePath = false) $baseImagePath = $this->getFilePath( $basePath, - Uploader::getNewFileName( + $this->fileNameLookup->getNewFileName( $this->mediaDirectory->getAbsolutePath( $this->getFilePath($basePath, $imageName) ) diff --git a/lib/internal/Magento/Framework/Test/Unit/_files/name.txt b/lib/internal/Magento/Framework/Test/Unit/_files/name.txt new file mode 100644 index 0000000000000..1d3a06547c706 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/_files/name.txt @@ -0,0 +1 @@ +source \ No newline at end of file diff --git a/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt b/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt new file mode 100644 index 0000000000000..1d3a06547c706 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt @@ -0,0 +1 @@ +source \ No newline at end of file From 7a92462e8eae3d69bb44b50f4fc92c39d87a512d Mon Sep 17 00:00:00 2001 From: David Manners <dmanners87@gmail.com> Date: Sat, 4 Apr 2020 16:30:56 +0200 Subject: [PATCH 0041/1013] Revert the constructor comment and update the content of the name files to be 'name' --- app/code/Magento/Catalog/Model/ImageUploader.php | 10 +++++----- .../Magento/Framework/Test/Unit/_files/name.txt | 2 +- .../Magento/Framework/Test/Unit/_files/name_1.txt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index 102e23b9f6a6a..6ce0387825b55 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -88,10 +88,10 @@ class ImageUploader * @param \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Psr\Log\LoggerInterface $logger - * @param $baseTmpPath - * @param $basePath - * @param $allowedExtensions - * @param array $allowedMimeTypes + * @param string $baseTmpPath + * @param string $basePath + * @param string[] $allowedExtensions + * @param string[] $allowedMimeTypes * @param Name|null $fileNameLookup */ public function __construct( @@ -104,7 +104,7 @@ public function __construct( $basePath, $allowedExtensions, $allowedMimeTypes = [], - Name $fileNameLookup = null + Name $fileNameLookup = null ) { $this->coreFileStorageDatabase = $coreFileStorageDatabase; $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); diff --git a/lib/internal/Magento/Framework/Test/Unit/_files/name.txt b/lib/internal/Magento/Framework/Test/Unit/_files/name.txt index 1d3a06547c706..f121bdbff4df6 100644 --- a/lib/internal/Magento/Framework/Test/Unit/_files/name.txt +++ b/lib/internal/Magento/Framework/Test/Unit/_files/name.txt @@ -1 +1 @@ -source \ No newline at end of file +name diff --git a/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt b/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt index 1d3a06547c706..94972ff069874 100644 --- a/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt +++ b/lib/internal/Magento/Framework/Test/Unit/_files/name_1.txt @@ -1 +1 @@ -source \ No newline at end of file +name_1 From b1676da8bcccac57a0e1d207ab8b25e828f70b27 Mon Sep 17 00:00:00 2001 From: David Manners <dmanners87@gmail.com> Date: Sat, 4 Apr 2020 18:33:02 +0200 Subject: [PATCH 0042/1013] Update the class name to add a description and declare strict type --- lib/internal/Magento/Framework/File/Name.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/Magento/Framework/File/Name.php b/lib/internal/Magento/Framework/File/Name.php index 20ce5515a33d7..359719293f735 100644 --- a/lib/internal/Magento/Framework/File/Name.php +++ b/lib/internal/Magento/Framework/File/Name.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\File; use Magento\Framework\App\Filesystem\DirectoryList; @@ -10,9 +12,7 @@ use Magento\Framework\Validation\ValidationException; /** - * Class Name - * - * @package Magento\Framework\File + * Utility for generating a unique file name */ class Name { From 56b10a16d37751c518e5ded3fe34e4d6f9e8bb5b Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz <lukasz.bajsarowicz@gmail.com> Date: Thu, 21 May 2020 12:19:23 +0200 Subject: [PATCH 0043/1013] Change the type of Exception thrown when no Identifier provided --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index d579b3b222c53..7a51763176320 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -111,9 +111,7 @@ private function filterOutput(string $content): string private function getCmsBlock(): BlockInterface { if (!$this->getIdentifier()) { - throw new NoSuchEntityException( - __('Expected value of `identifier` was not provided') - ); + throw new \InvalidArgumentException('Expected value of `identifier` was not provided'); } if (null === $this->cmsBlock) { From 6e2f90bb4ae20d337eba85380560db5ac6d28d4b Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz <lukasz.bajsarowicz@gmail.com> Date: Thu, 21 May 2020 12:21:01 +0200 Subject: [PATCH 0044/1013] Adjust invalid Exception annotation --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 7a51763176320..caf7ca44261fe 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -106,7 +106,7 @@ private function filterOutput(string $content): string * Loads the CMS block by `identifier` provided as an argument * * @return BlockInterface|BlockModel - * @throws NoSuchEntityException + * @throws \InvalidArgumentException */ private function getCmsBlock(): BlockInterface { From 476a551b9f4cdb5691b3068718b6828bc6e106ef Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz <lukasz.bajsarowicz@gmail.com> Date: Thu, 21 May 2020 12:28:48 +0200 Subject: [PATCH 0045/1013] Add verification if Block is Enabled --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index caf7ca44261fe..464a002563323 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -107,6 +107,7 @@ private function filterOutput(string $content): string * * @return BlockInterface|BlockModel * @throws \InvalidArgumentException + * @throws NoSuchEntityException */ private function getCmsBlock(): BlockInterface { @@ -119,6 +120,12 @@ private function getCmsBlock(): BlockInterface (string)$this->getIdentifier(), $this->getCurrentStoreId() ); + + if (!$this->cmsBlock->isActive()) { + throw new NoSuchEntityException( + __('The CMS block with identifier "%identifier" is not enabled.', $this->getIdentifier()) + ); + } } return $this->cmsBlock; From 3a6f94a19c4dad23fd091da1ec8e1cf91c648080 Mon Sep 17 00:00:00 2001 From: Can YILDIRIM <mcanyildirim@gmail.com> Date: Sun, 24 May 2020 20:21:57 +0100 Subject: [PATCH 0046/1013] Graphql events.xml is added for quote submit succes to trigger sales email --- app/code/Magento/QuoteGraphQl/etc/graphql/events.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/code/Magento/QuoteGraphQl/etc/graphql/events.xml diff --git a/app/code/Magento/QuoteGraphQl/etc/graphql/events.xml b/app/code/Magento/QuoteGraphQl/etc/graphql/events.xml new file mode 100644 index 0000000000000..1e9822bbf3ef8 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/etc/graphql/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="sales_model_service_quote_submit_success"> + <observer name="sendEmail" instance="Magento\Quote\Observer\SubmitObserver" /> + </event> +</config> From d114bea5a9d50b36e8455689d0777a251be169a7 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz <lukasz.bajsarowicz@gmail.com> Date: Tue, 26 May 2020 22:43:27 +0200 Subject: [PATCH 0047/1013] Fix to Unit Tests after adjustments of `isActive` to implementation --- .../Test/Unit/Block/BlockByIdentifierTest.php | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php index 612d077110d9f..44b94b059cb6d 100644 --- a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -36,7 +36,6 @@ class BlockByIdentifierTest extends TestCase private const ASSERT_EMPTY_BLOCK_HTML = ''; private const ASSERT_CONTENT_HTML = self::STUB_CONTENT; - private const ASSERT_NO_CACHE_IDENTITIES = []; private const ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES = [ BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER, BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER . '_' . self::STUB_DEFAULT_STORE @@ -68,15 +67,18 @@ protected function setUp(): void $this->filterProviderMock->method('getBlockFilter')->willReturn($this->getPassthroughFilterMock()); } - public function testBlockReturnsEmptyStringWhenNoIdentifierProvided(): void + public function testBlockThrowsInvalidArgumentExceptionWhenNoIdentifierProvided(): void { // Given $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null); $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); // Expect - $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); - $this->assertSame(self::ASSERT_NO_CACHE_IDENTITIES, $missingIdentifierBlock->getIdentities()); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected value of `identifier` was not provided'); + + // When + $missingIdentifierBlock->toHtml(); } public function testBlockReturnsEmptyStringWhenIdentifierProvidedNotFound(): void @@ -119,6 +121,7 @@ public function testBlockCacheIdentitiesContainCmsBlockIdentities(): void // Given $cmsBlockMock = $this->createMock(Block::class); $cmsBlockMock->method('getId')->willReturn(self::STUB_CMS_BLOCK_ID); + $cmsBlockMock->method('isActive')->willReturn(true); $cmsBlockMock->method('getIdentifier')->willReturn(self::STUB_EXISTING_IDENTIFIER); $cmsBlockMock->method('getIdentities')->willReturn( [ @@ -173,15 +176,21 @@ private function getTestedBlockUsingIdentifier(?string $identifier): BlockByIden * @param int $entityId * @param string $identifier * @param string $content + * @param bool $isActive * @return MockObject|BlockInterface */ - private function getCmsBlockMock(int $entityId, string $identifier, string $content): BlockInterface - { + private function getCmsBlockMock( + int $entityId, + string $identifier, + string $content, + bool $isActive = true + ): BlockInterface { $cmsBlock = $this->createMock(BlockInterface::class); $cmsBlock->method('getId')->willReturn($entityId); $cmsBlock->method('getIdentifier')->willReturn($identifier); $cmsBlock->method('getContent')->willReturn($content); + $cmsBlock->method('isActive')->willReturn($isActive); return $cmsBlock; } From 336b463c36be7aa92389b2d66de8a56d553fb596 Mon Sep 17 00:00:00 2001 From: Gabriel Somoza <gabriel@strategery.io> Date: Wed, 27 May 2020 15:08:37 +0200 Subject: [PATCH 0048/1013] Add a head.additional block to adminhtml layout --- app/code/Magento/Backend/view/adminhtml/layout/default.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Backend/view/adminhtml/layout/default.xml b/app/code/Magento/Backend/view/adminhtml/layout/default.xml index a7faab0bc4673..2b086791c5523 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/default.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/default.xml @@ -17,6 +17,7 @@ <body> <attribute name="id" value="html-body"/> <block name="require.js" class="Magento\Backend\Block\Page\RequireJs" template="Magento_Backend::page/js/require_js.phtml"/> + <block class="Magento\Framework\View\Element\Text\ListText" name="head.additional"/> <referenceContainer name="global.notices"> <block class="Magento\Backend\Block\Page\Notices" name="global_notices" as="global_notices" template="Magento_Backend::page/notices.phtml"/> </referenceContainer> From 0a0c8e4e104de06c1f20a91a919f725b07a759a0 Mon Sep 17 00:00:00 2001 From: niravkrish <niravpatel5393@gmail.com> Date: Mon, 1 Jun 2020 21:20:52 +0530 Subject: [PATCH 0049/1013] resolve issue with filter visibility with column visibility --- .../view/adminhtml/ui_component/bundle_product_listing.xml | 2 +- .../Cms/view/adminhtml/ui_component/cms_block_listing.xml | 2 +- .../Cms/view/adminhtml/ui_component/cms_page_listing.xml | 2 +- .../ui_component/configurable_associated_product_listing.xml | 2 +- .../view/adminhtml/ui_component/grouped_product_listing.xml | 2 +- .../adminhtml/ui_component/sales_order_creditmemo_grid.xml | 2 +- .../Sales/view/adminhtml/ui_component/sales_order_grid.xml | 2 +- .../view/adminhtml/ui_component/sales_order_invoice_grid.xml | 2 +- .../view/adminhtml/ui_component/sales_order_shipment_grid.xml | 2 +- .../ui_component/sales_order_view_creditmemo_grid.xml | 2 +- .../adminhtml/ui_component/sales_order_view_invoice_grid.xml | 2 +- .../adminhtml/ui_component/sales_order_view_shipment_grid.xml | 2 +- .../view/adminhtml/ui_component/search_synonyms_grid.xml | 4 ++-- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml b/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml index b259e3280bfd5..d69196a61c59d 100644 --- a/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml +++ b/app/code/Magento/Bundle/view/adminhtml/ui_component/bundle_product_listing.xml @@ -58,7 +58,7 @@ <label translate="true">Status</label> <dataScope>status</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml index af54df24b64f5..332c316396122 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_block_listing.xml @@ -64,7 +64,7 @@ <label translate="true">Store View</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml index 846356adf9429..12c3e8287ecd8 100644 --- a/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml +++ b/app/code/Magento/Cms/view/adminhtml/ui_component/cms_page_listing.xml @@ -69,7 +69,7 @@ <label translate="true">Store View</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml b/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml index 2a40caaabae04..c23141d44a2ec 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/ui_component/configurable_associated_product_listing.xml @@ -58,7 +58,7 @@ <label translate="true">Status</label> <dataScope>status</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml b/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml index 831dc5a765dfb..becd7ca8079da 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml +++ b/app/code/Magento/GroupedProduct/view/adminhtml/ui_component/grouped_product_listing.xml @@ -58,7 +58,7 @@ <label translate="true">Status</label> <dataScope>status</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml index e0b7dae8fdb1a..1fc8d41ce0900 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_creditmemo_grid.xml @@ -42,7 +42,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml index e1f047b372c95..f4a257c1f43d7 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_grid.xml @@ -53,7 +53,7 @@ <label translate="true">Purchase Point</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml index 1e60e4a806fce..c88bc91a16641 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_invoice_grid.xml @@ -42,7 +42,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml index 9e02c31a20635..f6474b5db2fd8 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_shipment_grid.xml @@ -42,7 +42,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml index cf536c27a0ac3..09be15c5a3cf9 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_creditmemo_grid.xml @@ -51,7 +51,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml index ac1233c5e4961..4b6c8b3518e06 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_invoice_grid.xml @@ -51,7 +51,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml index 5f8ebde290664..8a11bc63a4318 100644 --- a/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml +++ b/app/code/Magento/Sales/view/adminhtml/ui_component/sales_order_view_shipment_grid.xml @@ -51,7 +51,7 @@ <label translate="true">Purchased From</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> diff --git a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml index 42ebf1454fb7e..c95604f0afa49 100644 --- a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml +++ b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_grid.xml @@ -65,7 +65,7 @@ <label translate="true">Store View</label> <dataScope>store_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> @@ -76,7 +76,7 @@ <label translate="true">Website</label> <dataScope>website_id</dataScope> <imports> - <link name="visible">ns = ${ $.ns }, componentType = column, index = ${ $.index }:visible</link> + <link name="visible">ns = ${ $.ns }, index = ${ $.index }:visible</link> </imports> </settings> </filterSelect> From cd4846bb1ce8eaa6dcf5568346c34686f429e4bd Mon Sep 17 00:00:00 2001 From: Serhii Voloshkov <serhii.voloshkov@transoftgroup.com> Date: Tue, 2 Jun 2020 11:40:35 +0300 Subject: [PATCH 0050/1013] MC-34569: Improve ACL for customer --- app/code/Magento/Customer/etc/webapi.xml | 2 +- .../Customer/Api/CustomerRepositoryTest.php | 179 ++++++++++++------ 2 files changed, 122 insertions(+), 59 deletions(-) diff --git a/app/code/Magento/Customer/etc/webapi.xml b/app/code/Magento/Customer/etc/webapi.xml index 38717619406aa..68c8da8744a05 100644 --- a/app/code/Magento/Customer/etc/webapi.xml +++ b/app/code/Magento/Customer/etc/webapi.xml @@ -227,7 +227,7 @@ <route url="/V1/customers/:customerId" method="DELETE"> <service class="Magento\Customer\Api\CustomerRepositoryInterface" method="deleteById"/> <resources> - <resource ref="Magento_Customer::manage"/> + <resource ref="Magento_Customer::delete"/> </resources> </route> <route url="/V1/customers/isEmailAvailable" method="POST"> 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..2e23dcdddd05e 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -8,13 +8,23 @@ use Magento\Customer\Api\Data\CustomerInterface as Customer; use Magento\Customer\Api\Data\AddressInterface as Address; +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\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SortOrder; +use Magento\Framework\Api\SortOrderBuilder; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Integration\Api\IntegrationServiceInterface; +use Magento\Integration\Api\OauthServiceInterface; +use Magento\Integration\Model\Integration; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Customer as CustomerHelper; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -94,34 +104,20 @@ class CustomerRepositoryTest extends WebapiAbstract */ protected function setUp(): void { - $this->customerRegistry = Bootstrap::getObjectManager()->get( - \Magento\Customer\Model\CustomerRegistry::class - ); + $this->customerRegistry = Bootstrap::getObjectManager()->get(CustomerRegistry::class); $this->customerRepository = Bootstrap::getObjectManager()->get( - \Magento\Customer\Api\CustomerRepositoryInterface::class, + CustomerRepositoryInterface::class, ['customerRegistry' => $this->customerRegistry] ); - $this->dataObjectHelper = Bootstrap::getObjectManager()->create( - \Magento\Framework\Api\DataObjectHelper::class - ); - $this->customerDataFactory = Bootstrap::getObjectManager()->create( - \Magento\Customer\Api\Data\CustomerInterfaceFactory::class - ); - $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->create( - \Magento\Framework\Api\SearchCriteriaBuilder::class - ); - $this->sortOrderBuilder = Bootstrap::getObjectManager()->create( - \Magento\Framework\Api\SortOrderBuilder::class - ); - $this->filterGroupBuilder = Bootstrap::getObjectManager()->create( - \Magento\Framework\Api\Search\FilterGroupBuilder::class - ); + $this->dataObjectHelper = Bootstrap::getObjectManager()->create(DataObjectHelper::class); + $this->customerDataFactory = Bootstrap::getObjectManager()->create(CustomerInterfaceFactory::class); + $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->create(SearchCriteriaBuilder::class); + $this->sortOrderBuilder = Bootstrap::getObjectManager()->create(SortOrderBuilder::class); + $this->filterGroupBuilder = Bootstrap::getObjectManager()->create(FilterGroupBuilder::class); $this->customerHelper = new CustomerHelper(); - $this->dataObjectProcessor = Bootstrap::getObjectManager()->create( - \Magento\Framework\Reflection\DataObjectProcessor::class - ); + $this->dataObjectProcessor = Bootstrap::getObjectManager()->create(DataObjectProcessor::class); } protected function tearDown(): void @@ -131,7 +127,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, @@ -161,10 +157,7 @@ public function testInvalidCustomerUpdate() // get customer ID token /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ - //$customerTokenService = $this->objectManager->create(CustomerTokenServiceInterface::class); - $customerTokenService = Bootstrap::getObjectManager()->create( - \Magento\Integration\Api\CustomerTokenServiceInterface::class - ); + $customerTokenService = Bootstrap::getObjectManager()->create(CustomerTokenServiceInterface::class); $token = $customerTokenService->createCustomerAccessToken($firstCustomerData[Customer::EMAIL], 'test@123'); //Create second customer and update lastname. @@ -176,13 +169,13 @@ public function testInvalidCustomerUpdate() $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, 'token' => $token, ], 'soap' => [ @@ -195,12 +188,37 @@ public function testInvalidCustomerUpdate() $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; $this->_webApiCall($serviceInfo, $requestData); } + /** + * Create Integration and return token. + * + * @param string $name + * @param array $resource + * @return string + */ + private function createIntegrationToken(string $name, array $resource): string + { + /** @var IntegrationServiceInterface $integrationService */ + $integrationService = Bootstrap::getObjectManager()->get(IntegrationServiceInterface::class); + $oauthService = Bootstrap::getObjectManager()->get(OauthServiceInterface::class); + /** @var Integration $integration */ + $integration = $integrationService->create( + [ + 'name' => $name, + 'resource' => $resource, + ] + ); + /** @var OauthServiceInterface $oauthService */ + $oauthService->createAccessToken($integration->getConsumerId()); + + return $integrationService->get($integration->getId())->getToken(); + } + public function testDeleteCustomer() { $customerData = $this->_createCustomer(); @@ -209,7 +227,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, @@ -226,18 +244,63 @@ public function testDeleteCustomer() $this->assertTrue($response); //Verify if the customer is deleted - $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); + $this->expectException(NoSuchEntityException::class); $this->expectExceptionMessage(sprintf("No such entity with customerId = %s", $customerData[Customer::ID])); $this->_getCustomerData($customerData[Customer::ID]); } + /** + * Check that non authorized consumer can`t delete customer. + * + * @return void + */ + public function testDeleteCustomerNonAuthorized(): void + { + $resource = [ + 'Magento_Customer::customer', + 'Magento_Customer::manage', + ]; + $token = $this->createIntegrationToken('TestAPI' . bin2hex(random_bytes(5)), $resource); + + $customerData = $this->_createCustomer(); + $this->currentCustomerId = []; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $customerData[Customer::ID], + 'httpMethod' => Request::HTTP_METHOD_DELETE, + 'token' => $token, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'DeleteById', + 'token' => $token, + ], + ]; + try { + $this->_webApiCall($serviceInfo, ['customerId' => $customerData['id']]); + $this->fail("Expected exception is not thrown."); + } catch (\SoapFault $e) { + } catch (\Exception $e) { + $expectedMessage = 'The consumer isn\'t authorized to access %resources.'; + $errorObj = $this->processRestExceptionResult($e); + $this->assertEquals($expectedMessage, $errorObj['message']); + $this->assertEquals(['resources' => 'Magento_Customer::delete'], $errorObj['parameters']); + $this->assertEquals(HTTPExceptionCodes::HTTP_UNAUTHORIZED, $e->getCode()); + } + /** @var Customer $data */ + $data = $this->_getCustomerData($customerData[Customer::ID]); + $this->assertNotNull($data->getId()); + } + public function testDeleteCustomerInvalidCustomerId() { $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, @@ -276,13 +339,13 @@ public function testUpdateCustomer() $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, @@ -292,7 +355,7 @@ public function testUpdateCustomer() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; $response = $this->_webApiCall($serviceInfo, $requestData); @@ -316,13 +379,13 @@ public function testUpdateCustomerNoWebsiteId() $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,7 +395,7 @@ public function testUpdateCustomerNoWebsiteId() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); unset($newCustomerDataObject['website_id']); $requestData = ['customer' => $newCustomerDataObject]; @@ -367,13 +430,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 +446,7 @@ public function testUpdateCustomerException() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; @@ -413,7 +476,7 @@ public function testCreateCustomerWithoutAddressRequiresException() { $customerDataArray = $this->dataObjectProcessor->buildOutputDataArray( $this->customerHelper->createSampleCustomerDataObject(), - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); foreach ($customerDataArray[Customer::KEY_ADDRESSES] as & $address) { @@ -423,7 +486,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, @@ -557,7 +620,7 @@ public function subscriptionDataProvider(): array public function testSearchCustomersUsingGET() { $this->_markTestAsRestOnly('SOAP test is covered in testSearchCustomers'); - $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); + $builder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $customerData = $this->_createCustomer(); $filter = $builder ->setField(Customer::EMAIL) @@ -571,7 +634,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 +651,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 { @@ -611,7 +674,7 @@ public function testSearchCustomersUsingGETEmptyFilter() */ public function testSearchCustomersMultipleFiltersWithSort() { - $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); + $builder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $customerData1 = $this->_createCustomer(); $customerData2 = $this->_createCustomer(); $filter1 = $builder->setField(Customer::EMAIL) @@ -628,7 +691,7 @@ public function testSearchCustomersMultipleFiltersWithSort() /**@var \Magento\Framework\Api\SortOrderBuilder $sortOrderBuilder */ $sortOrderBuilder = Bootstrap::getObjectManager()->create( - \Magento\Framework\Api\SortOrderBuilder::class + SortOrderBuilder::class ); /** @var SortOrder $sortOrder */ $sortOrder = $sortOrderBuilder->setField(Customer::EMAIL)->setDirection(SortOrder::SORT_ASC)->create(); @@ -640,7 +703,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, @@ -660,7 +723,7 @@ public function testSearchCustomersMultipleFiltersWithSort() public function testSearchCustomersMultipleFiltersWithSortUsingGET() { $this->_markTestAsRestOnly('SOAP test is covered in testSearchCustomers'); - $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); + $builder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $customerData1 = $this->_createCustomer(); $customerData2 = $this->_createCustomer(); $filter1 = $builder->setField(Customer::EMAIL) @@ -682,7 +745,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); @@ -696,7 +759,7 @@ public function testSearchCustomersMultipleFiltersWithSortUsingGET() */ public function testSearchCustomersNonExistentMultipleFilters() { - $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); + $builder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $customerData1 = $this->_createCustomer(); $customerData2 = $this->_createCustomer(); $filter1 = $filter1 = $builder->setField(Customer::EMAIL) @@ -716,7 +779,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, @@ -734,7 +797,7 @@ public function testSearchCustomersNonExistentMultipleFilters() public function testSearchCustomersNonExistentMultipleFiltersGET() { $this->_markTestAsRestOnly('SOAP test is covered in testSearchCustomers'); - $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); + $builder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $customerData1 = $this->_createCustomer(); $customerData2 = $this->_createCustomer(); $filter1 = $filter1 = $builder->setField(Customer::EMAIL) @@ -755,7 +818,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); @@ -770,7 +833,7 @@ public function testSearchCustomersMultipleFilterGroups() $customerData1 = $this->_createCustomer(); /** @var \Magento\Framework\Api\FilterBuilder $builder */ - $builder = Bootstrap::getObjectManager()->create(\Magento\Framework\Api\FilterBuilder::class); + $builder = Bootstrap::getObjectManager()->create(FilterBuilder::class); $filter1 = $builder->setField(Customer::EMAIL) ->setValue($customerData1[Customer::EMAIL]) ->create(); @@ -793,7 +856,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, From 2d67fdd63bf5bec6ce2267660003b9d660e61aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= <jhuenig@maxcluster.de> Date: Thu, 4 Jun 2020 15:11:54 +0200 Subject: [PATCH 0051/1013] Rewrote Magento\Framework\DB\Adapter\Pdo\Mysql->isTableExists * Use INFORMATION_SCHEMA.TABLES instead of "SHOW TABLE STATUS" * Extend DDL cache for table existence and use it in isTableExists --- .../Framework/DB/Adapter/Pdo/Mysql.php | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 7db91c06d9649..b8be0a53fe4d3 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -55,6 +55,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface const DDL_CREATE = 2; const DDL_INDEX = 3; const DDL_FOREIGN_KEY = 4; + const DDL_EXISTS = 5; const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; @@ -1630,7 +1631,7 @@ public function resetDdlCache($tableName = null, $schemaName = null) } else { $cacheKey = $this->_getTableName($tableName, $schemaName); - $ddlTypes = [self::DDL_DESCRIBE, self::DDL_CREATE, self::DDL_INDEX, self::DDL_FOREIGN_KEY]; + $ddlTypes = [self::DDL_DESCRIBE, self::DDL_CREATE, self::DDL_INDEX, self::DDL_FOREIGN_KEY, self::DDL_EXISTS]; foreach ($ddlTypes as $ddlType) { unset($this->_ddlCache[$ddlType][$cacheKey]); } @@ -2657,7 +2658,29 @@ public function truncateTable($tableName, $schemaName = null) */ public function isTableExists($tableName, $schemaName = null) { - return $this->showTableStatus($tableName, $schemaName) !== false; + $cacheKey = $this->_getTableName($tableName, $schemaName); + + $ddl = $this->loadDdlCache($cacheKey, self::DDL_EXISTS); + if ($ddl !== false) { + return true; + } + + $fromDbName = 'DATABASE()'; + if ($schemaName !== null) { + $fromDbName = $this->quote($schemaName); + } + + $sql = sprintf('SELECT COUNT(1) AS tbl_exists FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = %s AND TABLE_SCHEMA = %s', + $this->quote($tableName), + $fromDbName + ); + $ddl = $this->rawFetchRow($sql, 'tbl_exists'); + if ($ddl) { + $this->saveDdlCache($cacheKey, self::DDL_EXISTS, $ddl); + return true; + } + + return false; } /** From 8db94303d32b4cfd9a6851946a19a6cbff30ad94 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@gmail.com> Date: Thu, 4 Jun 2020 16:15:57 +0300 Subject: [PATCH 0052/1013] MC-34729: Integration ACL changes --- app/code/Magento/Cms/etc/webapi.xml | 6 +++--- .../testsuite/Magento/Cms/Api/PageRepositoryTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Cms/etc/webapi.xml b/app/code/Magento/Cms/etc/webapi.xml index 5b66d0e3ed879..464f5146e6358 100644 --- a/app/code/Magento/Cms/etc/webapi.xml +++ b/app/code/Magento/Cms/etc/webapi.xml @@ -23,19 +23,19 @@ <route url="/V1/cmsPage" method="POST"> <service class="Magento\Cms\Api\PageRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Cms::page"/> + <resource ref="Magento_Cms::save"/> </resources> </route> <route url="/V1/cmsPage/:id" method="PUT"> <service class="Magento\Cms\Api\PageRepositoryInterface" method="save"/> <resources> - <resource ref="Magento_Cms::page"/> + <resource ref="Magento_Cms::save"/> </resources> </route> <route url="/V1/cmsPage/:pageId" method="DELETE"> <service class="Magento\Cms\Api\PageRepositoryInterface" method="deleteById"/> <resources> - <resource ref="Magento_Cms::page"/> + <resource ref="Magento_Cms::page_delete"/> </resources> </route> <!-- Cms Block --> diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index 757530c4da693..98bda8d60dac1 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -499,7 +499,7 @@ public function testSaveDesign(): void /** @var Rules $rules */ $rules = $this->rulesFactory->create(); $rules->setRoleId($role->getId()); - $rules->setResources(['Magento_Cms::page']); + $rules->setResources(['Magento_Cms::save']); $rules->saveRel(); //Using the admin user with custom role. $token = $this->adminTokens->createAdminAccessToken( @@ -549,7 +549,7 @@ public function testSaveDesign(): void /** @var Rules $rules */ $rules = Bootstrap::getObjectManager()->create(Rules::class); $rules->setRoleId($role->getId()); - $rules->setResources(['Magento_Cms::page', 'Magento_Cms::save_design']); + $rules->setResources(['Magento_Cms::save', 'Magento_Cms::save_design']); $rules->saveRel(); //Making the same request with design settings. $result = $this->_webApiCall($serviceInfo, $requestData); @@ -564,7 +564,7 @@ public function testSaveDesign(): void /** @var Rules $rules */ $rules = Bootstrap::getObjectManager()->create(Rules::class); $rules->setRoleId($role->getId()); - $rules->setResources(['Magento_Cms::page']); + $rules->setResources(['Magento_Cms::save']); $rules->saveRel(); //Updating the page but with the same design properties values. $result = $this->_webApiCall($serviceInfo, $requestData); From c77a8f555a93572d0eb5e45cf083427912d0fb64 Mon Sep 17 00:00:00 2001 From: Sebastian Lechte <leeps@users.noreply.github.com> Date: Thu, 4 Jun 2020 19:15:41 +0200 Subject: [PATCH 0053/1013] Conform to Codestyle Guidelines * Update Magento\Framework\DB\Adapter\Pdo\Mysql->isTableExists() to conform to code style guidelines (multi-line function calls, line length) --- lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index b8be0a53fe4d3..ba3764e8a26ac 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -2670,7 +2670,8 @@ public function isTableExists($tableName, $schemaName = null) $fromDbName = $this->quote($schemaName); } - $sql = sprintf('SELECT COUNT(1) AS tbl_exists FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = %s AND TABLE_SCHEMA = %s', + $sql = sprintf( + 'SELECT COUNT(1) AS tbl_exists FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = %s AND TABLE_SCHEMA = %s', $this->quote($tableName), $fromDbName ); From 4876d4e0d55385970afe9342397ea22691c916f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= <jhuenig@maxcluster.de> Date: Thu, 4 Jun 2020 19:59:21 +0200 Subject: [PATCH 0054/1013] Update Magento\Framework\DB\Adapter\Pdo\Mysql->resetDdlCache() to conform to code style guidelines (line length) --- lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index ba3764e8a26ac..04b88c9a3ed50 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -1631,7 +1631,13 @@ public function resetDdlCache($tableName = null, $schemaName = null) } else { $cacheKey = $this->_getTableName($tableName, $schemaName); - $ddlTypes = [self::DDL_DESCRIBE, self::DDL_CREATE, self::DDL_INDEX, self::DDL_FOREIGN_KEY, self::DDL_EXISTS]; + $ddlTypes = [ + self::DDL_DESCRIBE, + self::DDL_CREATE, + self::DDL_INDEX, + self::DDL_FOREIGN_KEY, + self::DDL_EXISTS + ]; foreach ($ddlTypes as $ddlType) { unset($this->_ddlCache[$ddlType][$cacheKey]); } From 86fb80c715c38a91b3352daee96796b459c0dc2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MARTINEZ?= <frederic.martinez@ph2m.com> Date: Fri, 5 Jun 2020 17:41:53 +0200 Subject: [PATCH 0055/1013] #274 Admin Global Search: Search categories --- .../CatalogSearch/Model/Search/Category.php | 121 ++++++++++++++++++ app/code/Magento/CatalogSearch/etc/di.xml | 4 + 2 files changed, 125 insertions(+) create mode 100644 app/code/Magento/CatalogSearch/Model/Search/Category.php diff --git a/app/code/Magento/CatalogSearch/Model/Search/Category.php b/app/code/Magento/CatalogSearch/Model/Search/Category.php new file mode 100644 index 0000000000000..2deee1027da74 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Model/Search/Category.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\CatalogSearch\Model\Search; +/** + * Search model for backend search + * + * @method Category setQuery(string $query) + * @method string|null getQuery() + * @method bool hasQuery() + * @method Category setStart(int $startPosition) + * @method int|null getStart() + * @method bool hasStart() + * @method Category setLimit(int $limit) + * @method int|null getLimit() + * @method bool hasLimit() + * @method Category setResults(array $results) + * @method array getResults() + * @api + */ +class Category extends \Magento\Framework\DataObject +{ + /** + * Adminhtml data + * + * @var \Magento\Backend\Helper\Data + */ + protected $_adminhtmlData = null; + + /** + * @var \Magento\Catalog\Api\CategoryListInterface + */ + protected $categoryRepository; + + /** + * @var \Magento\Framework\Api\SearchCriteriaBuilder + */ + protected $searchCriteriaBuilder; + + /** + * @var \Magento\Framework\Api\FilterBuilder + */ + protected $filterBuilder; + + /** + * Magento string lib + * + * @var \Magento\Framework\Stdlib\StringUtils + */ + protected $string; + + /** + * Initialize dependencies. + * + * @param \Magento\Backend\Helper\Data $adminhtmlData + * @param \Magento\Catalog\Api\CategoryListInterface $categoryRepository + * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\Api\FilterBuilder $filterBuilder + * @param \Magento\Framework\Stdlib\StringUtils $string + */ + public function __construct( + \Magento\Backend\Helper\Data $adminhtmlData, + \Magento\Catalog\Api\CategoryListInterface $categoryRepository, + \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, + \Magento\Framework\Api\FilterBuilder $filterBuilder, + \Magento\Framework\Stdlib\StringUtils $string + ) + { + $this->_adminhtmlData = $adminhtmlData; + $this->categoryRepository = $categoryRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterBuilder = $filterBuilder; + $this->string = $string; + } + + /** + * Load search results + * + * @return $this + */ + public function load() + { + $result = []; + if (!$this->hasStart() || !$this->hasLimit() || !$this->hasQuery()) { + $this->setResults($result); + return $this; + } + + $this->searchCriteriaBuilder->setCurrentPage($this->getStart()); + $this->searchCriteriaBuilder->setPageSize($this->getLimit()); + $searchFields = ['name']; + + $filters = []; + foreach ($searchFields as $field) { + $filters[] = $this->filterBuilder + ->setField($field) + ->setConditionType('like') + ->setValue('%' . $this->getQuery() . '%') + ->create(); + } + $this->searchCriteriaBuilder->addFilters($filters); + + $searchCriteria = $this->searchCriteriaBuilder->create(); + $searchResults = $this->categoryRepository->getList($searchCriteria); + + foreach ($searchResults->getItems() as $category) { + $description = strip_tags($category->getDescription()); + $result[] = [ + 'id' => 'category/1/' . $category->getId(), + 'type' => __('Category'), + 'name' => $category->getName(), + 'description' => $this->string->substr($description, 0, 30), + 'url' => $this->_adminhtmlData->getUrl('catalog/category/edit', ['id' => $category->getId()]), + ]; + } + $this->setResults($result); + return $this; + } +} diff --git a/app/code/Magento/CatalogSearch/etc/di.xml b/app/code/Magento/CatalogSearch/etc/di.xml index 6ff9119e78c2a..f8e2a262d73ca 100644 --- a/app/code/Magento/CatalogSearch/etc/di.xml +++ b/app/code/Magento/CatalogSearch/etc/di.xml @@ -66,6 +66,10 @@ <item name="class" xsi:type="string">Magento\CatalogSearch\Model\Search\Catalog</item> <item name="acl" xsi:type="string">Magento_Catalog::catalog</item> </item> + <item name="categories" xsi:type="array"> + <item name="class" xsi:type="string">Magento\CatalogSearch\Model\Search\Category</item> + <item name="acl" xsi:type="string">Magento_Catalog::categories</item> + </item> </argument> </arguments> </type> From 1ac6d641e6b6df3108bd369f0f65dcbd23ae37ee Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Fri, 5 Jun 2020 16:19:56 -0500 Subject: [PATCH 0056/1013] MC-34467: Updated jQuery File Upload plugin - Updated jquery File Upload plugin --- .../Theme/view/base/requirejs-config.js | 2 +- .../User/view/adminhtml/web/app-config.js | 2 +- lib/web/jquery/fileUploader/canvas-to-blob.js | 1 - .../cors/jquery.postmessage-transport.js | 211 +- .../fileUploader/cors/jquery.xdr-transport.js | 152 +- .../css/jquery.fileupload-noscript.css | 22 + .../css/jquery.fileupload-ui-noscript.css | 17 + .../fileUploader/css/jquery.fileupload-ui.css | 95 +- .../fileUploader/css/jquery.fileupload.css | 36 + lib/web/jquery/fileUploader/img/loading.gif | Bin 3796 -> 3897 bytes .../jquery/fileUploader/img/progressbar.gif | Bin 3364 -> 3323 bytes .../fileUploader/jquery.fileupload-audio.js | 101 + .../fileUploader/jquery.fileupload-fp.js | 219 -- .../fileUploader/jquery.fileupload-image.js | 355 +++ .../fileUploader/jquery.fileupload-process.js | 170 ++ .../fileUploader/jquery.fileupload-ui.js | 1460 +++++----- .../jquery.fileupload-validate.js | 119 + .../fileUploader/jquery.fileupload-video.js | 101 + .../jquery/fileUploader/jquery.fileupload.js | 2557 ++++++++++------- .../fileUploader/jquery.iframe-transport.js | 365 ++- lib/web/jquery/fileUploader/load-image.js | 1 - lib/web/jquery/fileUploader/locale.js | 29 - lib/web/jquery/fileUploader/main.js | 78 - .../fileUploader/vendor/jquery.ui.widget.js | 1082 +++++-- 24 files changed, 4452 insertions(+), 2723 deletions(-) delete mode 100644 lib/web/jquery/fileUploader/canvas-to-blob.js create mode 100644 lib/web/jquery/fileUploader/css/jquery.fileupload-noscript.css create mode 100644 lib/web/jquery/fileUploader/css/jquery.fileupload-ui-noscript.css create mode 100644 lib/web/jquery/fileUploader/css/jquery.fileupload.css create mode 100644 lib/web/jquery/fileUploader/jquery.fileupload-audio.js delete mode 100644 lib/web/jquery/fileUploader/jquery.fileupload-fp.js create mode 100644 lib/web/jquery/fileUploader/jquery.fileupload-image.js create mode 100644 lib/web/jquery/fileUploader/jquery.fileupload-process.js create mode 100644 lib/web/jquery/fileUploader/jquery.fileupload-validate.js create mode 100644 lib/web/jquery/fileUploader/jquery.fileupload-video.js delete mode 100644 lib/web/jquery/fileUploader/load-image.js delete mode 100644 lib/web/jquery/fileUploader/locale.js delete mode 100644 lib/web/jquery/fileUploader/main.js diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index f5580461f7d9e..77af920c8df86 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -31,7 +31,7 @@ var config = { 'paths': { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-fp', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', 'prototype': 'legacy-build.min', 'jquery/jquery-storageapi': 'jquery/jquery.storageapi.min', 'text': 'mage/requirejs/text', diff --git a/app/code/Magento/User/view/adminhtml/web/app-config.js b/app/code/Magento/User/view/adminhtml/web/app-config.js index 6387bec03ea90..491378d933ca2 100644 --- a/app/code/Magento/User/view/adminhtml/web/app-config.js +++ b/app/code/Magento/User/view/adminhtml/web/app-config.js @@ -26,7 +26,7 @@ require.config({ 'jquery/ui': 'jquery/jquery-ui-1.9.2', 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-fp', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', 'prototype': 'prototype/prototype-amd', 'text': 'requirejs/text', 'domReady': 'requirejs/domReady', diff --git a/lib/web/jquery/fileUploader/canvas-to-blob.js b/lib/web/jquery/fileUploader/canvas-to-blob.js deleted file mode 100644 index 4e855b9f3a592..0000000000000 --- a/lib/web/jquery/fileUploader/canvas-to-blob.js +++ /dev/null @@ -1 +0,0 @@ -(function(a){"use strict";var b=a.HTMLCanvasElement&&a.HTMLCanvasElement.prototype,c=a.Blob&&function(){try{return Boolean(new Blob)}catch(a){return!1}}(),d=c&&a.Uint8Array&&function(){try{return(new Blob([new Uint8Array(100)])).size===100}catch(a){return!1}}(),e=a.BlobBuilder||a.WebKitBlobBuilder||a.MozBlobBuilder||a.MSBlobBuilder,f=(c||e)&&a.atob&&a.ArrayBuffer&&a.Uint8Array&&function(a){var b,f,g,h,i,j;a.split(",")[0].indexOf("base64")>=0?b=atob(a.split(",")[1]):b=decodeURIComponent(a.split(",")[1]),f=new ArrayBuffer(b.length),g=new Uint8Array(f);for(h=0;h<b.length;h+=1)g[h]=b.charCodeAt(h);return i=a.split(",")[0].split(":")[1].split(";")[0],c?new Blob([d?g:f],{type:i}):(j=new e,j.append(f),j.getBlob(i))};a.HTMLCanvasElement&&!b.toBlob&&(b.mozGetAsFile?b.toBlob=function(a,b){a(this.mozGetAsFile("blob",b))}:b.toDataURL&&f&&(b.toBlob=function(a,b){a(f(this.toDataURL(b)))})),typeof define=="function"&&define.amd?define(function(){return f}):a.dataURLtoBlob=f})(this); \ No newline at end of file diff --git a/lib/web/jquery/fileUploader/cors/jquery.postmessage-transport.js b/lib/web/jquery/fileUploader/cors/jquery.postmessage-transport.js index 931b6352ba27d..5d5cc2f8d27c2 100644 --- a/lib/web/jquery/fileUploader/cors/jquery.postmessage-transport.js +++ b/lib/web/jquery/fileUploader/cors/jquery.postmessage-transport.js @@ -1,117 +1,126 @@ /* - * jQuery postMessage Transport Plugin 1.1 + * jQuery postMessage Transport Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT + * https://opensource.org/licenses/MIT */ -/*jslint unparam: true, nomen: true */ -/*global define, window, document */ +/* global define, require */ (function (factory) { - 'use strict'; - if (typeof define === 'function' && define.amd) { - // Register as an anonymous AMD module: - define(['jquery'], factory); - } else { - // Browser globals: - factory(window.jQuery); - } -}(function ($) { - 'use strict'; + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +})(function ($) { + 'use strict'; - var counter = 0, - names = [ - 'accepts', - 'cache', - 'contents', - 'contentType', - 'crossDomain', - 'data', - 'dataType', - 'headers', - 'ifModified', - 'mimeType', - 'password', - 'processData', - 'timeout', - 'traditional', - 'type', - 'url', - 'username' - ], - convert = function (p) { - return p; - }; + var counter = 0, + names = [ + 'accepts', + 'cache', + 'contents', + 'contentType', + 'crossDomain', + 'data', + 'dataType', + 'headers', + 'ifModified', + 'mimeType', + 'password', + 'processData', + 'timeout', + 'traditional', + 'type', + 'url', + 'username' + ], + convert = function (p) { + return p; + }; - $.ajaxSetup({ - converters: { - 'postmessage text': convert, - 'postmessage json': convert, - 'postmessage html': convert - } - }); + $.ajaxSetup({ + converters: { + 'postmessage text': convert, + 'postmessage json': convert, + 'postmessage html': convert + } + }); - $.ajaxTransport('postmessage', function (options) { - if (options.postMessage && window.postMessage) { - var iframe, - loc = $('<a>').prop('href', options.postMessage)[0], - target = loc.protocol + '//' + loc.host, - xhrUpload = options.xhr().upload; - return { - send: function (_, completeCallback) { - var message = { - id: 'postmessage-transport-' + (counter += 1) - }, - eventName = 'message.' + message.id; - iframe = $( - '<iframe style="display:none;" src="' + - options.postMessage + '" name="' + - message.id + '"></iframe>' - ).bind('load', function () { - $.each(names, function (i, name) { - message[name] = options[name]; - }); - message.dataType = message.dataType.replace('postmessage ', ''); - $(window).bind(eventName, function (e) { - e = e.originalEvent; - var data = e.data, - ev; - if (e.origin === target && data.id === message.id) { - if (data.type === 'progress') { - ev = document.createEvent('Event'); - ev.initEvent(data.type, false, true); - $.extend(ev, data); - xhrUpload.dispatchEvent(ev); - } else { - completeCallback( - data.status, - data.statusText, - {postmessage: data.result}, - data.headers - ); - iframe.remove(); - $(window).unbind(eventName); - } - } - }); - iframe[0].contentWindow.postMessage( - message, - target - ); - }).appendTo(document.body); - }, - abort: function () { - if (iframe) { - iframe.remove(); - } + $.ajaxTransport('postmessage', function (options) { + if (options.postMessage && window.postMessage) { + var iframe, + loc = $('<a></a>').prop('href', options.postMessage)[0], + target = loc.protocol + '//' + loc.host, + xhrUpload = options.xhr().upload; + // IE always includes the port for the host property of a link + // element, but not in the location.host or origin property for the + // default http port 80 and https port 443, so we strip it: + if (/^(http:\/\/.+:80)|(https:\/\/.+:443)$/.test(target)) { + target = target.replace(/:(80|443)$/, ''); + } + return { + send: function (_, completeCallback) { + counter += 1; + var message = { + id: 'postmessage-transport-' + counter + }, + eventName = 'message.' + message.id; + iframe = $( + '<iframe style="display:none;" src="' + + options.postMessage + + '" name="' + + message.id + + '"></iframe>' + ) + .on('load', function () { + $.each(names, function (i, name) { + message[name] = options[name]; + }); + message.dataType = message.dataType.replace('postmessage ', ''); + $(window).on(eventName, function (event) { + var e = event.originalEvent; + var data = e.data; + var ev; + if (e.origin === target && data.id === message.id) { + if (data.type === 'progress') { + ev = document.createEvent('Event'); + ev.initEvent(data.type, false, true); + $.extend(ev, data); + xhrUpload.dispatchEvent(ev); + } else { + completeCallback( + data.status, + data.statusText, + { postmessage: data.result }, + data.headers + ); + iframe.remove(); + $(window).off(eventName); + } } - }; + }); + iframe[0].contentWindow.postMessage(message, target); + }) + .appendTo(document.body); + }, + abort: function () { + if (iframe) { + iframe.remove(); + } } - }); - -})); + }; + } + }); +}); diff --git a/lib/web/jquery/fileUploader/cors/jquery.xdr-transport.js b/lib/web/jquery/fileUploader/cors/jquery.xdr-transport.js index c42c54828d8ff..9e81860b943fc 100644 --- a/lib/web/jquery/fileUploader/cors/jquery.xdr-transport.js +++ b/lib/web/jquery/fileUploader/cors/jquery.xdr-transport.js @@ -1,85 +1,97 @@ /* - * jQuery XDomainRequest Transport Plugin 1.1.2 + * jQuery XDomainRequest Transport Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT + * https://opensource.org/licenses/MIT * * Based on Julian Aubourg's ajaxHooks xdr.js: * https://github.com/jaubourg/ajaxHooks/ */ -/*jslint unparam: true */ -/*global define, window, XDomainRequest */ +/* global define, require, XDomainRequest */ (function (factory) { - 'use strict'; - if (typeof define === 'function' && define.amd) { - // Register as an anonymous AMD module: - define(['jquery'], factory); - } else { - // Browser globals: - factory(window.jQuery); - } -}(function ($) { - 'use strict'; - if (window.XDomainRequest && !$.support.cors) { - $.ajaxTransport(function (s) { - if (s.crossDomain && s.async) { - if (s.timeout) { - s.xdrTimeout = s.timeout; - delete s.timeout; - } - var xdr; - return { - send: function (headers, completeCallback) { - function callback(status, statusText, responses, responseHeaders) { - xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; - xdr = null; - completeCallback(status, statusText, responses, responseHeaders); - } - xdr = new XDomainRequest(); - // XDomainRequest only supports GET and POST: - if (s.type === 'DELETE') { - s.url = s.url + (/\?/.test(s.url) ? '&' : '?') + - '_method=DELETE'; - s.type = 'POST'; - } else if (s.type === 'PUT') { - s.url = s.url + (/\?/.test(s.url) ? '&' : '?') + - '_method=PUT'; - s.type = 'POST'; - } - xdr.open(s.type, s.url); - xdr.onload = function () { - callback( - 200, - 'OK', - {text: xdr.responseText}, - 'Content-Type: ' + xdr.contentType - ); - }; - xdr.onerror = function () { - callback(404, 'Not Found'); - }; - if (s.xdrTimeout) { - xdr.ontimeout = function () { - callback(0, 'timeout'); - }; - xdr.timeout = s.xdrTimeout; - } - xdr.send((s.hasContent && s.data) || null); - }, - abort: function () { - if (xdr) { - xdr.onerror = $.noop(); - xdr.abort(); - } - } - }; + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +})(function ($) { + 'use strict'; + if (window.XDomainRequest && !$.support.cors) { + $.ajaxTransport(function (s) { + if (s.crossDomain && s.async) { + if (s.timeout) { + s.xdrTimeout = s.timeout; + delete s.timeout; + } + var xdr; + return { + send: function (headers, completeCallback) { + var addParamChar = /\?/.test(s.url) ? '&' : '?'; + /** + * Callback wrapper function + * + * @param {number} status HTTP status code + * @param {string} statusText HTTP status text + * @param {object} [responses] Content-type specific responses + * @param {string} [responseHeaders] Response headers string + */ + function callback(status, statusText, responses, responseHeaders) { + xdr.onload = xdr.onerror = xdr.ontimeout = $.noop; + xdr = null; + completeCallback(status, statusText, responses, responseHeaders); } - }); - } -})); + xdr = new XDomainRequest(); + // XDomainRequest only supports GET and POST: + if (s.type === 'DELETE') { + s.url = s.url + addParamChar + '_method=DELETE'; + s.type = 'POST'; + } else if (s.type === 'PUT') { + s.url = s.url + addParamChar + '_method=PUT'; + s.type = 'POST'; + } else if (s.type === 'PATCH') { + s.url = s.url + addParamChar + '_method=PATCH'; + s.type = 'POST'; + } + xdr.open(s.type, s.url); + xdr.onload = function () { + callback( + 200, + 'OK', + { text: xdr.responseText }, + 'Content-Type: ' + xdr.contentType + ); + }; + xdr.onerror = function () { + callback(404, 'Not Found'); + }; + if (s.xdrTimeout) { + xdr.ontimeout = function () { + callback(0, 'timeout'); + }; + xdr.timeout = s.xdrTimeout; + } + xdr.send((s.hasContent && s.data) || null); + }, + abort: function () { + if (xdr) { + xdr.onerror = $.noop(); + xdr.abort(); + } + } + }; + } + }); + } +}); diff --git a/lib/web/jquery/fileUploader/css/jquery.fileupload-noscript.css b/lib/web/jquery/fileUploader/css/jquery.fileupload-noscript.css new file mode 100644 index 0000000000000..2409bfb0a6942 --- /dev/null +++ b/lib/web/jquery/fileUploader/css/jquery.fileupload-noscript.css @@ -0,0 +1,22 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin NoScript CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button input { + position: static; + opacity: 1; + filter: none; + font-size: inherit !important; + direction: inherit; +} +.fileinput-button span { + display: none; +} diff --git a/lib/web/jquery/fileUploader/css/jquery.fileupload-ui-noscript.css b/lib/web/jquery/fileUploader/css/jquery.fileupload-ui-noscript.css new file mode 100644 index 0000000000000..30651acf026c0 --- /dev/null +++ b/lib/web/jquery/fileUploader/css/jquery.fileupload-ui-noscript.css @@ -0,0 +1,17 @@ +@charset "UTF-8"; +/* + * jQuery File Upload UI Plugin NoScript CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button i, +.fileupload-buttonbar .delete, +.fileupload-buttonbar .toggle { + display: none; +} diff --git a/lib/web/jquery/fileUploader/css/jquery.fileupload-ui.css b/lib/web/jquery/fileUploader/css/jquery.fileupload-ui.css index 44b628efb481c..a6cfc7529198b 100644 --- a/lib/web/jquery/fileUploader/css/jquery.fileupload-ui.css +++ b/lib/web/jquery/fileUploader/css/jquery.fileupload-ui.css @@ -1,73 +1,68 @@ -@charset 'UTF-8'; +@charset "UTF-8"; /* - * jQuery File Upload UI Plugin CSS 6.3 + * jQuery File Upload UI Plugin CSS * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT + * https://opensource.org/licenses/MIT */ -.fileinput-button { - position: relative; - overflow: hidden; - float: left; - margin-right: 4px; -} -.fileinput-button input { - position: absolute; - top: 0; - right: 0; - margin: 0; - border: solid transparent; - border-width: 0 0 100px 200px; - opacity: 0; - filter: alpha(opacity=0); - -moz-transform: translate(-300px, 0) scale(4); - direction: ltr; - cursor: pointer; -} -.fileupload-buttonbar .btn, -.fileupload-buttonbar .toggle { - margin-bottom: 5px; -} -.files .progress { - width: 200px; -} +.progress-animated .progress-bar, .progress-animated .bar { - background: url(../img/progressbar.gif) !important; + background: url('../img/progressbar.gif') !important; filter: none; } -.fileupload-loading { - position: absolute; - left: 50%; - width: 128px; - height: 128px; - background: url(../img/loading.gif) center no-repeat; +.fileupload-process { + float: right; display: none; } -.fileupload-processing .fileupload-loading { +.fileupload-processing .fileupload-process, +.files .processing .preview { display: block; + width: 32px; + height: 32px; + background: url('../img/loading.gif') center no-repeat; + background-size: contain; +} +.files audio, +.files video { + max-width: 300px; +} +.files .name { + word-wrap: break-word; + overflow-wrap: anywhere; + -webkit-hyphens: auto; + hyphens: auto; +} +.files button { + margin-bottom: 5px; +} +.toggle[type='checkbox'] { + transform: scale(2); + margin-left: 10px; } -@media (max-width: 480px) { +@media (max-width: 767px) { + .fileupload-buttonbar .btn { + margin-bottom: 5px; + } + .fileupload-buttonbar .delete, + .fileupload-buttonbar .toggle, + .files .toggle, .files .btn span { display: none; } - .files .preview * { - width: 40px; - } - .files .name * { - width: 80px; - display: inline-block; - word-wrap: break-word; - } - .files .progress { - width: 20px; + .files audio, + .files video { + max-width: 80px; } - .files .delete { - width: 60px; +} + +@media (max-width: 480px) { + .files .image td:nth-child(2) { + display: none; } } diff --git a/lib/web/jquery/fileUploader/css/jquery.fileupload.css b/lib/web/jquery/fileUploader/css/jquery.fileupload.css new file mode 100644 index 0000000000000..5716f3e8a8aea --- /dev/null +++ b/lib/web/jquery/fileUploader/css/jquery.fileupload.css @@ -0,0 +1,36 @@ +@charset "UTF-8"; +/* + * jQuery File Upload Plugin CSS + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +.fileinput-button { + position: relative; + overflow: hidden; + display: inline-block; +} +.fileinput-button input { + position: absolute; + top: 0; + right: 0; + margin: 0; + height: 100%; + opacity: 0; + filter: alpha(opacity=0); + font-size: 200px !important; + direction: ltr; + cursor: pointer; +} + +/* Fixes for IE < 8 */ +@media screen\9 { + .fileinput-button input { + font-size: 150% !important; + } +} diff --git a/lib/web/jquery/fileUploader/img/loading.gif b/lib/web/jquery/fileUploader/img/loading.gif index 4ae663fa730eb029d66aeacdf410ded160745126..90f28cbdbb390b095e0d619cbe8d91208798e58f 100644 GIT binary patch delta 502 zcmV<S0SW%p9l0J1M@dFFIbnbRfB^CUkqjCp`2+<800aOb{|in<R8vDiP(?B>FarSq z001lk00IDf0DJ%d1OAar{sR7rlS~2q1HS(VlS~580sfO61O5UO@sk|{CjtJGMFjo= z715JL1t<ajlWqn60s~O9ZU!R*0sfPc3H|~UQInDiCjtJGx(faR6S1?p3mgJ}2?YQI z04xFk0swpfd;kCg{z$sq{xHf(tGzhu&Ab0#D2`-lo@lDBZ0o*oEYEZb+(509IKKa$ zUJppj2z|vQ<%m=|9n9u)N{HsDSSyyQ-A=n+rS%$4g3H7)+8kZ8neKEu4m{Io>buUa zkLCL_e1H-Gg#jUggARs<Ac=~93yhK<bC3{{m>rdti<p@kN1PF!o*YA>qokjxCaR^Z zt*(=>B(k%#BesmVwYr78BEG-CA;QGLw#T`y%FCq9y|>WLhtt;A*xB0K+}+;a;Nidm z<mKk)=;z$&?Cs{-?(yx}^7ZQ2_W1(z`t|Vr?$!ZJC$6Bug9sBUT*$Cxp~Hu021pzL zalyok5HD(^*imq!Mur|YDh%lnA<2>lPl`0C@*~TZ6HUUDm~tk^moO8~T$q#RNrxmW suB=D^BhjNslPX=xw5ijlP@_tnO0}xht5~yY-O9DA*RNp17D)gAJ8CHF^8f$< delta 400 zcmV;B0dM}f9@HHSM@dFFIbnbRfB^CYkqjCp{|in<R8vDiP(?B>FarSq001HR1O){E z1OO}o00IDf0DJ%d1OJgs{sM~slS~2q1OEuTlS~580soU71O5W>6O$bTCjtMHMFjo= z(G-(K1t<aklWqn60#E|8ZU!R*0soVd3H}066O)n(CjtMHx(faRu@bYo3mgJ}1qA>E z04xFk3IH$wR{#J5>qxrX{tHL|tG#Vb%)8Q>CsyD#o=j<?r@FGDOTn#6kMfP%dMD34 zOfD!K5-r9fiKm1ln~o<mBApVd)1NiUgap2U9+m(G>#*hYt;<NeVeEx_&MH;rbX3TK z&rkb)P=IfLgJoNWhgOMZg^Y-QkC1~~a)6YTc$aO2nwwvpprN9pq@|{(sHr*vtgWuE zu&*$&w6(4%wz;(@y1lX^zQF>#!o9e~wzJ5ws?E;N(9zP<)XxLg1Ebj6pxfTrm*3u$ u;@?)~;!x=6P3z`L?&I+B*y{Ax?f2mG^i%g+{Q$1hgf7%OQQWK~00292r?~zA diff --git a/lib/web/jquery/fileUploader/img/progressbar.gif b/lib/web/jquery/fileUploader/img/progressbar.gif index 74bb94e8e5d2b6393f3a9bbf5a06f714e07042c4..fbcce6bc9abfcc7893e65ef20b3e77ee16ec37b1 100644 GIT binary patch delta 1025 zcmZ1?^;=TF-P6s&GSPrRjA1u7!~3<#69rTvzaO3Q{oE7=1_s5SEUfGd{0xf!x&2&2 zf}I@$T#fV$m>GdmIzY?-(#F8DZNf><)ur06&sOJ6QtC^I<a1iJE@w7hnvV90?A;$Y z-qgJRymj`6ui8?K6MI*QNhilx@HQ#TYhE-*F>_PQu0zwj^O+eYzh}~$T+Qf?Us`;! zER!H(>EwxQqLVK$u}H6uUj51z!@yZ^11B<RFijT%DVHz%aME)*Uge=oQ-wiFb!&l2 z@9r{reP_mf1yhcc47=t<^BzQOir#f-ruKzf3~zRL+}qpPC+_mMYmy?v<W6Qkrhci( z8#yFoH19^4WZ#vzonXe{l;H*R;KPVb3A+wK+{4Eb%rr$2tnTHtSr)Hv;!rmeq)r{I z?m0enDy%?t24KGxf3V#?ZI8w4>lj8#dxFAt18We|WOI;GnW778zpmPAVSa4W|E3SU z*B5A{SarP2SoC)LF(Zb_7HlC*lN><m)CvpsdamBvyYcky$*~^1T}tzs6+qDyyX(+( z1_l%L=hJ#WPn9j0e3xBw@>ezmripGKU6T#iStW9NZl1jx1&m|3CSV+2V_=x<%bqWh zgIm&dvLuI=L^f^(Hj}4wNHT(BQ??iwMVZNKUfE*UbPH^g`D7_hS*FPl#h|E9#iLkf z@(fOGrfH%eFR<2sIB6+8`5~tm2g98$YTP?3m?!_|jAEKE4U`p=`Ec^5RI*9-+iSNH z%rv}!ajXE1W019dTwzR86+x!RR($ZhDKXpP^-HV@rMV-SX6Qf-JT?OubI-A=Tg?qL z&;Xbe#2HN|%kzkHHP|dKi+C=1=6l0r2OdAODVBnuEXoPWqDc9%WWq-G)q8KOZQY&U ir5@F+(DOoIQBvimdAklx_bvw}hZ8)(to`<a4AuaEWSm0) delta 951 zcmew@xkO69-P6s&GSPrRjNvms!}p_8CJLxXzh9gD{oE7=1_s6d+<vYh!Oo5Wu10zW z%!~{SlMR?ew6;w+>AAX8`}Nsswn<8TDUp0mtJdYr=1bGjUXi`~!;LpJ?>}#y{o$*& zl=Q^jRbq_EF&4Z{3iFy5%~8zU6tnBlH1B-p%ah+T=}oR?bjL3(K3SGYkg;`gB8#Ye z`3In_(W_tCVi-6JZs0^F4W_=?aOKPKDi39@nN<T)t6K*&`tB~1*E}=sE0}VmWY{$? zn)e`LQ}nJwGqo?=y6|R)$GyFsec~>EyCx+vPwrs$WA0ek1h%E@!%5A%Q6|~E61Nk~ zIGi%PfWCYfu_<BKA&8@RSb|x4X3U%cQ~dJUEQ{BiI22EXD4qp#|8snb<yeDRfhN!D z0jcIJ`(V3$+8&G7?2{+5iZB6#cR6biYxi`h%`&AI)_z^J*Mj-jrvFVJdap0gNU`d8 znX%~Y_G3ktC+o0<uy#!Yss)*(R$8#vbM@Yaji+}{j`iT}QkvJS0E)NRU5Bn;Ffmbo zKCSojRM~>bfgGBXpRp;hbxsB9>;&m#D*}dMHtS?V4pF8HCX)}cyH2iW*J5Nr2;?&| zBRDpbTR0>ctARQtm_0QUCOdLMwV6+5=agmcY6ZtSTLsWiRU}!R$t|4PEIm!LSwMCw zH+(p0$++9(^_ywHn9~5o+yZFK-MnyTiyHUN3iHV?Iir{-EM5#!&k8i|C&T1;E-{c3 z%elf>dzxm=21liA^#@N*iP;vfUtYVFV5Z>(j7kM)RNexrW#o=z?Q5AkZyv}P*~$-| z2W9|+`#Dy%ecWMeAamz{-N;<=fpPLbZgH+&o8@H@&n3@%@0~2q<Hy$BICIu)aG1+i xOxWnYdhd<3t-IN~)T5dedR_=DN~+v6Z`Yye-sQl^T+9>9*#wO1*(`z#)&Q2#moWeU diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-audio.js b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js new file mode 100644 index 0000000000000..e5c9202f9730a --- /dev/null +++ b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js @@ -0,0 +1,101 @@ +/* + * jQuery File Upload Audio Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', 'load-image', './jquery.fileupload-process'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory(window.jQuery, window.loadImage); + } +})(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadAudio', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableAudioPreview' + }, + { + action: 'setAudio', + name: '@audioPreviewName', + disabled: '@disableAudioPreview' + } + ); + + // The File Upload Audio Preview plugin extends the fileupload widget + // with audio preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + options: { + // The regular expression for the types of audio files to load, + // matched against the file type: + loadAudioFileTypes: /^audio\/.*$/ + }, + + _audioElement: document.createElement('audio'), + + processActions: { + // Loads the audio file given via data.files and data.index + // as audio element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadAudio: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + audio; + if ( + this._audioElement.canPlayType && + this._audioElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || options.fileTypes.test(file.type)) + ) { + url = loadImage.createObjectURL(file); + if (url) { + audio = this._audioElement.cloneNode(false); + audio.src = url; + audio.controls = true; + data.audio = audio; + return data; + } + } + return data; + }, + + // Sets the audio element as a property of the file object: + setAudio: function (data, options) { + if (data.audio && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.audio; + } + return data; + } + } + }); +}); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-fp.js b/lib/web/jquery/fileUploader/jquery.fileupload-fp.js deleted file mode 100644 index ee8f46342a93a..0000000000000 --- a/lib/web/jquery/fileUploader/jquery.fileupload-fp.js +++ /dev/null @@ -1,219 +0,0 @@ -/* - * jQuery File Upload File Processing Plugin 1.0 - * https://github.com/blueimp/jQuery-File-Upload - * - * Copyright 2012, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT - */ - -/*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document */ - -(function (factory) { - 'use strict'; - if (typeof define === 'function' && define.amd) { - // Register as an anonymous AMD module: - define([ - 'jquery', - 'jquery/fileUploader/load-image', - 'jquery/fileUploader/canvas-to-blob', - 'jquery/fileUploader/jquery.fileupload' - ], factory); - } else { - // Browser globals: - factory( - window.jQuery, - window.loadImage - ); - } -}(function ($, loadImage) { - 'use strict'; - - // The File Upload IP version extends the basic fileupload widget - // with file processing functionality: - $.widget('blueimpFP.fileupload', $.blueimp.fileupload, { - - options: { - // The list of file processing actions: - process: [ - /* - { - action: 'load', - fileTypes: /^image\/(gif|jpeg|png)$/, - maxFileSize: 20000000 // 20MB - }, - { - action: 'resize', - maxWidth: 1920, - maxHeight: 1200, - minWidth: 800, - minHeight: 600 - }, - { - action: 'save' - } - */ - ], - - // The add callback is invoked as soon as files are added to the - // fileupload widget (via file input selection, drag & drop or add - // API call). See the basic file upload widget for more information: - add: function (e, data) { - $(this).fileupload('process', data).done(function () { - data.submit(); - }); - } - }, - - processActions: { - // Loads the image given via data.files and data.index - // as canvas element. - // Accepts the options fileTypes (regular expression) - // and maxFileSize (integer) to limit the files to load: - load: function (data, options) { - var that = this, - file = data.files[data.index], - dfd = $.Deferred(); - if (window.HTMLCanvasElement && - window.HTMLCanvasElement.prototype.toBlob && - ($.type(options.maxFileSize) !== 'number' || - file.size < options.maxFileSize) && - (!options.fileTypes || - options.fileTypes.test(file.type))) { - loadImage( - file, - function (canvas) { - data.canvas = canvas; - dfd.resolveWith(that, [data]); - }, - {canvas: true} - ); - } else { - dfd.rejectWith(that, [data]); - } - return dfd.promise(); - }, - // Resizes the image given as data.canvas and updates - // data.canvas with the resized image. - // Accepts the options maxWidth, maxHeight, minWidth and - // minHeight to scale the given image: - resize: function (data, options) { - if (data.canvas) { - var canvas = loadImage.scale(data.canvas, options); - if (canvas.width !== data.canvas.width || - canvas.height !== data.canvas.height) { - data.canvas = canvas; - data.processed = true; - } - } - return data; - }, - // Saves the processed image given as data.canvas - // inplace at data.index of data.files: - save: function (data, options) { - // Do nothing if no processing has happened: - if (!data.canvas || !data.processed) { - return data; - } - var that = this, - file = data.files[data.index], - name = file.name, - dfd = $.Deferred(), - callback = function (blob) { - if (!blob.name) { - if (file.type === blob.type) { - blob.name = file.name; - } else if (file.name) { - blob.name = file.name.replace( - /\..+$/, - '.' + blob.type.substr(6) - ); - } - } - // Store the created blob at the position - // of the original file in the files list: - data.files[data.index] = blob; - dfd.resolveWith(that, [data]); - }; - // Use canvas.mozGetAsFile directly, to retain the filename, as - // Gecko doesn't support the filename option for FormData.append: - if (data.canvas.mozGetAsFile) { - callback(data.canvas.mozGetAsFile( - (/^image\/(jpeg|png)$/.test(file.type) && name) || - ((name && name.replace(/\..+$/, '')) || - 'blob') + '.png', - file.type - )); - } else { - data.canvas.toBlob(callback, file.type); - } - return dfd.promise(); - } - }, - - // Resizes the file at the given index and stores the created blob at - // the original position of the files list, returns a Promise object: - _processFile: function (files, index, options) { - var that = this, - dfd = $.Deferred().resolveWith(that, [{ - files: files, - index: index - }]), - chain = dfd.promise(); - that._processing += 1; - $.each(options.process, function (i, settings) { - chain = chain.pipe(function (data) { - return that.processActions[settings.action] - .call(this, data, settings); - }); - }); - chain.always(function () { - that._processing -= 1; - if (that._processing === 0) { - that.element - .removeClass('fileupload-processing'); - } - }); - if (that._processing === 1) { - that.element.addClass('fileupload-processing'); - } - return chain; - }, - - // Processes the files given as files property of the data parameter, - // returns a Promise object that allows to bind a done handler, which - // will be invoked after processing all files (inplace) is done: - process: function (data) { - var that = this, - options = $.extend({}, this.options, data); - if (options.process && options.process.length && - this._isXHRUpload(options)) { - $.each(data.files, function (index, file) { - that._processingQueue = that._processingQueue.pipe( - function () { - var dfd = $.Deferred(); - that._processFile(data.files, index, options) - .always(function () { - dfd.resolveWith(that); - }); - return dfd.promise(); - } - ); - }); - } - return this._processingQueue; - }, - - _create: function () { - $.blueimp.fileupload.prototype._create.call(this); - this._processing = 0; - this._processingQueue = $.Deferred().resolveWith(this) - .promise(); - } - - }); - -})); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-image.js b/lib/web/jquery/fileUploader/jquery.fileupload-image.js new file mode 100644 index 0000000000000..8598461031e2e --- /dev/null +++ b/lib/web/jquery/fileUploader/jquery.fileupload-image.js @@ -0,0 +1,355 @@ +/* + * jQuery File Upload Image Preview & Resize Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'load-image-meta', + 'load-image-scale', + 'load-image-exif', + 'load-image-orientation', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('blueimp-load-image/js/load-image-meta'), + require('blueimp-load-image/js/load-image-scale'), + require('blueimp-load-image/js/load-image-exif'), + require('blueimp-load-image/js/load-image-orientation'), + require('blueimp-canvas-to-blob'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory(window.jQuery, window.loadImage); + } +})(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImageMetaData', + maxMetaDataSize: '@', + disableImageHead: '@', + disableMetaDataParsers: '@', + disableExif: '@', + disableExifThumbnail: '@', + disableExifOffsets: '@', + includeExifTags: '@', + excludeExifTags: '@', + disableIptc: '@', + disableIptcOffsets: '@', + includeIptcTags: '@', + excludeIptcTags: '@', + disabled: '@disableImageMetaDataLoad' + }, + { + action: 'loadImage', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + noRevoke: '@', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + // Use "image" as prefix for the "@" options: + prefix: 'image', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + forceResize: '@', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + quality: '@imageQuality', + type: '@imageType', + disabled: '@disableImageResize' + }, + { + action: 'saveImageMetaData', + disabled: '@disableImageMetaDataSave' + }, + { + action: 'resizeImage', + // Use "preview" as prefix for the "@" options: + prefix: 'preview', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + thumbnail: '@', + canvas: '@', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + name: '@imagePreviewName', + disabled: '@disableImagePreview' + }, + { + action: 'deleteImageReferences', + disabled: '@disableImageReferencesDeletion' + } + ); + + // The File Upload Resize plugin extends the fileupload widget + // with image resize functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + options: { + // The regular expression for the types of images to load: + // matched against the file type: + loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 10000000, // 10MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // Defines the image orientation (1-8) or takes the orientation + // value from Exif data if set to true: + imageOrientation: true, + // Define if resized images should be cropped or only scaled: + imageCrop: false, + // Disable the resize image functionality by default: + disableImageResize: true, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // Defines the preview orientation (1-8) or takes the orientation + // value from Exif data if set to true: + previewOrientation: true, + // Create the preview using the Exif data thumbnail: + previewThumbnail: true, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewCanvas: true + }, + + processActions: { + // Loads the image given via data.files and data.index + // as img element, if the browser supports the File API. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + // eslint-disable-next-line new-cap + dfd = $.Deferred(); + if ( + ($.type(options.maxFileSize) === 'number' && + file.size > options.maxFileSize) || + (options.fileTypes && !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (img.src) { + data.img = img; + } + dfd.resolveWith(that, [data]); + }, + options + ) + ) { + return data; + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Also stores the resized image as preview property. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled || !(data.canvas || data.img)) { + return data; + } + // eslint-disable-next-line no-param-reassign + options = $.extend({ canvas: true }, options); + var that = this, + // eslint-disable-next-line new-cap + dfd = $.Deferred(), + img = (options.canvas && data.canvas) || data.img, + resolve = function (newImg) { + if ( + newImg && + (newImg.width !== img.width || + newImg.height !== img.height || + options.forceResize) + ) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + } + data.preview = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail, + thumbnailBlob; + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + thumbnailBlob = thumbnail && thumbnail.get('Blob'); + if (thumbnailBlob) { + loadImage(thumbnailBlob, resolve, options); + return dfd.promise(); + } + } + // Prevent orienting browser oriented images: + if (loadImage.orientation) { + data.orientation = data.orientation || options.orientation; + } + // Prevent orienting the same image twice: + if (data.orientation) { + delete options.orientation; + } else { + data.orientation = options.orientation; + } + } + if (img) { + resolve(loadImage.scale(img, options)); + return dfd.promise(); + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + // eslint-disable-next-line new-cap + dfd = $.Deferred(); + if (data.canvas.toBlob) { + data.canvas.toBlob( + function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\.\w+$/, + '.' + blob.type.substr(6) + ); + } + } + // Don't restore invalid meta data: + if (file.type !== blob.type) { + delete data.imageHead; + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }, + options.type || file.type, + options.quality + ); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + // eslint-disable-next-line new-cap + dfd = $.Deferred(); + loadImage.parseMetaData( + data.files[data.index], + function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, + options + ); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if ( + !( + data.imageHead && + data.canvas && + data.canvas.toBlob && + !options.disabled + ) + ) { + return data; + } + var that = this, + file = data.files[data.index], + // eslint-disable-next-line new-cap + dfd = $.Deferred(); + if (data.orientation && data.exifOffsets) { + // Reset Exif Orientation data: + loadImage.writeExifData(data.imageHead, data, 'Orientation', 1); + } + loadImage.replaceHead(file, data.imageHead, function (blob) { + blob.name = file.name; + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }); + return dfd.promise(); + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + if (data.preview && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.preview; + } + return data; + }, + + deleteImageReferences: function (data, options) { + if (!options.disabled) { + delete data.img; + delete data.canvas; + delete data.preview; + delete data.imageHead; + } + return data; + } + } + }); +}); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-process.js b/lib/web/jquery/fileUploader/jquery.fileupload-process.js new file mode 100644 index 0000000000000..130778e7f26a6 --- /dev/null +++ b/lib/web/jquery/fileUploader/jquery.fileupload-process.js @@ -0,0 +1,170 @@ +/* + * jQuery File Upload Processing Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', './jquery.fileupload'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery'), require('./jquery.fileupload')); + } else { + // Browser globals: + factory(window.jQuery); + } +})(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data, originalData) { + var that = this, + // eslint-disable-next-line new-cap + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + if (originalData.errorThrown) { + // eslint-disable-next-line new-cap + return $.Deferred().rejectWith(that, [originalData]).promise(); + } + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain[that._promisePipe](func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && value.charAt(0) === '@') { + settings[key] = + options[ + value.slice(1) || + (prefix + ? prefix + key.charAt(0).toUpperCase() + key.slice(1) + : key) + ]; + } else { + settings[key] = value; + } + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + if (data.errorThrown) { + // eslint-disable-next-line new-cap + return $.Deferred().rejectWith(that, [data]).promise(); + } + return that._processFile(opts, data); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue[that._promisePipe]( + func, + func + ).always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + // eslint-disable-next-line new-cap + this._processingQueue = $.Deferred().resolveWith(this).promise(); + } + }); +}); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js index 8dca3ce992671..9cc3d3fd0fb1f 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js @@ -1,745 +1,759 @@ /* - * jQuery File Upload User Interface Plugin 6.9.5 + * jQuery File Upload User Interface Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT + * https://opensource.org/licenses/MIT */ -/*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, URL, webkitURL, FileReader */ +/* global define, require */ (function (factory) { - 'use strict'; - if (typeof define === 'function' && define.amd) { - // Register as an anonymous AMD module: - define([ - 'jquery', - 'mage/template', - 'jquery/fileUploader/load-image', - 'jquery/fileUploader/jquery.fileupload-fp', - 'jquery/fileUploader/jquery.iframe-transport' - ], factory); - } else { - // Browser globals: - factory( - window.jQuery, - window.mageTemplate, - window.loadImage + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'blueimp-tmpl', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-tmpl'), + require('./jquery.fileupload-image'), + require('./jquery.fileupload-audio'), + require('./jquery.fileupload-video'), + require('./jquery.fileupload-validate') + ); + } else { + // Browser globals: + factory(window.jQuery, window.tmpl); + } +})(function ($, tmpl) { + 'use strict'; + + $.blueimp.fileupload.prototype._specialOptions.push( + 'filesContainer', + 'uploadTemplateId', + 'downloadTemplateId' + ); + + // The UI version extends the file upload widget + // and adds complete user interface interaction: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The class to show/hide UI elements: + showElementClass: 'in', + // The ID of the upload template: + uploadTemplateId: 'template-upload', + // The ID of the download template: + downloadTemplateId: 'template-download', + // The container for the list of files. If undefined, it is set to + // an element with class "files" inside of the widget element: + filesContainer: undefined, + // By default, files are appended to the files container. + // Set the following option to true, to prepend files instead: + prependFiles: false, + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // Error and info messages: + messages: { + unknownError: 'Unknown error' + }, + + // Function returning the current number of files, + // used by the maxNumberOfFiles validation: + getNumberOfFiles: function () { + return this.filesContainer.children().not('.processing').length; + }, + + // Callback to retrieve the list of files from the server response: + getFilesFromResponse: function (data) { + if (data.result && $.isArray(data.result.files)) { + return data.result.files; + } + return []; + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + that = $this.data('blueimp-fileupload') || $this.data('fileupload'), + options = that.options; + data.context = that + ._renderUpload(data.files) + .data('data', data) + .addClass('processing'); + options.filesContainer[options.prependFiles ? 'prepend' : 'append']( + data.context ); - } -}(function ($, tmpl, loadImage) { - 'use strict'; - - // The UI version extends the FP (file processing) version or the basic - // file upload widget and adds complete user interface interaction: - var parentWidget = ($.blueimpFP || $.blueimp).fileupload; - $.widget('blueimpUI.fileupload', parentWidget, { - - options: { - // By default, files added to the widget are uploaded as soon - // as the user clicks on the start buttons. To enable automatic - // uploads, set the following option to true: - autoUpload: false, - // The following option limits the number of files that are - // allowed to be uploaded using this widget: - maxNumberOfFiles: undefined, - // The maximum allowed file size: - maxFileSize: undefined, - // The minimum allowed file size: - minFileSize: undefined, - // The regular expression for allowed file types, matches - // against either file type or file name: - acceptFileTypes: /.+$/i, - // The regular expression to define for which files a preview - // image is shown, matched against the file type: - previewSourceFileTypes: /^image\/(gif|jpeg|png)$/, - // The maximum file size of images that are to be displayed as preview: - previewSourceMaxFileSize: 5000000, // 5MB - // The maximum width of the preview images: - previewMaxWidth: 80, - // The maximum height of the preview images: - previewMaxHeight: 80, - // By default, preview images are displayed as canvas elements - // if supported by the browser. Set the following option to false - // to always display preview images as img elements: - previewAsCanvas: true, - // The ID of the upload template: - uploadTemplateId: 'template-upload', - // The ID of the download template: - downloadTemplateId: 'template-download', - // The container for the list of files. If undefined, it is set to - // an element with class "files" inside of the widget element: - filesContainer: undefined, - // By default, files are appended to the files container. - // Set the following option to true, to prepend files instead: - prependFiles: false, - // The expected data type of the upload response, sets the dataType - // option of the $.ajax upload requests: - dataType: 'json', - - // The add callback is invoked as soon as files are added to the fileupload - // widget (via file input selection, drag & drop or add API call). - // See the basic file upload widget for more information: - add: function (e, data) { - var that = $(this).data('fileupload'), - options = that.options, - files = data.files; - $(this).fileupload('process', data).done(function () { - that._adjustMaxNumberOfFiles(-files.length); - data.maxNumberOfFilesAdjusted = true; - data.files.valid = data.isValidated = that._validate(files); - data.context = that._renderUpload(files).data('data', data); - options.filesContainer[ - options.prependFiles ? 'prepend' : 'append' - ](data.context); - that._renderPreviews(files, data.context); - that._forceReflow(data.context); - that._transition(data.context).done( - function () { - if ((that._trigger('added', e, data) !== false) && - (options.autoUpload || data.autoUpload) && - data.autoUpload !== false && data.isValidated) { - data.submit(); - } - } - ); - }); - }, - // Callback for the start of each file upload request: - send: function (e, data) { - var that = $(this).data('fileupload'); - if (!data.isValidated) { - if (!data.maxNumberOfFilesAdjusted) { - that._adjustMaxNumberOfFiles(-data.files.length); - data.maxNumberOfFilesAdjusted = true; - } - if (!that._validate(data.files)) { - return false; - } - } - if (data.context && data.dataType && - data.dataType.substr(0, 6) === 'iframe') { - // Iframe Transport does not support progress events. - // In lack of an indeterminate progress bar, we set - // the progress to 100%, showing the full animated bar: - data.context - .find('.progress').addClass( - !$.support.transition && 'progress-animated' - ) - .attr('aria-valuenow', 100) - .find('.bar').css( - 'width', - '100%' - ); - } - return that._trigger('sent', e, data); - }, - // Callback for successful uploads: - done: function (e, data) { - var that = $(this).data('fileupload'), - template; - if (data.context) { - data.context.each(function (index) { - var file = ($.isArray(data.result) && - data.result[index]) || {error: 'emptyResult'}; - if (file.error) { - that._adjustMaxNumberOfFiles(1); - } - that._transition($(this)).done( - function () { - var node = $(this); - template = that._renderDownload([file]) - .replaceAll(node); - that._forceReflow(template); - that._transition(template).done( - function () { - data.context = $(this); - that._trigger('completed', e, data); - } - ); - } - ); - }); - } else { - if ($.isArray(data.result)) { - $.each(data.result, function (index, file) { - if (data.maxNumberOfFilesAdjusted && file.error) { - that._adjustMaxNumberOfFiles(1); - } else if (!data.maxNumberOfFilesAdjusted && - !file.error) { - that._adjustMaxNumberOfFiles(-1); - } - }); - data.maxNumberOfFilesAdjusted = true; - } - template = that._renderDownload(data.result) - .appendTo(that.options.filesContainer); - that._forceReflow(template); - that._transition(template).done( - function () { - data.context = $(this); - that._trigger('completed', e, data); - } - ); - } - }, - // Callback for failed (abort or error) uploads: - fail: function (e, data) { - var that = $(this).data('fileupload'), - template; - if (data.maxNumberOfFilesAdjusted) { - that._adjustMaxNumberOfFiles(data.files.length); - } - if (data.context) { - data.context.each(function (index) { - if (data.errorThrown !== 'abort') { - var file = data.files[index]; - file.error = file.error || data.errorThrown || - true; - that._transition($(this)).done( - function () { - var node = $(this); - template = that._renderDownload([file]) - .replaceAll(node); - that._forceReflow(template); - that._transition(template).done( - function () { - data.context = $(this); - that._trigger('failed', e, data); - } - ); - } - ); - } else { - that._transition($(this)).done( - function () { - $(this).remove(); - that._trigger('failed', e, data); - } - ); - } - }); - } else if (data.errorThrown !== 'abort') { - data.context = that._renderUpload(data.files) - .appendTo(that.options.filesContainer) - .data('data', data); - that._forceReflow(data.context); - that._transition(data.context).done( - function () { - data.context = $(this); - that._trigger('failed', e, data); - } - ); - } else { - that._trigger('failed', e, data); - } - }, - // Callback for upload progress events: - progress: function (e, data) { - if (data.context) { - var progress = parseInt(data.loaded / data.total * 100, 10); - data.context.find('.progress') - .attr('aria-valuenow', progress) - .find('.bar').css( - 'width', - progress + '%' - ); - } - }, - // Callback for global upload progress events: - progressall: function (e, data) { - var $this = $(this), - progress = parseInt(data.loaded / data.total * 100, 10), - globalProgressNode = $this.find('.fileupload-progress'), - extendedProgressNode = globalProgressNode - .find('.progress-extended'); - if (extendedProgressNode.length) { - extendedProgressNode.html( - $this.data('fileupload')._renderExtendedProgress(data) - ); - } - globalProgressNode - .find('.progress') - .attr('aria-valuenow', progress) - .find('.bar').css( - 'width', - progress + '%' - ); - }, - // Callback for uploads start, equivalent to the global ajaxStart event: - start: function (e) { - var that = $(this).data('fileupload'); - that._transition($(this).find('.fileupload-progress')).done( - function () { - that._trigger('started', e); - } - ); - }, - // Callback for uploads stop, equivalent to the global ajaxStop event: - stop: function (e) { - var that = $(this).data('fileupload'); - that._transition($(this).find('.fileupload-progress')).done( - function () { - $(this).find('.progress') - .attr('aria-valuenow', '0') - .find('.bar').css('width', '0%'); - $(this).find('.progress-extended').html(' '); - that._trigger('stopped', e); - } - ); - }, - // Callback for file deletion: - destroy: function (e, data) { - var that = $(this).data('fileupload'); - if (data.url) { - $.ajax(data); - that._adjustMaxNumberOfFiles(1); - } - that._transition(data.context).done( - function () { - $(this).remove(); - that._trigger('destroyed', e, data); - } - ); - } - }, - - // Link handler, that allows to download files - // by drag & drop of the links to the desktop: - _enableDragToDesktop: function () { - var link = $(this), - url = link.prop('href'), - name = link.prop('download'), - type = 'application/octet-stream'; - link.bind('dragstart', function (e) { - try { - e.originalEvent.dataTransfer.setData( - 'DownloadURL', - [type, name, url].join(':') - ); - } catch (err) {} - }); - }, - - _adjustMaxNumberOfFiles: function (operand) { - if (typeof this.options.maxNumberOfFiles === 'number') { - this.options.maxNumberOfFiles += operand; - if (this.options.maxNumberOfFiles < 1) { - this._disableFileInputButton(); - } else { - this._enableFileInputButton(); - } - } - }, - - _formatFileSize: function (bytes) { - if (typeof bytes !== 'number') { - return ''; - } - if (bytes >= 1000000000) { - return (bytes / 1000000000).toFixed(2) + ' GB'; - } - if (bytes >= 1000000) { - return (bytes / 1000000).toFixed(2) + ' MB'; - } - return (bytes / 1000).toFixed(2) + ' KB'; - }, - - _formatBitrate: function (bits) { - if (typeof bits !== 'number') { - return ''; + that._forceReflow(data.context); + that._transition(data.context); + data + .process(function () { + return $this.fileupload('process', data); + }) + .always(function () { + data.context + .each(function (index) { + $(this) + .find('.size') + .text(that._formatFileSize(data.files[index].size)); + }) + .removeClass('processing'); + that._renderPreviews(data); + }) + .done(function () { + data.context.find('.edit,.start').prop('disabled', false); + if ( + that._trigger('added', e, data) !== false && + (options.autoUpload || data.autoUpload) && + data.autoUpload !== false + ) { + data.submit(); } - if (bits >= 1000000000) { - return (bits / 1000000000).toFixed(2) + ' Gbit/s'; - } - if (bits >= 1000000) { - return (bits / 1000000).toFixed(2) + ' Mbit/s'; - } - if (bits >= 1000) { - return (bits / 1000).toFixed(2) + ' kbit/s'; - } - return bits + ' bit/s'; - }, - - _formatTime: function (seconds) { - var date = new Date(seconds * 1000), - days = parseInt(seconds / 86400, 10); - days = days ? days + 'd ' : ''; - return days + - ('0' + date.getUTCHours()).slice(-2) + ':' + - ('0' + date.getUTCMinutes()).slice(-2) + ':' + - ('0' + date.getUTCSeconds()).slice(-2); - }, - - _formatPercentage: function (floatValue) { - return (floatValue * 100).toFixed(2) + ' %'; - }, - - _renderExtendedProgress: function (data) { - return this._formatBitrate(data.bitrate) + ' | ' + - this._formatTime( - (data.total - data.loaded) * 8 / data.bitrate - ) + ' | ' + - this._formatPercentage( - data.loaded / data.total - ) + ' | ' + - this._formatFileSize(data.loaded) + ' / ' + - this._formatFileSize(data.total); - }, - - _hasError: function (file) { - if (file.error) { - return file.error; - } - // The number of added files is subtracted from - // maxNumberOfFiles before validation, so we check if - // maxNumberOfFiles is below 0 (instead of below 1): - if (this.options.maxNumberOfFiles < 0) { - return 'maxNumberOfFiles'; - } - // Files are accepted if either the file type or the file name - // matches against the acceptFileTypes regular expression, as - // only browsers with support for the File API report the type: - if (!(this.options.acceptFileTypes.test(file.type) || - this.options.acceptFileTypes.test(file.name))) { - return 'acceptFileTypes'; - } - if (this.options.maxFileSize && - file.size > this.options.maxFileSize) { - return 'maxFileSize'; - } - if (typeof file.size === 'number' && - file.size < this.options.minFileSize) { - return 'minFileSize'; - } - return null; - }, - - _validate: function (files) { - var that = this, - valid = !!files.length; - $.each(files, function (index, file) { - file.error = that._hasError(file); - if (file.error) { - valid = false; + }) + .fail(function () { + if (data.files.error) { + data.context.each(function (index) { + var error = data.files[index].error; + if (error) { + $(this).find('.error').text(error); } - }); - return valid; - }, - - _renderTemplate: function (func, files) { - if (!func) { - return $(); + }); } - var result = func({ - files: files, - formatFileSize: this._formatFileSize, - options: this.options - }); - if (result instanceof $) { - return result; - } - return $(this.options.templatesContainer).html(result).children(); - }, - - _renderPreview: function (file, node) { - var that = this, - options = this.options, - dfd = $.Deferred(); - return ((loadImage && loadImage( - file, - function (img) { - node.append(img); - that._forceReflow(node); - that._transition(node).done(function () { - dfd.resolveWith(node); - }); - if (!$.contains(document.body, node[0])) { - // If the element is not part of the DOM, - // transition events are not triggered, - // so we have to resolve manually: - dfd.resolveWith(node); - } - }, - { - maxWidth: options.previewMaxWidth, - maxHeight: options.previewMaxHeight, - canvas: options.previewAsCanvas - } - )) || dfd.resolveWith(node)) && dfd; - }, - - _renderPreviews: function (files, nodes) { - var that = this, - options = this.options; - nodes.find('.preview span').each(function (index, element) { - var file = files[index]; - if (options.previewSourceFileTypes.test(file.type) && - ($.type(options.previewSourceMaxFileSize) !== 'number' || - file.size < options.previewSourceMaxFileSize)) { - that._processingQueue = that._processingQueue.pipe(function () { - var dfd = $.Deferred(); - that._renderPreview(file, $(element)).done( - function () { - dfd.resolveWith(that); - } - ); - return dfd.promise(); - }); - } + }); + }, + // Callback for the start of each file upload request: + send: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = + $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + if ( + data.context && + data.dataType && + data.dataType.substr(0, 6) === 'iframe' + ) { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context + .find('.progress') + .addClass(!$.support.transition && 'progress-animated') + .attr('aria-valuenow', 100) + .children() + .first() + .css('width', '100%'); + } + return that._trigger('sent', e, data); + }, + // Callback for successful uploads: + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = + $(this).data('blueimp-fileupload') || $(this).data('fileupload'), + getFilesFromResponse = + data.getFilesFromResponse || that.options.getFilesFromResponse, + files = getFilesFromResponse(data), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + var file = files[index] || { error: 'Empty file upload result' }; + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done(function () { + var node = $(this); + template = that._renderDownload([file]).replaceAll(node); + that._forceReflow(template); + that._transition(template).done(function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + }); }); - return this._processingQueue; - }, - - _renderUpload: function (files) { - return this._renderTemplate( - this.options.uploadTemplate, - files + }); + } else { + template = that + ._renderDownload(files) + [that.options.prependFiles ? 'prependTo' : 'appendTo']( + that.options.filesContainer ); - }, - - _renderDownload: function (files) { - return this._renderTemplate( - this.options.downloadTemplate, - files - ).find('a[download]').each(this._enableDragToDesktop).end(); - }, - - _startHandler: function (e) { - e.preventDefault(); - var button = $(this), - template = button.closest('.template-upload'), - data = template.data('data'); - if (data && data.submit && !data.jqXHR && data.submit()) { - button.prop('disabled', true); - } - }, - - _cancelHandler: function (e) { - e.preventDefault(); - var template = $(this).closest('.template-upload'), - data = template.data('data') || {}; - if (!data.jqXHR) { - data.errorThrown = 'abort'; - e.data.fileupload._trigger('fail', e, data); + that._forceReflow(template); + deferred = that._addFinishedDeferreds(); + that._transition(template).done(function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + }); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = + $(this).data('blueimp-fileupload') || $(this).data('fileupload'), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = + file.error || data.errorThrown || data.i18n('unknownError'); + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done(function () { + var node = $(this); + template = that._renderDownload([file]).replaceAll(node); + that._forceReflow(template); + that._transition(template).done(function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + }); + }); } else { - data.jqXHR.abort(); + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done(function () { + $(this).remove(); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + }); } - }, - - _deleteHandler: function (e) { - e.preventDefault(); - var button = $(this); - e.data.fileupload._trigger('destroy', e, { - context: button.closest('.template-download'), - url: button.attr('data-url'), - type: button.attr('data-type') || 'DELETE', - dataType: e.data.fileupload.options.dataType + }); + } else if (data.errorThrown !== 'abort') { + data.context = that + ._renderUpload(data.files) + [that.options.prependFiles ? 'prependTo' : 'appendTo']( + that.options.filesContainer + ) + .data('data', data); + that._forceReflow(data.context); + deferred = that._addFinishedDeferreds(); + that._transition(data.context).done(function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + }); + } else { + that._trigger('failed', e, data); + that._trigger('finished', e, data); + that._addFinishedDeferreds().resolve(); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var progress = Math.floor((data.loaded / data.total) * 100); + if (data.context) { + data.context.each(function () { + $(this) + .find('.progress') + .attr('aria-valuenow', progress) + .children() + .first() + .css('width', progress + '%'); + }); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + progress = Math.floor((data.loaded / data.total) * 100), + globalProgressNode = $this.find('.fileupload-progress'), + extendedProgressNode = globalProgressNode.find('.progress-extended'); + if (extendedProgressNode.length) { + extendedProgressNode.html( + ( + $this.data('blueimp-fileupload') || $this.data('fileupload') + )._renderExtendedProgress(data) + ); + } + globalProgressNode + .find('.progress') + .attr('aria-valuenow', progress) + .children() + .first() + .css('width', progress + '%'); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = + $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + that._resetFinishedDeferreds(); + that + ._transition($(this).find('.fileupload-progress')) + .done(function () { + that._trigger('started', e); + }); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = + $(this).data('blueimp-fileupload') || $(this).data('fileupload'), + deferred = that._addFinishedDeferreds(); + $.when.apply($, that._getFinishedDeferreds()).done(function () { + that._trigger('stopped', e); + }); + that + ._transition($(this).find('.fileupload-progress')) + .done(function () { + $(this) + .find('.progress') + .attr('aria-valuenow', '0') + .children() + .first() + .css('width', '0%'); + $(this).find('.progress-extended').html(' '); + deferred.resolve(); + }); + }, + processstart: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).addClass('fileupload-processing'); + }, + processstop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).removeClass('fileupload-processing'); + }, + // Callback for file deletion: + destroy: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = + $(this).data('blueimp-fileupload') || $(this).data('fileupload'), + removeNode = function () { + that._transition(data.context).done(function () { + $(this).remove(); + that._trigger('destroyed', e, data); }); - }, - - _forceReflow: function (node) { - return $.support.transition && node.length && - node[0].offsetWidth; - }, - - _transition: function (node) { - var dfd = $.Deferred(); - if ($.support.transition && node.hasClass('fade')) { - node.bind( - $.support.transition.end, - function (e) { - // Make sure we don't respond to other transitions events - // in the container element, e.g. from button elements: - if (e.target === node[0]) { - node.unbind($.support.transition.end); - dfd.resolveWith(node); - } - } - ).toggleClass('in'); - } else { - node.toggleClass('in'); - dfd.resolveWith(node); - } - return dfd; - }, - - _initButtonBarEventHandlers: function () { - var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), - filesList = this.options.filesContainer, - ns = this.options.namespace; - fileUploadButtonBar.find('.start') - .bind('click.' + ns, function (e) { - e.preventDefault(); - filesList.find('.start button').click(); - }); - fileUploadButtonBar.find('.cancel') - .bind('click.' + ns, function (e) { - e.preventDefault(); - filesList.find('.cancel button').click(); - }); - fileUploadButtonBar.find('.delete') - .bind('click.' + ns, function (e) { - e.preventDefault(); - filesList.find('.delete input:checked') - .siblings('button').click(); - fileUploadButtonBar.find('.toggle') - .prop('checked', false); - }); - fileUploadButtonBar.find('.toggle') - .bind('change.' + ns, function (e) { - filesList.find('.delete input').prop( - 'checked', - $(this).is(':checked') - ); - }); - }, - - _destroyButtonBarEventHandlers: function () { - this.element.find('.fileupload-buttonbar button') - .unbind('click.' + this.options.namespace); - this.element.find('.fileupload-buttonbar .toggle') - .unbind('change.' + this.options.namespace); - }, - - _initEventHandlers: function () { - parentWidget.prototype._initEventHandlers.call(this); - var eventData = {fileupload: this}; - this.options.filesContainer - .delegate( - '.start button', - 'click.' + this.options.namespace, - eventData, - this._startHandler - ) - .delegate( - '.cancel button', - 'click.' + this.options.namespace, - eventData, - this._cancelHandler - ) - .delegate( - '.delete button', - 'click.' + this.options.namespace, - eventData, - this._deleteHandler - ); - this._initButtonBarEventHandlers(); - }, - - _destroyEventHandlers: function () { - var options = this.options; - this._destroyButtonBarEventHandlers(); - options.filesContainer - .undelegate('.start button', 'click.' + options.namespace) - .undelegate('.cancel button', 'click.' + options.namespace) - .undelegate('.delete button', 'click.' + options.namespace); - parentWidget.prototype._destroyEventHandlers.call(this); - }, - - _enableFileInputButton: function () { - this.element.find('.fileinput-button input') - .prop('disabled', false) - .parent().removeClass('disabled'); - }, - - _disableFileInputButton: function () { - this.element.find('.fileinput-button input') - .prop('disabled', true) - .parent().addClass('disabled'); - }, - - _initTemplates: function () { - var options = this.options; - options.templatesContainer = document.createElement( - options.filesContainer.prop('nodeName') - ); - if (tmpl) { - if (options.uploadTemplateId) { - options.uploadTemplate = tmpl(options.uploadTemplateId); - } - if (options.downloadTemplateId) { - options.downloadTemplate = tmpl(options.downloadTemplateId); - } - } - }, - - _initFilesContainer: function () { - var options = this.options; - if (options.filesContainer === undefined) { - options.filesContainer = this.element.find('.files'); - } else if (!(options.filesContainer instanceof $)) { - options.filesContainer = $(options.filesContainer); - } - }, - - _stringToRegExp: function (str) { - var parts = str.split('/'), - modifiers = parts.pop(); - parts.shift(); - return new RegExp(parts.join('/'), modifiers); - }, - - _initRegExpOptions: function () { - var options = this.options; - if ($.type(options.acceptFileTypes) === 'string') { - options.acceptFileTypes = this._stringToRegExp( - options.acceptFileTypes - ); - } - if ($.type(options.previewSourceFileTypes) === 'string') { - options.previewSourceFileTypes = this._stringToRegExp( - options.previewSourceFileTypes - ); - } - }, - - _initSpecialOptions: function () { - parentWidget.prototype._initSpecialOptions.call(this); - this._initFilesContainer(); - this._initTemplates(); - this._initRegExpOptions(); - }, - - _create: function () { - parentWidget.prototype._create.call(this); - this._refreshOptionsList.push( - 'filesContainer', - 'uploadTemplateId', - 'downloadTemplateId' - ); - if (!$.blueimpFP) { - this._processingQueue = $.Deferred().resolveWith(this).promise(); - this.process = function () { - return this._processingQueue; - }; - } - }, - - enable: function () { - var wasDisabled = false; - if (this.options.disabled) { - wasDisabled = true; - } - parentWidget.prototype.enable.call(this); - if (wasDisabled) { - this.element.find('input, button').prop('disabled', false); - this._enableFileInputButton(); - } - }, - - disable: function () { - if (!this.options.disabled) { - this.element.find('input, button').prop('disabled', true); - this._disableFileInputButton(); + }; + if (data.url) { + data.dataType = data.dataType || that.options.dataType; + $.ajax(data) + .done(removeNode) + .fail(function () { + that._trigger('destroyfailed', e, data); + }); + } else { + removeNode(); + } + } + }, + + _resetFinishedDeferreds: function () { + this._finishedUploads = []; + }, + + _addFinishedDeferreds: function (deferred) { + // eslint-disable-next-line new-cap + var promise = deferred || $.Deferred(); + this._finishedUploads.push(promise); + return promise; + }, + + _getFinishedDeferreds: function () { + return this._finishedUploads; + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = link.prop('download'), + type = 'application/octet-stream'; + link.on('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (ignore) { + // Ignore exceptions + } + }); + }, + + _formatFileSize: function (bytes) { + if (typeof bytes !== 'number') { + return ''; + } + if (bytes >= 1000000000) { + return (bytes / 1000000000).toFixed(2) + ' GB'; + } + if (bytes >= 1000000) { + return (bytes / 1000000).toFixed(2) + ' MB'; + } + return (bytes / 1000).toFixed(2) + ' KB'; + }, + + _formatBitrate: function (bits) { + if (typeof bits !== 'number') { + return ''; + } + if (bits >= 1000000000) { + return (bits / 1000000000).toFixed(2) + ' Gbit/s'; + } + if (bits >= 1000000) { + return (bits / 1000000).toFixed(2) + ' Mbit/s'; + } + if (bits >= 1000) { + return (bits / 1000).toFixed(2) + ' kbit/s'; + } + return bits.toFixed(2) + ' bit/s'; + }, + + _formatTime: function (seconds) { + var date = new Date(seconds * 1000), + days = Math.floor(seconds / 86400); + days = days ? days + 'd ' : ''; + return ( + days + + ('0' + date.getUTCHours()).slice(-2) + + ':' + + ('0' + date.getUTCMinutes()).slice(-2) + + ':' + + ('0' + date.getUTCSeconds()).slice(-2) + ); + }, + + _formatPercentage: function (floatValue) { + return (floatValue * 100).toFixed(2) + ' %'; + }, + + _renderExtendedProgress: function (data) { + return ( + this._formatBitrate(data.bitrate) + + ' | ' + + this._formatTime(((data.total - data.loaded) * 8) / data.bitrate) + + ' | ' + + this._formatPercentage(data.loaded / data.total) + + ' | ' + + this._formatFileSize(data.loaded) + + ' / ' + + this._formatFileSize(data.total) + ); + }, + + _renderTemplate: function (func, files) { + if (!func) { + return $(); + } + var result = func({ + files: files, + formatFileSize: this._formatFileSize, + options: this.options + }); + if (result instanceof $) { + return result; + } + return $(this.options.templatesContainer).html(result).children(); + }, + + _renderPreviews: function (data) { + data.context.find('.preview').each(function (index, elm) { + $(elm).empty().append(data.files[index].preview); + }); + }, + + _renderUpload: function (files) { + return this._renderTemplate(this.options.uploadTemplate, files); + }, + + _renderDownload: function (files) { + return this._renderTemplate(this.options.downloadTemplate, files) + .find('a[download]') + .each(this._enableDragToDesktop) + .end(); + }, + + _editHandler: function (e) { + e.preventDefault(); + if (!this.options.edit) return; + var that = this, + button = $(e.currentTarget), + template = button.closest('.template-upload'), + data = template.data('data'), + index = button.data().index; + this.options.edit(data.files[index]).then(function (file) { + if (!file) return; + data.files[index] = file; + data.context.addClass('processing'); + template.find('.edit,.start').prop('disabled', true); + $(that.element) + .fileupload('process', data) + .always(function () { + template + .find('.size') + .text(that._formatFileSize(data.files[index].size)); + data.context.removeClass('processing'); + that._renderPreviews(data); + }) + .done(function () { + template.find('.edit,.start').prop('disabled', false); + }) + .fail(function () { + template.find('.edit').prop('disabled', false); + var error = data.files[index].error; + if (error) { + template.find('.error').text(error); } - parentWidget.prototype.disable.call(this); + }); + }); + }, + + _startHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget), + template = button.closest('.template-upload'), + data = template.data('data'); + button.prop('disabled', true); + if (data && data.submit) { + data.submit(); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var template = $(e.currentTarget).closest( + '.template-upload,.template-download' + ), + data = template.data('data') || {}; + data.context = data.context || template; + if (data.abort) { + data.abort(); + } else { + data.errorThrown = 'abort'; + this._trigger('fail', e, data); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget); + this._trigger( + 'destroy', + e, + $.extend( + { + context: button.closest('.template-download'), + type: 'DELETE' + }, + button.data() + ) + ); + }, + + _forceReflow: function (node) { + return $.support.transition && node.length && node[0].offsetWidth; + }, + + _transition: function (node) { + // eslint-disable-next-line new-cap + var dfd = $.Deferred(); + if ( + $.support.transition && + node.hasClass('fade') && + node.is(':visible') + ) { + var transitionEndHandler = function (e) { + // Make sure we don't respond to other transition events + // in the container element, e.g. from button elements: + if (e.target === node[0]) { + node.off($.support.transition.end, transitionEndHandler); + dfd.resolveWith(node); + } + }; + node + .on($.support.transition.end, transitionEndHandler) + .toggleClass(this.options.showElementClass); + } else { + node.toggleClass(this.options.showElementClass); + dfd.resolveWith(node); + } + return dfd; + }, + + _initButtonBarEventHandlers: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this.options.filesContainer; + this._on(fileUploadButtonBar.find('.start'), { + click: function (e) { + e.preventDefault(); + filesList.find('.start').trigger('click'); } - - }); - -})); + }); + this._on(fileUploadButtonBar.find('.cancel'), { + click: function (e) { + e.preventDefault(); + filesList.find('.cancel').trigger('click'); + } + }); + this._on(fileUploadButtonBar.find('.delete'), { + click: function (e) { + e.preventDefault(); + filesList + .find('.toggle:checked') + .closest('.template-download') + .find('.delete') + .trigger('click'); + fileUploadButtonBar.find('.toggle').prop('checked', false); + } + }); + this._on(fileUploadButtonBar.find('.toggle'), { + change: function (e) { + filesList + .find('.toggle') + .prop('checked', $(e.currentTarget).is(':checked')); + } + }); + }, + + _destroyButtonBarEventHandlers: function () { + this._off( + this.element + .find('.fileupload-buttonbar') + .find('.start, .cancel, .delete'), + 'click' + ); + this._off(this.element.find('.fileupload-buttonbar .toggle'), 'change.'); + }, + + _initEventHandlers: function () { + this._super(); + this._on(this.options.filesContainer, { + 'click .edit': this._editHandler, + 'click .start': this._startHandler, + 'click .cancel': this._cancelHandler, + 'click .delete': this._deleteHandler + }); + this._initButtonBarEventHandlers(); + }, + + _destroyEventHandlers: function () { + this._destroyButtonBarEventHandlers(); + this._off(this.options.filesContainer, 'click'); + this._super(); + }, + + _enableFileInputButton: function () { + this.element + .find('.fileinput-button input') + .prop('disabled', false) + .parent() + .removeClass('disabled'); + }, + + _disableFileInputButton: function () { + this.element + .find('.fileinput-button input') + .prop('disabled', true) + .parent() + .addClass('disabled'); + }, + + _initTemplates: function () { + var options = this.options; + options.templatesContainer = this.document[0].createElement( + options.filesContainer.prop('nodeName') + ); + if (tmpl) { + if (options.uploadTemplateId) { + options.uploadTemplate = tmpl(options.uploadTemplateId); + } + if (options.downloadTemplateId) { + options.downloadTemplate = tmpl(options.downloadTemplateId); + } + } + }, + + _initFilesContainer: function () { + var options = this.options; + if (options.filesContainer === undefined) { + options.filesContainer = this.element.find('.files'); + } else if (!(options.filesContainer instanceof $)) { + options.filesContainer = $(options.filesContainer); + } + }, + + _initSpecialOptions: function () { + this._super(); + this._initFilesContainer(); + this._initTemplates(); + }, + + _create: function () { + this._super(); + this._resetFinishedDeferreds(); + if (!$.support.fileInput) { + this._disableFileInputButton(); + } + }, + + enable: function () { + var wasDisabled = false; + if (this.options.disabled) { + wasDisabled = true; + } + this._super(); + if (wasDisabled) { + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + } + }, + + disable: function () { + if (!this.options.disabled) { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + } + this._super(); + } + }); +}); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-validate.js b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js new file mode 100644 index 0000000000000..a277efc46d774 --- /dev/null +++ b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js @@ -0,0 +1,119 @@ +/* + * jQuery File Upload Validation Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', './jquery.fileupload-process'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery'), require('./jquery.fileupload-process')); + } else { + // Browser globals: + factory(window.jQuery); + } +})(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push({ + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + }); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + validate: function (data, options) { + if (options.disabled) { + return data; + } + // eslint-disable-next-line new-cap + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index], + fileSize; + if (options.minFileSize || options.maxFileSize) { + fileSize = file.size; + } + if ( + $.type(options.maxNumberOfFiles) === 'number' && + (settings.getNumberOfFiles() || 0) + data.files.length > + options.maxNumberOfFiles + ) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if ( + options.acceptFileTypes && + !( + options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name) + ) + ) { + file.error = settings.i18n('acceptFileTypes'); + } else if (fileSize > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ( + $.type(fileSize) === 'number' && + fileSize < options.minFileSize + ) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + } + }); +}); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-video.js b/lib/web/jquery/fileUploader/jquery.fileupload-video.js new file mode 100644 index 0000000000000..5dc78f36bb829 --- /dev/null +++ b/lib/web/jquery/fileUploader/jquery.fileupload-video.js @@ -0,0 +1,101 @@ +/* + * jQuery File Upload Video Preview Plugin + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, require */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', 'load-image', './jquery.fileupload-process'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('blueimp-load-image/js/load-image'), + require('./jquery.fileupload-process') + ); + } else { + // Browser globals: + factory(window.jQuery, window.loadImage); + } +})(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadVideo', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableVideoPreview' + }, + { + action: 'setVideo', + name: '@videoPreviewName', + disabled: '@disableVideoPreview' + } + ); + + // The File Upload Video Preview plugin extends the fileupload widget + // with video preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + options: { + // The regular expression for the types of video files to load, + // matched against the file type: + loadVideoFileTypes: /^video\/.*$/ + }, + + _videoElement: document.createElement('video'), + + processActions: { + // Loads the video file given via data.files and data.index + // as video element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadVideo: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + video; + if ( + this._videoElement.canPlayType && + this._videoElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || options.fileTypes.test(file.type)) + ) { + url = loadImage.createObjectURL(file); + if (url) { + video = this._videoElement.cloneNode(false); + video.src = url; + video.controls = true; + data.video = video; + return data; + } + } + return data; + }, + + // Sets the video element as a property of the file object: + setVideo: function (data, options) { + if (data.video && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.video; + } + return data; + } + } + }); +}); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload.js b/lib/web/jquery/fileUploader/jquery.fileupload.js index 676f8aa1e8058..184d347216409 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload.js @@ -1,1081 +1,1606 @@ /* - * jQuery File Upload Plugin 5.16.4 + * jQuery File Upload Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2010, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT + * https://opensource.org/licenses/MIT */ -/*jslint nomen: true, unparam: true, regexp: true */ -/*global define, window, document, Blob, FormData, location */ +/* global define, require */ +/* eslint-disable new-cap */ (function (factory) { - 'use strict'; - if (typeof define === 'function' && define.amd) { - // Register as an anonymous AMD module: - define([ - 'jquery', - 'jquery-ui-modules/widget', - 'jquery/fileUploader/jquery.iframe-transport' - ], factory); - } else { - // Browser globals: - factory(window.jQuery); - } -}(function ($) { - 'use strict'; - - // The FileReader API is not actually used, but works as feature detection, - // as e.g. Safari supports XHR file uploads via the FormData API, - // but not non-multipart XHR file uploads: - $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader); - $.support.xhrFormDataFileUpload = !!window.FormData; - - // The fileupload widget listens for change events on file input fields defined - // via fileInput setting and paste or drop events of the given dropZone. - // In addition to the default jQuery Widget methods, the fileupload widget - // exposes the "add" and "send" methods, to add or directly send files using - // the fileupload API. - // By default, files added via file input selection, paste, drag & drop or - // "add" method are uploaded immediately, but it is possible to override - // the "add" callback option to queue file uploads. - $.widget('blueimp.fileupload', { - - options: { - // The namespace used for event handler binding on the dropZone and - // fileInput collections. - // If not set, the name of the widget ("fileupload") is used. - namespace: undefined, - // The drop target collection, by the default the complete document. - // Set to null or an empty collection to disable drag & drop support: - dropZone: $(document), - // The file input field collection, that is listened for change events. - // If undefined, it is set to the file input fields inside - // of the widget element on plugin initialization. - // Set to null or an empty collection to disable the change listener. - fileInput: undefined, - // By default, the file input field is replaced with a clone after - // each input field change event. This is required for iframe transport - // queues and allows change events to be fired for the same file - // selection, but can be disabled by setting the following option to false: - replaceFileInput: true, - // The parameter name for the file form data (the request argument name). - // If undefined or empty, the name property of the file input field is - // used, or "files[]" if the file input name property is also empty, - // can be a string or an array of strings: - paramName: undefined, - // By default, each file of a selection is uploaded using an individual - // request for XHR type uploads. Set to false to upload file - // selections in one request each: - singleFileUploads: true, - // To limit the number of files uploaded with one XHR request, - // set the following option to an integer greater than 0: - limitMultiFileUploads: undefined, - // Set the following option to true to issue all file upload requests - // in a sequential order: - sequentialUploads: false, - // To limit the number of concurrent uploads, - // set the following option to an integer greater than 0: - limitConcurrentUploads: undefined, - // Set the following option to true to force iframe transport uploads: - forceIframeTransport: false, - // Set the following option to the location of a redirect url on the - // origin server, for cross-domain iframe transport uploads: - redirect: undefined, - // The parameter name for the redirect url, sent as part of the form - // data and set to 'redirect' if this option is empty: - redirectParamName: undefined, - // Set the following option to the location of a postMessage window, - // to enable postMessage transport uploads: - postMessage: undefined, - // By default, XHR file uploads are sent as multipart/form-data. - // The iframe transport is always using multipart/form-data. - // Set to false to enable non-multipart XHR uploads: - multipart: true, - // To upload large files in smaller chunks, set the following option - // to a preferred maximum chunk size. If set to 0, null or undefined, - // or the browser does not support the required Blob API, files will - // be uploaded as a whole. - maxChunkSize: undefined, - // When a non-multipart upload or a chunked multipart upload has been - // aborted, this option can be used to resume the upload by setting - // it to the size of the already uploaded bytes. This option is most - // useful when modifying the options object inside of the "add" or - // "send" callbacks, as the options are cloned for each file upload. - uploadedBytes: undefined, - // By default, failed (abort or error) file uploads are removed from the - // global progress calculation. Set the following option to false to - // prevent recalculating the global progress data: - recalculateProgress: true, - // Interval in milliseconds to calculate and trigger progress events: - progressInterval: 100, - // Interval in milliseconds to calculate progress bitrate: - bitrateInterval: 500, - - // Additional form data to be sent along with the file uploads can be set - // using this option, which accepts an array of objects with name and - // value properties, a function returning such an array, a FormData - // object (for XHR file uploads), or a simple object. - // The form of the first fileInput is given as parameter to the function: - formData: function (form) { - return form.serializeArray(); - }, - - // The add callback is invoked as soon as files are added to the fileupload - // widget (via file input selection, drag & drop, paste or add API call). - // If the singleFileUploads option is enabled, this callback will be - // called once for each file in the selection for XHR file uplaods, else - // once for each file selection. - // The upload starts when the submit method is invoked on the data parameter. - // The data object contains a files property holding the added files - // and allows to override plugin options as well as define ajax settings. - // Listeners for this callback can also be bound the following way: - // .bind('fileuploadadd', func); - // data.submit() returns a Promise object and allows to attach additional - // handlers using jQuery's Deferred callbacks: - // data.submit().done(func).fail(func).always(func); - add: function (e, data) { - data.submit(); - }, - - // Other callbacks: - // Callback for the submit event of each file upload: - // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); - // Callback for the start of each file upload request: - // send: function (e, data) {}, // .bind('fileuploadsend', func); - // Callback for successful uploads: - // done: function (e, data) {}, // .bind('fileuploaddone', func); - // Callback for failed (abort or error) uploads: - // fail: function (e, data) {}, // .bind('fileuploadfail', func); - // Callback for completed (success, abort or error) requests: - // always: function (e, data) {}, // .bind('fileuploadalways', func); - // Callback for upload progress events: - // progress: function (e, data) {}, // .bind('fileuploadprogress', func); - // Callback for global upload progress events: - // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); - // Callback for uploads start, equivalent to the global ajaxStart event: - // start: function (e) {}, // .bind('fileuploadstart', func); - // Callback for uploads stop, equivalent to the global ajaxStop event: - // stop: function (e) {}, // .bind('fileuploadstop', func); - // Callback for change events of the fileInput collection: - // change: function (e, data) {}, // .bind('fileuploadchange', func); - // Callback for paste events to the dropZone collection: - // paste: function (e, data) {}, // .bind('fileuploadpaste', func); - // Callback for drop events of the dropZone collection: - // drop: function (e, data) {}, // .bind('fileuploaddrop', func); - // Callback for dragover events of the dropZone collection: - // dragover: function (e) {}, // .bind('fileuploaddragover', func); - - // The plugin options are used as settings object for the ajax calls. - // The following are jQuery ajax settings required for the file uploads: - processData: false, - contentType: false, - cache: false - }, + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', 'jquery-ui/ui/widget'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery'), require('./vendor/jquery.ui.widget')); + } else { + // Browser globals: + factory(window.jQuery); + } +})(function ($) { + 'use strict'; - // A list of options that require a refresh after assigning a new value: - _refreshOptionsList: [ - 'namespace', - 'dropZone', - 'fileInput', - 'multipart', - 'forceIframeTransport' - ], - - _BitrateTimer: function () { - this.timestamp = +(new Date()); - this.loaded = 0; - this.bitrate = 0; - this.getBitrate = function (now, loaded, interval) { - var timeDiff = now - this.timestamp; - if (!this.bitrate || !interval || timeDiff > interval) { - this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; - this.loaded = loaded; - this.timestamp = now; - } - return this.bitrate; - }; - }, + // Detect file input support, based on + // https://viljamis.com/2012/file-upload-support-on-mobile/ + $.support.fileInput = !( + new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('<input type="file"/>').prop('disabled') + ); - _isXHRUpload: function (options) { - return !options.forceIframeTransport && - ((!options.multipart && $.support.xhrFileUpload) || - $.support.xhrFormDataFileUpload); - }, + // The FileReader API is not actually used, but works as feature detection, + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; - _getFormData: function (options) { - var formData; - if (typeof options.formData === 'function') { - return options.formData(options.form); - } - if ($.isArray(options.formData)) { - return options.formData; - } - if (options.formData) { - formData = []; - $.each(options.formData, function (name, value) { - formData.push({name: name, value: value}); - }); - return formData; - } - return []; - }, + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = + window.Blob && + (Blob.prototype.slice || + Blob.prototype.webkitSlice || + Blob.prototype.mozSlice); - _getTotal: function (files) { - var total = 0; - $.each(files, function (index, file) { - total += file.size || 1; - }); - return total; - }, + /** + * Helper function to create drag handlers for dragover/dragenter/dragleave + * + * @param {string} type Event type + * @returns {Function} Drag handler + */ + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if ( + dataTransfer && + $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger(type, $.Event(type, { delegatedEvent: e })) !== false + ) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + // By default, duplicate file names are expected to be handled on + // the server-side. If this is not possible (e.g. when uploading + // files directly to Amazon S3), the following option can be set to + // an empty object or an object mapping existing filenames, e.g.: + // { "image.jpg": true, "image (1).jpg": true } + // If it is set, all files will be uploaded with unique filenames, + // adding increasing number suffixes if necessary, e.g.: + // "image (2).jpg" + uniqueFilenames: undefined, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + // eslint-disable-next-line no-param-reassign + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + // eslint-disable-next-line no-param-reassign + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .on('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + if ( + data.autoUpload || + (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload')) + ) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .on('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .on('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .on('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .on('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .on('fileuploadalways', func); - _onProgress: function (e, data) { - if (e.lengthComputable) { - var now = +(new Date()), - total, - loaded; - if (data._time && data.progressInterval && - (now - data._time < data.progressInterval) && - e.loaded !== e.total) { - return; + // Callback for upload progress events: + // progress: function (e, data) {}, // .on('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .on('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .on('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .on('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .on('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .on('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .on('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .on('fileuploaddragover', func); + + // Callback before the start of each chunk upload request (before form data initialization): + // chunkbeforesend: function (e, data) {}, // .on('fileuploadchunkbeforesend', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .on('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .on('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .on('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .on('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false, + timeout: 0 + }, + + // jQuery versions before 1.8 require promise.pipe if the return value is + // used, as promise.then in older versions has a different behavior, see: + // https://blog.jquery.com/2012/08/09/jquery-1-8-released/ + // https://bugs.jquery.com/ticket/11010 + // https://github.com/blueimp/jQuery-File-Upload/pull/3435 + _promisePipe: (function () { + var parts = $.fn.jquery.split('.'); + return Number(parts[0]) > 1 || Number(parts[1]) > 7 ? 'then' : 'pipe'; + })(), + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: + $.support.blobSlice && + function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = Date.now ? Date.now() : new Date().getTime(); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return ( + !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload) + ); + }, + + _getFormData: function (options) { + var formData; + if ($.type(options.formData) === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({ name: name, value: value }); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (Object.prototype.hasOwnProperty.call(obj._response, prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = Date.now ? Date.now() : new Date().getTime(), + loaded; + if ( + data._time && + data.progressInterval && + now - data._time < data.progressInterval && + e.loaded !== e.total + ) { + return; + } + data._time = now; + loaded = + Math.floor( + (e.loaded / e.total) * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += loaded - data._progress.loaded; + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger( + 'progress', + $.Event('progress', { delegatedEvent: e }), + data + ); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger( + 'progressall', + $.Event('progressall', { delegatedEvent: e }), + this._progress + ); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).on('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _deinitProgressListener: function (options) { + var xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + if (xhr.upload) { + $(xhr.upload).off('progress'); + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _getUniqueFilename: function (name, map) { + // eslint-disable-next-line no-param-reassign + name = String(name); + if (map[name]) { + // eslint-disable-next-line no-param-reassign + name = name.replace(/(?: \(([\d]+)\))?(\.[^.]+)?$/, function ( + _, + p1, + p2 + ) { + var index = p1 ? Number(p1) + 1 : 1; + var ext = p2 || ''; + return ' (' + index + ')' + ext; + }); + return this._getUniqueFilename(name, map); + } + map[name] = true; + return name; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = + $.type(options.paramName) === 'array' + ? options.paramName[0] + : options.paramName; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = + 'attachment; filename="' + + encodeURI(file.uploadName || file.name) + + '"'; + } + if (!multipart) { + options.contentType = file.type || 'application/octet-stream'; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: + ($.type(options.paramName) === 'array' && + options.paramName[index]) || + paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append( + paramName, + options.blob, + file.uploadName || file.name + ); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if ( + that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file) + ) { + var fileName = file.uploadName || file.name; + if (options.uniqueFilenames) { + fileName = that._getUniqueFilename( + fileName, + options.uniqueFilenames + ); } - data._time = now; - total = data.total || this._getTotal(data.files); - loaded = parseInt( - e.loaded / e.total * (data.chunkSize || total), - 10 - ) + (data.uploadedBytes || 0); - this._loaded += loaded - (data.loaded || data.uploadedBytes || 0); - data.lengthComputable = true; - data.loaded = loaded; - data.total = total; - data.bitrate = data._bitrateTimer.getBitrate( - now, - loaded, - data.bitrateInterval + formData.append( + ($.type(options.paramName) === 'array' && + options.paramName[index]) || + paramName, + file, + fileName ); - // Trigger a custom progress event with a total data property set - // to the file size(s) of the current upload and a loaded data - // property calculated accordingly: - this._trigger('progress', e, data); - // Trigger a global progress event for all current file uploads, - // including ajax calls queued for sequential file uploads: - this._trigger('progressall', e, { - lengthComputable: true, - loaded: this._loaded, - total: this._total, - bitrate: this._bitrateTimer.getBitrate( - now, - this._loaded, - data.bitrateInterval - ) - }); - } - }, + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, - _initProgressListener: function (options) { - var that = this, - xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); - // Accesss to the native XHR object is required to add event listeners - // for the upload progress event: - if (xhr.upload) { - $(xhr.upload).bind('progress', function (e) { - var oe = e.originalEvent; - // Make sure the progress event properties get copied over: - e.lengthComputable = oe.lengthComputable; - e.loaded = oe.loaded; - e.total = oe.total; - that._onProgress(e, options); - }); - options.xhr = function () { - return xhr; - }; - } - }, + _initIframeSettings: function (options) { + var targetHost = $('<a></a>').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, - _initXHRData: function (options) { - var formData, - file = options.files[0], - // Ignore non-multipart setting if not supported: - multipart = options.multipart || !$.support.xhrFileUpload, - paramName = options.paramName[0]; - if (!multipart || options.blob) { - // For non-multipart uploads and chunked uploads, - // file meta data is not part of the request body, - // so we transmit this data as part of the HTTP headers. - // For cross domain requests, these headers must be allowed - // via Access-Control-Allow-Headers or removed using - // the beforeSend callback: - options.headers = $.extend(options.headers, { - 'X-File-Name': file.name, - 'X-File-Type': file.type, - 'X-File-Size': file.size - }); - if (!options.blob) { - // Non-chunked non-multipart upload: - options.contentType = file.type; - options.data = file; - } else if (!multipart) { - // Chunked non-multipart upload: - options.contentType = 'application/octet-stream'; - options.data = options.blob; - } - } - if (multipart && $.support.xhrFormDataFileUpload) { - if (options.postMessage) { - // window.postMessage does not allow sending FormData - // objects, so we just add the File/Blob objects to - // the formData array and let the postMessage window - // create the FormData object out of this array: - formData = this._getFormData(options); - if (options.blob) { - formData.push({ - name: paramName, - value: options.blob - }); - } else { - $.each(options.files, function (index, file) { - formData.push({ - name: options.paramName[index] || paramName, - value: file - }); - }); - } - } else { - if (options.formData instanceof FormData) { - formData = options.formData; - } else { - formData = new FormData(); - $.each(this._getFormData(options), function (index, field) { - formData.append(field.name, field.value); - }); - } - if (options.blob) { - formData.append(paramName, options.blob, file.name); - } else { - $.each(options.files, function (index, file) { - // File objects are also Blob instances. - // This check allows the tests to run with - // dummy objects: - if (file instanceof Blob) { - formData.append( - options.paramName[index] || paramName, - file, - file.name - ); - } - }); - } - } - options.data = formData; - } - // Blob reference is not needed anymore, free memory: - options.blob = null; - }, + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, - _initIframeSettings: function (options) { - // Setting the dataType to iframe enables the iframe transport: - options.dataType = 'iframe ' + (options.dataType || ''); - // The iframe transport accepts a serialized array as form data: - options.formData = this._getFormData(options); - // Add redirect url to form data on cross-domain uploads: - if (options.redirect && $('<a></a>').prop('href', options.url) - .prop('host') !== location.host) { - options.formData.push({ - name: options.redirectParamName || 'redirect', - value: options.redirect - }); - } - }, + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, - _initDataSettings: function (options) { - if (this._isXHRUpload(options)) { - if (!this._chunkedUpload(options, true)) { - if (!options.data) { - this._initXHRData(options); - } - this._initProgressListener(options); - } - if (options.postMessage) { - // Setting the dataType to postmessage enables the - // postMessage transport: - options.dataType = 'postmessage ' + (options.dataType || ''); - } - } else { - this._initIframeSettings(options, 'iframe'); - } - }, + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = ( + options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || + '' + ).toUpperCase(); + if ( + options.type !== 'POST' && + options.type !== 'PUT' && + options.type !== 'PATCH' + ) { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, - _getParamName: function (options) { - var fileInput = $(options.fileInput), - paramName = options.paramName; - if (!paramName) { - paramName = []; - fileInput.each(function () { - var input = $(this), - name = input.prop('name') || 'files[]', - i = (input.prop('files') || [1]).length; - while (i) { - paramName.push(name); - i -= 1; - } - }); - if (!paramName.length) { - paramName = [fileInput.prop('name') || 'files[]']; - } - } else if (!$.isArray(paramName)) { - paramName = [paramName]; - } - return paramName; - }, + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, - _initFormSettings: function (options) { - // Retrieve missing options from the input field and the - // associated form, if available: - if (!options.form || !options.form.length) { - options.form = $(options.fileInput.prop('form')); - } - options.paramName = this._getParamName(options); - if (!options.url) { - options.url = options.form.prop('action') || location.href; - } - // The HTTP request method must be "POST" or "PUT": - options.type = (options.type || options.form.prop('method') || '') - .toUpperCase(); - if (options.type !== 'POST' && options.type !== 'PUT') { - options.type = 'POST'; - } - if (!options.formAcceptCharset) { - options.formAcceptCharset = options.form.attr('accept-charset'); - } - }, + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, - _getAJAXSettings: function (data) { - var options = $.extend({}, this.options, data); - this._initFormSettings(options); - this._initDataSettings(options); - return options; - }, + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, - // Maps jqXHR callbacks to the equivalent - // methods of the given Promise object: - _enhancePromise: function (promise) { - promise.success = promise.done; - promise.error = promise.fail; - promise.complete = promise.always; - return promise; - }, + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + // eslint-disable-next-line no-param-reassign + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, - // Creates and returns a Promise object enhanced with - // the jqXHR methods abort, success, error and complete: - _getXHRPromise: function (resolveOrReject, context, args) { - var dfd = $.Deferred(), - promise = dfd.promise(); - context = context || this.options.context || promise; - if (resolveOrReject === true) { - dfd.resolveWith(context, args); - } else if (resolveOrReject === false) { - dfd.rejectWith(context, args); - } - promise.abort = dfd.promise; - return this._enhancePromise(promise); - }, + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = (this._processQueue || + getPromise([this])) + [that._promisePipe](function () { + if (data.errorThrown) { + return $.Deferred().rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + }) + [that._promisePipe](resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + that._trigger( + 'submit', + $.Event('submit', { delegatedEvent: e }), + this + ) !== false && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return ( + !this.jqXHR && + this._processQueue && + that._getDeferredState(this._processQueue) === 'pending' + ); + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, - // Uploads a file in multiple, sequential requests - // by splitting the file up in multiple blob chunks. - // If the second parameter is true, only tests if the file - // should be uploaded in chunks, but does not invoke any - // upload requests: - _chunkedUpload: function (options, testOnly) { - var that = this, - file = options.files[0], - fs = file.size, - ub = options.uploadedBytes = options.uploadedBytes || 0, - mcs = options.maxChunkSize || fs, - // Use the Blob methods with the slice implementation - // according to the W3C Blob API specification: - slice = file.webkitSlice || file.mozSlice || file.slice, - upload, - n, - jqXHR, - pipe; - if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || - options.data) { - return false; - } - if (testOnly) { - return true; + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if ( + !( + this._isXHRUpload(options) && + slice && + (ub || ($.type(mcs) === 'function' ? mcs(options) : mcs) < fs) + ) || + options.data + ) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise(false, options.context, [ + null, + 'error', + file.error + ]); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + ($.type(mcs) === 'function' ? mcs(o) : mcs), + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = + 'bytes ' + ub + '-' + (ub + o.chunkSize - 1) + '/' + fs; + // Trigger chunkbeforesend to allow form data to be updated for this chunk + that._trigger('chunkbeforesend', null, o); + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ( + (that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context) + ) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || ub + o.chunkSize; + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress( + $.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), + o + ); } - if (ub >= fs) { - file.error = 'uploadedBytes'; - return this._getXHRPromise( - false, - options.context, - [null, 'error', file.error] - ); + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith(o.context, [result, textStatus, jqXHR]); } - // n is the number of blobs to upload, - // calculated via filesize, uploaded bytes and max chunk size: - n = Math.ceil((fs - ub) / mcs); - // The chunk upload method accepting the chunk number as parameter: - upload = function (i) { - if (!i) { - return that._getXHRPromise(true, options.context); - } - // Upload the blobs in sequential order: - return upload(i -= 1).pipe(function () { - // Clone the options object for each chunk upload: - var o = $.extend({}, options); - o.blob = slice.call( - file, - ub + i * mcs, - ub + (i + 1) * mcs - ); - // Expose the chunk index: - o.chunkIndex = i; - // Expose the number of chunks: - o.chunksNumber = n; - // Store the current chunk size, as the blob itself - // will be dereferenced after data processing: - o.chunkSize = o.blob.size; - // Process the upload data (the blob and potential form data): - that._initXHRData(o); - // Add progress listeners for this chunk upload: - that._initProgressListener(o); - jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context)) - .done(function () { - // Create a progress event if upload is done and - // no progress event has been invoked for this chunk: - if (!o.loaded) { - that._onProgress($.Event('progress', { - lengthComputable: true, - loaded: o.chunkSize, - total: o.chunkSize - }), o); - } - options.uploadedBytes = o.uploadedBytes += - o.chunkSize; - }); - return jqXHR; - }); - }; - // Return the piped Promise object, enhanced with an abort method, - // which is delegated to the jqXHR object of the current upload, - // and jqXHR callbacks mapped to the equivalent Promise methods: - pipe = upload(n); - pipe.abort = function () { - return jqXHR.abort(); - }; - return this._enhancePromise(pipe); - }, + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith(o.context, [jqXHR, textStatus, errorThrown]); + }) + .always(function () { + that._deinitProgressListener(o); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, - _beforeSend: function (e, data) { - if (this._active === 0) { - // the start callback is triggered when an upload starts - // and no other uploads are currently running, - // equivalent to the global ajaxStart event: - this._trigger('start'); - // Set timer for global bitrate progress calculation: - this._bitrateTimer = new this._BitrateTimer(); - } - this._active += 1; - // Initialize the global progress values: - this._loaded += data.uploadedBytes || 0; - this._total += this._getTotal(data.files); - }, + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, - _onDone: function (result, textStatus, jqXHR, options) { - if (!this._isXHRUpload(options)) { - // Create a progress event for each iframe load: - this._onProgress($.Event('progress', { - lengthComputable: true, - loaded: 1, - total: 1 - }), options); - } - options.result = result; - options.textStatus = textStatus; - options.jqXHR = jqXHR; - this._trigger('done', null, options); - }, + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress( + $.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), + options + ); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, - _onFail: function (jqXHR, textStatus, errorThrown, options) { - options.jqXHR = jqXHR; - options.textStatus = textStatus; - options.errorThrown = errorThrown; - this._trigger('fail', null, options); - if (options.recalculateProgress) { - // Remove the failed (error or abort) file upload from - // the global progress calculation: - this._loaded -= options.loaded || options.uploadedBytes || 0; - this._total -= options.total || this._getTotal(options.files); - } - }, + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, - _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { - this._active -= 1; - options.textStatus = textStatus; - if (jqXHRorError && jqXHRorError.always) { - options.jqXHR = jqXHRorError; - options.result = jqXHRorResult; - } else { - options.jqXHR = jqXHRorResult; - options.errorThrown = jqXHRorError; - } - this._trigger('always', null, options); - if (this._active === 0) { - // The stop callback is triggered when all uploads have - // been completed, equivalent to the global ajaxStop event: - this._trigger('stop'); - // Reset the global progress values: - this._loaded = this._total = 0; - this._bitrateTimer = null; - } - }, + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, - _onSend: function (e, data) { - var that = this, - jqXHR, - slot, - pipe, - options = that._getAJAXSettings(data), - send = function (resolve, args) { - that._sending += 1; - // Set timer for bitrate progress calculation: - options._bitrateTimer = new that._BitrateTimer(); - jqXHR = jqXHR || ( - (resolve !== false && - that._trigger('send', e, options) !== false && - (that._chunkedUpload(options) || $.ajax(options))) || - that._getXHRPromise(false, options.context, args) - ).done(function (result, textStatus, jqXHR) { - that._onDone(result, textStatus, jqXHR, options); - }).fail(function (jqXHR, textStatus, errorThrown) { - that._onFail(jqXHR, textStatus, errorThrown, options); - }).always(function (jqXHRorResult, textStatus, jqXHRorError) { - that._sending -= 1; - that._onAlways( - jqXHRorResult, - textStatus, - jqXHRorError, - options - ); - if (options.limitConcurrentUploads && - options.limitConcurrentUploads > that._sending) { - // Start the next queued upload, - // that has not been aborted: - var nextSlot = that._slots.shift(), - isPending; - while (nextSlot) { - // jQuery 1.6 doesn't provide .state(), - // while jQuery 1.8+ removed .isRejected(): - isPending = nextSlot.state ? - nextSlot.state() === 'pending' : - !nextSlot.isRejected(); - if (isPending) { - nextSlot.resolve(); - break; - } - nextSlot = that._slots.shift(); - } - } - }); - return jqXHR; - }; - this._beforeSend(e, options); - if (this.options.sequentialUploads || - (this.options.limitConcurrentUploads && - this.options.limitConcurrentUploads <= this._sending)) { - if (this.options.limitConcurrentUploads > 1) { - slot = $.Deferred(); - this._slots.push(slot); - pipe = slot.pipe(send); - } else { - pipe = (this._sequence = this._sequence.pipe(send, send)); - } - // Return the piped Promise object, enhanced with an abort method, - // which is delegated to the jqXHR object of the current upload, - // and jqXHR callbacks mapped to the equivalent Promise methods: - pipe.abort = function () { - var args = [undefined, 'abort', 'abort']; - if (!jqXHR) { - if (slot) { - slot.rejectWith(pipe, args); - } - return send(false, args); + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = + jqXHR || + ( + ((aborted || + that._trigger( + 'send', + $.Event('send', { delegatedEvent: e }), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || + $.ajax(options) + ) + .done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }) + .fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }) + .always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._deinitProgressListener(options); + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if ( + options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending + ) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; } - return jqXHR.abort(); - }; - return this._enhancePromise(pipe); + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if ( + this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending) + ) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot[that._promisePipe](send); + } else { + this._sequence = this._sequence[that._promisePipe](send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); } return send(); - }, + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, - _onAdd: function (e, data) { - var that = this, - result = true, - options = $.extend({}, this.options, data), - limit = options.limitMultiFileUploads, - paramName = this._getParamName(options), - paramNameSet, - paramNameSlice, - fileSet, - i; - if (!(options.singleFileUploads || limit) || - !this._isXHRUpload(options)) { - fileSet = [data.files]; - paramNameSet = [paramName]; - } else if (!options.singleFileUploads && limit) { - fileSet = []; - paramNameSet = []; - for (i = 0; i < data.files.length; i += limit) { - fileSet.push(data.files.slice(i, i + limit)); - paramNameSlice = paramName.slice(i, i + limit); - if (!paramNameSlice.length) { - paramNameSlice = paramName; - } - paramNameSet.push(paramNameSlice); - } - } else { - paramNameSet = paramName; + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, + limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i, + j = 0; + if (!filesLength) { + return false; + } + if (limitSize && files[0].size === undefined) { + limitSize = undefined; + } + if ( + !(options.singleFileUploads || limit || limitSize) || + !this._isXHRUpload(options) + ) { + fileSet = [files]; + paramNameSet = [paramName]; + } else if (!(options.singleFileUploads || limitSize) && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if ( + i + 1 === filesLength || + batchSize + files[i + 1].size + overhead > limitSize || + (limit && i + 1 - j >= limit) + ) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; } - data.originalFiles = data.files; - $.each(fileSet || data.files, function (index, element) { - var newData = $.extend({}, data); - newData.files = fileSet ? element : [element]; - newData.paramName = paramNameSet[index]; - newData.submit = function () { - newData.jqXHR = this.jqXHR = - (that._trigger('submit', e, this) !== false) && - that._onSend(e, this); - return this.jqXHR; - }; - return (result = that._trigger('add', e, newData)); - }); - return result; - }, + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } + } else { + paramNameSet = paramName; + } + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', { delegatedEvent: e }), + newData + ); + return result; + }); + return result; + }, - _replaceFileInput: function (input) { - var inputClone = input.clone(true); - $('<form></form>').append(inputClone)[0].reset(); - // Detaching allows to insert the fileInput on another form - // without loosing the file input value: - input.after(inputClone).detach(); - // Avoid memory leaks with the detached file input: - $.cleanData(input.unbind('remove')); - // Replace the original file input element in the fileInput - // collection with the clone, which has been copied including - // event handlers: - this.options.fileInput = this.options.fileInput.map(function (i, el) { - if (el === input[0]) { - return inputClone[0]; - } - return el; - }); - // If the widget has been initialized on the file input itself, - // override this.element with the file input clone: - if (input[0] === this.element[0]) { - this.element = inputClone; - } - }, + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true), + restoreFocus = input.is(document.activeElement); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; + $('<form></form>').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // If the fileInput had focus before it was detached, + // restore focus to the inputClone. + if (restoreFocus) { + inputClone.trigger('focus'); + } + // Avoid memory leaks with the detached file input: + $.cleanData(input.off('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, - _handleFileTreeEntry: function (entry, path) { - var that = this, - dfd = $.Deferred(), - errorHandler = function () { - dfd.reject(); - }, - dirReader; - path = path || ''; - if (entry.isFile) { - entry.file(function (file) { - file.relativePath = path; - dfd.resolve(file); - }, errorHandler); - } else if (entry.isDirectory) { - dirReader = entry.createReader(); - dirReader.readEntries(function (entries) { - that._handleFileTreeEntries( - entries, - path + entry.name + '/' - ).done(function (files) { - dfd.resolve(files); - }).fail(errorHandler); - }, errorHandler); + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + entries = [], + dirReader, + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + successHandler = function (entries) { + that + ._handleFileTreeEntries(entries, path + entry.name + '/') + .done(function (files) { + dfd.resolve(files); + }) + .fail(errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); } else { - errorHandler(); + entries = entries.concat(results); + readEntries(); } - return dfd.promise(); - }, + }, errorHandler); + }; + // eslint-disable-next-line no-param-reassign + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + readEntries(); + } else { + // Return an empty list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, - _handleFileTreeEntries: function (entries, path) { - var that = this; - return $.when.apply( - $, - $.map(entries, function (entry) { - return that._handleFileTreeEntry(entry, path); - }) - ).pipe(function () { - return Array.prototype.concat.apply( - [], - arguments - ); - }); - }, + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when + .apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ) + [this._promisePipe](function () { + return Array.prototype.concat.apply([], arguments); + }); + }, - _getDroppedFiles: function (dataTransfer) { - dataTransfer = dataTransfer || {}; - var items = dataTransfer.items; - if (items && items.length && (items[0].webkitGetAsEntry || - items[0].getAsEntry)) { - return this._handleFileTreeEntries( - $.map(items, function (item) { - if (item.webkitGetAsEntry) { - return item.webkitGetAsEntry(); - } - return item.getAsEntry(); - }) - ); + _getDroppedFiles: function (dataTransfer) { + // eslint-disable-next-line no-param-reassign + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if ( + items && + items.length && + (items[0].webkitGetAsEntry || items[0].getAsEntry) + ) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; } - return $.Deferred().resolve( - $.makeArray(dataTransfer.files) - ).promise(); - }, + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve($.makeArray(dataTransfer.files)).promise(); + }, - _getFileInputFiles: function (fileInput) { - fileInput = $(fileInput); - var entries = fileInput.prop('webkitEntries') || - fileInput.prop('entries'), - files, - value; - if (entries && entries.length) { - return this._handleFileTreeEntries(entries); - } - files = $.makeArray(fileInput.prop('files')); - if (!files.length) { - value = fileInput.prop('value'); - if (!value) { - return $.Deferred().reject([]).promise(); - } - // If the files property is not available, the browser does not - // support the File API and we add a pseudo File object with - // the input value as name with path information removed: - files = [{name: value.replace(/^.*\\/, '')}]; - } - return $.Deferred().resolve(files).promise(); - }, + _getSingleFileInputFiles: function (fileInput) { + // eslint-disable-next-line no-param-reassign + fileInput = $(fileInput); + var entries = + fileInput.prop('webkitEntries') || fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{ name: value.replace(/^.*\\/, '') }]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, - _onChange: function (e) { - var that = e.data.fileupload, - data = { - fileInput: $(e.target), - form: $(e.target.form) - }; - that._getFileInputFiles(data.fileInput).always(function (files) { - data.files = files; - if (that.options.replaceFileInput) { - that._replaceFileInput(data.fileInput); - } - if (that._trigger('change', e, data) !== false) { - that._onAdd(e, data); - } - }); - }, + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when + .apply($, $.map(fileInput, this._getSingleFileInputFiles)) + [this._promisePipe](function () { + return Array.prototype.concat.apply([], arguments); + }); + }, - _onPaste: function (e) { - var that = e.data.fileupload, - cbd = e.originalEvent.clipboardData, - items = (cbd && cbd.items) || [], - data = {files: []}; - $.each(items, function (index, item) { - var file = item.getAsFile && item.getAsFile(); - if (file) { - data.files.push(file); - } - }); - if (that._trigger('paste', e, data) === false || - that._onAdd(e, data) === false) { - return false; - } - }, + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data); + } + if ( + that._trigger( + 'change', + $.Event('change', { delegatedEvent: e }), + data + ) !== false + ) { + that._onAdd(e, data); + } + }); + }, - _onDrop: function (e) { - e.preventDefault(); - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer, - data = {}; - that._getDroppedFiles(dataTransfer).always(function (files) { - data.files = files; - if (that._trigger('drop', e, data) !== false) { - that._onAdd(e, data); - } - }); - }, + _onPaste: function (e) { + var items = + e.originalEvent && + e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = { files: [] }; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if ( + this._trigger( + 'paste', + $.Event('paste', { delegatedEvent: e }), + data + ) !== false + ) { + this._onAdd(e, data); + } + } + }, - _onDragOver: function (e) { - var that = e.data.fileupload, - dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer; - if (that._trigger('dragover', e) === false) { - return false; - } - if (dataTransfer) { - dataTransfer.dropEffect = 'copy'; - } - e.preventDefault(); - }, + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if ( + that._trigger( + 'drop', + $.Event('drop', { delegatedEvent: e }), + data + ) !== false + ) { + that._onAdd(e, data); + } + }); + } + }, - _initEventHandlers: function () { - var ns = this.options.namespace; - if (this._isXHRUpload(this.options)) { - this.options.dropZone - .bind('dragover.' + ns, {fileupload: this}, this._onDragOver) - .bind('drop.' + ns, {fileupload: this}, this._onDrop) - .bind('paste.' + ns, {fileupload: this}, this._onPaste); - } - this.options.fileInput - .bind('change.' + ns, {fileupload: this}, this._onChange); - }, + _onDragOver: getDragHandler('dragover'), - _destroyEventHandlers: function () { - var ns = this.options.namespace; - this.options.dropZone - .unbind('dragover.' + ns, this._onDragOver) - .unbind('drop.' + ns, this._onDrop) - .unbind('paste.' + ns, this._onPaste); - this.options.fileInput - .unbind('change.' + ns, this._onChange); - }, + _onDragEnter: getDragHandler('dragenter'), - _setOption: function (key, value) { - var refresh = $.inArray(key, this._refreshOptionsList) !== -1; - if (refresh) { - this._destroyEventHandlers(); - } - $.Widget.prototype._setOption.call(this, key, value); - if (refresh) { - this._initSpecialOptions(); - this._initEventHandlers(); - } - }, + _onDragLeave: getDragHandler('dragleave'), - _initSpecialOptions: function () { - var options = this.options; - if (options.fileInput === undefined) { - options.fileInput = this.element.is('input[type="file"]') ? - this.element : this.element.find('input[type="file"]'); - } else if (!(options.fileInput instanceof $)) { - options.fileInput = $(options.fileInput); - } - if (!(options.dropZone instanceof $)) { - options.dropZone = $(options.dropZone); - } - }, + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); + } + }, - _create: function () { - var options = this.options; - // Initialize options set via HTML5 data-attributes: - $.extend(options, $(this.element[0].cloneNode(false)).data()); - options.namespace = options.namespace || this.widgetName; - this._initSpecialOptions(); - this._slots = []; - this._sequence = this._getXHRPromise(true); - this._sending = this._active = this._loaded = this._total = 0; - this._initEventHandlers(); - }, + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, - destroy: function () { - this._destroyEventHandlers(); - $.Widget.prototype.destroy.call(this); - }, + _destroy: function () { + this._destroyEventHandlers(); + }, - enable: function () { - var wasDisabled = false; - if (this.options.disabled) { - wasDisabled = true; - } - $.Widget.prototype.enable.call(this); - if (wasDisabled) { - this._initEventHandlers(); - } - }, + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, - disable: function () { - if (!this.options.disabled) { - this._destroyEventHandlers(); - } - $.Widget.prototype.disable.call(this); - }, + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') + ? this.element + : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, - // This method is exposed to the widget API and allows adding files - // using the fileupload API. The data parameter accepts an object which - // must have a files property and can contain additional options: - // .fileupload('add', {files: filesList}); - add: function (data) { - var that = this; - if (!data || this.options.disabled) { - return; - } - if (data.fileInput && !data.files) { - this._getFileInputFiles(data.fileInput).always(function (files) { - data.files = files; - that._onAdd(null, data); - }); - } else { - data.files = $.makeArray(data.files); - this._onAdd(null, data); - } - }, + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, - // This method is exposed to the widget API and allows sending files - // using the fileupload API. The data parameter accepts an object which - // must have a files or fileInput property and can contain additional options: - // .fileupload('send', {files: filesList}); - // The method returns a Promise object for the file upload call. - send: function (data) { - if (data && !this.options.disabled) { - if (data.fileInput && !data.files) { - var that = this, - dfd = $.Deferred(), - promise = dfd.promise(), - jqXHR, - aborted; - promise.abort = function () { - aborted = true; - if (jqXHR) { - return jqXHR.abort(); - } - dfd.reject(null, 'abort', 'abort'); - return promise; - }; - this._getFileInputFiles(data.fileInput).always( - function (files) { - if (aborted) { - return; - } - data.files = files; - jqXHR = that._onSend(null, data).then( - function (result, textStatus, jqXHR) { - dfd.resolve(result, textStatus, jqXHR); - }, - function (jqXHR, textStatus, errorThrown) { - dfd.reject(jqXHR, textStatus, errorThrown); - } - ); - } - ); - return this._enhancePromise(promise); - } - data.files = $.makeArray(data.files); - if (data.files.length) { - return this._onSend(null, data); - } - } - return this._getXHRPromise(false, data && data.context); + _isRegExpOption: function (key, value) { + return ( + key !== 'url' && + $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value) + ); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options, + data = this.element.data(); + // Initialize options set via HTML5 data-attributes: + $.each(this.element[0].attributes, function (index, attr) { + var key = attr.name.toLowerCase(), + value; + if (/^data-/.test(key)) { + // Convert hyphen-ated key to camelCase: + key = key.slice(5).replace(/-[a-z]/g, function (str) { + return str.charAt(1).toUpperCase(); + }); + value = data[key]; + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; } + }); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, - }); + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, -})); + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data); + jqXHR.then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + }); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + }); +}); diff --git a/lib/web/jquery/fileUploader/jquery.iframe-transport.js b/lib/web/jquery/fileUploader/jquery.iframe-transport.js index 4749f46993652..3e3b9a93b05df 100644 --- a/lib/web/jquery/fileUploader/jquery.iframe-transport.js +++ b/lib/web/jquery/fileUploader/jquery.iframe-transport.js @@ -1,172 +1,227 @@ /* - * jQuery Iframe Transport Plugin 1.5 + * jQuery Iframe Transport Plugin * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT + * https://opensource.org/licenses/MIT */ -/*jslint unparam: true, nomen: true */ -/*global define, window, document */ +/* global define, require */ (function (factory) { - 'use strict'; - if (typeof define === 'function' && define.amd) { - // Register as an anonymous AMD module: - define(['jquery'], factory); - } else { - // Browser globals: - factory(window.jQuery); - } -}(function ($) { - 'use strict'; + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory(require('jquery')); + } else { + // Browser globals: + factory(window.jQuery); + } +})(function ($) { + 'use strict'; - // Helper variable to create unique names for the transport iframes: - var counter = 0; + // Helper variable to create unique names for the transport iframes: + var counter = 0, + jsonAPI = $, + jsonParse = 'parseJSON'; - // The iframe transport accepts three additional options: - // options.fileInput: a jQuery collection of file input fields - // options.paramName: the parameter name for the file form data, - // overrides the name property of the file input field(s), - // can be a string or an array of strings. - // options.formData: an array of objects with name and value properties, - // equivalent to the return data of .serializeArray(), e.g.: - // [{name: 'a', value: 1}, {name: 'b', value: 2}] - $.ajaxTransport('iframe', function (options) { - if (options.async && (options.type === 'POST' || options.type === 'GET')) { - var form, - iframe; - return { - send: function (_, completeCallback) { - form = $('<form style="display:none;"></form>'); - form.attr('accept-charset', options.formAcceptCharset); - // javascript:false as initial iframe src - // prevents warning popups on HTTPS in IE6. - // IE versions below IE8 cannot set the name property of - // elements that have already been added to the DOM, - // so we set the name along with the iframe HTML markup: - iframe = $( - '<iframe src="javascript:false;" name="iframe-transport-' + - (counter += 1) + '"></iframe>' - ).bind('load', function () { - var fileInputClones, - paramNames = $.isArray(options.paramName) ? - options.paramName : [options.paramName]; - iframe - .unbind('load') - .bind('load', function () { - var response; - // Wrap in a try/catch block to catch exceptions thrown - // when trying to access cross-domain iframe contents: - try { - response = iframe.contents(); - // Google Chrome and Firefox do not throw an - // exception when calling iframe.contents() on - // cross-domain requests, so we unify the response: - if (!response.length || !response[0].firstChild) { - throw new Error(); - } - } catch (e) { - response = undefined; - } - // The complete callback returns the - // iframe content document as response object: - completeCallback( - 200, - 'success', - {'iframe': response} - ); - // Fix for IE endless progress bar activity bug - // (happens on form submits to iframe targets): - $('<iframe src="javascript:false;"></iframe>') - .appendTo(form); - form.remove(); - }); - form - .prop('target', iframe.prop('name')) - .prop('action', options.url) - .prop('method', options.type); - if (options.formData) { - $.each(options.formData, function (index, field) { - $('<input type="hidden"/>') - .prop('name', field.name) - .val(field.value) - .appendTo(form); - }); - } - if (options.fileInput && options.fileInput.length && - options.type === 'POST') { - fileInputClones = options.fileInput.clone(); - // Insert a clone for each file input field: - options.fileInput.after(function (index) { - return fileInputClones[index]; - }); - if (options.paramName) { - options.fileInput.each(function (index) { - $(this).prop( - 'name', - paramNames[index] || options.paramName - ); - }); - } - // Appending the file input fields to the hidden form - // removes them from their original location: - form - .append(options.fileInput) - .prop('enctype', 'multipart/form-data') - // enctype must be set as encoding for IE: - .prop('encoding', 'multipart/form-data'); - } - form.submit(); - // Insert the file input fields at their original location - // by replacing the clones with the originals: - if (fileInputClones && fileInputClones.length) { - options.fileInput.each(function (index, input) { - var clone = $(fileInputClones[index]); - $(input).prop('name', clone.prop('name')); - clone.replaceWith(input); - }); - } - }); - form.append(iframe).appendTo(document.body); - }, - abort: function () { - if (iframe) { - // javascript:false as iframe src aborts the request - // and prevents warning popups on HTTPS in IE6. - // concat is used to avoid the "Script URL" JSLint error: - iframe - .unbind('load') - .prop('src', 'javascript'.concat(':false;')); - } - if (form) { - form.remove(); - } - } - }; - } - }); + if ('JSON' in window && 'parse' in JSON) { + jsonAPI = JSON; + jsonParse = 'parse'; + } - // The iframe transport returns the iframe content document as response. - // The following adds converters from iframe to text, json, html, and script: - $.ajaxSetup({ - converters: { - 'iframe text': function (iframe) { - return $(iframe[0].body).text(); - }, - 'iframe json': function (iframe) { - return $.parseJSON($(iframe[0].body).text()); - }, - 'iframe html': function (iframe) { - return $(iframe[0].body).html(); - }, - 'iframe script': function (iframe) { - return $.globalEval($(iframe[0].body).text()); + // The iframe transport accepts four additional options: + // options.fileInput: a jQuery collection of file input fields + // options.paramName: the parameter name for the file form data, + // overrides the name property of the file input field(s), + // can be a string or an array of strings. + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: 'a', value: 1}, {name: 'b', value: 2}] + // options.initialIframeSrc: the URL of the initial iframe src, + // by default set to "javascript:false;" + $.ajaxTransport('iframe', function (options) { + if (options.async) { + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6: + // eslint-disable-next-line no-script-url + var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', + form, + iframe, + addParamChar; + return { + send: function (_, completeCallback) { + form = $('<form style="display:none;"></form>'); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + counter += 1; + iframe = $( + '<iframe src="' + + initialIframeSrc + + '" name="iframe-transport-' + + counter + + '"></iframe>' + ).on('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) + ? options.paramName + : [options.paramName]; + iframe.off('load').on('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback(200, 'success', { iframe: response }); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('<iframe src="' + initialIframeSrc + '"></iframe>').appendTo( + form + ); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('<input type="hidden"/>') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); } + if ( + options.fileInput && + options.fileInput.length && + options.type === 'POST' + ) { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function (index) { + $(this).prop('name', paramNames[index] || options.paramName); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + // Remove the HTML5 form attribute from the input(s): + options.fileInput.removeAttr('form'); + } + window.setTimeout(function () { + // Submitting the form in a setTimeout call fixes an issue with + // Safari 13 not triggering the iframe load event after resetting + // the load event handler, see also: + // https://github.com/blueimp/jQuery-File-Upload/issues/3633 + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + // Restore the original name and form properties: + $(input) + .prop('name', clone.prop('name')) + .attr('form', clone.attr('form')); + clone.replaceWith(input); + }); + } + }, 0); + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + iframe.off('load').prop('src', initialIframeSrc); + } + if (form) { + form.remove(); + } } - }); + }; + } + }); -})); + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe && $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return iframe && jsonAPI[jsonParse]($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) + ? xmlDoc + : $.parseXML( + (xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html() + ); + }, + 'iframe script': function (iframe) { + return iframe && $.globalEval($(iframe[0].body).text()); + } + } + }); +}); diff --git a/lib/web/jquery/fileUploader/load-image.js b/lib/web/jquery/fileUploader/load-image.js deleted file mode 100644 index f6817db48c045..0000000000000 --- a/lib/web/jquery/fileUploader/load-image.js +++ /dev/null @@ -1 +0,0 @@ -(function(a){"use strict";var b=function(a,c,d){var e=document.createElement("img"),f,g;return e.onerror=c,e.onload=function(){g&&(!d||!d.noRevoke)&&b.revokeObjectURL(g),c(b.scale(e,d))},window.Blob&&a instanceof Blob||window.File&&a instanceof File?f=g=b.createObjectURL(a):f=a,f?(e.src=f,e):b.readFile(a,function(a){e.src=a})},c=window.createObjectURL&&window||window.URL&&URL.revokeObjectURL&&URL||window.webkitURL&&webkitURL;b.scale=function(a,b){b=b||{};var c=document.createElement("canvas"),d=a.width,e=a.height,f=Math.max((b.minWidth||d)/d,(b.minHeight||e)/e);return f>1&&(d=parseInt(d*f,10),e=parseInt(e*f,10)),f=Math.min((b.maxWidth||d)/d,(b.maxHeight||e)/e),f<1&&(d=parseInt(d*f,10),e=parseInt(e*f,10)),a.getContext||b.canvas&&c.getContext?(c.width=d,c.height=e,c.getContext("2d").drawImage(a,0,0,d,e),c):(a.width=d,a.height=e,a)},b.createObjectURL=function(a){return c?c.createObjectURL(a):!1},b.revokeObjectURL=function(a){return c?c.revokeObjectURL(a):!1},b.readFile=function(a,b){if(window.FileReader&&FileReader.prototype.readAsDataURL){var c=new FileReader;return c.onload=function(a){b(a.target.result)},c.readAsDataURL(a),c}return!1},typeof define=="function"&&define.amd?define(function(){return b}):a.loadImage=b})(this); \ No newline at end of file diff --git a/lib/web/jquery/fileUploader/locale.js b/lib/web/jquery/fileUploader/locale.js deleted file mode 100644 index ea64b0a870ff7..0000000000000 --- a/lib/web/jquery/fileUploader/locale.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * jQuery File Upload Plugin Localization Example 6.5.1 - * https://github.com/blueimp/jQuery-File-Upload - * - * Copyright 2012, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT - */ - -/*global window */ - -window.locale = { - "fileupload": { - "errors": { - "maxFileSize": "File is too big", - "minFileSize": "File is too small", - "acceptFileTypes": "Filetype not allowed", - "maxNumberOfFiles": "Max number of files exceeded", - "uploadedBytes": "Uploaded bytes exceed file size", - "emptyResult": "Empty file upload result" - }, - "error": "Error", - "start": "Start", - "cancel": "Cancel", - "destroy": "Delete" - } -}; diff --git a/lib/web/jquery/fileUploader/main.js b/lib/web/jquery/fileUploader/main.js deleted file mode 100644 index 7231899276c4b..0000000000000 --- a/lib/web/jquery/fileUploader/main.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * jQuery File Upload Plugin JS Example 6.7 - * https://github.com/blueimp/jQuery-File-Upload - * - * Copyright 2010, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT - */ - -/*jslint nomen: true, unparam: true, regexp: true */ -/*global $, window, document */ - -$(function () { - 'use strict'; - - // Initialize the jQuery File Upload widget: - $('#fileupload').fileupload(); - - // Enable iframe cross-domain access via redirect option: - $('#fileupload').fileupload( - 'option', - 'redirect', - window.location.href.replace( - /\/[^\/]*$/, - '/cors/result.html?%s' - ) - ); - - if (window.location.hostname === 'blueimp.github.com') { - // Demo settings: - $('#fileupload').fileupload('option', { - url: '//jquery-file-upload.appspot.com/', - maxFileSize: 5000000, - acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, - process: [ - { - action: 'load', - fileTypes: /^image\/(gif|jpeg|png)$/, - maxFileSize: 20000000 // 20MB - }, - { - action: 'resize', - maxWidth: 1440, - maxHeight: 900 - }, - { - action: 'save' - } - ] - }); - // Upload server status check for browsers with CORS support: - if ($.support.cors) { - $.ajax({ - url: '//jquery-file-upload.appspot.com/', - type: 'HEAD' - }).fail(function () { - $('<span class="alert alert-error"/>') - .text($.mage.__('Upload server currently unavailable - ') + - new Date()) - .appendTo('#fileupload'); - }); - } - } else { - // Load existing files: - $('#fileupload').each(function () { - var that = this; - $.getJSON(this.action, function (result) { - if (result && result.length) { - $(that).fileupload('option', 'done') - .call(that, null, {result: result}); - } - }); - }); - } - -}); diff --git a/lib/web/jquery/fileUploader/vendor/jquery.ui.widget.js b/lib/web/jquery/fileUploader/vendor/jquery.ui.widget.js index 39287bd41ab09..69096aaa35ef4 100644 --- a/lib/web/jquery/fileUploader/vendor/jquery.ui.widget.js +++ b/lib/web/jquery/fileUploader/vendor/jquery.ui.widget.js @@ -1,282 +1,808 @@ -/* - * jQuery UI Widget 1.8.23+amd - * https://github.com/blueimp/jQuery-File-Upload - * - * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Widget - */ +/*! jQuery UI - v1.12.1+0b7246b6eeadfa9e2696e22f3230f6452f8129dc - 2020-02-20 + * http://jqueryui.com + * Includes: widget.js + * Copyright jQuery Foundation and other contributors; Licensed MIT */ + +/* global define, require */ +/* eslint-disable no-param-reassign, new-cap, jsdoc/require-jsdoc */ (function (factory) { - if (typeof define === "function" && define.amd) { - // Register as an anonymous AMD module: - define(["jquery"], factory); + 'use strict'; + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + factory(require('jquery')); + } else { + // Browser globals + factory(window.jQuery); + } +})(function ($) { + ('use strict'); + + $.ui = $.ui || {}; + + $.ui.version = '1.12.1'; + + /*! + * jQuery UI Widget 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + */ + + //>>label: Widget + //>>group: Core + //>>description: Provides a factory for creating stateful widgets with a common API. + //>>docs: http://api.jqueryui.com/jQuery.widget/ + //>>demos: http://jqueryui.com/widget/ + + // Support: jQuery 1.9.x or older + // $.expr[ ":" ] is deprecated. + if (!$.expr.pseudos) { + $.expr.pseudos = $.expr[':']; + } + + // Support: jQuery 1.11.x or older + // $.unique has been renamed to $.uniqueSort + if (!$.uniqueSort) { + $.uniqueSort = $.unique; + } + + var widgetUuid = 0; + var widgetHasOwnProperty = Array.prototype.hasOwnProperty; + var widgetSlice = Array.prototype.slice; + + $.cleanData = (function (orig) { + return function (elems) { + var events, elem, i; + // eslint-disable-next-line eqeqeq + for (i = 0; (elem = elems[i]) != null; i++) { + // Only trigger remove when necessary to save time + events = $._data(elem, 'events'); + if (events && events.remove) { + $(elem).triggerHandler('remove'); + } + } + orig(elems); + }; + })($.cleanData); + + $.widget = function (name, base, prototype) { + var existingConstructor, constructor, basePrototype; + + // ProxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + var proxiedPrototype = {}; + + var namespace = name.split('.')[0]; + name = name.split('.')[1]; + var fullName = namespace + '-' + name; + + if (!prototype) { + prototype = base; + base = $.Widget; + } + + if ($.isArray(prototype)) { + prototype = $.extend.apply(null, [{}].concat(prototype)); + } + + // Create selector for plugin + $.expr.pseudos[fullName.toLowerCase()] = function (elem) { + return !!$.data(elem, fullName); + }; + + $[namespace] = $[namespace] || {}; + existingConstructor = $[namespace][name]; + constructor = $[namespace][name] = function (options, element) { + // Allow instantiation without "new" keyword + if (!this._createWidget) { + return new constructor(options, element); + } + + // Allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if (arguments.length) { + this._createWidget(options, element); + } + }; + + // Extend with the existing constructor to carry over any static properties + $.extend(constructor, existingConstructor, { + version: prototype.version, + + // Copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend({}, prototype), + + // Track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + + // We need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend({}, basePrototype.options); + $.each(prototype, function (prop, value) { + if (!$.isFunction(value)) { + proxiedPrototype[prop] = value; + return; + } + proxiedPrototype[prop] = (function () { + function _super() { + return base.prototype[prop].apply(this, arguments); + } + + function _superApply(args) { + return base.prototype[prop].apply(this, args); + } + + return function () { + var __super = this._super; + var __superApply = this._superApply; + var returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply(this, arguments); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( + basePrototype, + { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor + ? basePrototype.widgetEventPrefix || name + : name + }, + proxiedPrototype, + { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + } + ); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if (existingConstructor) { + $.each(existingConstructor._childConstructors, function (i, child) { + var childPrototype = child.prototype; + + // Redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( + childPrototype.namespace + '.' + childPrototype.widgetName, + constructor, + child._proto + ); + }); + + // Remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; } else { - // Browser globals: - factory(jQuery); + base._childConstructors.push(constructor); + } + + $.widget.bridge(name, constructor); + + return constructor; + }; + + $.widget.extend = function (target) { + var input = widgetSlice.call(arguments, 1); + var inputIndex = 0; + var inputLength = input.length; + var key; + var value; + + for (; inputIndex < inputLength; inputIndex++) { + for (key in input[inputIndex]) { + value = input[inputIndex][key]; + if ( + widgetHasOwnProperty.call(input[inputIndex], key) && + value !== undefined + ) { + // Clone objects + if ($.isPlainObject(value)) { + target[key] = $.isPlainObject(target[key]) + ? $.widget.extend({}, target[key], value) + : // Don't extend strings, arrays, etc. with objects + $.widget.extend({}, value); + + // Copy everything else by reference + } else { + target[key] = value; + } + } + } + } + return target; + }; + + $.widget.bridge = function (name, object) { + var fullName = object.prototype.widgetFullName || name; + $.fn[name] = function (options) { + var isMethodCall = typeof options === 'string'; + var args = widgetSlice.call(arguments, 1); + var returnValue = this; + + if (isMethodCall) { + // If this is an empty collection, we need to have the instance method + // return undefined instead of the jQuery instance + if (!this.length && options === 'instance') { + returnValue = undefined; + } else { + this.each(function () { + var methodValue; + var instance = $.data(this, fullName); + + if (options === 'instance') { + returnValue = instance; + return false; + } + + if (!instance) { + return $.error( + 'cannot call methods on ' + + name + + ' prior to initialization; ' + + "attempted to call method '" + + options + + "'" + ); + } + + if (!$.isFunction(instance[options]) || options.charAt(0) === '_') { + return $.error( + "no such method '" + + options + + "' for " + + name + + ' widget instance' + ); + } + + methodValue = instance[options].apply(instance, args); + + if (methodValue !== instance && methodValue !== undefined) { + returnValue = + methodValue && methodValue.jquery + ? returnValue.pushStack(methodValue.get()) + : methodValue; + return false; + } + }); + } + } else { + // Allow multiple hashes to be passed on init + if (args.length) { + options = $.widget.extend.apply(null, [options].concat(args)); + } + + this.each(function () { + var instance = $.data(this, fullName); + if (instance) { + instance.option(options || {}); + if (instance._init) { + instance._init(); + } + } else { + $.data(this, fullName, new object(options, this)); + } + }); + } + + return returnValue; + }; + }; + + $.Widget = function (/* options, element */) {}; + $.Widget._childConstructors = []; + + $.Widget.prototype = { + widgetName: 'widget', + widgetEventPrefix: '', + defaultElement: '<div>', + + options: { + classes: {}, + disabled: false, + + // Callbacks + create: null + }, + + _createWidget: function (options, element) { + element = $(element || this.defaultElement || this)[0]; + this.element = $(element); + this.uuid = widgetUuid++; + this.eventNamespace = '.' + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + this.classesElementLookup = {}; + + if (element !== this) { + $.data(element, this.widgetFullName, this); + this._on(true, this.element, { + remove: function (event) { + if (event.target === element) { + this.destroy(); + } + } + }); + this.document = $( + element.style + ? // Element within the document + element.ownerDocument + : // Element is window or document + element.document || element + ); + this.window = $( + this.document[0].defaultView || this.document[0].parentWindow + ); + } + + this.options = $.widget.extend( + {}, + this.options, + this._getCreateOptions(), + options + ); + + this._create(); + + if (this.options.disabled) { + this._setOptionDisabled(this.options.disabled); + } + + this._trigger('create', null, this._getCreateEventData()); + this._init(); + }, + + _getCreateOptions: function () { + return {}; + }, + + _getCreateEventData: $.noop, + + _create: $.noop, + + _init: $.noop, + + destroy: function () { + var that = this; + + this._destroy(); + $.each(this.classesElementLookup, function (key, value) { + that._removeClass(value, key); + }); + + // We can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element.off(this.eventNamespace).removeData(this.widgetFullName); + this.widget().off(this.eventNamespace).removeAttr('aria-disabled'); + + // Clean up events and states + this.bindings.off(this.eventNamespace); + }, + + _destroy: $.noop, + + widget: function () { + return this.element; + }, + + option: function (key, value) { + var options = key; + var parts; + var curOption; + var i; + + if (arguments.length === 0) { + // Don't return a reference to the internal hash + return $.widget.extend({}, this.options); + } + + if (typeof key === 'string') { + // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split('.'); + key = parts.shift(); + if (parts.length) { + curOption = options[key] = $.widget.extend({}, this.options[key]); + for (i = 0; i < parts.length - 1; i++) { + curOption[parts[i]] = curOption[parts[i]] || {}; + curOption = curOption[parts[i]]; + } + key = parts.pop(); + if (arguments.length === 1) { + return curOption[key] === undefined ? null : curOption[key]; + } + curOption[key] = value; + } else { + if (arguments.length === 1) { + return this.options[key] === undefined ? null : this.options[key]; + } + options[key] = value; + } + } + + this._setOptions(options); + + return this; + }, + + _setOptions: function (options) { + var key; + + for (key in options) { + this._setOption(key, options[key]); + } + + return this; + }, + + _setOption: function (key, value) { + if (key === 'classes') { + this._setOptionClasses(value); + } + + this.options[key] = value; + + if (key === 'disabled') { + this._setOptionDisabled(value); + } + + return this; + }, + + _setOptionClasses: function (value) { + var classKey, elements, currentElements; + + for (classKey in value) { + currentElements = this.classesElementLookup[classKey]; + if ( + value[classKey] === this.options.classes[classKey] || + !currentElements || + !currentElements.length + ) { + continue; + } + + // We are doing this to create a new jQuery object because the _removeClass() call + // on the next line is going to destroy the reference to the current elements being + // tracked. We need to save a copy of this collection so that we can add the new classes + // below. + elements = $(currentElements.get()); + this._removeClass(currentElements, classKey); + + // We don't use _addClass() here, because that uses this.options.classes + // for generating the string of classes. We want to use the value passed in from + // _setOption(), this is the new value of the classes option which was passed to + // _setOption(). We pass this value directly to _classes(). + elements.addClass( + this._classes({ + element: elements, + keys: classKey, + classes: value, + add: true + }) + ); + } + }, + + _setOptionDisabled: function (value) { + this._toggleClass( + this.widget(), + this.widgetFullName + '-disabled', + null, + !!value + ); + + // If the widget is becoming disabled, then nothing is interactive + if (value) { + this._removeClass(this.hoverable, null, 'ui-state-hover'); + this._removeClass(this.focusable, null, 'ui-state-focus'); + } + }, + + enable: function () { + return this._setOptions({ disabled: false }); + }, + + disable: function () { + return this._setOptions({ disabled: true }); + }, + + _classes: function (options) { + var full = []; + var that = this; + + options = $.extend( + { + element: this.element, + classes: this.options.classes || {} + }, + options + ); + + function bindRemoveEvent() { + options.element.each(function (_, element) { + var isTracked = $.map(that.classesElementLookup, function (elements) { + return elements; + }).some(function (elements) { + return elements.is(element); + }); + + if (!isTracked) { + that._on($(element), { + remove: '_untrackClassesElement' + }); + } + }); + } + + function processClassString(classes, checkOption) { + var current, i; + for (i = 0; i < classes.length; i++) { + current = that.classesElementLookup[classes[i]] || $(); + if (options.add) { + bindRemoveEvent(); + current = $( + $.uniqueSort(current.get().concat(options.element.get())) + ); + } else { + current = $(current.not(options.element).get()); + } + that.classesElementLookup[classes[i]] = current; + full.push(classes[i]); + if (checkOption && options.classes[classes[i]]) { + full.push(options.classes[classes[i]]); + } + } + } + + if (options.keys) { + processClassString(options.keys.match(/\S+/g) || [], true); + } + if (options.extra) { + processClassString(options.extra.match(/\S+/g) || []); + } + + return full.join(' '); + }, + + _untrackClassesElement: function (event) { + var that = this; + $.each(that.classesElementLookup, function (key, value) { + if ($.inArray(event.target, value) !== -1) { + that.classesElementLookup[key] = $(value.not(event.target).get()); + } + }); + + this._off($(event.target)); + }, + + _removeClass: function (element, keys, extra) { + return this._toggleClass(element, keys, extra, false); + }, + + _addClass: function (element, keys, extra) { + return this._toggleClass(element, keys, extra, true); + }, + + _toggleClass: function (element, keys, extra, add) { + add = typeof add === 'boolean' ? add : extra; + var shift = typeof element === 'string' || element === null, + options = { + extra: shift ? keys : extra, + keys: shift ? element : keys, + element: shift ? this.element : element, + add: add + }; + options.element.toggleClass(this._classes(options), add); + return this; + }, + + _on: function (suppressDisabledCheck, element, handlers) { + var delegateElement; + var instance = this; + + // No suppressDisabledCheck flag, shuffle arguments + if (typeof suppressDisabledCheck !== 'boolean') { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // No element argument, shuffle and use this.element + if (!handlers) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $(element); + this.bindings = this.bindings.add(element); + } + + $.each(handlers, function (event, handler) { + function handlerProxy() { + // Allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( + !suppressDisabledCheck && + (instance.options.disabled === true || + $(this).hasClass('ui-state-disabled')) + ) { + return; + } + return (typeof handler === 'string' + ? instance[handler] + : handler + ).apply(instance, arguments); + } + + // Copy the guid so direct unbinding works + if (typeof handler !== 'string') { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match(/^([\w:-]*)\s*(.*)$/); + var eventName = match[1] + instance.eventNamespace; + var selector = match[2]; + + if (selector) { + delegateElement.on(eventName, selector, handlerProxy); + } else { + element.on(eventName, handlerProxy); + } + }); + }, + + _off: function (element, eventName) { + eventName = + (eventName || '').split(' ').join(this.eventNamespace + ' ') + + this.eventNamespace; + element.off(eventName); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $(this.bindings.not(element).get()); + this.focusable = $(this.focusable.not(element).get()); + this.hoverable = $(this.hoverable.not(element).get()); + }, + + _delay: function (handler, delay) { + var instance = this; + function handlerProxy() { + return (typeof handler === 'string' + ? instance[handler] + : handler + ).apply(instance, arguments); + } + return setTimeout(handlerProxy, delay || 0); + }, + + _hoverable: function (element) { + this.hoverable = this.hoverable.add(element); + this._on(element, { + mouseenter: function (event) { + this._addClass($(event.currentTarget), null, 'ui-state-hover'); + }, + mouseleave: function (event) { + this._removeClass($(event.currentTarget), null, 'ui-state-hover'); + } + }); + }, + + _focusable: function (element) { + this.focusable = this.focusable.add(element); + this._on(element, { + focusin: function (event) { + this._addClass($(event.currentTarget), null, 'ui-state-focus'); + }, + focusout: function (event) { + this._removeClass($(event.currentTarget), null, 'ui-state-focus'); + } + }); + }, + + _trigger: function (type, event, data) { + var prop, orig; + var callback = this.options[type]; + + data = data || {}; + event = $.Event(event); + event.type = (type === this.widgetEventPrefix + ? type + : this.widgetEventPrefix + type + ).toLowerCase(); + + // The original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[0]; + + // Copy original event properties over to the new event + orig = event.originalEvent; + if (orig) { + for (prop in orig) { + if (!(prop in event)) { + event[prop] = orig[prop]; + } + } + } + + this.element.trigger(event, data); + return !( + ($.isFunction(callback) && + callback.apply(this.element[0], [event].concat(data)) === false) || + event.isDefaultPrevented() + ); } -}(function( $, undefined ) { - -// jQuery 1.4+ -if ( $.cleanData ) { - var _cleanData = $.cleanData; - $.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - } - _cleanData( elems ); - }; -} else { - var _remove = $.fn.remove; - $.fn.remove = function( selector, keepData ) { - return this.each(function() { - if ( !keepData ) { - if ( !selector || $.filter( selector, [ this ] ).length ) { - $( "*", this ).add( [ this ] ).each(function() { - try { - $( this ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - }); - } - } - return _remove.call( $(this), selector, keepData ); - }); - }; -} - -$.widget = function( name, base, prototype ) { - var namespace = name.split( "." )[ 0 ], - fullName; - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; - - if ( !prototype ) { - prototype = base; - base = $.Widget; - } - - // create selector for plugin - $.expr[ ":" ][ fullName ] = function( elem ) { - return !!$.data( elem, name ); - }; - - $[ namespace ] = $[ namespace ] || {}; - $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without initializing for simple inheritance - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - - var basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from -// $.each( basePrototype, function( key, val ) { -// if ( $.isPlainObject(val) ) { -// basePrototype[ key ] = $.extend( {}, val ); -// } -// }); - basePrototype.options = $.extend( true, {}, basePrototype.options ); - $[ namespace ][ name ].prototype = $.extend( true, basePrototype, { - namespace: namespace, - widgetName: name, - widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name, - widgetBaseClass: fullName - }, prototype ); - - $.widget.bridge( name, $[ namespace ][ name ] ); -}; - -$.widget.bridge = function( name, object ) { - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = Array.prototype.slice.call( arguments, 1 ), - returnValue = this; - - // allow multiple hashes to be passed on init - options = !isMethodCall && args.length ? - $.extend.apply( null, [ true, options ].concat(args) ) : - options; - - // prevent calls to internal methods - if ( isMethodCall && options.charAt( 0 ) === "_" ) { - return returnValue; - } - - if ( isMethodCall ) { - this.each(function() { - var instance = $.data( this, name ), - methodValue = instance && $.isFunction( instance[options] ) ? - instance[ options ].apply( instance, args ) : - instance; - // TODO: add this back in 1.9 and use $.error() (see #5972) -// if ( !instance ) { -// throw "cannot call methods on " + name + " prior to initialization; " + -// "attempted to call method '" + options + "'"; -// } -// if ( !$.isFunction( instance[options] ) ) { -// throw "no such method '" + options + "' for " + name + " widget instance"; -// } -// var methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue; - return false; - } - }); - } else { - this.each(function() { - var instance = $.data( this, name ); - if ( instance ) { - instance.option( options || {} )._init(); - } else { - $.data( this, name, new object( options, this ) ); - } - }); - } - - return returnValue; - }; -}; - -$.Widget = function( options, element ) { - // allow instantiation without initializing for simple inheritance - if ( arguments.length ) { - this._createWidget( options, element ); - } -}; - -$.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - options: { - disabled: false - }, - _createWidget: function( options, element ) { - // $.widget.bridge stores the plugin instance, but we do it anyway - // so that it's stored even before the _create function runs - $.data( element, this.widgetName, this ); - this.element = $( element ); - this.options = $.extend( true, {}, - this.options, - this._getCreateOptions(), - options ); - - var self = this; - this.element.bind( "remove." + this.widgetName, function() { - self.destroy(); - }); - - this._create(); - this._trigger( "create" ); - this._init(); - }, - _getCreateOptions: function() { - return $.metadata && $.metadata.get( this.element[0] )[ this.widgetName ]; - }, - _create: function() {}, - _init: function() {}, - - destroy: function() { - this.element - .unbind( "." + this.widgetName ) - .removeData( this.widgetName ); - this.widget() - .unbind( "." + this.widgetName ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetBaseClass + "-disabled " + - "ui-state-disabled" ); - }, - - widget: function() { - return this.element; - }, - - option: function( key, value ) { - var options = key; - - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.extend( {}, this.options ); - } - - if (typeof key === "string" ) { - if ( value === undefined ) { - return this.options[ key ]; - } - options = {}; - options[ key ] = value; - } - - this._setOptions( options ); - - return this; - }, - _setOptions: function( options ) { - var self = this; - $.each( options, function( key, value ) { - self._setOption( key, value ); - }); - - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; - - if ( key === "disabled" ) { - this.widget() - [ value ? "addClass" : "removeClass"]( - this.widgetBaseClass + "-disabled" + " " + - "ui-state-disabled" ) - .attr( "aria-disabled", value ); - } - - return this; - }, - - enable: function() { - return this._setOption( "disabled", false ); - }, - disable: function() { - return this._setOption( "disabled", true ); - }, - - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; - - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; - - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } - - this.element.trigger( event, data ); - - return !( $.isFunction(callback) && - callback.call( this.element[0], event, data ) === false || - event.isDefaultPrevented() ); - } -}; - -})); + }; + + $.each({ show: 'fadeIn', hide: 'fadeOut' }, function (method, defaultEffect) { + $.Widget.prototype['_' + method] = function (element, options, callback) { + if (typeof options === 'string') { + options = { effect: options }; + } + + var hasOptions; + var effectName = !options + ? method + : options === true || typeof options === 'number' + ? defaultEffect + : options.effect || defaultEffect; + + options = options || {}; + if (typeof options === 'number') { + options = { duration: options }; + } + + hasOptions = !$.isEmptyObject(options); + options.complete = callback; + + if (options.delay) { + element.delay(options.delay); + } + + if (hasOptions && $.effects && $.effects.effect[effectName]) { + element[method](options); + } else if (effectName !== method && element[effectName]) { + element[effectName](options.duration, options.easing, callback); + } else { + element.queue(function (next) { + $(this)[method](); + if (callback) { + callback.call(element[0]); + } + next(); + }); + } + }; + }); +}); From 9119b9a395d85d2a6e2a21ae8899b5f83c02c272 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Sun, 7 Jun 2020 20:31:05 -0500 Subject: [PATCH 0057/1013] MC-34467: Updated jQuery File Upload plugin - Fixed dependency failures - Fixed static test failure --- app/code/Magento/Theme/view/base/requirejs-config.js | 2 ++ app/code/Magento/User/view/adminhtml/web/app-config.js | 1 + lib/web/jquery/fileUploader/jquery.fileupload-audio.js | 4 ++-- lib/web/jquery/fileUploader/jquery.fileupload-image.js | 4 ++-- lib/web/jquery/fileUploader/jquery.fileupload-process.js | 4 ++-- lib/web/jquery/fileUploader/jquery.fileupload-validate.js | 6 +++--- lib/web/jquery/fileUploader/jquery.fileupload-video.js | 4 ++-- lib/web/jquery/fileUploader/jquery.fileupload.js | 2 +- 8 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index 77af920c8df86..f5f6c01e31e88 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -32,6 +32,8 @@ var config = { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', + 'jquery/file-upload': 'jquery/fileUploader/jquery.fileupload', + 'jquery-ui/ui/widget': 'jquery/fileUploader/vendor/jquery.ui.widget', 'prototype': 'legacy-build.min', 'jquery/jquery-storageapi': 'jquery/jquery.storageapi.min', 'text': 'mage/requirejs/text', diff --git a/app/code/Magento/User/view/adminhtml/web/app-config.js b/app/code/Magento/User/view/adminhtml/web/app-config.js index 491378d933ca2..b8eae1c4495d1 100644 --- a/app/code/Magento/User/view/adminhtml/web/app-config.js +++ b/app/code/Magento/User/view/adminhtml/web/app-config.js @@ -27,6 +27,7 @@ require.config({ 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', + 'jquery/fileupload.js': 'jquery/fileUploader/jquery.fileupload', 'prototype': 'prototype/prototype-amd', 'text': 'requirejs/text', 'domReady': 'requirejs/domReady', diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-audio.js b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js index e5c9202f9730a..c5bcf940fc66b 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-audio.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js @@ -15,13 +15,13 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'load-image', './jquery.fileupload-process'], factory); + define(['jquery', 'load-image', 'jquery/file-uploader'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), require('blueimp-load-image/js/load-image'), - require('./jquery.fileupload-process') + require('jquery/file-uploader') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-image.js b/lib/web/jquery/fileUploader/jquery.fileupload-image.js index 8598461031e2e..15713e6123dfc 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-image.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-image.js @@ -23,7 +23,7 @@ 'load-image-exif', 'load-image-orientation', 'canvas-to-blob', - './jquery.fileupload-process' + 'jquery/file-uploader' ], factory); } else if (typeof exports === 'object') { // Node/CommonJS: @@ -35,7 +35,7 @@ require('blueimp-load-image/js/load-image-exif'), require('blueimp-load-image/js/load-image-orientation'), require('blueimp-canvas-to-blob'), - require('./jquery.fileupload-process') + require('jquery/file-uploader') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-process.js b/lib/web/jquery/fileUploader/jquery.fileupload-process.js index 130778e7f26a6..d8e13a7773020 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-process.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-process.js @@ -15,10 +15,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', './jquery.fileupload'], factory); + define(['jquery', 'jquery/file-upload'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('./jquery.fileupload')); + factory(require('jquery'), require('jquery/file-upload')); } else { // Browser globals: factory(window.jQuery); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-validate.js b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js index a277efc46d774..4826efb2c4844 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-validate.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js @@ -15,10 +15,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', './jquery.fileupload-process'], factory); + define(['jquery', 'jquery/file-uploader'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('./jquery.fileupload-process')); + factory(require('jquery'), require('jquery/file-uploader')); } else { // Browser globals: factory(window.jQuery); @@ -57,7 +57,7 @@ */ // Function returning the current number of files, - // has to be overriden for maxNumberOfFiles validation: + // has to be over-ridden for maxNumberOfFiles validation: getNumberOfFiles: $.noop, // Error and info messages: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-video.js b/lib/web/jquery/fileUploader/jquery.fileupload-video.js index 5dc78f36bb829..e2484630aa2a4 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-video.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-video.js @@ -15,13 +15,13 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'load-image', './jquery.fileupload-process'], factory); + define(['jquery', 'load-image', 'jquery/file-uploader'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), require('blueimp-load-image/js/load-image'), - require('./jquery.fileupload-process') + require('jquery/file-uploader') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload.js b/lib/web/jquery/fileUploader/jquery.fileupload.js index 184d347216409..dda71f0413c71 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload.js @@ -19,7 +19,7 @@ define(['jquery', 'jquery-ui/ui/widget'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('./vendor/jquery.ui.widget')); + factory(require('jquery'), require('jquery-ui/ui/widget')); } else { // Browser globals: factory(window.jQuery); From 31028f856e650a608d6f9855a4335c55d3470c7b Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Mon, 8 Jun 2020 10:55:32 -0500 Subject: [PATCH 0058/1013] MC-34467: Updated jQuery File Upload plugin - Fixed dependency issues --- .../Magento/Theme/view/base/requirejs-config.js | 2 -- .../User/view/adminhtml/web/app-config.js | 1 - .../fileUploader/jquery.fileupload-audio.js | 4 ++-- .../fileUploader/jquery.fileupload-image.js | 4 ++-- .../fileUploader/jquery.fileupload-process.js | 4 ++-- .../jquery/fileUploader/jquery.fileupload-ui.js | 16 ++++++++-------- .../fileUploader/jquery.fileupload-validate.js | 6 +++--- .../fileUploader/jquery.fileupload-video.js | 4 ++-- lib/web/jquery/fileUploader/jquery.fileupload.js | 4 ++-- 9 files changed, 21 insertions(+), 24 deletions(-) diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index f5f6c01e31e88..77af920c8df86 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -32,8 +32,6 @@ var config = { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', - 'jquery/file-upload': 'jquery/fileUploader/jquery.fileupload', - 'jquery-ui/ui/widget': 'jquery/fileUploader/vendor/jquery.ui.widget', 'prototype': 'legacy-build.min', 'jquery/jquery-storageapi': 'jquery/jquery.storageapi.min', 'text': 'mage/requirejs/text', diff --git a/app/code/Magento/User/view/adminhtml/web/app-config.js b/app/code/Magento/User/view/adminhtml/web/app-config.js index b8eae1c4495d1..491378d933ca2 100644 --- a/app/code/Magento/User/view/adminhtml/web/app-config.js +++ b/app/code/Magento/User/view/adminhtml/web/app-config.js @@ -27,7 +27,6 @@ require.config({ 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', - 'jquery/fileupload.js': 'jquery/fileUploader/jquery.fileupload', 'prototype': 'prototype/prototype-amd', 'text': 'requirejs/text', 'domReady': 'requirejs/domReady', diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-audio.js b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js index c5bcf940fc66b..0bd4c27553dc0 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-audio.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js @@ -15,13 +15,13 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'load-image', 'jquery/file-uploader'], factory); + define(['jquery', 'load-image', 'jquery/fileUploader/jquery.fileupload-process'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), require('blueimp-load-image/js/load-image'), - require('jquery/file-uploader') + require('jquery/fileUploader/jquery.fileupload-process') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-image.js b/lib/web/jquery/fileUploader/jquery.fileupload-image.js index 15713e6123dfc..84349fc9fbf92 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-image.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-image.js @@ -23,7 +23,7 @@ 'load-image-exif', 'load-image-orientation', 'canvas-to-blob', - 'jquery/file-uploader' + 'jquery/fileUploader/jquery.fileupload-process' ], factory); } else if (typeof exports === 'object') { // Node/CommonJS: @@ -35,7 +35,7 @@ require('blueimp-load-image/js/load-image-exif'), require('blueimp-load-image/js/load-image-orientation'), require('blueimp-canvas-to-blob'), - require('jquery/file-uploader') + require('jquery/fileUploader/jquery.fileupload-process') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-process.js b/lib/web/jquery/fileUploader/jquery.fileupload-process.js index d8e13a7773020..a2f1009e508d1 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-process.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-process.js @@ -15,10 +15,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'jquery/file-upload'], factory); + define(['jquery', 'jquery/fileUploader/jquery.fileupload'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('jquery/file-upload')); + factory(require('jquery'), require('jquery/fileUploader/jquery.fileupload')); } else { // Browser globals: factory(window.jQuery); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js index 9cc3d3fd0fb1f..4ee566b2b3bcb 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js @@ -18,20 +18,20 @@ define([ 'jquery', 'blueimp-tmpl', - './jquery.fileupload-image', - './jquery.fileupload-audio', - './jquery.fileupload-video', - './jquery.fileupload-validate' + 'jquery/fileUploader/jquery.fileupload-image', + 'jquery/fileUploader/jquery.fileupload-audio', + 'jquery/fileUploader/jquery.fileupload-video', + 'jquery/fileUploader/jquery.fileupload-validate' ], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), require('blueimp-tmpl'), - require('./jquery.fileupload-image'), - require('./jquery.fileupload-audio'), - require('./jquery.fileupload-video'), - require('./jquery.fileupload-validate') + require('jquery/fileUploader/jquery.fileupload-image'), + require('jquery/fileUploader/jquery.fileupload-audio'), + require('jquery/fileUploader/jquery.fileupload-video'), + require('jquery/fileUploader/jquery.fileupload-validate') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-validate.js b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js index 4826efb2c4844..9353b01cdcf41 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-validate.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js @@ -15,10 +15,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'jquery/file-uploader'], factory); + define(['jquery', 'jquery/fileUploader/jquery.fileupload-process'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('jquery/file-uploader')); + factory(require('jquery'), require('jquery/fileUploader/jquery.fileupload-process')); } else { // Browser globals: factory(window.jQuery); @@ -57,7 +57,7 @@ */ // Function returning the current number of files, - // has to be over-ridden for maxNumberOfFiles validation: + // has to be overriden for maxNumberOfFiles validation: getNumberOfFiles: $.noop, // Error and info messages: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-video.js b/lib/web/jquery/fileUploader/jquery.fileupload-video.js index e2484630aa2a4..70cd9f1dcfc16 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-video.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-video.js @@ -15,13 +15,13 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'load-image', 'jquery/file-uploader'], factory); + define(['jquery', 'load-image', 'jquery/fileUploader/jquery.fileupload-process'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), require('blueimp-load-image/js/load-image'), - require('jquery/file-uploader') + require('jquery/fileUploader/jquery.fileupload-process') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileupload.js b/lib/web/jquery/fileUploader/jquery.fileupload.js index dda71f0413c71..8f0ff0d4faf03 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload.js @@ -16,10 +16,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'jquery-ui/ui/widget'], factory); + define(['jquery', 'jquery/fileUploader/vendor/jquery.ui.widget'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('jquery-ui/ui/widget')); + factory(require('jquery'), require('jquery/fileUploader/vendor/jquery.ui.widget')); } else { // Browser globals: factory(window.jQuery); From 36167ed430c4e98150e5e60946f08b67c327fa86 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Mon, 8 Jun 2020 13:44:53 -0500 Subject: [PATCH 0059/1013] MC-34467: Updated jQuery File Upload plugin - Fixed static test failure --- lib/web/jquery/fileUploader/jquery.fileupload-validate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-validate.js b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js index 9353b01cdcf41..d23e0f4a24ef7 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-validate.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-validate.js @@ -57,7 +57,7 @@ */ // Function returning the current number of files, - // has to be overriden for maxNumberOfFiles validation: + // has to be overridden for maxNumberOfFiles validation: getNumberOfFiles: $.noop, // Error and info messages: From f7b94572d5ba20286785af773df55e884ba8244e Mon Sep 17 00:00:00 2001 From: Stas Kozar <stas.kozar@transoftgroup.com> Date: Tue, 9 Jun 2020 09:02:37 +0300 Subject: [PATCH 0060/1013] MC-34727: Improve error path view file url --- .../Magento/Framework/Error/ProcessorTest.php | 17 ++++++++++++- pub/errors/processor.php | 24 +++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php index af2f8208afab1..3a2a02a0a5776 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php @@ -31,7 +31,10 @@ protected function setUp(): void protected function tearDown(): void { $reportDir = $this->processor->_reportDir; - $this->removeDirRecursively($reportDir); + + if (is_dir($reportDir)) { + $this->removeDirRecursively($reportDir); + } } /** @@ -137,4 +140,16 @@ private function removeDirRecursively(string $dir, int $i = 0): bool } return rmdir($dir); } + + /** + * @return void + */ + public function testGetViewFileUrl(): void + { + $this->processor->_indexDir = __DIR__ . '/version1/magento2'; + $this->processor->_errorDir = __DIR__ . '/version2/magento2'; + + $this->assertStringNotContainsString('version2/magento2', $this->processor->getViewFileUrl()); + $this->assertStringContainsString('pub/errors/', $this->processor->getViewFileUrl()); + } } diff --git a/pub/errors/processor.php b/pub/errors/processor.php index 7cab4add51a92..ac335211f97e0 100644 --- a/pub/errors/processor.php +++ b/pub/errors/processor.php @@ -7,6 +7,7 @@ namespace Magento\Framework\Error; +use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Escaper; use Magento\Framework\App\ObjectManager; @@ -149,21 +150,29 @@ class Processor */ private $escaper; + /** + * @var DocumentRoot + */ + private $documentRoot; + /** * @param Http $response * @param Json $serializer * @param Escaper $escaper + * @param DocumentRoot|null $documentRoot */ public function __construct( Http $response, Json $serializer = null, - Escaper $escaper = null + Escaper $escaper = null, + DocumentRoot $documentRoot = null ) { $this->_response = $response; $this->_errorDir = __DIR__ . '/'; $this->_reportDir = dirname(dirname($this->_errorDir)) . '/var/report/'; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); $this->escaper = $escaper ?: ObjectManager::getInstance()->get(Escaper::class); + $this->documentRoot = $documentRoot ?? ObjectManager::getInstance()->get(DocumentRoot::class); if (!empty($_SERVER['SCRIPT_NAME'])) { if (in_array(basename($_SERVER['SCRIPT_NAME'], '.php'), ['404', '503', 'report'])) { @@ -255,12 +264,13 @@ public function processReport() public function getViewFileUrl() { //The url needs to be updated base on Document root path. - return $this->getBaseUrl() . - str_replace( - str_replace('\\', '/', $this->_indexDir), - '', - str_replace('\\', '/', $this->_errorDir) - ) . $this->_config->skin . '/'; + $indexDir = str_replace('\\', '/', $this->_indexDir); + $errorDir = str_replace('\\', '/', $this->_errorDir); + $errorPathSuffix = $this->documentRoot->isPub() ? 'errors/' : 'pub/errors/'; + $errorPath = strpos($errorDir, $indexDir) === 0 ? + str_replace($indexDir, '', $errorDir) : $errorPathSuffix; + + return $this->getBaseUrl() . $errorPath . $this->_config->skin . '/'; } /** From 6df864fb33d76cffbe9d795462ddb44cf0291cd4 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Tue, 9 Jun 2020 02:32:02 -0500 Subject: [PATCH 0061/1013] MC-34467: Updated jQuery File Upload plugin - Fixed mftf test failures --- .../view/adminhtml/web/js/get-video-information.js | 8 ++++++-- lib/web/mage/backend/floating-header.js | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js index 5756356d4ff24..7386e438e9961 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js @@ -129,7 +129,9 @@ define([ * Abstract destroying command */ destroy: function () { - this._player.destroy(); + if (this._player) { + this._player.destroy(); + } }, /** @@ -288,7 +290,9 @@ define([ */ destroy: function () { this.stop(); - this._player.destroy(); + if (this._player) { + this._player.destroy(); + } } }); diff --git a/lib/web/mage/backend/floating-header.js b/lib/web/mage/backend/floating-header.js index a6f767259488a..1f3b49149a6e8 100644 --- a/lib/web/mage/backend/floating-header.js +++ b/lib/web/mage/backend/floating-header.js @@ -101,7 +101,9 @@ define([ * @private */ _destroy: function () { - this._placeholder.remove(); + if (this._placeholder) { + this._placeholder.remove(); + } this._off($(window)); } }); From f58e4fc92883aba5cef016a231d5789bfd2c489f Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Tue, 9 Jun 2020 11:07:42 +0300 Subject: [PATCH 0062/1013] add test coverage --- .../Quote/Customer/CheckoutEndToEndTest.php | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php index 7b686ea8c92f9..dba712869e6ce 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/CheckoutEndToEndTest.php @@ -13,6 +13,7 @@ use Magento\Framework\Registry; use Magento\Quote\Model\ResourceModel\Quote\CollectionFactory as QuoteCollectionFactory; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\OrderFactory; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -57,23 +58,31 @@ class CheckoutEndToEndTest extends GraphQlAbstract */ private $orderRepository; + /** + * @var OrderFactory + */ + private $orderFactory; + /** * @var array */ private $headers = []; + /** + * @inheritdoc + */ protected function setUp(): void { - parent::setUp(); - $objectManager = Bootstrap::getObjectManager(); + $this->registry = $objectManager->get(Registry::class); $this->quoteCollectionFactory = $objectManager->get(QuoteCollectionFactory::class); $this->quoteResource = $objectManager->get(QuoteResource::class); $this->quoteIdMaskFactory = $objectManager->get(QuoteIdMaskFactory::class); - $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->orderFactory = $objectManager->get(OrderFactory::class); } /** @@ -97,8 +106,13 @@ public function testCheckoutWorkflow() $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); $this->setPaymentMethod($cartId, $paymentMethod); - $orderId = $this->placeOrder($cartId); - $this->checkOrderInHistory($orderId); + $orderIncrementId = $this->placeOrder($cartId); + + $order = $this->orderFactory->create(); + $order->loadByIncrementId($orderIncrementId); + + $this->checkOrderInHistory($orderIncrementId); + $this->assertNotEmpty($order->getEmailSent()); } /** @@ -208,7 +222,7 @@ private function createEmptyCart(): string private function addProductToCart(string $cartId, float $qty, string $sku): void { $query = <<<QUERY -mutation { +mutation { addSimpleProductsToCart( input: { cart_id: "{$cartId}" @@ -350,7 +364,7 @@ private function setShippingMethod(string $cartId, array $method): array $query = <<<QUERY mutation { setShippingMethodsOnCart(input: { - cart_id: "{$cartId}", + cart_id: "{$cartId}", shipping_methods: [ { carrier_code: "{$method['carrier_code']}" From 67fae82c6278827f2c9b0f63083486a9d7740366 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@gmail.com> Date: Tue, 9 Jun 2020 13:19:12 +0300 Subject: [PATCH 0063/1013] MC-34197: Admin user improvement --- .../Model/ResourceModel/Role.php | 2 + .../Magento/Backend/Model/Auth/Session.php | 31 +++++++++- .../Security/Model/UserExpirationManager.php | 2 +- .../Security/Observer/AfterAdminUserSave.php | 2 +- .../Unit/Observer/AfterAdminUserSaveTest.php | 52 +++++++++++++++-- .../Adminhtml/User/Role/SaveRole.php | 8 ++- .../Magento/User/Model/ResourceModel/User.php | 49 ++++++++++------ app/code/Magento/User/Model/User.php | 27 ++++++++- .../_files/expired_users_rollback.php | 26 +++++++++ .../testsuite/Magento/User/Model/UserTest.php | 58 ++++++++++++------- .../_files/user_with_custom_role_rollback.php | 20 +++++-- 11 files changed, 223 insertions(+), 54 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Security/_files/expired_users_rollback.php diff --git a/app/code/Magento/Authorization/Model/ResourceModel/Role.php b/app/code/Magento/Authorization/Model/ResourceModel/Role.php index 48fe65e7f8b92..d23d039b2433d 100644 --- a/app/code/Magento/Authorization/Model/ResourceModel/Role.php +++ b/app/code/Magento/Authorization/Model/ResourceModel/Role.php @@ -119,6 +119,8 @@ protected function _afterDelete(\Magento\Framework\Model\AbstractModel $role) $connection->delete($this->_ruleTable, ['role_id = ?' => (int)$role->getId()]); + $this->_cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [\Magento\Backend\Block\Menu::CACHE_TAGS]); + return $this; } diff --git a/app/code/Magento/Backend/Model/Auth/Session.php b/app/code/Magento/Backend/Model/Auth/Session.php index 809b78b7b98bc..8f959d873243a 100644 --- a/app/code/Magento/Backend/Model/Auth/Session.php +++ b/app/code/Magento/Backend/Model/Auth/Session.php @@ -5,8 +5,10 @@ */ namespace Magento\Backend\Model\Auth; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\Cookie\CookieMetadataFactory; use Magento\Framework\Stdlib\CookieManagerInterface; +use Magento\Framework\Message\ManagerInterface; /** * Backend Auth session model @@ -56,6 +58,11 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage */ protected $_config; + /** + * @var ManagerInterface + */ + private $messageManager; + /** * @param \Magento\Framework\App\Request\Http $request * @param \Magento\Framework\Session\SidResolverInterface $sidResolver @@ -69,6 +76,7 @@ class Session extends \Magento\Framework\Session\SessionManager implements \Mage * @param \Magento\Framework\Acl\Builder $aclBuilder * @param \Magento\Backend\Model\UrlInterface $backendUrl * @param \Magento\Backend\App\ConfigInterface $config + * @param ManagerInterface $messageManager * @throws \Magento\Framework\Exception\SessionException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -84,11 +92,13 @@ public function __construct( \Magento\Framework\App\State $appState, \Magento\Framework\Acl\Builder $aclBuilder, \Magento\Backend\Model\UrlInterface $backendUrl, - \Magento\Backend\App\ConfigInterface $config + \Magento\Backend\App\ConfigInterface $config, + ManagerInterface $messageManager = null ) { $this->_config = $config; $this->_aclBuilder = $aclBuilder; $this->_backendUrl = $backendUrl; + $this->messageManager = $messageManager ?? ObjectManager::getInstance()->get(ManagerInterface::class); parent::__construct( $request, $sidResolver, @@ -171,6 +181,25 @@ public function isLoggedIn() */ public function prolong() { + $sessionUser = $this->getUser(); + $errorMessage = ''; + if ($sessionUser !== null) { + if ((int)$sessionUser->getIsActive() !== 1) { + $errorMessage = 'The account sign-in was incorrect or your account is disabled temporarily. ' + . 'Please wait and try again later.'; + } + if (!$sessionUser->hasAssigned2Role($sessionUser->getId())) { + $errorMessage = 'More permissions are needed to access this.'; + } + + if (!empty($errorMessage)) { + $this->destroy(); + $this->messageManager->addErrorMessage(__($errorMessage)); + + return; + } + } + $lifetime = $this->_config->getValue(self::XML_PATH_SESSION_LIFETIME); $cookieValue = $this->cookieManager->getCookie($this->getName()); diff --git a/app/code/Magento/Security/Model/UserExpirationManager.php b/app/code/Magento/Security/Model/UserExpirationManager.php index fe6b87de5a8ec..667ff4841165c 100644 --- a/app/code/Magento/Security/Model/UserExpirationManager.php +++ b/app/code/Magento/Security/Model/UserExpirationManager.php @@ -122,12 +122,12 @@ private function processExpiredUsers(ExpiredUsersCollection $expiredRecords): vo // delete expired records $expiredRecordIds = $expiredRecords->getAllIds(); + $expiredRecords->walk('delete'); // set user is_active to 0 $users = $this->userCollectionFactory->create() ->addFieldToFilter('main_table.user_id', ['in' => $expiredRecordIds]); $users->setDataToAll('is_active', 0)->save(); - $expiredRecords->walk('delete'); } /** diff --git a/app/code/Magento/Security/Observer/AfterAdminUserSave.php b/app/code/Magento/Security/Observer/AfterAdminUserSave.php index d11c1bfdcdf17..096b0f85f5056 100644 --- a/app/code/Magento/Security/Observer/AfterAdminUserSave.php +++ b/app/code/Magento/Security/Observer/AfterAdminUserSave.php @@ -53,7 +53,7 @@ public function execute(Observer $observer) { /* @var $user \Magento\User\Model\User */ $user = $observer->getEvent()->getObject(); - if ($user->getId()) { + if ($user->getId() && $user->hasData('expires_at')) { $expiresAt = $user->getExpiresAt(); /** @var \Magento\Security\Model\UserExpiration $userExpiration */ $userExpiration = $this->userExpirationFactory->create(); diff --git a/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php index 6a2a6107e3330..f142b2addfd87 100644 --- a/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php +++ b/app/code/Magento/Security/Test/Unit/Observer/AfterAdminUserSaveTest.php @@ -86,7 +86,7 @@ protected function setUp(): void ->getMock(); $this->userMock = $this->getMockBuilder(User::class) ->addMethods(['getExpiresAt']) - ->onlyMethods(['getId']) + ->onlyMethods(['getId', 'hasData']) ->disableOriginalConstructor() ->getMock(); $this->userExpirationMock = $this->createPartialMock( @@ -95,13 +95,20 @@ protected function setUp(): void ); } - public function testSaveNewUserExpiration() + /** + * @return void + */ + public function testSaveNewUserExpiration(): void { $userId = '123'; $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock); $this->userMock->expects(static::exactly(3))->method('getId')->willReturn($userId); $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn($this->getExpiresDateTime()); + $this->userMock->expects(static::once()) + ->method('hasData') + ->with('expires_at') + ->willReturn(true); $this->userExpirationFactoryMock->expects(static::once())->method('create') ->willReturn($this->userExpirationMock); $this->userExpirationResourceMock->expects(static::once())->method('load') @@ -119,7 +126,7 @@ public function testSaveNewUserExpiration() /** * @throws \Exception */ - public function testClearUserExpiration() + public function testClearUserExpiration(): void { $userId = '123'; $this->userExpirationMock->setId($userId); @@ -128,6 +135,10 @@ public function testClearUserExpiration() $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock); $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($userId); $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn(null); + $this->userMock->expects(static::once()) + ->method('hasData') + ->with('expires_at') + ->willReturn(true); $this->userExpirationFactoryMock->expects(static::once())->method('create') ->willReturn($this->userExpirationMock); $this->userExpirationResourceMock->expects(static::once())->method('load') @@ -139,7 +150,10 @@ public function testClearUserExpiration() $this->observer->execute($this->eventObserverMock); } - public function testChangeUserExpiration() + /** + * @return void + */ + public function testChangeUserExpiration(): void { $userId = '123'; $this->userExpirationMock->setId($userId); @@ -148,6 +162,10 @@ public function testChangeUserExpiration() $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock); $this->userMock->expects(static::exactly(2))->method('getId')->willReturn($userId); $this->userMock->expects(static::once())->method('getExpiresAt')->willReturn($this->getExpiresDateTime()); + $this->userMock->expects(static::once()) + ->method('hasData') + ->with('expires_at') + ->willReturn(true); $this->userExpirationFactoryMock->expects(static::once())->method('create') ->willReturn($this->userExpirationMock); $this->userExpirationResourceMock->expects(static::once())->method('load') @@ -161,11 +179,35 @@ public function testChangeUserExpiration() $this->observer->execute($this->eventObserverMock); } + /** + * @return void + */ + public function testExecuteWithoutUserExpiration(): void + { + $userId = '123'; + $this->userExpirationMock->setId($userId); + + $this->eventObserverMock->expects(static::once())->method('getEvent')->willReturn($this->eventMock); + $this->eventMock->expects(static::once())->method('getObject')->willReturn($this->userMock); + $this->userMock->expects(static::once())->method('getId')->willReturn($userId); + $this->userMock->expects(static::once()) + ->method('hasData') + ->with('expires_at') + ->willReturn(false); + $this->userExpirationFactoryMock->expects(static::never())->method('create'); + $this->userExpirationResourceMock->expects(static::never())->method('load'); + + $this->userExpirationMock->expects(static::never())->method('getId'); + $this->userExpirationMock->expects(static::never())->method('setExpiresAt'); + $this->userExpirationResourceMock->expects(static::never())->method('save'); + $this->observer->execute($this->eventObserverMock); + } + /** * @return string * @throws \Exception */ - private function getExpiresDateTime() + private function getExpiresDateTime(): string { $testDate = new \DateTime(); $testDate->modify('+10 days'); diff --git a/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php b/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php index 97ecb778b8cb1..39ee382709e56 100644 --- a/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php +++ b/app/code/Magento/User/Controller/Adminhtml/User/Role/SaveRole.php @@ -102,11 +102,12 @@ public function execute() 'admin_permissions_role_prepare_save', ['object' => $role, 'request' => $this->getRequest()] ); - $role->save(); - - $this->_rulesFactory->create()->setRoleId($role->getId())->setResources($resource)->saveRel(); $this->processPreviousUsers($role, $oldRoleUsers); $this->processCurrentUsers($role, $roleUsers); + + $role->save(); + $this->_rulesFactory->create()->setRoleId($role->getId())->setResources($resource)->saveRel(); + $this->messageManager->addSuccessMessage(__('You saved the role.')); } catch (UserLockedException $e) { $this->_auth->logout(); @@ -155,6 +156,7 @@ protected function validateUser() private function parseRequestVariable($paramName): array { $value = $this->getRequest()->getParam($paramName, null); + // phpcs:ignore Magento2.Functions.DiscouragedFunction parse_str($value, $value); $value = array_keys($value); return $value; diff --git a/app/code/Magento/User/Model/ResourceModel/User.php b/app/code/Magento/User/Model/ResourceModel/User.php index d9bc555b8e391..5fa099c041165 100644 --- a/app/code/Magento/User/Model/ResourceModel/User.php +++ b/app/code/Magento/User/Model/ResourceModel/User.php @@ -14,6 +14,7 @@ use Magento\Framework\Acl\Data\CacheInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Model\AbstractModel; use Magento\User\Model\Backend\Config\ObserverConfig; use Magento\User\Model\User as ModelUser; @@ -146,7 +147,7 @@ public function hasAssigned2Role($user) { if (is_numeric($user)) { $userId = $user; - } elseif ($user instanceof \Magento\Framework\Model\AbstractModel) { + } elseif ($user instanceof AbstractModel) { $userId = $user->getUserId(); } else { return null; @@ -171,13 +172,25 @@ public function hasAssigned2Role($user) } } + /** + * @inheritDoc + */ + protected function _beforeSave(AbstractModel $user) + { + if ($user->hasRoleId()) { + $user->setReloadAclFlag(1); + } + + return parent::_beforeSave($user); + } + /** * Unserialize user extra data after user save * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return $this */ - protected function _afterSave(\Magento\Framework\Model\AbstractModel $user) + protected function _afterSave(AbstractModel $user) { $user->setExtra($this->getSerializer()->unserialize($user->getExtra())); if ($user->hasRoleId()) { @@ -234,10 +247,10 @@ protected function _createUserRole($parentId, ModelUser $user) /** * Unserialize user extra data after user load * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return $this */ - protected function _afterLoad(\Magento\Framework\Model\AbstractModel $user) + protected function _afterLoad(AbstractModel $user) { if (is_string($user->getExtra())) { $user->setExtra($this->getSerializer()->unserialize($user->getExtra())); @@ -248,11 +261,11 @@ protected function _afterLoad(\Magento\Framework\Model\AbstractModel $user) /** * Delete user role record with user * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return bool * @throws LocalizedException */ - public function delete(\Magento\Framework\Model\AbstractModel $user) + public function delete(AbstractModel $user) { $uid = $user->getId(); if (!$uid) { @@ -283,10 +296,10 @@ public function delete(\Magento\Framework\Model\AbstractModel $user) /** * Get user roles * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return array */ - public function getRoles(\Magento\Framework\Model\AbstractModel $user) + public function getRoles(AbstractModel $user) { if (!$user->getId()) { return []; @@ -324,10 +337,10 @@ public function getRoles(\Magento\Framework\Model\AbstractModel $user) /** * Delete user role * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return $this */ - public function deleteFromRole(\Magento\Framework\Model\AbstractModel $user) + public function deleteFromRole(AbstractModel $user) { if ($user->getUserId() <= 0) { return $this; @@ -351,10 +364,10 @@ public function deleteFromRole(\Magento\Framework\Model\AbstractModel $user) /** * Check if role user exists * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return array */ - public function roleUserExists(\Magento\Framework\Model\AbstractModel $user) + public function roleUserExists(AbstractModel $user) { if ($user->getUserId() > 0) { $roleTable = $this->getTable('authorization_role'); @@ -381,10 +394,10 @@ public function roleUserExists(\Magento\Framework\Model\AbstractModel $user) /** * Check if user exists * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return array */ - public function userExists(\Magento\Framework\Model\AbstractModel $user) + public function userExists(AbstractModel $user) { $connection = $this->getConnection(); $select = $connection->select(); @@ -409,10 +422,10 @@ public function userExists(\Magento\Framework\Model\AbstractModel $user) /** * Whether a user's identity is confirmed * - * @param \Magento\Framework\Model\AbstractModel $user + * @param AbstractModel $user * @return bool */ - public function isUserUnique(\Magento\Framework\Model\AbstractModel $user) + public function isUserUnique(AbstractModel $user) { return !$this->userExists($user); } @@ -420,7 +433,7 @@ public function isUserUnique(\Magento\Framework\Model\AbstractModel $user) /** * Save user extra data * - * @param \Magento\Framework\Model\AbstractModel $object + * @param AbstractModel $object * @param string $data * @return $this */ diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index 00d2aa140a991..cd969bab27840 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -149,6 +149,11 @@ class User extends AbstractModel implements StorageInterface, UserInterface */ private $deploymentConfig; + /** + * @var string + */ + protected $_cacheTag = \Magento\Backend\Block\Menu::CACHE_TAGS; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -684,7 +689,27 @@ public function loadByUsername($username) */ public function hasAssigned2Role($user) { - return $this->getResource()->hasAssigned2Role($user); + if ($user instanceof AbstractModel) { + $userId = $user->getUserId(); + } elseif (is_numeric($user) && (int)$user !== 0) { + $userId = $user; + } else { + return null; + } + $data = $this->_cacheManager->load('assigned_role_' . $userId); + if (false === $data) { + $data = $this->getResource()->hasAssigned2Role($user); + + $this->_cacheManager->save( + $this->serializer->serialize($data), + 'assigned_role_' . $userId, + [\Magento\Backend\Block\Menu::CACHE_TAGS] + ); + } else { + $data = $this->serializer->unserialize($data); + } + + return $data; } /** diff --git a/dev/tests/integration/testsuite/Magento/Security/_files/expired_users_rollback.php b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users_rollback.php new file mode 100644 index 0000000000000..afee49321e309 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Security/_files/expired_users_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\TestFramework\Helper\Bootstrap; +use Magento\User\Model\UserFactory; +use Magento\User\Model\User; + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +$userFactory = $objectManager->get(UserFactory::class); +$userNames = ['adminUserNotExpired', 'adminUserExpired']; + +foreach ($userNames as $userName) { + /** @var User $user */ + $user = $userFactory->create(); + $user->load($userName, 'username'); + + if ($user->getId() !== null) { + $user->delete(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php index 04e3e2493cc83..feb50b60a8e4a 100644 --- a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php @@ -6,25 +6,32 @@ namespace Magento\User\Model; +use Magento\Authorization\Model\Role; +use Magento\Framework\App\CacheInterface; use Magento\Framework\Encryption\Encryptor; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\DateTime; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User as UserModel; /** * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class UserTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\User\Model\User + * @var UserModel */ protected $_model; /** - * @var \Magento\Framework\Stdlib\DateTime + * @var DateTime */ protected $_dateTime; /** - * @var \Magento\Authorization\Model\Role + * @var Role */ protected static $_newRole; @@ -33,17 +40,26 @@ class UserTest extends \PHPUnit\Framework\TestCase */ private $encryptor; + /** + * @var CacheInterface + */ + private $cache; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\User\Model\User::class - ); - $this->_dateTime = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Framework\Stdlib\DateTime::class - ); - $this->encryptor = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - Encryptor::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->_model = $this->objectManager->create(UserModel::class); + $this->_dateTime = $this->objectManager->get(DateTime::class); + $this->encryptor = $this->objectManager->get(Encryptor::class); + $this->cache = $this->objectManager->get(CacheInterface::class); } /** @@ -109,8 +125,8 @@ public function testUpdateRoleOnSave() */ public static function roleDataFixture() { - self::$_newRole = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Authorization\Model\Role::class + self::$_newRole = Bootstrap::getObjectManager()->create( + Role::class ); self::$_newRole->setName('admin_role')->setRoleType('G')->setPid('1'); self::$_newRole->save(); @@ -150,7 +166,7 @@ public function testGetRole() { $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); $role = $this->_model->getRole(); - $this->assertInstanceOf(\Magento\Authorization\Model\Role::class, $role); + $this->assertInstanceOf(Role::class, $role); $this->assertEquals(\Magento\TestFramework\Bootstrap::ADMIN_ROLE_NAME, $this->_model->getRole()->getRoleName()); $this->_model->setRoleId(self::$_newRole->getId())->save(); $role = $this->_model->getRole(); @@ -198,7 +214,7 @@ public function testGetName() public function testGetUninitializedAclRole() { - $newuser = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + $newuser = $this->objectManager->create(UserModel::class); $newuser->setUserId(10); $this->assertNull($newuser->getAclRole(), "User role was not initialized and is expected to be empty."); } @@ -251,17 +267,18 @@ public function testAuthenticateInactiveUser() } /** + * @magentoDataFixture Magento/User/_files/user_with_custom_role.php * @magentoDbIsolation enabled */ public function testAuthenticateUserWithoutRole() { $this->expectException(\Magento\Framework\Exception\AuthenticationException::class); - $this->_model->loadByUsername(\Magento\TestFramework\Bootstrap::ADMIN_NAME); + $this->_model->loadByUsername('customRoleUser'); $roles = $this->_model->getRoles(); $this->_model->setRoleId(reset($roles))->deleteFromRole(); $this->_model->authenticate( - \Magento\TestFramework\Bootstrap::ADMIN_NAME, + 'customRoleUser', \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD ); } @@ -315,6 +332,7 @@ public function testHasAssigned2Role() $this->assertArrayHasKey('role_id', $role[0]); $roles = $this->_model->getRoles(); $this->_model->setRoleId(reset($roles))->deleteFromRole(); + $this->cache->clean([\Magento\Backend\Block\Menu::CACHE_TAGS]); $this->assertEmpty($this->_model->hasAssigned2Role($this->_model)); } @@ -369,8 +387,8 @@ public function testBeforeSavePasswordHash() 'Salt is expected to be saved along with the password' ); - /** @var \Magento\User\Model\User $model */ - $model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\User\Model\User::class); + /** @var UserModel $model */ + $model = $this->objectManager->create(UserModel::class); $model->load($this->_model->getId()); $this->assertEquals( $this->_model->getPassword(), diff --git a/dev/tests/integration/testsuite/Magento/User/_files/user_with_custom_role_rollback.php b/dev/tests/integration/testsuite/Magento/User/_files/user_with_custom_role_rollback.php index f3c061236a1c3..ab1b87b261ff6 100644 --- a/dev/tests/integration/testsuite/Magento/User/_files/user_with_custom_role_rollback.php +++ b/dev/tests/integration/testsuite/Magento/User/_files/user_with_custom_role_rollback.php @@ -9,20 +9,32 @@ use Magento\Authorization\Model\RoleFactory; use Magento\Authorization\Model\Role; use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\UserFactory; use Magento\User\Model\User; use Magento\Authorization\Model\RulesFactory; use Magento\Authorization\Model\Rules; //Deleting the user and the role. +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); /** @var User $user */ -$user = Bootstrap::getObjectManager()->create(User::class); +$user = $objectManager->create(UserFactory::class)->create(); $user->load('customRoleUser', 'username'); -$user->delete(); + +if ($user->getId() !== null) { + $user->delete(); +} + /** @var Role $role */ $role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); $role->load('test_custom_role', 'role_name'); /** @var Rules $rules */ $rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); $rules->load($role->getId(), 'role_id'); -$rules->delete(); -$role->delete(); + +if ($rules->getId() !== null) { + $rules->delete(); +} +if ($role->getId() !== null) { + $role->delete(); +} From 404a047e80ea5ce08243f81ae46eff9312864698 Mon Sep 17 00:00:00 2001 From: Stas Kozar <stas.kozar@transoftgroup.com> Date: Tue, 9 Jun 2020 15:40:58 +0300 Subject: [PATCH 0064/1013] MC-34748: Improve zip archive filename validation --- .../Magento/Framework/Archive/Zip.php | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Archive/Zip.php b/lib/internal/Magento/Framework/Archive/Zip.php index 20f408a605c10..e86d6911546f0 100644 --- a/lib/internal/Magento/Framework/Archive/Zip.php +++ b/lib/internal/Magento/Framework/Archive/Zip.php @@ -53,8 +53,9 @@ public function unpack($source, $destination) { $zip = new \ZipArchive(); if ($zip->open($source) === true) { - $zip->renameIndex(0, basename($destination)); - $filename = $zip->getNameIndex(0) ?: ''; + $baseName = basename($destination); + $filename = $this->getFilenameFromZip($zip, $baseName); + if ($filename) { $zip->extractTo(dirname($destination), $filename); } else { @@ -67,4 +68,25 @@ public function unpack($source, $destination) return $destination; } + + /** + * Retrieve filename for import from zip archive. + * + * @param \ZipArchive $zip + * @param string $baseName + * + * @return string + */ + private function getFilenameFromZip(\ZipArchive $zip, string $baseName): string + { + $index = 0; + + do { + $zip->renameIndex($index, $baseName); + $filename = $zip->getNameIndex($index); + $index++; + } while ($baseName !== $filename && $filename !== false); + + return $filename === $baseName ? $filename : ''; + } } From 74c0fb5b64b679ba59c152f4152ebaf45578816f Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Tue, 9 Jun 2020 11:23:55 -0500 Subject: [PATCH 0065/1013] MC-34467: Updated jQuery File Upload plugin - Fixed static test failures --- .../ProductVideo/view/adminhtml/web/js/get-video-information.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js index 7386e438e9961..cb56a085304a7 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/get-video-information.js @@ -290,6 +290,7 @@ define([ */ destroy: function () { this.stop(); + if (this._player) { this._player.destroy(); } From 54a508edf6d93963537a2cacf5f713fe45d49655 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Tue, 9 Jun 2020 12:59:47 -0500 Subject: [PATCH 0066/1013] MC-34467: Updated jQuery File Upload plugin - Fixed mftf test failures --- lib/web/jquery/fileUploader/jquery.fileupload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web/jquery/fileUploader/jquery.fileupload.js b/lib/web/jquery/fileUploader/jquery.fileupload.js index 8f0ff0d4faf03..dda71f0413c71 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload.js @@ -16,10 +16,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'jquery/fileUploader/vendor/jquery.ui.widget'], factory); + define(['jquery', 'jquery-ui/ui/widget'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('jquery/fileUploader/vendor/jquery.ui.widget')); + factory(require('jquery'), require('jquery-ui/ui/widget')); } else { // Browser globals: factory(window.jQuery); From 7862cde162b05fc387cbb42300ae2a70d0530bb0 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Tue, 9 Jun 2020 14:35:08 -0500 Subject: [PATCH 0067/1013] MC-34467: Updated jQuery File Upload plugin - Fixed mftf test failures --- lib/web/jquery/fileUploader/jquery.fileupload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/web/jquery/fileUploader/jquery.fileupload.js b/lib/web/jquery/fileUploader/jquery.fileupload.js index dda71f0413c71..8852817410478 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload.js @@ -16,10 +16,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'jquery-ui/ui/widget'], factory); + define(['jquery', 'jquery-ui-modules/widget'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('jquery-ui/ui/widget')); + factory(require('jquery'), require('jquery-ui-modules/widget')); } else { // Browser globals: factory(window.jQuery); From 4817eb506ae2735d5a5bd298c1e59350a150cb66 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Tue, 9 Jun 2020 17:05:29 -0500 Subject: [PATCH 0068/1013] MC-34467: Updated jQuery File Upload plugin - Testing cause of error --- .../Magento/Ui/view/base/web/js/form/element/file-uploader.js | 2 +- lib/web/jquery/fileUploader/jquery.fileupload.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 73bef62910644..a77f29b1326f7 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -426,7 +426,7 @@ define([ } else { // construct message from all aggregatedErrors _.each(this.aggregatedErrors, function (error) { notification().add({ - error: true, + error: 'testing', message: '%s' + error.message, // %s to be used as placeholder for html injection /** diff --git a/lib/web/jquery/fileUploader/jquery.fileupload.js b/lib/web/jquery/fileUploader/jquery.fileupload.js index 8852817410478..8f0ff0d4faf03 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload.js @@ -16,10 +16,10 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'jquery-ui-modules/widget'], factory); + define(['jquery', 'jquery/fileUploader/vendor/jquery.ui.widget'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: - factory(require('jquery'), require('jquery-ui-modules/widget')); + factory(require('jquery'), require('jquery/fileUploader/vendor/jquery.ui.widget')); } else { // Browser globals: factory(window.jQuery); From 53a3b7c568a90258739dc9c4422601a09dfbaa97 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Tue, 9 Jun 2020 18:25:12 -0500 Subject: [PATCH 0069/1013] MC-34467: Updated jQuery File Upload plugin - Testing cause of error --- .../Magento/Ui/view/base/web/js/form/element/file-uploader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index a77f29b1326f7..536622ef3e6d4 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -438,7 +438,7 @@ define([ var escapedFileName = $('<div>').text(error.filename).html(), errorMsgBodyHtml = '<strong>%s</strong> %s.<br>' .replace('%s', escapedFileName) - .replace('%s', $t('was not uploaded')); + .replace('%s', $t('was not uploaded test')); // html is escaped in message body for notification widget; prepend unescaped html here constructedMessage = constructedMessage.replace('%s', errorMsgBodyHtml); From 7bba93bb98b4f13f9a654f86062488393fc68010 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Tue, 9 Jun 2020 19:33:51 -0500 Subject: [PATCH 0070/1013] MC-34467: Updated jQuery File Upload plugin - Removed test changes --- .../Magento/Ui/view/base/web/js/form/element/file-uploader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 536622ef3e6d4..73bef62910644 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -426,7 +426,7 @@ define([ } else { // construct message from all aggregatedErrors _.each(this.aggregatedErrors, function (error) { notification().add({ - error: 'testing', + error: true, message: '%s' + error.message, // %s to be used as placeholder for html injection /** @@ -438,7 +438,7 @@ define([ var escapedFileName = $('<div>').text(error.filename).html(), errorMsgBodyHtml = '<strong>%s</strong> %s.<br>' .replace('%s', escapedFileName) - .replace('%s', $t('was not uploaded test')); + .replace('%s', $t('was not uploaded')); // html is escaped in message body for notification widget; prepend unescaped html here constructedMessage = constructedMessage.replace('%s', errorMsgBodyHtml); From 3409af0c89d202ba48ce017d1cb38e57ece5b5f5 Mon Sep 17 00:00:00 2001 From: Barny Shergold <barnyshergold@MacBook-Pro-9.local> Date: Wed, 10 Jun 2020 23:25:35 +0100 Subject: [PATCH 0071/1013] Changed so that store view category is always used to create product URLs --- .../Model/ProductScopeRewriteGenerator.php | 28 ++----------------- .../ProductScopeRewriteGeneratorTest.php | 11 +++++++- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index 9d26184e2c2d4..693084b7aba18 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -174,8 +174,9 @@ public function generateForSpecificStoreView($storeId, $productCategories, Produ continue; } - // category should be loaded per appropriate store if category's URL key has been changed - $categories[] = $this->getCategoryWithOverriddenUrlKey($storeId, $category); + // Category should be loaded per appropriate store at all times. This is because whilst the URL key on the + // category in focus might be unchanged, parent category URL keys might be + $categories[] = $this->categoryRepository->get($category->getEntityId(), $storeId); } $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); @@ -234,29 +235,6 @@ public function isCategoryProperForGenerating(Category $category, $storeId) return false; } - /** - * Check if URL key has been changed - * - * Checks if URL key has been changed for provided category and returns reloaded category, - * in other case - returns provided category. - * - * @param int $storeId - * @param Category $category - * @return Category - */ - private function getCategoryWithOverriddenUrlKey($storeId, Category $category) - { - $isUrlKeyOverridden = $this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( - $storeId, - $category->getEntityId(), - Category::ENTITY - ); - - if (!$isUrlKeyOverridden) { - return $category; - } - return $this->categoryRepository->get($category->getEntityId(), $storeId); - } /** * Check config value of generate_category_product_rewrites diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php index 7b18461a580fe..f54805158cfb2 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php @@ -7,6 +7,7 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Model; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\CatalogUrlRewrite\Model\ObjectRegistry; @@ -69,6 +70,9 @@ class ProductScopeRewriteGeneratorTest extends TestCase /** @var ScopeConfigInterface|MockObject */ private $configMock; + /** @var CategoryRepositoryInterface|MockObject */ + private $categoryRepository; + protected function setUp(): void { $this->serializer = $this->createMock(Json::class); @@ -126,6 +130,8 @@ function ($value) { $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) ->getMock(); + $this->categoryRepository = $this->getMockForAbstractClass(CategoryRepositoryInterface::class); + $this->productScopeGenerator = (new ObjectManager($this))->getObject( ProductScopeRewriteGenerator::class, [ @@ -137,7 +143,8 @@ function ($value) { 'storeViewService' => $this->storeViewService, 'storeManager' => $this->storeManager, 'mergeDataProviderFactory' => $mergeDataProviderFactory, - 'config' => $this->configMock + 'config' => $this->configMock, + 'categoryRepository' => $this->categoryRepository ] ); $this->categoryMock = $this->getMockBuilder(Category::class) @@ -215,6 +222,8 @@ public function testGenerationForSpecificStore() $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') ->willReturn([]); + $this->categoryRepository->expects($this->once())->method('get')->willReturn($this->categoryMock); + $this->assertEquals( ['category-1_1' => $canonical], $this->productScopeGenerator->generateForSpecificStoreView(1, [$this->categoryMock], $product, 1) From ede9debe73864d002fad9967ca3252c448570f83 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Wed, 10 Jun 2020 18:46:26 -0500 Subject: [PATCH 0072/1013] MC-34467: Updated jQuery File Upload plugin - Updated file-uploader dependency --- .../Theme/view/base/requirejs-config.js | 2 +- .../User/view/adminhtml/web/app-config.js | 2 +- lib/web/jquery/fileUploader/LICENSE.txt | 20 + lib/web/jquery/fileUploader/README.md | 225 ++++ lib/web/jquery/fileUploader/SECURITY.md | 227 ++++ .../fileUploader/jquery.fileupload-audio.js | 4 +- .../fileUploader/jquery.fileupload-image.js | 59 +- .../fileUploader/jquery.fileupload-ui.js | 4 +- .../fileUploader/jquery.fileupload-video.js | 4 +- .../vendor/blueimp-canvas-to-blob/LICENSE.txt | 20 + .../vendor/blueimp-canvas-to-blob/README.md | 135 +++ .../js/canvas-to-blob.js | 143 +++ .../vendor/blueimp-load-image/LICENSE.txt | 20 + .../vendor/blueimp-load-image/README.md | 1070 +++++++++++++++++ .../vendor/blueimp-load-image/js/index.js | 12 + .../js/load-image-exif-map.js | 420 +++++++ .../blueimp-load-image/js/load-image-exif.js | 460 +++++++ .../blueimp-load-image/js/load-image-fetch.js | 103 ++ .../js/load-image-iptc-map.js | 169 +++ .../blueimp-load-image/js/load-image-iptc.js | 239 ++++ .../blueimp-load-image/js/load-image-meta.js | 259 ++++ .../js/load-image-orientation.js | 481 ++++++++ .../blueimp-load-image/js/load-image-scale.js | 327 +++++ .../js/load-image.all.min.js | 2 + .../js/load-image.all.min.js.map | 1 + .../blueimp-load-image/js/load-image.js | 229 ++++ .../vendor/blueimp-tmpl/LICENSE.txt | 20 + .../vendor/blueimp-tmpl/README.md | 436 +++++++ .../vendor/blueimp-tmpl/js/compile.js | 91 ++ .../vendor/blueimp-tmpl/js/runtime.js | 50 + .../vendor/blueimp-tmpl/js/tmpl.js | 98 ++ .../vendor/blueimp-tmpl/js/tmpl.min.js | 2 + .../vendor/blueimp-tmpl/js/tmpl.min.js.map | 1 + 33 files changed, 5293 insertions(+), 42 deletions(-) create mode 100644 lib/web/jquery/fileUploader/LICENSE.txt create mode 100644 lib/web/jquery/fileUploader/README.md create mode 100644 lib/web/jquery/fileUploader/SECURITY.md create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/LICENSE.txt create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/README.md create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/js/canvas-to-blob.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/LICENSE.txt create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/index.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif-map.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-fetch.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc-map.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-orientation.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js.map create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/LICENSE.txt create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/README.md create mode 100755 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/compile.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/runtime.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js create mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index 77af920c8df86..cba1b3307407b 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -31,7 +31,7 @@ var config = { 'paths': { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-ui', 'prototype': 'legacy-build.min', 'jquery/jquery-storageapi': 'jquery/jquery.storageapi.min', 'text': 'mage/requirejs/text', diff --git a/app/code/Magento/User/view/adminhtml/web/app-config.js b/app/code/Magento/User/view/adminhtml/web/app-config.js index 491378d933ca2..f5c8cb9dd19c8 100644 --- a/app/code/Magento/User/view/adminhtml/web/app-config.js +++ b/app/code/Magento/User/view/adminhtml/web/app-config.js @@ -26,7 +26,7 @@ require.config({ 'jquery/ui': 'jquery/jquery-ui-1.9.2', 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-process', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-ui', 'prototype': 'prototype/prototype-amd', 'text': 'requirejs/text', 'domReady': 'requirejs/domReady', diff --git a/lib/web/jquery/fileUploader/LICENSE.txt b/lib/web/jquery/fileUploader/LICENSE.txt new file mode 100644 index 0000000000000..ca9e708c6718f --- /dev/null +++ b/lib/web/jquery/fileUploader/LICENSE.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright © 2010 Sebastian Tschan, https://blueimp.net + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/web/jquery/fileUploader/README.md b/lib/web/jquery/fileUploader/README.md new file mode 100644 index 0000000000000..5a13ef4252e44 --- /dev/null +++ b/lib/web/jquery/fileUploader/README.md @@ -0,0 +1,225 @@ +# jQuery File Upload + +## Contents + +- [Description](#description) +- [Demo](#demo) +- [Features](#features) +- [Security](#security) +- [Setup](#setup) +- [Requirements](#requirements) + - [Mandatory requirements](#mandatory-requirements) + - [Optional requirements](#optional-requirements) + - [Cross-domain requirements](#cross-domain-requirements) +- [Browsers](#browsers) + - [Desktop browsers](#desktop-browsers) + - [Mobile browsers](#mobile-browsers) + - [Extended browser support information](#extended-browser-support-information) +- [Testing](#testing) +- [Support](#support) +- [License](#license) + +## Description + +> File Upload widget with multiple file selection, drag&drop support, +> progress bars, validation and preview images, audio and video for jQuery. +> Supports cross-domain, chunked and resumable file uploads and client-side +> image resizing. +> Works with any server-side platform (PHP, Python, Ruby on Rails, Java, +> Node.js, Go etc.) that supports standard HTML form file uploads. + +## Demo + +[Demo File Upload](https://blueimp.github.io/jQuery-File-Upload/) + +## Features + +- **Multiple file upload:** + Allows to select multiple files at once and upload them simultaneously. +- **Drag & Drop support:** + Allows to upload files by dragging them from your desktop or file manager and + dropping them on your browser window. +- **Upload progress bar:** + Shows a progress bar indicating the upload progress for individual files and + for all uploads combined. +- **Cancelable uploads:** + Individual file uploads can be canceled to stop the upload progress. +- **Resumable uploads:** + Aborted uploads can be resumed with browsers supporting the Blob API. +- **Chunked uploads:** + Large files can be uploaded in smaller chunks with browsers supporting the + Blob API. +- **Client-side image resizing:** + Images can be automatically resized on client-side with browsers supporting + the required JS APIs. +- **Preview images, audio and video:** + A preview of image, audio and video files can be displayed before uploading + with browsers supporting the required APIs. +- **No browser plugins (e.g. Adobe Flash) required:** + The implementation is based on open standards like HTML5 and JavaScript and + requires no additional browser plugins. +- **Graceful fallback for legacy browsers:** + Uploads files via XMLHttpRequests if supported and uses iframes as fallback + for legacy browsers. +- **HTML file upload form fallback:** + Allows progressive enhancement by using a standard HTML file upload form as + widget element. +- **Cross-site file uploads:** + Supports uploading files to a different domain with cross-site XMLHttpRequests + or iframe redirects. +- **Multiple plugin instances:** + Allows to use multiple plugin instances on the same webpage. +- **Customizable and extensible:** + Provides an API to set individual options and define callback methods for + various upload events. +- **Multipart and file contents stream uploads:** + Files can be uploaded as standard "multipart/form-data" or file contents + stream (HTTP PUT file upload). +- **Compatible with any server-side application platform:** + Works with any server-side platform (PHP, Python, Ruby on Rails, Java, + Node.js, Go etc.) that supports standard HTML form file uploads. + +## Security + +⚠️ Please read the [VULNERABILITIES](VULNERABILITIES.md) document for a list of +fixed vulnerabilities + +Please also read the [SECURITY](SECURITY.md) document for instructions on how to +securely configure your Webserver for file uploads. + +## Setup + +jQuery File Upload can be installed via [NPM](https://www.npmjs.com/): + +```sh +npm install blueimp-file-upload +``` + +This allows you to include [jquery.fileupload.js](js/jquery.fileupload.js) and +its extensions via `node_modules`, e.g: + +```html +<script src="node_modules/blueimp-file-upload/js/jquery.fileupload.js"></script> +``` + +The widget can then be initialized on a file upload form the following way: + +```js +$('#fileupload').fileupload(); +``` + +For further information, please refer to the following guides: + +- [Main documentation page](https://github.com/blueimp/jQuery-File-Upload/wiki) +- [List of all available Options](https://github.com/blueimp/jQuery-File-Upload/wiki/Options) +- [The plugin API](https://github.com/blueimp/jQuery-File-Upload/wiki/API) +- [How to setup the plugin on your website](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) +- [How to use only the basic plugin.](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin) + +## Requirements + +### Mandatory requirements + +- [jQuery](https://jquery.com/) v1.7+ +- [jQuery UI widget factory](https://api.jqueryui.com/jQuery.widget/) v1.9+ + (included): Required for the basic File Upload plugin, but very lightweight + without any other dependencies from the jQuery UI suite. +- [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) + (included): Required for + [browsers without XHR file upload support](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +### Optional requirements + +- [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates) + v3+: Used to render the selected and uploaded files for the Basic Plus UI and + jQuery UI versions. +- [JavaScript Load Image library](https://github.com/blueimp/JavaScript-Load-Image) + v2+: Required for the image previews and resizing functionality. +- [JavaScript Canvas to Blob polyfill](https://github.com/blueimp/JavaScript-Canvas-to-Blob) + v3+:Required for the image previews and resizing functionality. +- [blueimp Gallery](https://github.com/blueimp/Gallery) v2+: Used to display the + uploaded images in a lightbox. +- [Bootstrap](https://getbootstrap.com/) v3+: Used for the demo design. +- [Glyphicons](https://glyphicons.com/) Icon set used by Bootstrap. + +### Cross-domain requirements + +[Cross-domain File Uploads](https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads) +using the +[Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) +require a redirect back to the origin server to retrieve the upload results. The +[example implementation](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/main.js) +makes use of +[result.html](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/result.html) +as a static redirect page for the origin server. + +The repository also includes the +[jQuery XDomainRequest Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/cors/jquery.xdr-transport.js), +which enables limited cross-domain AJAX requests in Microsoft Internet Explorer +8 and 9 (IE 10 supports cross-domain XHR requests). +The XDomainRequest object allows GET and POST requests only and doesn't support +file uploads. It is used on the +[Demo](https://blueimp.github.io/jQuery-File-Upload/) to delete uploaded files +from the cross-domain demo file upload service. + +## Browsers + +### Desktop browsers + +The File Upload plugin is regularly tested with the latest browser versions and +supports the following minimal versions: + +- Google Chrome +- Apple Safari 4.0+ +- Mozilla Firefox 3.0+ +- Opera 11.0+ +- Microsoft Internet Explorer 6.0+ + +### Mobile browsers + +The File Upload plugin has been tested with and supports the following mobile +browsers: + +- Apple Safari on iOS 6.0+ +- Google Chrome on iOS 6.0+ +- Google Chrome on Android 4.0+ +- Default Browser on Android 2.3+ +- Opera Mobile 12.0+ + +### Extended browser support information + +For a detailed overview of the features supported by each browser version and +known operating system / browser bugs, please have a look at the +[Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +## Testing + +The project comes with three sets of tests: + +1. Code linting using [ESLint](https://eslint.org/). +2. Unit tests using [Mocha](https://mochajs.org/). +3. End-to-end tests using [blueimp/wdio](https://github.com/blueimp/wdio). + +To run the tests, follow these steps: + +1. Start [Docker](https://docs.docker.com/). +2. Install development dependencies: + ```sh + npm install + ``` +3. Run the tests: + ```sh + npm test + ``` + +## Support + +This project is actively maintained, but there is no official support channel. +If you have a question that another developer might help you with, please post +to +[Stack Overflow](https://stackoverflow.com/questions/tagged/blueimp+jquery+file-upload) +and tag your question with `blueimp jquery file upload`. + +## License + +Released under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/lib/web/jquery/fileUploader/SECURITY.md b/lib/web/jquery/fileUploader/SECURITY.md new file mode 100644 index 0000000000000..433a6853cdb3a --- /dev/null +++ b/lib/web/jquery/fileUploader/SECURITY.md @@ -0,0 +1,227 @@ +# File Upload Security + +## Contents + +- [Introduction](#introduction) +- [Purpose of this project](#purpose-of-this-project) +- [Mitigations against file upload risks](#mitigations-against-file-upload-risks) + - [Prevent code execution on the server](#prevent-code-execution-on-the-server) + - [Prevent code execution in the browser](#prevent-code-execution-in-the-browser) + - [Prevent distribution of malware](#prevent-distribution-of-malware) +- [Secure file upload serving configurations](#secure-file-upload-serving-configurations) + - [Apache config](#apache-config) + - [NGINX config](#nginx-config) +- [Secure image processing configurations](#secure-image-processing-configurations) +- [ImageMagick config](#imagemagick-config) + +## Introduction + +For an in-depth understanding of the potential security risks of providing file +uploads and possible mitigations, please refer to the +[OWASP - Unrestricted File Upload](https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload) +documentation. + +To securely setup the project to serve uploaded files, please refer to the +sample +[Secure file upload serving configurations](#secure-file-upload-serving-configurations). + +To mitigate potential vulnerabilities in image processing libraries, please +refer to the +[Secure image processing configurations](#secure-image-processing-configurations). + +By default, all sample upload handlers allow only upload of image files, which +mitigates some attack vectors, but should not be relied on as the only +protection. + +Please also have a look at the +[list of fixed vulnerabilities](VULNERABILITIES.md) in jQuery File Upload, which +relates mostly to the sample server-side upload handlers and how they have been +configured. + +## Purpose of this project + +Please note that this project is not a complete file management product, but +foremost a client-side file upload library for [jQuery](https://jquery.com/). +The server-side sample upload handlers are just examples to demonstrate the +client-side file upload functionality. + +To make this very clear, there is **no user authentication** by default: + +- **everyone can upload files** +- **everyone can delete uploaded files** + +In some cases this can be acceptable, but for most projects you will want to +extend the sample upload handlers to integrate user authentication, or implement +your own. + +It is also up to you to configure your web server to securely serve the uploaded +files, e.g. using the +[sample server configurations](#secure-file-upload-serving-configurations). + +## Mitigations against file upload risks + +### Prevent code execution on the server + +To prevent execution of scripts or binaries on server-side, the upload directory +must be configured to not execute files in the upload directory (e.g. +`server/php/files` as the default for the PHP upload handler) and only treat +uploaded files as static content. + +The recommended way to do this is to configure the upload directory path to +point outside of the web application root. +Then the web server can be configured to serve files from the upload directory +with their default static files handler only. + +Limiting file uploads to a whitelist of safe file types (e.g. image files) also +mitigates this issue, but should not be the only protection. + +### Prevent code execution in the browser + +To prevent execution of scripts on client-side, the following headers must be +sent when delivering generic uploaded files to the client: + +``` +Content-Type: application/octet-stream +X-Content-Type-Options: nosniff +``` + +The `Content-Type: application/octet-stream` header instructs browsers to +display a download dialog instead of parsing it and possibly executing script +content e.g. in HTML files. + +The `X-Content-Type-Options: nosniff` header prevents browsers to try to detect +the file mime type despite the given content-type header. + +For known safe files, the content-type header can be adjusted using a +**whitelist**, e.g. sending `Content-Type: image/png` for PNG files. + +### Prevent distribution of malware + +To prevent attackers from uploading and distributing malware (e.g. computer +viruses), it is recommended to limit file uploads only to a whitelist of safe +file types. + +Please note that the detection of file types in the sample file upload handlers +is based on the file extension and not the actual file content. This makes it +still possible for attackers to upload malware by giving their files an image +file extension, but should prevent automatic execution on client computers when +opening those files. + +It does not protect at all from exploiting vulnerabilities in image display +programs, nor from users renaming file extensions to inadvertently execute the +contained malicious code. + +## Secure file upload serving configurations + +The following configurations serve uploaded files as static files with the +proper headers as +[mitigation against file upload risks](#mitigations-against-file-upload-risks). +Please do not simply copy&paste these configurations, but make sure you +understand what they are doing and that you have implemented them correctly. + +> Always test your own setup and make sure that it is secure! + +e.g. try uploading PHP scripts (as "example.php", "example.php.png" and +"example.png") to see if they get executed by your web server, e.g. the content +of the following sample: + +```php +GIF89ad <?php echo mime_content_type(__FILE__); phpinfo(); +``` + +### Apache config + +Add the following directive to the Apache config (e.g. +/etc/apache2/apache2.conf), replacing the directory path with the absolute path +to the upload directory: + +```ApacheConf +<Directory "/path/to/project/server/php/files"> + # Some of the directives require the Apache Headers module. If it is not + # already enabled, please execute the following command and reload Apache: + # sudo a2enmod headers + # + # Please note that the order of directives across configuration files matters, + # see also: + # https://httpd.apache.org/docs/current/sections.html#merging + + # The following directive matches all files and forces them to be handled as + # static content, which prevents the server from parsing and executing files + # that are associated with a dynamic runtime, e.g. PHP files. + # It also forces their Content-Type header to "application/octet-stream" and + # adds a "Content-Disposition: attachment" header to force a download dialog, + # which prevents browsers from interpreting files in the context of the + # web server, e.g. HTML files containing JavaScript. + # Lastly it also prevents browsers from MIME-sniffing the Content-Type, + # preventing them from interpreting a file as a different Content-Type than + # the one sent by the webserver. + <FilesMatch ".*"> + SetHandler default-handler + ForceType application/octet-stream + Header set Content-Disposition attachment + Header set X-Content-Type-Options nosniff + </FilesMatch> + + # The following directive matches known image files and unsets the forced + # Content-Type so they can be served with their original mime type. + # It also unsets the Content-Disposition header to allow displaying them + # inline in the browser. + <FilesMatch ".+\.(?i:(gif|jpe?g|png))$"> + ForceType none + Header unset Content-Disposition + </FilesMatch> +</Directory> +``` + +### NGINX config + +Add the following directive to the NGINX config, replacing the directory path +with the absolute path to the upload directory: + +```Nginx +location ^~ /path/to/project/server/php/files { + root html; + default_type application/octet-stream; + types { + image/gif gif; + image/jpeg jpg; + image/png png; + } + add_header X-Content-Type-Options 'nosniff'; + if ($request_filename ~ /(((?!\.(jpg)|(png)|(gif)$)[^/])+$)) { + add_header Content-Disposition 'attachment; filename="$1"'; + # Add X-Content-Type-Options again, as using add_header in a new context + # dismisses all previous add_header calls: + add_header X-Content-Type-Options 'nosniff'; + } +} +``` + +## Secure image processing configurations + +The following configuration mitigates +[potential image processing vulnerabilities with ImageMagick](VULNERABILITIES.md#potential-vulnerabilities-with-php-imagemagick) +by limiting the attack vectors to a small subset of image types +(`GIF/JPEG/PNG`). + +Please also consider using alternative, safer image processing libraries like +[libvips](https://github.com/libvips/libvips) or +[imageflow](https://github.com/imazen/imageflow). + +## ImageMagick config + +It is recommended to disable all non-required ImageMagick coders via +[policy.xml](https://wiki.debian.org/imagemagick/security). +To do so, locate the ImageMagick `policy.xml` configuration file and add the +following policies: + +```xml +<?xml version="1.0" encoding="UTF-8"?> +<!-- ... --> +<policymap> + <!-- ... --> + <policy domain="delegate" rights="none" pattern="*" /> + <policy domain="coder" rights="none" pattern="*" /> + <policy domain="coder" rights="read | write" pattern="{GIF,JPEG,JPG,PNG}" /> +</policymap> +``` diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-audio.js b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js index 0bd4c27553dc0..1435ef20af2f6 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-audio.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-audio.js @@ -15,12 +15,12 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'load-image', 'jquery/fileUploader/jquery.fileupload-process'], factory); + define(['jquery', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image', 'jquery/fileUploader/jquery.fileupload-process'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), - require('blueimp-load-image/js/load-image'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), require('jquery/fileUploader/jquery.fileupload-process') ); } else { diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-image.js b/lib/web/jquery/fileUploader/jquery.fileupload-image.js index 84349fc9fbf92..11c63c236247c 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-image.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-image.js @@ -17,24 +17,24 @@ // Register as an anonymous AMD module: define([ 'jquery', - 'load-image', - 'load-image-meta', - 'load-image-scale', - 'load-image-exif', - 'load-image-orientation', - 'canvas-to-blob', + 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image', + 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta', + 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale', + 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif', + 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-orientation', + 'jquery/fileUploader/vendor/blueimp-canvas-to-blob/js/canvas-to-blob', 'jquery/fileUploader/jquery.fileupload-process' ], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), - require('blueimp-load-image/js/load-image'), - require('blueimp-load-image/js/load-image-meta'), - require('blueimp-load-image/js/load-image-scale'), - require('blueimp-load-image/js/load-image-exif'), - require('blueimp-load-image/js/load-image-orientation'), - require('blueimp-canvas-to-blob'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-orientation'), + require('jquery/fileUploader/vendor/blueimp-canvas-to-blob/js/canvas-to-blob'), require('jquery/fileUploader/jquery.fileupload-process') ); } else { @@ -52,7 +52,6 @@ disableImageHead: '@', disableMetaDataParsers: '@', disableExif: '@', - disableExifThumbnail: '@', disableExifOffsets: '@', includeExifTags: '@', excludeExifTags: '@', @@ -216,31 +215,23 @@ }, thumbnail, thumbnailBlob; - if (data.exif) { - if (options.orientation === true) { + if (data.exif && options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + thumbnailBlob = thumbnail && thumbnail.get('Blob'); + if (thumbnailBlob) { options.orientation = data.exif.get('Orientation'); + loadImage(thumbnailBlob, resolve, options); + return dfd.promise(); } - if (options.thumbnail) { - thumbnail = data.exif.get('Thumbnail'); - thumbnailBlob = thumbnail && thumbnail.get('Blob'); - if (thumbnailBlob) { - loadImage(thumbnailBlob, resolve, options); - return dfd.promise(); - } - } - // Prevent orienting browser oriented images: - if (loadImage.orientation) { - data.orientation = data.orientation || options.orientation; - } + } + if (data.orientation) { // Prevent orienting the same image twice: - if (data.orientation) { - delete options.orientation; - } else { - data.orientation = options.orientation; - } + delete options.orientation; + } else { + data.orientation = options.orientation || loadImage.orientation; } if (img) { - resolve(loadImage.scale(img, options)); + resolve(loadImage.scale(img, options, data)); return dfd.promise(); } return data; @@ -320,7 +311,7 @@ file = data.files[data.index], // eslint-disable-next-line new-cap dfd = $.Deferred(); - if (data.orientation && data.exifOffsets) { + if (data.orientation === true && data.exifOffsets) { // Reset Exif Orientation data: loadImage.writeExifData(data.imageHead, data, 'Orientation', 1); } diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js index 4ee566b2b3bcb..caacf95c507bb 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js @@ -17,7 +17,7 @@ // Register as an anonymous AMD module: define([ 'jquery', - 'blueimp-tmpl', + 'jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl', 'jquery/fileUploader/jquery.fileupload-image', 'jquery/fileUploader/jquery.fileupload-audio', 'jquery/fileUploader/jquery.fileupload-video', @@ -27,7 +27,7 @@ // Node/CommonJS: factory( require('jquery'), - require('blueimp-tmpl'), + require('jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl'), require('jquery/fileUploader/jquery.fileupload-image'), require('jquery/fileUploader/jquery.fileupload-audio'), require('jquery/fileUploader/jquery.fileupload-video'), diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-video.js b/lib/web/jquery/fileUploader/jquery.fileupload-video.js index 70cd9f1dcfc16..bf247f38280a5 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-video.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-video.js @@ -15,12 +15,12 @@ 'use strict'; if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: - define(['jquery', 'load-image', 'jquery/fileUploader/jquery.fileupload-process'], factory); + define(['jquery', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image', 'jquery/fileUploader/jquery.fileupload-process'], factory); } else if (typeof exports === 'object') { // Node/CommonJS: factory( require('jquery'), - require('blueimp-load-image/js/load-image'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), require('jquery/fileUploader/jquery.fileupload-process') ); } else { diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/LICENSE.txt b/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/LICENSE.txt new file mode 100644 index 0000000000000..e1ad73662d4a5 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/LICENSE.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright © 2012 Sebastian Tschan, https://blueimp.net + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/README.md b/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/README.md new file mode 100644 index 0000000000000..92e16c63ce7cd --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/README.md @@ -0,0 +1,135 @@ +# JavaScript Canvas to Blob + +## Contents + +- [Description](#description) +- [Setup](#setup) +- [Usage](#usage) +- [Requirements](#requirements) +- [Browsers](#browsers) +- [API](#api) +- [Test](#test) +- [License](#license) + +## Description + +Canvas to Blob is a +[polyfill](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill) for +Browsers that don't support the standard JavaScript +[HTMLCanvasElement.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) +method. + +It can be used to create +[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects from an +HTML [canvas](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas) +element. + +## Setup + +Install via [NPM](https://www.npmjs.com/package/blueimp-canvas-to-blob): + +```sh +npm install blueimp-canvas-to-blob +``` + +This will install the JavaScript files inside +`./node_modules/blueimp-canvas-to-blob/js/` relative to your current directory, +from where you can copy them into a folder that is served by your web server. + +Next include the minified JavaScript Canvas to Blob script in your HTML markup: + +```html +<script src="js/canvas-to-blob.min.js"></script> +``` + +Or alternatively, include the non-minified version: + +```html +<script src="js/canvas-to-blob.js"></script> +``` + +## Usage + +You can use the `canvas.toBlob()` method in the same way as the native +implementation: + +```js +var canvas = document.createElement('canvas') +// Edit the canvas ... +if (canvas.toBlob) { + canvas.toBlob(function (blob) { + // Do something with the blob object, + // e.g. create multipart form data for file uploads: + var formData = new FormData() + formData.append('file', blob, 'image.jpg') + // ... + }, 'image/jpeg') +} +``` + +## Requirements + +The JavaScript Canvas to Blob function has zero dependencies. + +However, it is a very suitable complement to the +[JavaScript Load Image](https://github.com/blueimp/JavaScript-Load-Image) +function. + +## Browsers + +The following browsers have native support for +[HTMLCanvasElement.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob): + +- Chrome 50+ +- Firefox 19+ +- Safari 11+ +- Mobile Chrome 50+ (Android) +- Mobile Firefox 4+ (Android) +- Mobile Safari 11+ (iOS) +- Edge 79+ + +Browsers which implement the following APIs support `canvas.toBlob()` via +polyfill: + +- [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) +- [HTMLCanvasElement.toDataURL](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) +- [Blob() constructor](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob) +- [atob](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/atob) +- [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) +- [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) + +This includes the following browsers: + +- Chrome 20+ +- Firefox 13+ +- Safari 8+ +- Mobile Chrome 25+ (Android) +- Mobile Firefox 14+ (Android) +- Mobile Safari 8+ (iOS) +- Edge 74+ +- Edge Legacy 12+ +- Internet Explorer 10+ + +## API + +In addition to the `canvas.toBlob()` polyfill, the JavaScript Canvas to Blob +script exposes its helper function `dataURLtoBlob(url)`: + +```js +// Uncomment the following line when using a module loader like webpack: +// var dataURLtoBlob = require('blueimp-canvas-to-blob') + +// black+white 3x2 GIF, base64 data: +var b64 = 'R0lGODdhAwACAPEAAAAAAP///yZFySZFySH5BAEAAAIALAAAAAADAAIAAAIDRAJZADs=' +var url = 'data:image/gif;base64,' + b64 +var blob = dataURLtoBlob(url) +``` + +## Test + +[Unit tests](https://blueimp.github.io/JavaScript-Canvas-to-Blob/test/) + +## License + +The JavaScript Canvas to Blob script is released under the +[MIT license](https://opensource.org/licenses/MIT). diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/js/canvas-to-blob.js b/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/js/canvas-to-blob.js new file mode 100644 index 0000000000000..8cd717bc1205f --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-canvas-to-blob/js/canvas-to-blob.js @@ -0,0 +1,143 @@ +/* + * JavaScript Canvas to Blob + * https://github.com/blueimp/JavaScript-Canvas-to-Blob + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on stackoverflow user Stoive's code snippet: + * http://stackoverflow.com/q/4998908 + */ + +/* global define, Uint8Array, ArrayBuffer, module */ + +;(function (window) { + 'use strict' + + var CanvasPrototype = + window.HTMLCanvasElement && window.HTMLCanvasElement.prototype + var hasBlobConstructor = + window.Blob && + (function () { + try { + return Boolean(new Blob()) + } catch (e) { + return false + } + })() + var hasArrayBufferViewSupport = + hasBlobConstructor && + window.Uint8Array && + (function () { + try { + return new Blob([new Uint8Array(100)]).size === 100 + } catch (e) { + return false + } + })() + var BlobBuilder = + window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder + var dataURIPattern = /^data:((.*?)(;charset=.*?)?)(;base64)?,/ + var dataURLtoBlob = + (hasBlobConstructor || BlobBuilder) && + window.atob && + window.ArrayBuffer && + window.Uint8Array && + function (dataURI) { + var matches, + mediaType, + isBase64, + dataString, + byteString, + arrayBuffer, + intArray, + i, + bb + // Parse the dataURI components as per RFC 2397 + matches = dataURI.match(dataURIPattern) + if (!matches) { + throw new Error('invalid data URI') + } + // Default to text/plain;charset=US-ASCII + mediaType = matches[2] + ? matches[1] + : 'text/plain' + (matches[3] || ';charset=US-ASCII') + isBase64 = !!matches[4] + dataString = dataURI.slice(matches[0].length) + if (isBase64) { + // Convert base64 to raw binary data held in a string: + byteString = atob(dataString) + } else { + // Convert base64/URLEncoded data component to raw binary: + byteString = decodeURIComponent(dataString) + } + // Write the bytes of the string to an ArrayBuffer: + arrayBuffer = new ArrayBuffer(byteString.length) + intArray = new Uint8Array(arrayBuffer) + for (i = 0; i < byteString.length; i += 1) { + intArray[i] = byteString.charCodeAt(i) + } + // Write the ArrayBuffer (or ArrayBufferView) to a blob: + if (hasBlobConstructor) { + return new Blob([hasArrayBufferViewSupport ? intArray : arrayBuffer], { + type: mediaType + }) + } + bb = new BlobBuilder() + bb.append(arrayBuffer) + return bb.getBlob(mediaType) + } + if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) { + if (CanvasPrototype.mozGetAsFile) { + CanvasPrototype.toBlob = function (callback, type, quality) { + var self = this + setTimeout(function () { + if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) { + callback(dataURLtoBlob(self.toDataURL(type, quality))) + } else { + callback(self.mozGetAsFile('blob', type)) + } + }) + } + } else if (CanvasPrototype.toDataURL && dataURLtoBlob) { + if (CanvasPrototype.msToBlob) { + CanvasPrototype.toBlob = function (callback, type, quality) { + var self = this + setTimeout(function () { + if ( + ((type && type !== 'image/png') || quality) && + CanvasPrototype.toDataURL && + dataURLtoBlob + ) { + callback(dataURLtoBlob(self.toDataURL(type, quality))) + } else { + callback(self.msToBlob(type)) + } + }) + } + } else { + CanvasPrototype.toBlob = function (callback, type, quality) { + var self = this + setTimeout(function () { + callback(dataURLtoBlob(self.toDataURL(type, quality))) + }) + } + } + } + } + if (typeof define === 'function' && define.amd) { + define(function () { + return dataURLtoBlob + }) + } else if (typeof module === 'object' && module.exports) { + module.exports = dataURLtoBlob + } else { + window.dataURLtoBlob = dataURLtoBlob + } +})(window) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/LICENSE.txt b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/LICENSE.txt new file mode 100644 index 0000000000000..d6a9d74758be3 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/LICENSE.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright © 2011 Sebastian Tschan, https://blueimp.net + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md new file mode 100644 index 0000000000000..5759a126aa172 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/README.md @@ -0,0 +1,1070 @@ +# JavaScript Load Image + +> A JavaScript library to load and transform image files. + +## Contents + +- [Demo](https://blueimp.github.io/JavaScript-Load-Image/) +- [Description](#description) +- [Setup](#setup) +- [Usage](#usage) + - [Image loading](#image-loading) + - [Image scaling](#image-scaling) +- [Requirements](#requirements) +- [Browser support](#browser-support) +- [API](#api) + - [Callback](#callback) + - [Function signature](#function-signature) + - [Cancel image loading](#cancel-image-loading) + - [Callback arguments](#callback-arguments) + - [Error handling](#error-handling) + - [Promise](#promise) +- [Options](#options) + - [maxWidth](#maxwidth) + - [maxHeight](#maxheight) + - [minWidth](#minwidth) + - [minHeight](#minheight) + - [sourceWidth](#sourcewidth) + - [sourceHeight](#sourceheight) + - [top](#top) + - [right](#right) + - [bottom](#bottom) + - [left](#left) + - [contain](#contain) + - [cover](#cover) + - [aspectRatio](#aspectratio) + - [pixelRatio](#pixelratio) + - [downsamplingRatio](#downsamplingratio) + - [imageSmoothingEnabled](#imagesmoothingenabled) + - [imageSmoothingQuality](#imagesmoothingquality) + - [crop](#crop) + - [orientation](#orientation) + - [meta](#meta) + - [canvas](#canvas) + - [crossOrigin](#crossorigin) + - [noRevoke](#norevoke) +- [Metadata parsing](#metadata-parsing) + - [Image head](#image-head) + - [Exif parser](#exif-parser) + - [Exif Thumbnail](#exif-thumbnail) + - [Exif IFD](#exif-ifd) + - [GPSInfo IFD](#gpsinfo-ifd) + - [Interoperability IFD](#interoperability-ifd) + - [Exif parser options](#exif-parser-options) + - [Exif writer](#exif-writer) + - [IPTC parser](#iptc-parser) + - [IPTC parser options](#iptc-parser-options) +- [License](#license) +- [Credits](#credits) + +## Description + +JavaScript Load Image is a library to load images provided as `File` or `Blob` +objects or via `URL`. It returns an optionally **scaled**, **cropped** or +**rotated** HTML `img` or `canvas` element. + +It also provides methods to parse image metadata to extract +[IPTC](https://iptc.org/standards/photo-metadata/) and +[Exif](https://en.wikipedia.org/wiki/Exif) tags as well as embedded thumbnail +images, to overwrite the Exif Orientation value and to restore the complete +image header after resizing. + +## Setup + +Install via [NPM](https://www.npmjs.com/package/blueimp-load-image): + +```sh +npm install blueimp-load-image +``` + +This will install the JavaScript files inside +`./node_modules/blueimp-load-image/js/` relative to your current directory, from +where you can copy them into a folder that is served by your web server. + +Next include the combined and minified JavaScript Load Image script in your HTML +markup: + +```html +<script src="js/load-image.all.min.js"></script> +``` + +Or alternatively, choose which components you want to include: + +```html +<!-- required for all operations --> +<script src="js/load-image.js"></script> + +<!-- required for scaling, cropping and as dependency for rotation --> +<script src="js/load-image-scale.js"></script> + +<!-- required to parse meta data and to restore the complete image head --> +<script src="js/load-image-meta.js"></script> + +<!-- required to parse meta data from images loaded via URL --> +<script src="js/load-image-fetch.js"></script> + +<!-- required for rotation and cross-browser image orientation --> +<script src="js/load-image-orientation.js"></script> + +<!-- required to parse Exif tags and cross-browser image orientation --> +<script src="js/load-image-exif.js"></script> + +<!-- required to display text mappings for Exif tags --> +<script src="js/load-image-exif-map.js"></script> + +<!-- required to parse IPTC tags --> +<script src="js/load-image-iptc.js"></script> + +<!-- required to display text mappings for IPTC tags --> +<script src="js/load-image-iptc-map.js"></script> +``` + +## Usage + +### Image loading + +In your application code, use the `loadImage()` function with +[callback](#callback) style: + +```js +document.getElementById('file-input').onchange = function () { + loadImage( + this.files[0], + function (img) { + document.body.appendChild(img) + }, + { maxWidth: 600 } // Options + ) +} +``` + +Or use the [Promise](#promise) based API like this ([requires](#requirements) a +polyfill for older browsers): + +```js +document.getElementById('file-input').onchange = function () { + loadImage(this.files[0], { maxWidth: 600 }).then(function (data) { + document.body.appendChild(data.image) + }) +} +``` + +With +[async/await](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await) +(requires a modern browser or a code transpiler like +[Babel](https://babeljs.io/) or [TypeScript](https://www.typescriptlang.org/)): + +```js +document.getElementById('file-input').onchange = async function () { + let data = await loadImage(this.files[0], { maxWidth: 600 }) + document.body.appendChild(data.image) +} +``` + +### Image scaling + +It is also possible to use the image scaling functionality directly with an +existing image: + +```js +var scaledImage = loadImage.scale( + img, // img or canvas element + { maxWidth: 600 } +) +``` + +## Requirements + +The JavaScript Load Image library has zero dependencies, but benefits from the +following two +[polyfills](https://developer.mozilla.org/en-US/docs/Glossary/Polyfill): + +- [blueimp-canvas-to-blob](https://github.com/blueimp/JavaScript-Canvas-to-Blob) + for browsers without native + [HTMLCanvasElement.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) + support, to create `Blob` objects out of `canvas` elements. +- [promise-polyfill](https://github.com/taylorhakes/promise-polyfill) to be able + to use the + [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) + based `loadImage` API in Browsers without native `Promise` support. + +## Browser support + +Browsers which implement the following APIs support all options: + +- Loading images from File and Blob objects: + - [URL.createObjectURL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) + or + [FileReader.readAsDataURL](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) +- Parsing meta data: + - [FileReader.readAsArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsArrayBuffer) + - [Blob.slice](https://developer.mozilla.org/en-US/docs/Web/API/Blob/slice) + - [DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) + (no [BigInt](https://developer.mozilla.org/en-US/docs/Glossary/BigInt) + support required) +- Parsing meta data from images loaded via URL: + - [fetch Response.blob](https://developer.mozilla.org/en-US/docs/Web/API/Body/blob) + or + [XMLHttpRequest.responseType blob](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType#blob) +- Promise based API: + - [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) + +This includes (but is not limited to) the following browsers: + +- Chrome 32+ +- Firefox 29+ +- Safari 8+ +- Mobile Chrome 42+ (Android) +- Mobile Firefox 50+ (Android) +- Mobile Safari 8+ (iOS) +- Edge 74+ +- Edge Legacy 12+ +- Internet Explorer 10+ `*` + +`*` Internet Explorer [requires](#requirements) a polyfill for the `Promise` +based API. + +Loading an image from a URL and applying transformations (scaling, cropping and +rotating - except `orientation:true`, which requires reading meta data) is +supported by all browsers which implement the +[HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) +interface. + +Loading an image from a URL and scaling it in size is supported by all browsers +which implement the +[img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) element and +has been tested successfully with browser engines as old as Internet Explorer 5 +(via +[IE11's emulation mode](<https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/samples/dn255001(v=vs.85)>)). + +The `loadImage()` function applies options using +[progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) +and falls back to a configuration that is supported by the browser, e.g. if the +`canvas` element is not supported, an equivalent `img` element is returned. + +## API + +### Callback + +#### Function signature + +The `loadImage()` function accepts a +[File](https://developer.mozilla.org/en-US/docs/Web/API/File) or +[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object or an image +URL as first argument. + +If a [File](https://developer.mozilla.org/en-US/docs/Web/API/File) or +[Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) is passed as +parameter, it returns an HTML `img` element if the browser supports the +[URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) API, alternatively a +[FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) object +if the `FileReader` API is supported, or `false`. + +It always returns an HTML +[img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Img) element +when passing an image URL: + +```js +var loadingImage = loadImage( + 'https://example.org/image.png', + function (img) { + document.body.appendChild(img) + }, + { maxWidth: 600 } +) +``` + +#### Cancel image loading + +Some browsers (e.g. Chrome) will cancel the image loading process if the `src` +property of an `img` element is changed. +To avoid unnecessary requests, we can use the +[data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) +of a 1x1 pixel transparent GIF image as `src` target to cancel the original +image download. + +To disable callback handling, we can also unset the image event handlers and for +maximum browser compatibility, cancel the file reading process if the returned +object is a +[FileReader](https://developer.mozilla.org/en-US/docs/Web/API/FileReader) +instance: + +```js +var loadingImage = loadImage( + 'https://example.org/image.png', + function (img) { + document.body.appendChild(img) + }, + { maxWidth: 600 } +) + +if (loadingImage) { + // Unset event handling for the loading image: + loadingImage.onload = loadingImage.onerror = null + + // Cancel image loading process: + if (loadingImage.abort) { + // FileReader instance, stop the file reading process: + loadingImage.abort() + } else { + // HTMLImageElement element, cancel the original image request by changing + // the target source to the data URL of a 1x1 pixel transparent image GIF: + loadingImage.src = + 'data:image/gif;base64,' + + 'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' + } +} +``` + +**Please note:** +The `img` element (or `FileReader` instance) for the loading image is only +returned when using the callback style API and not available with the +[Promise](#promise) based API. + +#### Callback arguments + +For the callback style API, the second argument to `loadImage()` must be a +`callback` function, which is called when the image has been loaded or an error +occurred while loading the image. + +The callback function is passed two arguments: + +1. An HTML [img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) + element or + [canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) + element, or an + [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) object of + type `error`. +2. An object with the original image dimensions as properties and potentially + additional [metadata](#metadata-parsing). + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + document.body.appendChild(img) + console.log('Original image width: ', data.originalWidth) + console.log('Original image height: ', data.originalHeight) + }, + { maxWidth: 600, meta: true } +) +``` + +**Please note:** +The original image dimensions reflect the natural width and height of the loaded +image before applying any transformation. +For consistent values across browsers, [metadata](#metadata-parsing) parsing has +to be enabled via `meta:true`, so `loadImage` can detect automatic image +orientation and normalize the dimensions. + +#### Error handling + +Example code implementing error handling: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + if (img.type === 'error') { + console.error('Error loading image file') + } else { + document.body.appendChild(img) + } + }, + { maxWidth: 600 } +) +``` + +### Promise + +If the `loadImage()` function is called without a `callback` function as second +argument and the +[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +API is available, it returns a `Promise` object: + +```js +loadImage(fileOrBlobOrUrl, { maxWidth: 600, meta: true }) + .then(function (data) { + document.body.appendChild(data.image) + console.log('Original image width: ', data.originalWidth) + console.log('Original image height: ', data.originalHeight) + }) + .catch(function (err) { + // Handling image loading errors + console.log(err) + }) +``` + +The `Promise` resolves with an object with the following properties: + +- `image`: An HTML + [img](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) or + [canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) element. +- `originalWidth`: The original width of the image. +- `originalHeight`: The original height of the image. + +Please also read the note about original image dimensions normalization in the +[callback arguments](#callback-arguments) section. + +If [metadata](#metadata-parsing) has been parsed, additional properties might be +present on the object. + +If image loading fails, the `Promise` rejects with an +[Event](https://developer.mozilla.org/en-US/docs/Web/API/Event) object of type +`error`. + +## Options + +The optional options argument to `loadImage()` allows to configure the image +loading. + +It can be used the following way with the callback style: + +```js +loadImage( + fileOrBlobOrUrl, + function (img) { + document.body.appendChild(img) + }, + { + maxWidth: 600, + maxHeight: 300, + minWidth: 100, + minHeight: 50, + canvas: true + } +) +``` + +Or the following way with the `Promise` based API: + +```js +loadImage(fileOrBlobOrUrl, { + maxWidth: 600, + maxHeight: 300, + minWidth: 100, + minHeight: 50, + canvas: true +}).then(function (data) { + document.body.appendChild(data.image) +}) +``` + +All settings are optional. By default, the image is returned as HTML `img` +element without any image size restrictions. + +### maxWidth + +Defines the maximum width of the `img`/`canvas` element. + +### maxHeight + +Defines the maximum height of the `img`/`canvas` element. + +### minWidth + +Defines the minimum width of the `img`/`canvas` element. + +### minHeight + +Defines the minimum height of the `img`/`canvas` element. + +### sourceWidth + +The width of the sub-rectangle of the source image to draw into the destination +canvas. +Defaults to the source image width and requires `canvas: true`. + +### sourceHeight + +The height of the sub-rectangle of the source image to draw into the destination +canvas. +Defaults to the source image height and requires `canvas: true`. + +### top + +The top margin of the sub-rectangle of the source image. +Defaults to `0` and requires `canvas: true`. + +### right + +The right margin of the sub-rectangle of the source image. +Defaults to `0` and requires `canvas: true`. + +### bottom + +The bottom margin of the sub-rectangle of the source image. +Defaults to `0` and requires `canvas: true`. + +### left + +The left margin of the sub-rectangle of the source image. +Defaults to `0` and requires `canvas: true`. + +### contain + +Scales the image up/down to contain it in the max dimensions if set to `true`. +This emulates the CSS feature +[background-image: contain](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Backgrounds_and_Borders/Resizing_background_images#contain). + +### cover + +Scales the image up/down to cover the max dimensions with the image dimensions +if set to `true`. +This emulates the CSS feature +[background-image: cover](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Backgrounds_and_Borders/Resizing_background_images#cover). + +### aspectRatio + +Crops the image to the given aspect ratio (e.g. `16/9`). +Setting the `aspectRatio` also enables the `crop` option. + +### pixelRatio + +Defines the ratio of the canvas pixels to the physical image pixels on the +screen. +Should be set to +[window.devicePixelRatio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) +unless the scaled image is not rendered on screen. +Defaults to `1` and requires `canvas: true`. + +### downsamplingRatio + +Defines the ratio in which the image is downsampled (scaled down in steps). +By default, images are downsampled in one step. +With a ratio of `0.5`, each step scales the image to half the size, before +reaching the target dimensions. +Requires `canvas: true`. + +### imageSmoothingEnabled + +If set to `false`, +[disables image smoothing](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled). +Defaults to `true` and requires `canvas: true`. + +### imageSmoothingQuality + +Sets the +[quality of image smoothing](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingQuality). +Possible values: `'low'`, `'medium'`, `'high'` +Defaults to `'low'` and requires `canvas: true`. + +### crop + +Crops the image to the `maxWidth`/`maxHeight` constraints if set to `true`. +Enabling the `crop` option also enables the `canvas` option. + +### orientation + +Transform the canvas according to the specified Exif orientation, which can be +an `integer` in the range of `1` to `8` or the boolean value `true`. + +When set to `true`, it will set the orientation value based on the Exif data of +the image, which will be parsed automatically if the Exif extension is +available. + +Exif orientation values to correctly display the letter F: + +``` + 1 2 + ██████ ██████ + ██ ██ + ████ ████ + ██ ██ + ██ ██ + + 3 4 + ██ ██ + ██ ██ + ████ ████ + ██ ██ + ██████ ██████ + + 5 6 +██████████ ██ +██ ██ ██ ██ +██ ██████████ + + 7 8 + ██ ██████████ + ██ ██ ██ ██ +██████████ ██ +``` + +Setting `orientation` to `true` enables the `canvas` and `meta` options, unless +the browser supports automatic image orientation (see +[browser support for image-orientation](https://caniuse.com/#feat=css-image-orientation)). + +Setting `orientation` to `1` enables the `canvas` and `meta` options if the +browser does support automatic image orientation (to allow reset of the +orientation). + +Setting `orientation` to an integer in the range of `2` to `8` always enables +the `canvas` option and also enables the `meta` option if the browser supports +automatic image orientation (again to allow reset). + +### meta + +Automatically parses the image metadata if set to `true`. + +If metadata has been found, the data object passed as second argument to the +callback function has additional properties (see +[metadata parsing](#metadata-parsing)). + +If the file is given as URL and the browser supports the +[fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or the +XHR +[responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) +`blob`, fetches the file as `Blob` to be able to parse the metadata. + +### canvas + +Returns the image as +[canvas](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) element if +set to `true`. + +### crossOrigin + +Sets the `crossOrigin` property on the `img` element for loading +[CORS enabled images](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image). + +### noRevoke + +By default, the +[created object URL](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) +is revoked after the image has been loaded, except when this option is set to +`true`. + +## Metadata parsing + +If the Load Image Meta extension is included, it is possible to parse image meta +data automatically with the `meta` option: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + console.log('Original image head: ', data.imageHead) + console.log('Exif data: ', data.exif) // requires exif extension + console.log('IPTC data: ', data.iptc) // requires iptc extension + }, + { meta: true } +) +``` + +Or alternatively via `loadImage.parseMetaData`, which can be used with an +available `File` or `Blob` object as first argument: + +```js +loadImage.parseMetaData( + fileOrBlob, + function (data) { + console.log('Original image head: ', data.imageHead) + console.log('Exif data: ', data.exif) // requires exif extension + console.log('IPTC data: ', data.iptc) // requires iptc extension + }, + { + maxMetaDataSize: 262144 + } +) +``` + +Or using the +[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +based API: + +```js +loadImage + .parseMetaData(fileOrBlob, { + maxMetaDataSize: 262144 + }) + .then(function (data) { + console.log('Original image head: ', data.imageHead) + console.log('Exif data: ', data.exif) // requires exif extension + console.log('IPTC data: ', data.iptc) // requires iptc extension + }) +``` + +The Metadata extension adds additional options used for the `parseMetaData` +method: + +- `maxMetaDataSize`: Maximum number of bytes of metadata to parse. +- `disableImageHead`: Disable parsing the original image head. +- `disableMetaDataParsers`: Disable parsing metadata (image head only) + +### Image head + +Resized JPEG images can be combined with their original image head via +`loadImage.replaceHead`, which requires the resized image as `Blob` object as +first argument and an `ArrayBuffer` image head as second argument. + +With callback style, the third argument must be a `callback` function, which is +called with the new `Blob` object: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + if (data.imageHead) { + img.toBlob(function (blob) { + loadImage.replaceHead(blob, data.imageHead, function (newBlob) { + // do something with the new Blob object + }) + }, 'image/jpeg') + } + }, + { meta: true, canvas: true, maxWidth: 800 } +) +``` + +Or using the +[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +based API like this: + +```js +loadImage(fileOrBlobOrUrl, { meta: true, canvas: true, maxWidth: 800 }) + .then(function (data) { + if (!data.imageHead) throw new Error('Could not parse image metadata') + return new Promise(function (resolve) { + data.image.toBlob(function (blob) { + data.blob = blob + resolve(data) + }, 'image/jpeg') + }) + }) + .then(function (data) { + return loadImage.replaceHead(data.blob, data.imageHead) + }) + .then(function (blob) { + // do something with the new Blob object + }) + .catch(function (err) { + console.error(err) + }) +``` + +**Please note:** +`Blob` objects of resized images can be created via +[HTMLCanvasElement.toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob). +[blueimp-canvas-to-blob](https://github.com/blueimp/JavaScript-Canvas-to-Blob) +provides a polyfill for browsers without native `canvas.toBlob()` support. + +### Exif parser + +If you include the Load Image Exif Parser extension, the argument passed to the +callback for `parseMetaData` will contain the following additional properties if +Exif data could be found in the given image: + +- `exif`: The parsed Exif tags +- `exifOffsets`: The parsed Exif tag offsets +- `exifTiffOffset`: TIFF header offset (used for offset pointers) +- `exifLittleEndian`: little endian order if true, big endian if false + +The `exif` object stores the parsed Exif tags: + +```js +var orientation = data.exif[0x0112] // Orientation +``` + +The `exif` and `exifOffsets` objects also provide a `get()` method to retrieve +the tag value/offset via the tag's mapped name: + +```js +var orientation = data.exif.get('Orientation') +var orientationOffset = data.exifOffsets.get('Orientation') +``` + +By default, only the following names are mapped: + +- `Orientation` +- `Thumbnail` (see [Exif Thumbnail](#exif-thumbnail)) +- `Exif` (see [Exif IFD](#exif-ifd)) +- `GPSInfo` (see [GPSInfo IFD](#gpsinfo-ifd)) +- `Interoperability` (see [Interoperability IFD](#interoperability-ifd)) + +If you also include the Load Image Exif Map library, additional tag mappings +become available, as well as three additional methods: + +- `exif.getText()` +- `exif.getName()` +- `exif.getAll()` + +```js +var orientationText = data.exif.getText('Orientation') // e.g. "Rotate 90° CW" + +var name = data.exif.getName(0x0112) // "Orientation" + +// A map of all parsed tags with their mapped names/text as keys/values: +var allTags = data.exif.getAll() +``` + +#### Exif Thumbnail + +Example code displaying a thumbnail image embedded into the Exif metadata: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + var exif = data.exif + var thumbnail = exif && exif.get('Thumbnail') + var blob = thumbnail && thumbnail.get('Blob') + if (blob) { + loadImage( + blob, + function (thumbImage) { + document.body.appendChild(thumbImage) + }, + { orientation: exif.get('Orientation') } + ) + } + }, + { meta: true } +) +``` + +#### Exif IFD + +Example code displaying data from the Exif IFD (Image File Directory) that +contains Exif specified TIFF tags: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + var exifIFD = data.exif && data.exif.get('Exif') + if (exifIFD) { + // Map of all Exif IFD tags with their mapped names/text as keys/values: + console.log(exifIFD.getAll()) + // A specific Exif IFD tag value: + console.log(exifIFD.get('UserComment')) + } + }, + { meta: true } +) +``` + +#### GPSInfo IFD + +Example code displaying data from the Exif IFD (Image File Directory) that +contains [GPS](https://en.wikipedia.org/wiki/Global_Positioning_System) info: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + var gpsInfo = data.exif && data.exif.get('GPSInfo') + if (gpsInfo) { + // Map of all GPSInfo tags with their mapped names/text as keys/values: + console.log(gpsInfo.getAll()) + // A specific GPSInfo tag value: + console.log(gpsInfo.get('GPSLatitude')) + } + }, + { meta: true } +) +``` + +#### Interoperability IFD + +Example code displaying data from the Exif IFD (Image File Directory) that +contains Interoperability data: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + var interoperabilityData = data.exif && data.exif.get('Interoperability') + if (interoperabilityData) { + // The InteroperabilityIndex tag value: + console.log(interoperabilityData.get('InteroperabilityIndex')) + } + }, + { meta: true } +) +``` + +#### Exif parser options + +The Exif parser adds additional options: + +- `disableExif`: Disables Exif parsing when `true`. +- `disableExifOffsets`: Disables storing Exif tag offsets when `true`. +- `includeExifTags`: A map of Exif tags to include for parsing (includes all but + the excluded tags by default). +- `excludeExifTags`: A map of Exif tags to exclude from parsing (defaults to + exclude `Exif` `MakerNote`). + +An example parsing only Orientation, Thumbnail and ExifVersion tags: + +```js +loadImage.parseMetaData( + fileOrBlob, + function (data) { + console.log('Exif data: ', data.exif) + }, + { + includeExifTags: { + 0x0112: true, // Orientation + ifd1: { + 0x0201: true, // JPEGInterchangeFormat (Thumbnail data offset) + 0x0202: true // JPEGInterchangeFormatLength (Thumbnail data length) + }, + 0x8769: { + // ExifIFDPointer + 0x9000: true // ExifVersion + } + } + } +) +``` + +An example excluding `Exif` `MakerNote` and `GPSInfo`: + +```js +loadImage.parseMetaData( + fileOrBlob, + function (data) { + console.log('Exif data: ', data.exif) + }, + { + excludeExifTags: { + 0x8769: { + // ExifIFDPointer + 0x927c: true // MakerNote + }, + 0x8825: true // GPSInfoIFDPointer + } + } +) +``` + +### Exif writer + +The Exif parser extension also includes a minimal writer that allows to override +the Exif `Orientation` value in the parsed `imageHead` `ArrayBuffer`: + +```js +loadImage( + fileOrBlobOrUrl, + function (img, data) { + if (data.imageHead && data.exif) { + // Reset Exif Orientation data: + loadImage.writeExifData(data.imageHead, data, 'Orientation', 1) + img.toBlob(function (blob) { + loadImage.replaceHead(blob, data.imageHead, function (newBlob) { + // do something with newBlob + }) + }, 'image/jpeg') + } + }, + { meta: true, orientation: true, canvas: true, maxWidth: 800 } +) +``` + +**Please note:** +The Exif writer relies on the Exif tag offsets being available as +`data.exifOffsets` property, which requires that Exif data has been parsed from +the image. +The Exif writer can only change existing values, not add new tags, e.g. it +cannot add an Exif `Orientation` tag for an image that does not have one. + +### IPTC parser + +If you include the Load Image IPTC Parser extension, the argument passed to the +callback for `parseMetaData` will contain the following additional properties if +IPTC data could be found in the given image: + +- `iptc`: The parsed IPTC tags +- `iptcOffsets`: The parsed IPTC tag offsets + +The `iptc` object stores the parsed IPTC tags: + +```js +var objectname = data.iptc[5] +``` + +The `iptc` and `iptcOffsets` objects also provide a `get()` method to retrieve +the tag value/offset via the tag's mapped name: + +```js +var objectname = data.iptc.get('ObjectName') +``` + +By default, only the following names are mapped: + +- `ObjectName` + +If you also include the Load Image IPTC Map library, additional tag mappings +become available, as well as three additional methods: + +- `iptc.getText()` +- `iptc.getName()` +- `iptc.getAll()` + +```js +var keywords = data.iptc.getText('Keywords') // e.g.: ['Weather','Sky'] + +var name = data.iptc.getName(5) // ObjectName + +// A map of all parsed tags with their mapped names/text as keys/values: +var allTags = data.iptc.getAll() +``` + +#### IPTC parser options + +The IPTC parser adds additional options: + +- `disableIptc`: Disables IPTC parsing when true. +- `disableIptcOffsets`: Disables storing IPTC tag offsets when `true`. +- `includeIptcTags`: A map of IPTC tags to include for parsing (includes all but + the excluded tags by default). +- `excludeIptcTags`: A map of IPTC tags to exclude from parsing (defaults to + exclude `ObjectPreviewData`). + +An example parsing only the `ObjectName` tag: + +```js +loadImage.parseMetaData( + fileOrBlob, + function (data) { + console.log('IPTC data: ', data.iptc) + }, + { + includeIptcTags: { + 5: true // ObjectName + } + } +) +``` + +An example excluding `ApplicationRecordVersion` and `ObjectPreviewData`: + +```js +loadImage.parseMetaData( + fileOrBlob, + function (data) { + console.log('IPTC data: ', data.iptc) + }, + { + excludeIptcTags: { + 0: true, // ApplicationRecordVersion + 202: true // ObjectPreviewData + } + } +) +``` + +## License + +The JavaScript Load Image library is released under the +[MIT license](https://opensource.org/licenses/MIT). + +## Credits + +- Original image metadata handling implemented with the help and contribution of + Achim Stöhr. +- Original Exif tags mapping based on Jacob Seidelin's + [exif-js](https://github.com/exif-js/exif-js) library. +- Original IPTC parser implementation by + [Dave Bevan](https://github.com/bevand10). diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/index.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/index.js new file mode 100644 index 0000000000000..20875a2d08535 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/index.js @@ -0,0 +1,12 @@ +/* global module, require */ + +module.exports = require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image') + +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale') +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta') +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-fetch') +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif') +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif-map') +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc') +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc-map') +require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-orientation') diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif-map.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif-map.js new file mode 100644 index 0000000000000..29f11aff226fc --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif-map.js @@ -0,0 +1,420 @@ +/* + * JavaScript Load Image Exif Map + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Exif tags mapping based on + * https://github.com/jseidelin/exif-js + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + var ExifMapProto = loadImage.ExifMap.prototype + + ExifMapProto.tags = { + // ================= + // TIFF tags (IFD0): + // ================= + 0x0100: 'ImageWidth', + 0x0101: 'ImageHeight', + 0x0102: 'BitsPerSample', + 0x0103: 'Compression', + 0x0106: 'PhotometricInterpretation', + 0x0112: 'Orientation', + 0x0115: 'SamplesPerPixel', + 0x011c: 'PlanarConfiguration', + 0x0212: 'YCbCrSubSampling', + 0x0213: 'YCbCrPositioning', + 0x011a: 'XResolution', + 0x011b: 'YResolution', + 0x0128: 'ResolutionUnit', + 0x0111: 'StripOffsets', + 0x0116: 'RowsPerStrip', + 0x0117: 'StripByteCounts', + 0x0201: 'JPEGInterchangeFormat', + 0x0202: 'JPEGInterchangeFormatLength', + 0x012d: 'TransferFunction', + 0x013e: 'WhitePoint', + 0x013f: 'PrimaryChromaticities', + 0x0211: 'YCbCrCoefficients', + 0x0214: 'ReferenceBlackWhite', + 0x0132: 'DateTime', + 0x010e: 'ImageDescription', + 0x010f: 'Make', + 0x0110: 'Model', + 0x0131: 'Software', + 0x013b: 'Artist', + 0x8298: 'Copyright', + 0x8769: { + // ExifIFDPointer + 0x9000: 'ExifVersion', // EXIF version + 0xa000: 'FlashpixVersion', // Flashpix format version + 0xa001: 'ColorSpace', // Color space information tag + 0xa002: 'PixelXDimension', // Valid width of meaningful image + 0xa003: 'PixelYDimension', // Valid height of meaningful image + 0xa500: 'Gamma', + 0x9101: 'ComponentsConfiguration', // Information about channels + 0x9102: 'CompressedBitsPerPixel', // Compressed bits per pixel + 0x927c: 'MakerNote', // Any desired information written by the manufacturer + 0x9286: 'UserComment', // Comments by user + 0xa004: 'RelatedSoundFile', // Name of related sound file + 0x9003: 'DateTimeOriginal', // Date and time when the original image was generated + 0x9004: 'DateTimeDigitized', // Date and time when the image was stored digitally + 0x9010: 'OffsetTime', // Time zone when the image file was last changed + 0x9011: 'OffsetTimeOriginal', // Time zone when the image was stored digitally + 0x9012: 'OffsetTimeDigitized', // Time zone when the image was stored digitally + 0x9290: 'SubSecTime', // Fractions of seconds for DateTime + 0x9291: 'SubSecTimeOriginal', // Fractions of seconds for DateTimeOriginal + 0x9292: 'SubSecTimeDigitized', // Fractions of seconds for DateTimeDigitized + 0x829a: 'ExposureTime', // Exposure time (in seconds) + 0x829d: 'FNumber', + 0x8822: 'ExposureProgram', // Exposure program + 0x8824: 'SpectralSensitivity', // Spectral sensitivity + 0x8827: 'PhotographicSensitivity', // EXIF 2.3, ISOSpeedRatings in EXIF 2.2 + 0x8828: 'OECF', // Optoelectric conversion factor + 0x8830: 'SensitivityType', + 0x8831: 'StandardOutputSensitivity', + 0x8832: 'RecommendedExposureIndex', + 0x8833: 'ISOSpeed', + 0x8834: 'ISOSpeedLatitudeyyy', + 0x8835: 'ISOSpeedLatitudezzz', + 0x9201: 'ShutterSpeedValue', // Shutter speed + 0x9202: 'ApertureValue', // Lens aperture + 0x9203: 'BrightnessValue', // Value of brightness + 0x9204: 'ExposureBias', // Exposure bias + 0x9205: 'MaxApertureValue', // Smallest F number of lens + 0x9206: 'SubjectDistance', // Distance to subject in meters + 0x9207: 'MeteringMode', // Metering mode + 0x9208: 'LightSource', // Kind of light source + 0x9209: 'Flash', // Flash status + 0x9214: 'SubjectArea', // Location and area of main subject + 0x920a: 'FocalLength', // Focal length of the lens in mm + 0xa20b: 'FlashEnergy', // Strobe energy in BCPS + 0xa20c: 'SpatialFrequencyResponse', + 0xa20e: 'FocalPlaneXResolution', // Number of pixels in width direction per FPRUnit + 0xa20f: 'FocalPlaneYResolution', // Number of pixels in height direction per FPRUnit + 0xa210: 'FocalPlaneResolutionUnit', // Unit for measuring the focal plane resolution + 0xa214: 'SubjectLocation', // Location of subject in image + 0xa215: 'ExposureIndex', // Exposure index selected on camera + 0xa217: 'SensingMethod', // Image sensor type + 0xa300: 'FileSource', // Image source (3 == DSC) + 0xa301: 'SceneType', // Scene type (1 == directly photographed) + 0xa302: 'CFAPattern', // Color filter array geometric pattern + 0xa401: 'CustomRendered', // Special processing + 0xa402: 'ExposureMode', // Exposure mode + 0xa403: 'WhiteBalance', // 1 = auto white balance, 2 = manual + 0xa404: 'DigitalZoomRatio', // Digital zoom ratio + 0xa405: 'FocalLengthIn35mmFilm', + 0xa406: 'SceneCaptureType', // Type of scene + 0xa407: 'GainControl', // Degree of overall image gain adjustment + 0xa408: 'Contrast', // Direction of contrast processing applied by camera + 0xa409: 'Saturation', // Direction of saturation processing applied by camera + 0xa40a: 'Sharpness', // Direction of sharpness processing applied by camera + 0xa40b: 'DeviceSettingDescription', + 0xa40c: 'SubjectDistanceRange', // Distance to subject + 0xa420: 'ImageUniqueID', // Identifier assigned uniquely to each image + 0xa430: 'CameraOwnerName', + 0xa431: 'BodySerialNumber', + 0xa432: 'LensSpecification', + 0xa433: 'LensMake', + 0xa434: 'LensModel', + 0xa435: 'LensSerialNumber' + }, + 0x8825: { + // GPSInfoIFDPointer + 0x0000: 'GPSVersionID', + 0x0001: 'GPSLatitudeRef', + 0x0002: 'GPSLatitude', + 0x0003: 'GPSLongitudeRef', + 0x0004: 'GPSLongitude', + 0x0005: 'GPSAltitudeRef', + 0x0006: 'GPSAltitude', + 0x0007: 'GPSTimeStamp', + 0x0008: 'GPSSatellites', + 0x0009: 'GPSStatus', + 0x000a: 'GPSMeasureMode', + 0x000b: 'GPSDOP', + 0x000c: 'GPSSpeedRef', + 0x000d: 'GPSSpeed', + 0x000e: 'GPSTrackRef', + 0x000f: 'GPSTrack', + 0x0010: 'GPSImgDirectionRef', + 0x0011: 'GPSImgDirection', + 0x0012: 'GPSMapDatum', + 0x0013: 'GPSDestLatitudeRef', + 0x0014: 'GPSDestLatitude', + 0x0015: 'GPSDestLongitudeRef', + 0x0016: 'GPSDestLongitude', + 0x0017: 'GPSDestBearingRef', + 0x0018: 'GPSDestBearing', + 0x0019: 'GPSDestDistanceRef', + 0x001a: 'GPSDestDistance', + 0x001b: 'GPSProcessingMethod', + 0x001c: 'GPSAreaInformation', + 0x001d: 'GPSDateStamp', + 0x001e: 'GPSDifferential', + 0x001f: 'GPSHPositioningError' + }, + 0xa005: { + // InteroperabilityIFDPointer + 0x0001: 'InteroperabilityIndex' + } + } + + // IFD1 directory can contain any IFD0 tags: + ExifMapProto.tags.ifd1 = ExifMapProto.tags + + ExifMapProto.stringValues = { + ExposureProgram: { + 0: 'Undefined', + 1: 'Manual', + 2: 'Normal program', + 3: 'Aperture priority', + 4: 'Shutter priority', + 5: 'Creative program', + 6: 'Action program', + 7: 'Portrait mode', + 8: 'Landscape mode' + }, + MeteringMode: { + 0: 'Unknown', + 1: 'Average', + 2: 'CenterWeightedAverage', + 3: 'Spot', + 4: 'MultiSpot', + 5: 'Pattern', + 6: 'Partial', + 255: 'Other' + }, + LightSource: { + 0: 'Unknown', + 1: 'Daylight', + 2: 'Fluorescent', + 3: 'Tungsten (incandescent light)', + 4: 'Flash', + 9: 'Fine weather', + 10: 'Cloudy weather', + 11: 'Shade', + 12: 'Daylight fluorescent (D 5700 - 7100K)', + 13: 'Day white fluorescent (N 4600 - 5400K)', + 14: 'Cool white fluorescent (W 3900 - 4500K)', + 15: 'White fluorescent (WW 3200 - 3700K)', + 17: 'Standard light A', + 18: 'Standard light B', + 19: 'Standard light C', + 20: 'D55', + 21: 'D65', + 22: 'D75', + 23: 'D50', + 24: 'ISO studio tungsten', + 255: 'Other' + }, + Flash: { + 0x0000: 'Flash did not fire', + 0x0001: 'Flash fired', + 0x0005: 'Strobe return light not detected', + 0x0007: 'Strobe return light detected', + 0x0009: 'Flash fired, compulsory flash mode', + 0x000d: 'Flash fired, compulsory flash mode, return light not detected', + 0x000f: 'Flash fired, compulsory flash mode, return light detected', + 0x0010: 'Flash did not fire, compulsory flash mode', + 0x0018: 'Flash did not fire, auto mode', + 0x0019: 'Flash fired, auto mode', + 0x001d: 'Flash fired, auto mode, return light not detected', + 0x001f: 'Flash fired, auto mode, return light detected', + 0x0020: 'No flash function', + 0x0041: 'Flash fired, red-eye reduction mode', + 0x0045: 'Flash fired, red-eye reduction mode, return light not detected', + 0x0047: 'Flash fired, red-eye reduction mode, return light detected', + 0x0049: 'Flash fired, compulsory flash mode, red-eye reduction mode', + 0x004d: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected', + 0x004f: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected', + 0x0059: 'Flash fired, auto mode, red-eye reduction mode', + 0x005d: 'Flash fired, auto mode, return light not detected, red-eye reduction mode', + 0x005f: 'Flash fired, auto mode, return light detected, red-eye reduction mode' + }, + SensingMethod: { + 1: 'Undefined', + 2: 'One-chip color area sensor', + 3: 'Two-chip color area sensor', + 4: 'Three-chip color area sensor', + 5: 'Color sequential area sensor', + 7: 'Trilinear sensor', + 8: 'Color sequential linear sensor' + }, + SceneCaptureType: { + 0: 'Standard', + 1: 'Landscape', + 2: 'Portrait', + 3: 'Night scene' + }, + SceneType: { + 1: 'Directly photographed' + }, + CustomRendered: { + 0: 'Normal process', + 1: 'Custom process' + }, + WhiteBalance: { + 0: 'Auto white balance', + 1: 'Manual white balance' + }, + GainControl: { + 0: 'None', + 1: 'Low gain up', + 2: 'High gain up', + 3: 'Low gain down', + 4: 'High gain down' + }, + Contrast: { + 0: 'Normal', + 1: 'Soft', + 2: 'Hard' + }, + Saturation: { + 0: 'Normal', + 1: 'Low saturation', + 2: 'High saturation' + }, + Sharpness: { + 0: 'Normal', + 1: 'Soft', + 2: 'Hard' + }, + SubjectDistanceRange: { + 0: 'Unknown', + 1: 'Macro', + 2: 'Close view', + 3: 'Distant view' + }, + FileSource: { + 3: 'DSC' + }, + ComponentsConfiguration: { + 0: '', + 1: 'Y', + 2: 'Cb', + 3: 'Cr', + 4: 'R', + 5: 'G', + 6: 'B' + }, + Orientation: { + 1: 'Original', + 2: 'Horizontal flip', + 3: 'Rotate 180° CCW', + 4: 'Vertical flip', + 5: 'Vertical flip + Rotate 90° CW', + 6: 'Rotate 90° CW', + 7: 'Horizontal flip + Rotate 90° CW', + 8: 'Rotate 90° CCW' + } + } + + ExifMapProto.getText = function (name) { + var value = this.get(name) + switch (name) { + case 'LightSource': + case 'Flash': + case 'MeteringMode': + case 'ExposureProgram': + case 'SensingMethod': + case 'SceneCaptureType': + case 'SceneType': + case 'CustomRendered': + case 'WhiteBalance': + case 'GainControl': + case 'Contrast': + case 'Saturation': + case 'Sharpness': + case 'SubjectDistanceRange': + case 'FileSource': + case 'Orientation': + return this.stringValues[name][value] + case 'ExifVersion': + case 'FlashpixVersion': + if (!value) return + return String.fromCharCode(value[0], value[1], value[2], value[3]) + case 'ComponentsConfiguration': + if (!value) return + return ( + this.stringValues[name][value[0]] + + this.stringValues[name][value[1]] + + this.stringValues[name][value[2]] + + this.stringValues[name][value[3]] + ) + case 'GPSVersionID': + if (!value) return + return value[0] + '.' + value[1] + '.' + value[2] + '.' + value[3] + } + return String(value) + } + + ExifMapProto.getAll = function () { + var map = {} + var prop + var obj + var name + for (prop in this) { + if (Object.prototype.hasOwnProperty.call(this, prop)) { + obj = this[prop] + if (obj && obj.getAll) { + map[this.ifds[prop].name] = obj.getAll() + } else { + name = this.tags[prop] + if (name) map[name] = this.getText(name) + } + } + } + return map + } + + ExifMapProto.getName = function (tagCode) { + var name = this.tags[tagCode] + if (typeof name === 'object') return this.ifds[tagCode].name + return name + } + + // Extend the map of tag names to tag codes: + ;(function () { + var tags = ExifMapProto.tags + var prop + var ifd + var subTags + // Map the tag names to tags: + for (prop in tags) { + if (Object.prototype.hasOwnProperty.call(tags, prop)) { + ifd = ExifMapProto.ifds[prop] + if (ifd) { + subTags = tags[prop] + for (prop in subTags) { + if (Object.prototype.hasOwnProperty.call(subTags, prop)) { + ifd.map[subTags[prop]] = Number(prop) + } + } + } else { + ExifMapProto.map[tags[prop]] = Number(prop) + } + } + } + })() +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif.js new file mode 100644 index 0000000000000..3c0937b8b590a --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-exif.js @@ -0,0 +1,460 @@ +/* + * JavaScript Load Image Exif Parser + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require, DataView */ + +/* eslint-disable no-console */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + /** + * Exif tag map + * + * @name ExifMap + * @class + * @param {number|string} tagCode IFD tag code + */ + function ExifMap(tagCode) { + if (tagCode) { + Object.defineProperty(this, 'map', { + value: this.ifds[tagCode].map + }) + Object.defineProperty(this, 'tags', { + value: (this.tags && this.tags[tagCode]) || {} + }) + } + } + + ExifMap.prototype.map = { + Orientation: 0x0112, + Thumbnail: 'ifd1', + Blob: 0x0201, // Alias for JPEGInterchangeFormat + Exif: 0x8769, + GPSInfo: 0x8825, + Interoperability: 0xa005 + } + + ExifMap.prototype.ifds = { + ifd1: { name: 'Thumbnail', map: ExifMap.prototype.map }, + 0x8769: { name: 'Exif', map: {} }, + 0x8825: { name: 'GPSInfo', map: {} }, + 0xa005: { name: 'Interoperability', map: {} } + } + + /** + * Retrieves exif tag value + * + * @param {number|string} id Exif tag code or name + * @returns {object} Exif tag value + */ + ExifMap.prototype.get = function (id) { + return this[id] || this[this.map[id]] + } + + /** + * Returns the Exif Thumbnail data as Blob. + * + * @param {DataView} dataView Data view interface + * @param {number} offset Thumbnail data offset + * @param {number} length Thumbnail data length + * @returns {undefined|Blob} Returns the Thumbnail Blob or undefined + */ + function getExifThumbnail(dataView, offset, length) { + if (!length) return + if (offset + length > dataView.byteLength) { + console.log('Invalid Exif data: Invalid thumbnail data.') + return + } + return new Blob( + [loadImage.bufferSlice.call(dataView.buffer, offset, offset + length)], + { + type: 'image/jpeg' + } + ) + } + + var ExifTagTypes = { + // byte, 8-bit unsigned int: + 1: { + getValue: function (dataView, dataOffset) { + return dataView.getUint8(dataOffset) + }, + size: 1 + }, + // ascii, 8-bit byte: + 2: { + getValue: function (dataView, dataOffset) { + return String.fromCharCode(dataView.getUint8(dataOffset)) + }, + size: 1, + ascii: true + }, + // short, 16 bit int: + 3: { + getValue: function (dataView, dataOffset, littleEndian) { + return dataView.getUint16(dataOffset, littleEndian) + }, + size: 2 + }, + // long, 32 bit int: + 4: { + getValue: function (dataView, dataOffset, littleEndian) { + return dataView.getUint32(dataOffset, littleEndian) + }, + size: 4 + }, + // rational = two long values, first is numerator, second is denominator: + 5: { + getValue: function (dataView, dataOffset, littleEndian) { + return ( + dataView.getUint32(dataOffset, littleEndian) / + dataView.getUint32(dataOffset + 4, littleEndian) + ) + }, + size: 8 + }, + // slong, 32 bit signed int: + 9: { + getValue: function (dataView, dataOffset, littleEndian) { + return dataView.getInt32(dataOffset, littleEndian) + }, + size: 4 + }, + // srational, two slongs, first is numerator, second is denominator: + 10: { + getValue: function (dataView, dataOffset, littleEndian) { + return ( + dataView.getInt32(dataOffset, littleEndian) / + dataView.getInt32(dataOffset + 4, littleEndian) + ) + }, + size: 8 + } + } + // undefined, 8-bit byte, value depending on field: + ExifTagTypes[7] = ExifTagTypes[1] + + /** + * Returns Exif tag value. + * + * @param {DataView} dataView Data view interface + * @param {number} tiffOffset TIFF offset + * @param {number} offset Tag offset + * @param {number} type Tag type + * @param {number} length Tag length + * @param {boolean} littleEndian Little endian encoding + * @returns {object} Tag value + */ + function getExifValue( + dataView, + tiffOffset, + offset, + type, + length, + littleEndian + ) { + var tagType = ExifTagTypes[type] + var tagSize + var dataOffset + var values + var i + var str + var c + if (!tagType) { + console.log('Invalid Exif data: Invalid tag type.') + return + } + tagSize = tagType.size * length + // Determine if the value is contained in the dataOffset bytes, + // or if the value at the dataOffset is a pointer to the actual data: + dataOffset = + tagSize > 4 + ? tiffOffset + dataView.getUint32(offset + 8, littleEndian) + : offset + 8 + if (dataOffset + tagSize > dataView.byteLength) { + console.log('Invalid Exif data: Invalid data offset.') + return + } + if (length === 1) { + return tagType.getValue(dataView, dataOffset, littleEndian) + } + values = [] + for (i = 0; i < length; i += 1) { + values[i] = tagType.getValue( + dataView, + dataOffset + i * tagType.size, + littleEndian + ) + } + if (tagType.ascii) { + str = '' + // Concatenate the chars: + for (i = 0; i < values.length; i += 1) { + c = values[i] + // Ignore the terminating NULL byte(s): + if (c === '\u0000') { + break + } + str += c + } + return str + } + return values + } + + /** + * Determines if the given tag should be included. + * + * @param {object} includeTags Map of tags to include + * @param {object} excludeTags Map of tags to exclude + * @param {number|string} tagCode Tag code to check + * @returns {boolean} True if the tag should be included + */ + function shouldIncludeTag(includeTags, excludeTags, tagCode) { + return ( + (!includeTags || includeTags[tagCode]) && + (!excludeTags || excludeTags[tagCode] !== true) + ) + } + + /** + * Parses Exif tags. + * + * @param {DataView} dataView Data view interface + * @param {number} tiffOffset TIFF offset + * @param {number} dirOffset Directory offset + * @param {boolean} littleEndian Little endian encoding + * @param {ExifMap} tags Map to store parsed exif tags + * @param {ExifMap} tagOffsets Map to store parsed exif tag offsets + * @param {object} includeTags Map of tags to include + * @param {object} excludeTags Map of tags to exclude + * @returns {number} Next directory offset + */ + function parseExifTags( + dataView, + tiffOffset, + dirOffset, + littleEndian, + tags, + tagOffsets, + includeTags, + excludeTags + ) { + var tagsNumber, dirEndOffset, i, tagOffset, tagNumber, tagValue + if (dirOffset + 6 > dataView.byteLength) { + console.log('Invalid Exif data: Invalid directory offset.') + return + } + tagsNumber = dataView.getUint16(dirOffset, littleEndian) + dirEndOffset = dirOffset + 2 + 12 * tagsNumber + if (dirEndOffset + 4 > dataView.byteLength) { + console.log('Invalid Exif data: Invalid directory size.') + return + } + for (i = 0; i < tagsNumber; i += 1) { + tagOffset = dirOffset + 2 + 12 * i + tagNumber = dataView.getUint16(tagOffset, littleEndian) + if (!shouldIncludeTag(includeTags, excludeTags, tagNumber)) continue + tagValue = getExifValue( + dataView, + tiffOffset, + tagOffset, + dataView.getUint16(tagOffset + 2, littleEndian), // tag type + dataView.getUint32(tagOffset + 4, littleEndian), // tag length + littleEndian + ) + tags[tagNumber] = tagValue + if (tagOffsets) { + tagOffsets[tagNumber] = tagOffset + } + } + // Return the offset to the next directory: + return dataView.getUint32(dirEndOffset, littleEndian) + } + + /** + * Parses tags in a given IFD (Image File Directory). + * + * @param {object} data Data object to store exif tags and offsets + * @param {number|string} tagCode IFD tag code + * @param {DataView} dataView Data view interface + * @param {number} tiffOffset TIFF offset + * @param {boolean} littleEndian Little endian encoding + * @param {object} includeTags Map of tags to include + * @param {object} excludeTags Map of tags to exclude + */ + function parseExifIFD( + data, + tagCode, + dataView, + tiffOffset, + littleEndian, + includeTags, + excludeTags + ) { + var dirOffset = data.exif[tagCode] + if (dirOffset) { + data.exif[tagCode] = new ExifMap(tagCode) + if (data.exifOffsets) { + data.exifOffsets[tagCode] = new ExifMap(tagCode) + } + parseExifTags( + dataView, + tiffOffset, + tiffOffset + dirOffset, + littleEndian, + data.exif[tagCode], + data.exifOffsets && data.exifOffsets[tagCode], + includeTags && includeTags[tagCode], + excludeTags && excludeTags[tagCode] + ) + } + } + + loadImage.parseExifData = function (dataView, offset, length, data, options) { + if (options.disableExif) { + return + } + var includeTags = options.includeExifTags + var excludeTags = options.excludeExifTags || { + 0x8769: { + // ExifIFDPointer + 0x927c: true // MakerNote + } + } + var tiffOffset = offset + 10 + var littleEndian + var dirOffset + var thumbnailIFD + // Check for the ASCII code for "Exif" (0x45786966): + if (dataView.getUint32(offset + 4) !== 0x45786966) { + // No Exif data, might be XMP data instead + return + } + if (tiffOffset + 8 > dataView.byteLength) { + console.log('Invalid Exif data: Invalid segment size.') + return + } + // Check for the two null bytes: + if (dataView.getUint16(offset + 8) !== 0x0000) { + console.log('Invalid Exif data: Missing byte alignment offset.') + return + } + // Check the byte alignment: + switch (dataView.getUint16(tiffOffset)) { + case 0x4949: + littleEndian = true + break + case 0x4d4d: + littleEndian = false + break + default: + console.log('Invalid Exif data: Invalid byte alignment marker.') + return + } + // Check for the TIFF tag marker (0x002A): + if (dataView.getUint16(tiffOffset + 2, littleEndian) !== 0x002a) { + console.log('Invalid Exif data: Missing TIFF marker.') + return + } + // Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal: + dirOffset = dataView.getUint32(tiffOffset + 4, littleEndian) + // Create the exif object to store the tags: + data.exif = new ExifMap() + if (!options.disableExifOffsets) { + data.exifOffsets = new ExifMap() + data.exifTiffOffset = tiffOffset + data.exifLittleEndian = littleEndian + } + // Parse the tags of the main image directory (IFD0) and retrieve the + // offset to the next directory (IFD1), usually the thumbnail directory: + dirOffset = parseExifTags( + dataView, + tiffOffset, + tiffOffset + dirOffset, + littleEndian, + data.exif, + data.exifOffsets, + includeTags, + excludeTags + ) + if (dirOffset && shouldIncludeTag(includeTags, excludeTags, 'ifd1')) { + data.exif.ifd1 = dirOffset + if (data.exifOffsets) { + data.exifOffsets.ifd1 = tiffOffset + dirOffset + } + } + Object.keys(data.exif.ifds).forEach(function (tagCode) { + parseExifIFD( + data, + tagCode, + dataView, + tiffOffset, + littleEndian, + includeTags, + excludeTags + ) + }) + thumbnailIFD = data.exif.ifd1 + // Check for JPEG Thumbnail offset and data length: + if (thumbnailIFD && thumbnailIFD[0x0201]) { + thumbnailIFD[0x0201] = getExifThumbnail( + dataView, + tiffOffset + thumbnailIFD[0x0201], + thumbnailIFD[0x0202] // Thumbnail data length + ) + } + } + + // Registers the Exif parser for the APP1 JPEG metadata segment: + loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData) + + loadImage.exifWriters = { + // Orientation writer: + 0x0112: function (buffer, data, value) { + var orientationOffset = data.exifOffsets[0x0112] + if (!orientationOffset) return buffer + var view = new DataView(buffer, orientationOffset + 8, 2) + view.setUint16(0, value, data.exifLittleEndian) + return buffer + } + } + + loadImage.writeExifData = function (buffer, data, id, value) { + loadImage.exifWriters[data.exif.map[id]](buffer, data, value) + } + + loadImage.ExifMap = ExifMap + + // Adds the following properties to the parseMetaData callback data: + // - exif: The parsed Exif tags + // - exifOffsets: The parsed Exif tag offsets + // - exifTiffOffset: TIFF header offset (used for offset pointers) + // - exifLittleEndian: little endian order if true, big endian if false + + // Adds the following options to the parseMetaData method: + // - disableExif: Disables Exif parsing when true. + // - disableExifOffsets: Disables storing Exif tag offsets when true. + // - includeExifTags: A map of Exif tags to include for parsing. + // - excludeExifTags: A map of Exif tags to exclude from parsing. +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-fetch.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-fetch.js new file mode 100644 index 0000000000000..28a28fb83e6cd --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-fetch.js @@ -0,0 +1,103 @@ +/* + * JavaScript Load Image Fetch + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2017, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require, Promise */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + var global = loadImage.global + + if ( + global.fetch && + global.Request && + global.Response && + global.Response.prototype.blob + ) { + loadImage.fetchBlob = function (url, callback, options) { + /** + * Fetch response handler. + * + * @param {Response} response Fetch response + * @returns {Blob} Fetched Blob. + */ + function responseHandler(response) { + return response.blob() + } + if (global.Promise && typeof callback !== 'function') { + return fetch(new Request(url, callback)).then(responseHandler) + } + fetch(new Request(url, options)) + .then(responseHandler) + .then(callback) + [ + // Avoid parsing error in IE<9, where catch is a reserved word. + // eslint-disable-next-line dot-notation + 'catch' + ](function (err) { + callback(null, err) + }) + } + } else if ( + global.XMLHttpRequest && + // https://xhr.spec.whatwg.org/#the-responsetype-attribute + new XMLHttpRequest().responseType === '' + ) { + loadImage.fetchBlob = function (url, callback, options) { + /** + * Promise executor + * + * @param {Function} resolve Resolution function + * @param {Function} reject Rejection function + */ + function executor(resolve, reject) { + options = options || {} // eslint-disable-line no-param-reassign + var req = new XMLHttpRequest() + req.open(options.method || 'GET', url) + if (options.headers) { + Object.keys(options.headers).forEach(function (key) { + req.setRequestHeader(key, options.headers[key]) + }) + } + req.withCredentials = options.credentials === 'include' + req.responseType = 'blob' + req.onload = function () { + resolve(req.response) + } + req.onerror = req.onabort = req.ontimeout = function (err) { + if (resolve === reject) { + // Not using Promises + reject(null, err) + } else { + reject(err) + } + } + req.send(options.body) + } + if (global.Promise && typeof callback !== 'function') { + options = callback // eslint-disable-line no-param-reassign + return new Promise(executor) + } + return executor(callback, callback) + } + } +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc-map.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc-map.js new file mode 100644 index 0000000000000..cd959a24b3541 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc-map.js @@ -0,0 +1,169 @@ +/* + * JavaScript Load Image IPTC Map + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * Copyright 2018, Dave Bevan + * + * IPTC tags mapping based on + * https://iptc.org/standards/photo-metadata + * https://exiftool.org/TagNames/IPTC.html + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + var IptcMapProto = loadImage.IptcMap.prototype + + IptcMapProto.tags = { + 0: 'ApplicationRecordVersion', + 3: 'ObjectTypeReference', + 4: 'ObjectAttributeReference', + 5: 'ObjectName', + 7: 'EditStatus', + 8: 'EditorialUpdate', + 10: 'Urgency', + 12: 'SubjectReference', + 15: 'Category', + 20: 'SupplementalCategories', + 22: 'FixtureIdentifier', + 25: 'Keywords', + 26: 'ContentLocationCode', + 27: 'ContentLocationName', + 30: 'ReleaseDate', + 35: 'ReleaseTime', + 37: 'ExpirationDate', + 38: 'ExpirationTime', + 40: 'SpecialInstructions', + 42: 'ActionAdvised', + 45: 'ReferenceService', + 47: 'ReferenceDate', + 50: 'ReferenceNumber', + 55: 'DateCreated', + 60: 'TimeCreated', + 62: 'DigitalCreationDate', + 63: 'DigitalCreationTime', + 65: 'OriginatingProgram', + 70: 'ProgramVersion', + 75: 'ObjectCycle', + 80: 'Byline', + 85: 'BylineTitle', + 90: 'City', + 92: 'Sublocation', + 95: 'State', + 100: 'CountryCode', + 101: 'Country', + 103: 'OriginalTransmissionReference', + 105: 'Headline', + 110: 'Credit', + 115: 'Source', + 116: 'CopyrightNotice', + 118: 'Contact', + 120: 'Caption', + 121: 'LocalCaption', + 122: 'Writer', + 125: 'RasterizedCaption', + 130: 'ImageType', + 131: 'ImageOrientation', + 135: 'LanguageIdentifier', + 150: 'AudioType', + 151: 'AudioSamplingRate', + 152: 'AudioSamplingResolution', + 153: 'AudioDuration', + 154: 'AudioOutcue', + 184: 'JobID', + 185: 'MasterDocumentID', + 186: 'ShortDocumentID', + 187: 'UniqueDocumentID', + 188: 'OwnerID', + 200: 'ObjectPreviewFileFormat', + 201: 'ObjectPreviewFileVersion', + 202: 'ObjectPreviewData', + 221: 'Prefs', + 225: 'ClassifyState', + 228: 'SimilarityIndex', + 230: 'DocumentNotes', + 231: 'DocumentHistory', + 232: 'ExifCameraInfo', + 255: 'CatalogSets' + } + + IptcMapProto.stringValues = { + 10: { + 0: '0 (reserved)', + 1: '1 (most urgent)', + 2: '2', + 3: '3', + 4: '4', + 5: '5 (normal urgency)', + 6: '6', + 7: '7', + 8: '8 (least urgent)', + 9: '9 (user-defined priority)' + }, + 75: { + a: 'Morning', + b: 'Both Morning and Evening', + p: 'Evening' + }, + 131: { + L: 'Landscape', + P: 'Portrait', + S: 'Square' + } + } + + IptcMapProto.getText = function (id) { + var value = this.get(id) + var tagCode = this.map[id] + var stringValue = this.stringValues[tagCode] + if (stringValue) return stringValue[value] + return String(value) + } + + IptcMapProto.getAll = function () { + var map = {} + var prop + var name + for (prop in this) { + if (Object.prototype.hasOwnProperty.call(this, prop)) { + name = this.tags[prop] + if (name) map[name] = this.getText(name) + } + } + return map + } + + IptcMapProto.getName = function (tagCode) { + return this.tags[tagCode] + } + + // Extend the map of tag names to tag codes: + ;(function () { + var tags = IptcMapProto.tags + var map = IptcMapProto.map || {} + var prop + // Map the tag names to tags: + for (prop in tags) { + if (Object.prototype.hasOwnProperty.call(tags, prop)) { + map[tags[prop]] = Number(prop) + } + } + })() +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc.js new file mode 100644 index 0000000000000..f6b4594f9e130 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-iptc.js @@ -0,0 +1,239 @@ +/* + * JavaScript Load Image IPTC Parser + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * Copyright 2018, Dave Bevan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require, DataView */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + /** + * IPTC tag map + * + * @name IptcMap + * @class + */ + function IptcMap() {} + + IptcMap.prototype.map = { + ObjectName: 5 + } + + IptcMap.prototype.types = { + 0: 'Uint16', // ApplicationRecordVersion + 200: 'Uint16', // ObjectPreviewFileFormat + 201: 'Uint16', // ObjectPreviewFileVersion + 202: 'binary' // ObjectPreviewData + } + + /** + * Retrieves IPTC tag value + * + * @param {number|string} id IPTC tag code or name + * @returns {object} IPTC tag value + */ + IptcMap.prototype.get = function (id) { + return this[id] || this[this.map[id]] + } + + /** + * Retrieves string for the given DataView and range + * + * @param {DataView} dataView Data view interface + * @param {number} offset Offset start + * @param {number} length Offset length + * @returns {string} String value + */ + function getStringValue(dataView, offset, length) { + var outstr = '' + var end = offset + length + for (var n = offset; n < end; n += 1) { + outstr += String.fromCharCode(dataView.getUint8(n)) + } + return outstr + } + + /** + * Retrieves tag value for the given DataView and range + * + * @param {number} tagCode tag code + * @param {IptcMap} map IPTC tag map + * @param {DataView} dataView Data view interface + * @param {number} offset Range start + * @param {number} length Range length + * @returns {object} Tag value + */ + function getTagValue(tagCode, map, dataView, offset, length) { + if (map.types[tagCode] === 'binary') { + return new Blob([dataView.buffer.slice(offset, offset + length)]) + } + if (map.types[tagCode] === 'Uint16') { + return dataView.getUint16(offset) + } + return getStringValue(dataView, offset, length) + } + + /** + * Combines IPTC value with existing ones. + * + * @param {object} value Existing IPTC field value + * @param {object} newValue New IPTC field value + * @returns {object} Resulting IPTC field value + */ + function combineTagValues(value, newValue) { + if (value === undefined) return newValue + if (value instanceof Array) { + value.push(newValue) + return value + } + return [value, newValue] + } + + /** + * Parses IPTC tags. + * + * @param {DataView} dataView Data view interface + * @param {number} segmentOffset Segment offset + * @param {number} segmentLength Segment length + * @param {object} data Data export object + * @param {object} includeTags Map of tags to include + * @param {object} excludeTags Map of tags to exclude + */ + function parseIptcTags( + dataView, + segmentOffset, + segmentLength, + data, + includeTags, + excludeTags + ) { + var value, tagSize, tagCode + var segmentEnd = segmentOffset + segmentLength + var offset = segmentOffset + while (offset < segmentEnd) { + if ( + dataView.getUint8(offset) === 0x1c && // tag marker + dataView.getUint8(offset + 1) === 0x02 // record number, only handles v2 + ) { + tagCode = dataView.getUint8(offset + 2) + if ( + (!includeTags || includeTags[tagCode]) && + (!excludeTags || !excludeTags[tagCode]) + ) { + tagSize = dataView.getInt16(offset + 3) + value = getTagValue(tagCode, data.iptc, dataView, offset + 5, tagSize) + data.iptc[tagCode] = combineTagValues(data.iptc[tagCode], value) + if (data.iptcOffsets) { + data.iptcOffsets[tagCode] = offset + } + } + } + offset += 1 + } + } + + /** + * Tests if field segment starts at offset. + * + * @param {DataView} dataView Data view interface + * @param {number} offset Segment offset + * @returns {boolean} True if '8BIM<EOT><EOT>' exists at offset + */ + function isSegmentStart(dataView, offset) { + return ( + dataView.getUint32(offset) === 0x3842494d && // Photoshop segment start + dataView.getUint16(offset + 4) === 0x0404 // IPTC segment start + ) + } + + /** + * Returns header length. + * + * @param {DataView} dataView Data view interface + * @param {number} offset Segment offset + * @returns {number} Header length + */ + function getHeaderLength(dataView, offset) { + var length = dataView.getUint8(offset + 7) + if (length % 2 !== 0) length += 1 + // Check for pre photoshop 6 format + if (length === 0) { + // Always 4 + length = 4 + } + return length + } + + loadImage.parseIptcData = function (dataView, offset, length, data, options) { + if (options.disableIptc) { + return + } + var markerLength = offset + length + while (offset + 8 < markerLength) { + if (isSegmentStart(dataView, offset)) { + var headerLength = getHeaderLength(dataView, offset) + var segmentOffset = offset + 8 + headerLength + if (segmentOffset > markerLength) { + // eslint-disable-next-line no-console + console.log('Invalid IPTC data: Invalid segment offset.') + break + } + var segmentLength = dataView.getUint16(offset + 6 + headerLength) + if (offset + segmentLength > markerLength) { + // eslint-disable-next-line no-console + console.log('Invalid IPTC data: Invalid segment size.') + break + } + // Create the iptc object to store the tags: + data.iptc = new IptcMap() + if (!options.disableIptcOffsets) { + data.iptcOffsets = new IptcMap() + } + parseIptcTags( + dataView, + segmentOffset, + segmentLength, + data, + options.includeIptcTags, + options.excludeIptcTags || { 202: true } // ObjectPreviewData + ) + return + } + // eslint-disable-next-line no-param-reassign + offset += 1 + } + } + + // Registers this IPTC parser for the APP13 JPEG metadata segment: + loadImage.metaDataParsers.jpeg[0xffed].push(loadImage.parseIptcData) + + loadImage.IptcMap = IptcMap + + // Adds the following properties to the parseMetaData callback data: + // - iptc: The iptc tags, parsed by the parseIptcData method + + // Adds the following options to the parseMetaData method: + // - disableIptc: Disables IPTC parsing when true. + // - disableIptcOffsets: Disables storing IPTC tag offsets when true. + // - includeIptcTags: A map of IPTC tags to include for parsing. + // - excludeIptcTags: A map of IPTC tags to exclude from parsing. +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta.js new file mode 100644 index 0000000000000..20a06184c640d --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta.js @@ -0,0 +1,259 @@ +/* + * JavaScript Load Image Meta + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Image metadata handling implementation + * based on the help and contribution of + * Achim Stöhr. + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require, Promise, DataView, Uint8Array, ArrayBuffer */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + var global = loadImage.global + var originalTransform = loadImage.transform + + var blobSlice = + global.Blob && + (Blob.prototype.slice || + Blob.prototype.webkitSlice || + Blob.prototype.mozSlice) + + var bufferSlice = + (global.ArrayBuffer && ArrayBuffer.prototype.slice) || + function (begin, end) { + // Polyfill for IE10, which does not support ArrayBuffer.slice + // eslint-disable-next-line no-param-reassign + end = end || this.byteLength - begin + var arr1 = new Uint8Array(this, begin, end) + var arr2 = new Uint8Array(end) + arr2.set(arr1) + return arr2.buffer + } + + var metaDataParsers = { + jpeg: { + 0xffe1: [], // APP1 marker + 0xffed: [] // APP13 marker + } + } + + /** + * Parses image metadata and calls the callback with an object argument + * with the following property: + * - imageHead: The complete image head as ArrayBuffer + * The options argument accepts an object and supports the following + * properties: + * - maxMetaDataSize: Defines the maximum number of bytes to parse. + * - disableImageHead: Disables creating the imageHead property. + * + * @param {Blob} file Blob object + * @param {Function} [callback] Callback function + * @param {object} [options] Parsing options + * @param {object} [data] Result data object + * @returns {Promise<object>|undefined} Returns Promise if no callback given. + */ + function parseMetaData(file, callback, options, data) { + var that = this + /** + * Promise executor + * + * @param {Function} resolve Resolution function + * @param {Function} reject Rejection function + * @returns {undefined} Undefined + */ + function executor(resolve, reject) { + if ( + !( + global.DataView && + blobSlice && + file && + file.size >= 12 && + file.type === 'image/jpeg' + ) + ) { + // Nothing to parse + return resolve(data) + } + // 256 KiB should contain all EXIF/ICC/IPTC segments: + var maxMetaDataSize = options.maxMetaDataSize || 262144 + if ( + !loadImage.readFile( + blobSlice.call(file, 0, maxMetaDataSize), + function (buffer) { + // Note on endianness: + // Since the marker and length bytes in JPEG files are always + // stored in big endian order, we can leave the endian parameter + // of the DataView methods undefined, defaulting to big endian. + var dataView = new DataView(buffer) + // Check for the JPEG marker (0xffd8): + if (dataView.getUint16(0) !== 0xffd8) { + return reject( + new Error('Invalid JPEG file: Missing JPEG marker.') + ) + } + var offset = 2 + var maxOffset = dataView.byteLength - 4 + var headLength = offset + var markerBytes + var markerLength + var parsers + var i + while (offset < maxOffset) { + markerBytes = dataView.getUint16(offset) + // Search for APPn (0xffeN) and COM (0xfffe) markers, + // which contain application-specific metadata like + // Exif, ICC and IPTC data and text comments: + if ( + (markerBytes >= 0xffe0 && markerBytes <= 0xffef) || + markerBytes === 0xfffe + ) { + // The marker bytes (2) are always followed by + // the length bytes (2), indicating the length of the + // marker segment, which includes the length bytes, + // but not the marker bytes, so we add 2: + markerLength = dataView.getUint16(offset + 2) + 2 + if (offset + markerLength > dataView.byteLength) { + // eslint-disable-next-line no-console + console.log('Invalid JPEG metadata: Invalid segment size.') + break + } + parsers = metaDataParsers.jpeg[markerBytes] + if (parsers && !options.disableMetaDataParsers) { + for (i = 0; i < parsers.length; i += 1) { + parsers[i].call( + that, + dataView, + offset, + markerLength, + data, + options + ) + } + } + offset += markerLength + headLength = offset + } else { + // Not an APPn or COM marker, probably safe to + // assume that this is the end of the metadata + break + } + } + // Meta length must be longer than JPEG marker (2) + // plus APPn marker (2), followed by length bytes (2): + if (!options.disableImageHead && headLength > 6) { + data.imageHead = bufferSlice.call(buffer, 0, headLength) + } + resolve(data) + }, + reject, + 'readAsArrayBuffer' + ) + ) { + // No support for the FileReader interface, nothing to parse + resolve(data) + } + } + options = options || {} // eslint-disable-line no-param-reassign + if (global.Promise && typeof callback !== 'function') { + options = callback || {} // eslint-disable-line no-param-reassign + data = options // eslint-disable-line no-param-reassign + return new Promise(executor) + } + data = data || {} // eslint-disable-line no-param-reassign + return executor(callback, callback) + } + + /** + * Replaces the head of a JPEG Blob + * + * @param {Blob} blob Blob object + * @param {ArrayBuffer} oldHead Old JPEG head + * @param {ArrayBuffer} newHead New JPEG head + * @returns {Blob} Combined Blob + */ + function replaceJPEGHead(blob, oldHead, newHead) { + if (!blob || !oldHead || !newHead) return null + return new Blob([newHead, blobSlice.call(blob, oldHead.byteLength)], { + type: 'image/jpeg' + }) + } + + /** + * Replaces the image head of a JPEG blob with the given one. + * Returns a Promise or calls the callback with the new Blob. + * + * @param {Blob} blob Blob object + * @param {ArrayBuffer} head New JPEG head + * @param {Function} [callback] Callback function + * @returns {Promise<Blob|null>|undefined} Combined Blob + */ + function replaceHead(blob, head, callback) { + var options = { maxMetaDataSize: 256, disableMetaDataParsers: true } + if (!callback && global.Promise) { + return parseMetaData(blob, options).then(function (data) { + return replaceJPEGHead(blob, data.imageHead, head) + }) + } + parseMetaData( + blob, + function (data) { + callback(replaceJPEGHead(blob, data.imageHead, head)) + }, + options + ) + } + + loadImage.transform = function (img, options, callback, file, data) { + if (loadImage.requiresMetaData(options)) { + data = data || {} // eslint-disable-line no-param-reassign + parseMetaData( + file, + function (result) { + if (result !== data) { + // eslint-disable-next-line no-console + if (global.console) console.log(result) + result = data // eslint-disable-line no-param-reassign + } + originalTransform.call( + loadImage, + img, + options, + callback, + file, + result + ) + }, + options, + data + ) + } else { + originalTransform.apply(loadImage, arguments) + } + } + + loadImage.blobSlice = blobSlice + loadImage.bufferSlice = bufferSlice + loadImage.replaceHead = replaceHead + loadImage.parseMetaData = parseMetaData + loadImage.metaDataParsers = metaDataParsers +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-orientation.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-orientation.js new file mode 100644 index 0000000000000..2b32a368e5f54 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-orientation.js @@ -0,0 +1,481 @@ +/* + * JavaScript Load Image Orientation + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* +Exif orientation values to correctly display the letter F: + + 1 2 + ██████ ██████ + ██ ██ + ████ ████ + ██ ██ + ██ ██ + + 3 4 + ██ ██ + ██ ██ + ████ ████ + ██ ██ + ██████ ██████ + + 5 6 +██████████ ██ +██ ██ ██ ██ +██ ██████████ + + 7 8 + ██ ██████████ + ██ ██ ██ ██ +██████████ ██ + +*/ + +/* global define, module, require */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale', 'jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta'], factory) + } else if (typeof module === 'object' && module.exports) { + factory( + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale'), + require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image-meta') + ) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + var originalTransform = loadImage.transform + var originalRequiresCanvas = loadImage.requiresCanvas + var originalRequiresMetaData = loadImage.requiresMetaData + var originalTransformCoordinates = loadImage.transformCoordinates + var originalGetTransformedOptions = loadImage.getTransformedOptions + + ;(function ($) { + // Guard for non-browser environments (e.g. server-side rendering): + if (!$.global.document) return + // black+white 3x2 JPEG, with the following meta information set: + // - EXIF Orientation: 6 (Rotated 90° CCW) + // Image data layout (B=black, F=white): + // BFF + // BBB + var testImageURL = + 'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' + + 'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' + + 'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' + + 'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAIAAwMBEQACEQEDEQH/x' + + 'ABRAAEAAAAAAAAAAAAAAAAAAAAKEAEBAQADAQEAAAAAAAAAAAAGBQQDCAkCBwEBAAAAAAA' + + 'AAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AG8T9NfSMEVMhQ' + + 'voP3fFiRZ+MTHDifa/95OFSZU5OzRzxkyejv8ciEfhSceSXGjS8eSdLnZc2HDm4M3BxcXw' + + 'H/9k=' + var img = document.createElement('img') + img.onload = function () { + // Check if the browser supports automatic image orientation: + $.orientation = img.width === 2 && img.height === 3 + if ($.orientation) { + var canvas = $.createCanvas(1, 1, true) + var ctx = canvas.getContext('2d') + ctx.drawImage(img, 1, 1, 1, 1, 0, 0, 1, 1) + // Check if the source image coordinates (sX, sY, sWidth, sHeight) are + // correctly applied to the auto-orientated image, which should result + // in a white opaque pixel (e.g. in Safari). + // Browsers that show a transparent pixel (e.g. Chromium) fail to crop + // auto-oriented images correctly and require a workaround, e.g. + // drawing the complete source image to an intermediate canvas first. + // See https://bugs.chromium.org/p/chromium/issues/detail?id=1074354 + $.orientationCropBug = + ctx.getImageData(0, 0, 1, 1).data.toString() !== '255,255,255,255' + } + } + img.src = testImageURL + })(loadImage) + + /** + * Determines if the orientation requires a canvas element. + * + * @param {object} [options] Options object + * @param {boolean} [withMetaData] Is metadata required for orientation + * @returns {boolean} Returns true if orientation requires canvas/meta + */ + function requiresCanvasOrientation(options, withMetaData) { + var orientation = options && options.orientation + return ( + // Exif orientation for browsers without automatic image orientation: + (orientation === true && !loadImage.orientation) || + // Orientation reset for browsers with automatic image orientation: + (orientation === 1 && loadImage.orientation) || + // Orientation to defined value, requires meta for orientation reset only: + ((!withMetaData || loadImage.orientation) && + orientation > 1 && + orientation < 9) + ) + } + + /** + * Determines if the image requires an orientation change. + * + * @param {number} [orientation] Defined orientation value + * @param {number} [autoOrientation] Auto-orientation based on Exif data + * @returns {boolean} Returns true if an orientation change is required + */ + function requiresOrientationChange(orientation, autoOrientation) { + return ( + orientation !== autoOrientation && + ((orientation === 1 && autoOrientation > 1 && autoOrientation < 9) || + (orientation > 1 && orientation < 9)) + ) + } + + /** + * Determines orientation combinations that require a rotation by 180°. + * + * The following is a list of combinations that return true: + * + * 2 (flip) => 5 (rot90,flip), 7 (rot90,flip), 6 (rot90), 8 (rot90) + * 4 (flip) => 5 (rot90,flip), 7 (rot90,flip), 6 (rot90), 8 (rot90) + * + * 5 (rot90,flip) => 2 (flip), 4 (flip), 6 (rot90), 8 (rot90) + * 7 (rot90,flip) => 2 (flip), 4 (flip), 6 (rot90), 8 (rot90) + * + * 6 (rot90) => 2 (flip), 4 (flip), 5 (rot90,flip), 7 (rot90,flip) + * 8 (rot90) => 2 (flip), 4 (flip), 5 (rot90,flip), 7 (rot90,flip) + * + * @param {number} [orientation] Defined orientation value + * @param {number} [autoOrientation] Auto-orientation based on Exif data + * @returns {boolean} Returns true if rotation by 180° is required + */ + function requiresRot180(orientation, autoOrientation) { + if (autoOrientation > 1 && autoOrientation < 9) { + switch (orientation) { + case 2: + case 4: + return autoOrientation > 4 + case 5: + case 7: + return autoOrientation % 2 === 0 + case 6: + case 8: + return ( + autoOrientation === 2 || + autoOrientation === 4 || + autoOrientation === 5 || + autoOrientation === 7 + ) + } + } + return false + } + + // Determines if the target image should be a canvas element: + loadImage.requiresCanvas = function (options) { + return ( + requiresCanvasOrientation(options) || + originalRequiresCanvas.call(loadImage, options) + ) + } + + // Determines if metadata should be loaded automatically: + loadImage.requiresMetaData = function (options) { + return ( + requiresCanvasOrientation(options, true) || + originalRequiresMetaData.call(loadImage, options) + ) + } + + loadImage.transform = function (img, options, callback, file, data) { + originalTransform.call( + loadImage, + img, + options, + function (img, data) { + if (data) { + var autoOrientation = + loadImage.orientation && data.exif && data.exif.get('Orientation') + if (autoOrientation > 4 && autoOrientation < 9) { + // Automatic image orientation switched image dimensions + var originalWidth = data.originalWidth + var originalHeight = data.originalHeight + data.originalWidth = originalHeight + data.originalHeight = originalWidth + } + } + callback(img, data) + }, + file, + data + ) + } + + // Transforms coordinate and dimension options + // based on the given orientation option: + loadImage.getTransformedOptions = function (img, opts, data) { + var options = originalGetTransformedOptions.call(loadImage, img, opts) + var exifOrientation = data.exif && data.exif.get('Orientation') + var orientation = options.orientation + var autoOrientation = loadImage.orientation && exifOrientation + if (orientation === true) orientation = exifOrientation + if (!requiresOrientationChange(orientation, autoOrientation)) { + return options + } + var top = options.top + var right = options.right + var bottom = options.bottom + var left = options.left + var newOptions = {} + for (var i in options) { + if (Object.prototype.hasOwnProperty.call(options, i)) { + newOptions[i] = options[i] + } + } + newOptions.orientation = orientation + if ( + (orientation > 4 && !(autoOrientation > 4)) || + (orientation < 5 && autoOrientation > 4) + ) { + // Image dimensions and target dimensions are switched + newOptions.maxWidth = options.maxHeight + newOptions.maxHeight = options.maxWidth + newOptions.minWidth = options.minHeight + newOptions.minHeight = options.minWidth + newOptions.sourceWidth = options.sourceHeight + newOptions.sourceHeight = options.sourceWidth + } + if (autoOrientation > 1) { + // Browsers which correctly apply source image coordinates to + // auto-oriented images + switch (autoOrientation) { + case 2: + // Horizontal flip + right = options.left + left = options.right + break + case 3: + // 180° Rotate CCW + top = options.bottom + right = options.left + bottom = options.top + left = options.right + break + case 4: + // Vertical flip + top = options.bottom + bottom = options.top + break + case 5: + // Horizontal flip + 90° Rotate CCW + top = options.left + right = options.bottom + bottom = options.right + left = options.top + break + case 6: + // 90° Rotate CCW + top = options.left + right = options.top + bottom = options.right + left = options.bottom + break + case 7: + // Vertical flip + 90° Rotate CCW + top = options.right + right = options.top + bottom = options.left + left = options.bottom + break + case 8: + // 90° Rotate CW + top = options.right + right = options.bottom + bottom = options.left + left = options.top + break + } + // Some orientation combinations require additional rotation by 180°: + if (requiresRot180(orientation, autoOrientation)) { + var tmpTop = top + var tmpRight = right + top = bottom + right = left + bottom = tmpTop + left = tmpRight + } + } + newOptions.top = top + newOptions.right = right + newOptions.bottom = bottom + newOptions.left = left + // Account for defined browser orientation: + switch (orientation) { + case 2: + // Horizontal flip + newOptions.right = left + newOptions.left = right + break + case 3: + // 180° Rotate CCW + newOptions.top = bottom + newOptions.right = left + newOptions.bottom = top + newOptions.left = right + break + case 4: + // Vertical flip + newOptions.top = bottom + newOptions.bottom = top + break + case 5: + // Vertical flip + 90° Rotate CW + newOptions.top = left + newOptions.right = bottom + newOptions.bottom = right + newOptions.left = top + break + case 6: + // 90° Rotate CW + newOptions.top = right + newOptions.right = bottom + newOptions.bottom = left + newOptions.left = top + break + case 7: + // Horizontal flip + 90° Rotate CW + newOptions.top = right + newOptions.right = top + newOptions.bottom = left + newOptions.left = bottom + break + case 8: + // 90° Rotate CCW + newOptions.top = left + newOptions.right = top + newOptions.bottom = right + newOptions.left = bottom + break + } + return newOptions + } + + // Transform image orientation based on the given EXIF orientation option: + loadImage.transformCoordinates = function (canvas, options, data) { + originalTransformCoordinates.call(loadImage, canvas, options, data) + var orientation = options.orientation + var autoOrientation = + loadImage.orientation && data.exif && data.exif.get('Orientation') + if (!requiresOrientationChange(orientation, autoOrientation)) { + return + } + var ctx = canvas.getContext('2d') + var width = canvas.width + var height = canvas.height + var sourceWidth = width + var sourceHeight = height + if ( + (orientation > 4 && !(autoOrientation > 4)) || + (orientation < 5 && autoOrientation > 4) + ) { + // Image dimensions and target dimensions are switched + canvas.width = height + canvas.height = width + } + if (orientation > 4) { + // Destination and source dimensions are switched + sourceWidth = height + sourceHeight = width + } + // Reset automatic browser orientation: + switch (autoOrientation) { + case 2: + // Horizontal flip + ctx.translate(sourceWidth, 0) + ctx.scale(-1, 1) + break + case 3: + // 180° Rotate CCW + ctx.translate(sourceWidth, sourceHeight) + ctx.rotate(Math.PI) + break + case 4: + // Vertical flip + ctx.translate(0, sourceHeight) + ctx.scale(1, -1) + break + case 5: + // Horizontal flip + 90° Rotate CCW + ctx.rotate(-0.5 * Math.PI) + ctx.scale(-1, 1) + break + case 6: + // 90° Rotate CCW + ctx.rotate(-0.5 * Math.PI) + ctx.translate(-sourceWidth, 0) + break + case 7: + // Vertical flip + 90° Rotate CCW + ctx.rotate(-0.5 * Math.PI) + ctx.translate(-sourceWidth, sourceHeight) + ctx.scale(1, -1) + break + case 8: + // 90° Rotate CW + ctx.rotate(0.5 * Math.PI) + ctx.translate(0, -sourceHeight) + break + } + // Some orientation combinations require additional rotation by 180°: + if (requiresRot180(orientation, autoOrientation)) { + ctx.translate(sourceWidth, sourceHeight) + ctx.rotate(Math.PI) + } + switch (orientation) { + case 2: + // Horizontal flip + ctx.translate(width, 0) + ctx.scale(-1, 1) + break + case 3: + // 180° Rotate CCW + ctx.translate(width, height) + ctx.rotate(Math.PI) + break + case 4: + // Vertical flip + ctx.translate(0, height) + ctx.scale(1, -1) + break + case 5: + // Vertical flip + 90° Rotate CW + ctx.rotate(0.5 * Math.PI) + ctx.scale(1, -1) + break + case 6: + // 90° Rotate CW + ctx.rotate(0.5 * Math.PI) + ctx.translate(0, -height) + break + case 7: + // Horizontal flip + 90° Rotate CW + ctx.rotate(0.5 * Math.PI) + ctx.translate(width, -height) + ctx.scale(-1, 1) + break + case 8: + // 90° Rotate CCW + ctx.rotate(-0.5 * Math.PI) + ctx.translate(-width, 0) + break + } + } +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale.js new file mode 100644 index 0000000000000..80cc5e544fecb --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image-scale.js @@ -0,0 +1,327 @@ +/* + * JavaScript Load Image Scaling + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, require */ + +;(function (factory) { + 'use strict' + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery/fileUploader/vendor/blueimp-load-image/js/load-image'], factory) + } else if (typeof module === 'object' && module.exports) { + factory(require('jquery/fileUploader/vendor/blueimp-load-image/js/load-image')) + } else { + // Browser globals: + factory(window.loadImage) + } +})(function (loadImage) { + 'use strict' + + var originalTransform = loadImage.transform + + loadImage.createCanvas = function (width, height, offscreen) { + if (offscreen && loadImage.global.OffscreenCanvas) { + return new OffscreenCanvas(width, height) + } + var canvas = document.createElement('canvas') + canvas.width = width + canvas.height = height + return canvas + } + + loadImage.transform = function (img, options, callback, file, data) { + originalTransform.call( + loadImage, + loadImage.scale(img, options, data), + options, + callback, + file, + data + ) + } + + // Transform image coordinates, allows to override e.g. + // the canvas orientation based on the orientation option, + // gets canvas, options and data passed as arguments: + loadImage.transformCoordinates = function () {} + + // Returns transformed options, allows to override e.g. + // maxWidth, maxHeight and crop options based on the aspectRatio. + // gets img, options, data passed as arguments: + loadImage.getTransformedOptions = function (img, options) { + var aspectRatio = options.aspectRatio + var newOptions + var i + var width + var height + if (!aspectRatio) { + return options + } + newOptions = {} + for (i in options) { + if (Object.prototype.hasOwnProperty.call(options, i)) { + newOptions[i] = options[i] + } + } + newOptions.crop = true + width = img.naturalWidth || img.width + height = img.naturalHeight || img.height + if (width / height > aspectRatio) { + newOptions.maxWidth = height * aspectRatio + newOptions.maxHeight = height + } else { + newOptions.maxWidth = width + newOptions.maxHeight = width / aspectRatio + } + return newOptions + } + + // Canvas render method, allows to implement a different rendering algorithm: + loadImage.drawImage = function ( + img, + canvas, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + destWidth, + destHeight, + options + ) { + var ctx = canvas.getContext('2d') + if (options.imageSmoothingEnabled === false) { + ctx.msImageSmoothingEnabled = false + ctx.imageSmoothingEnabled = false + } else if (options.imageSmoothingQuality) { + ctx.imageSmoothingQuality = options.imageSmoothingQuality + } + ctx.drawImage( + img, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + 0, + 0, + destWidth, + destHeight + ) + return ctx + } + + // Determines if the target image should be a canvas element: + loadImage.requiresCanvas = function (options) { + return options.canvas || options.crop || !!options.aspectRatio + } + + // Scales and/or crops the given image (img or canvas HTML element) + // using the given options: + loadImage.scale = function (img, options, data) { + // eslint-disable-next-line no-param-reassign + options = options || {} + // eslint-disable-next-line no-param-reassign + data = data || {} + var useCanvas = + img.getContext || + (loadImage.requiresCanvas(options) && + !!loadImage.global.HTMLCanvasElement) + var width = img.naturalWidth || img.width + var height = img.naturalHeight || img.height + var destWidth = width + var destHeight = height + var maxWidth + var maxHeight + var minWidth + var minHeight + var sourceWidth + var sourceHeight + var sourceX + var sourceY + var pixelRatio + var downsamplingRatio + var tmp + var canvas + /** + * Scales up image dimensions + */ + function scaleUp() { + var scale = Math.max( + (minWidth || destWidth) / destWidth, + (minHeight || destHeight) / destHeight + ) + if (scale > 1) { + destWidth *= scale + destHeight *= scale + } + } + /** + * Scales down image dimensions + */ + function scaleDown() { + var scale = Math.min( + (maxWidth || destWidth) / destWidth, + (maxHeight || destHeight) / destHeight + ) + if (scale < 1) { + destWidth *= scale + destHeight *= scale + } + } + if (useCanvas) { + // eslint-disable-next-line no-param-reassign + options = loadImage.getTransformedOptions(img, options, data) + sourceX = options.left || 0 + sourceY = options.top || 0 + if (options.sourceWidth) { + sourceWidth = options.sourceWidth + if (options.right !== undefined && options.left === undefined) { + sourceX = width - sourceWidth - options.right + } + } else { + sourceWidth = width - sourceX - (options.right || 0) + } + if (options.sourceHeight) { + sourceHeight = options.sourceHeight + if (options.bottom !== undefined && options.top === undefined) { + sourceY = height - sourceHeight - options.bottom + } + } else { + sourceHeight = height - sourceY - (options.bottom || 0) + } + destWidth = sourceWidth + destHeight = sourceHeight + } + maxWidth = options.maxWidth + maxHeight = options.maxHeight + minWidth = options.minWidth + minHeight = options.minHeight + if (useCanvas && maxWidth && maxHeight && options.crop) { + destWidth = maxWidth + destHeight = maxHeight + tmp = sourceWidth / sourceHeight - maxWidth / maxHeight + if (tmp < 0) { + sourceHeight = (maxHeight * sourceWidth) / maxWidth + if (options.top === undefined && options.bottom === undefined) { + sourceY = (height - sourceHeight) / 2 + } + } else if (tmp > 0) { + sourceWidth = (maxWidth * sourceHeight) / maxHeight + if (options.left === undefined && options.right === undefined) { + sourceX = (width - sourceWidth) / 2 + } + } + } else { + if (options.contain || options.cover) { + minWidth = maxWidth = maxWidth || minWidth + minHeight = maxHeight = maxHeight || minHeight + } + if (options.cover) { + scaleDown() + scaleUp() + } else { + scaleUp() + scaleDown() + } + } + if (useCanvas) { + pixelRatio = options.pixelRatio + if ( + pixelRatio > 1 && + // Check if the image has not yet had the device pixel ratio applied: + !( + img.style.width && + Math.floor(parseFloat(img.style.width, 10)) === + Math.floor(width / pixelRatio) + ) + ) { + destWidth *= pixelRatio + destHeight *= pixelRatio + } + // Check if workaround for Chromium orientation crop bug is required: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1074354 + if ( + loadImage.orientationCropBug && + !img.getContext && + (sourceX || sourceY || sourceWidth !== width || sourceHeight !== height) + ) { + // Write the complete source image to an intermediate canvas first: + tmp = img + // eslint-disable-next-line no-param-reassign + img = loadImage.createCanvas(width, height, true) + loadImage.drawImage( + tmp, + img, + 0, + 0, + width, + height, + width, + height, + options + ) + } + downsamplingRatio = options.downsamplingRatio + if ( + downsamplingRatio > 0 && + downsamplingRatio < 1 && + destWidth < sourceWidth && + destHeight < sourceHeight + ) { + while (sourceWidth * downsamplingRatio > destWidth) { + canvas = loadImage.createCanvas( + sourceWidth * downsamplingRatio, + sourceHeight * downsamplingRatio, + true + ) + loadImage.drawImage( + img, + canvas, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + canvas.width, + canvas.height, + options + ) + sourceX = 0 + sourceY = 0 + sourceWidth = canvas.width + sourceHeight = canvas.height + // eslint-disable-next-line no-param-reassign + img = canvas + } + } + canvas = loadImage.createCanvas(destWidth, destHeight) + loadImage.transformCoordinates(canvas, options, data) + if (pixelRatio > 1) { + canvas.style.width = canvas.width / pixelRatio + 'px' + } + loadImage + .drawImage( + img, + canvas, + sourceX, + sourceY, + sourceWidth, + sourceHeight, + destWidth, + destHeight, + options + ) + .setTransform(1, 0, 0, 1, 0, 0) // reset to the identity matrix + return canvas + } + img.width = destWidth + img.height = destHeight + return img + } +}) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js new file mode 100644 index 0000000000000..8651c3489378a --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js @@ -0,0 +1,2 @@ +!function(c){"use strict";var t=c.URL||c.webkitURL;function f(e){return!!t&&t.createObjectURL(e)}function i(e){return!!t&&t.revokeObjectURL(e)}function u(e,t){!e||"blob:"!==e.slice(0,5)||t&&t.noRevoke||i(e)}function d(e,t,i,a){if(!c.FileReader)return!1;var n=new FileReader;n.onload=function(){t.call(n,this.result)},i&&(n.onabort=n.onerror=function(){i.call(n,this.error)});var r=n[a||"readAsDataURL"];return r?(r.call(n,e),n):void 0}function g(e,t){return Object.prototype.toString.call(t)==="[object "+e+"]"}function m(s,e,l){function t(i,a){var n,r=document.createElement("img");function o(e,t){i!==a?e instanceof Error?a(e):((t=t||{}).image=e,i(t)):i&&i(e,t)}function e(e,t){t&&c.console&&console.log(t),e&&g("Blob",e)?n=f(s=e):(n=s,l&&l.crossOrigin&&(r.crossOrigin=l.crossOrigin)),r.src=n}return r.onerror=function(e){u(n,l),a&&a.call(r,e)},r.onload=function(){u(n,l);var e={originalWidth:r.naturalWidth||r.width,originalHeight:r.naturalHeight||r.height};try{m.transform(r,l,o,s,e)}catch(t){a&&a(t)}},"string"==typeof s?(m.requiresMetaData(l)?m.fetchBlob(s,e,l):e(),r):g("Blob",s)||g("File",s)?(n=f(s))?(r.src=n,r):d(s,function(e){r.src=e},a):void 0}return c.Promise&&"function"!=typeof e?(l=e,new Promise(t)):t(e,e)}m.requiresMetaData=function(e){return e&&e.meta},m.fetchBlob=function(e,t){t()},m.transform=function(e,t,i,a,n){i(e,n)},m.global=c,m.readFile=d,m.isInstanceOf=g,m.createObjectURL=f,m.revokeObjectURL=i,"function"==typeof define&&define.amd?define(function(){return m}):"object"==typeof module&&module.exports?module.exports=m:c.loadImage=m}("undefined"!=typeof window&&window||this),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image"],e):"object"==typeof module&&module.exports?e(require("./load-image")):e(window.loadImage)}(function(E){"use strict";var r=E.transform;E.createCanvas=function(e,t,i){if(i&&E.global.OffscreenCanvas)return new OffscreenCanvas(e,t);var a=document.createElement("canvas");return a.width=e,a.height=t,a},E.transform=function(e,t,i,a,n){r.call(E,E.scale(e,t,n),t,i,a,n)},E.transformCoordinates=function(){},E.getTransformedOptions=function(e,t){var i,a,n,r,o=t.aspectRatio;if(!o)return t;for(a in i={},t)Object.prototype.hasOwnProperty.call(t,a)&&(i[a]=t[a]);return i.crop=!0,o<(n=e.naturalWidth||e.width)/(r=e.naturalHeight||e.height)?(i.maxWidth=r*o,i.maxHeight=r):(i.maxWidth=n,i.maxHeight=n/o),i},E.drawImage=function(e,t,i,a,n,r,o,s,l){var c=t.getContext("2d");return!1===l.imageSmoothingEnabled?(c.msImageSmoothingEnabled=!1,c.imageSmoothingEnabled=!1):l.imageSmoothingQuality&&(c.imageSmoothingQuality=l.imageSmoothingQuality),c.drawImage(e,i,a,n,r,0,0,o,s),c},E.requiresCanvas=function(e){return e.canvas||e.crop||!!e.aspectRatio},E.scale=function(e,t,i){t=t||{},i=i||{};var a,n,r,o,s,l,c,f,u,d,g,m,h=e.getContext||E.requiresCanvas(t)&&!!E.global.HTMLCanvasElement,p=e.naturalWidth||e.width,A=e.naturalHeight||e.height,b=p,y=A;function S(){var e=Math.max((r||b)/b,(o||y)/y);1<e&&(b*=e,y*=e)}function v(){var e=Math.min((a||b)/b,(n||y)/y);e<1&&(b*=e,y*=e)}if(h&&(c=(t=E.getTransformedOptions(e,t,i)).left||0,f=t.top||0,t.sourceWidth?(s=t.sourceWidth,t.right!==undefined&&t.left===undefined&&(c=p-s-t.right)):s=p-c-(t.right||0),t.sourceHeight?(l=t.sourceHeight,t.bottom!==undefined&&t.top===undefined&&(f=A-l-t.bottom)):l=A-f-(t.bottom||0),b=s,y=l),a=t.maxWidth,n=t.maxHeight,r=t.minWidth,o=t.minHeight,h&&a&&n&&t.crop?(g=s/l-(b=a)/(y=n))<0?(l=n*s/a,t.top===undefined&&t.bottom===undefined&&(f=(A-l)/2)):0<g&&(s=a*l/n,t.left===undefined&&t.right===undefined&&(c=(p-s)/2)):((t.contain||t.cover)&&(r=a=a||r,o=n=n||o),t.cover?(v(),S()):(S(),v())),h){if(1<(u=t.pixelRatio)&&(!e.style.width||Math.floor(parseFloat(e.style.width,10))!==Math.floor(p/u))&&(b*=u,y*=u),E.orientationCropBug&&!e.getContext&&(c||f||s!==p||l!==A)&&(g=e,e=E.createCanvas(p,A,!0),E.drawImage(g,e,0,0,p,A,p,A,t)),0<(d=t.downsamplingRatio)&&d<1&&b<s&&y<l)for(;b<s*d;)m=E.createCanvas(s*d,l*d,!0),E.drawImage(e,m,c,f,s,l,m.width,m.height,t),f=c=0,s=m.width,l=m.height,e=m;return m=E.createCanvas(b,y),E.transformCoordinates(m,t,i),1<u&&(m.style.width=m.width/u+"px"),E.drawImage(e,m,c,f,s,l,b,y,t).setTransform(1,0,0,1,0,0),m}return e.width=b,e.height=y,e}}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image"],e):"object"==typeof module&&module.exports?e(require("./load-image")):e(window.loadImage)}(function(o){"use strict";var s=o.global,l=o.transform,a=s.Blob&&(Blob.prototype.slice||Blob.prototype.webkitSlice||Blob.prototype.mozSlice),m=s.ArrayBuffer&&ArrayBuffer.prototype.slice||function(e,t){t=t||this.byteLength-e;var i=new Uint8Array(this,e,t),a=new Uint8Array(t);return a.set(i),a.buffer},h={jpeg:{65505:[],65517:[]}};function c(t,e,u,d){var g=this;function i(c,f){if(!(s.DataView&&a&&t&&12<=t.size&&"image/jpeg"===t.type))return c(d);var e=u.maxMetaDataSize||262144;o.readFile(a.call(t,0,e),function(e){var t=new DataView(e);if(65496!==t.getUint16(0))return f(new Error("Invalid JPEG file: Missing JPEG marker."));for(var i,a,n,r,o=2,s=t.byteLength-4,l=o;o<s&&(65504<=(i=t.getUint16(o))&&i<=65519||65534===i);){if(o+(a=t.getUint16(o+2)+2)>t.byteLength){console.log("Invalid JPEG metadata: Invalid segment size.");break}if((n=h.jpeg[i])&&!u.disableMetaDataParsers)for(r=0;r<n.length;r+=1)n[r].call(g,t,o,a,d,u);l=o+=a}!u.disableImageHead&&6<l&&(d.imageHead=m.call(e,0,l)),c(d)},f,"readAsArrayBuffer")||c(d)}return u=u||{},s.Promise&&"function"!=typeof e?(d=u=e||{},new Promise(i)):(d=d||{},i(e,e))}function n(e,t,i){return e&&t&&i?new Blob([i,a.call(e,t.byteLength)],{type:"image/jpeg"}):null}o.transform=function(t,i,a,n,r){o.requiresMetaData(i)?c(n,function(e){e!==r&&(s.console&&console.log(e),e=r),l.call(o,t,i,a,n,e)},i,r=r||{}):l.apply(o,arguments)},o.blobSlice=a,o.bufferSlice=m,o.replaceHead=function(t,i,a){var e={maxMetaDataSize:256,disableMetaDataParsers:!0};if(!a&&s.Promise)return c(t,e).then(function(e){return n(t,e.imageHead,i)});c(t,function(e){a(n(t,e.imageHead,i))},e)},o.parseMetaData=c,o.metaDataParsers=h}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image"],e):"object"==typeof module&&module.exports?e(require("./load-image")):e(window.loadImage)}(function(e){"use strict";var r=e.global;r.fetch&&r.Request&&r.Response&&r.Response.prototype.blob?e.fetchBlob=function(e,t,i){function a(e){return e.blob()}if(r.Promise&&"function"!=typeof t)return fetch(new Request(e,t)).then(a);fetch(new Request(e,i)).then(a).then(t)["catch"](function(e){t(null,e)})}:r.XMLHttpRequest&&""===(new XMLHttpRequest).responseType&&(e.fetchBlob=function(e,t,n){function i(t,i){n=n||{};var a=new XMLHttpRequest;a.open(n.method||"GET",e),n.headers&&Object.keys(n.headers).forEach(function(e){a.setRequestHeader(e,n.headers[e])}),a.withCredentials="include"===n.credentials,a.responseType="blob",a.onload=function(){t(a.response)},a.onerror=a.onabort=a.ontimeout=function(e){t===i?i(null,e):i(e)},a.send(n.body)}return r.Promise&&"function"!=typeof t?(n=t,new Promise(i)):i(t,t)})}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-scale","./load-image-meta"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-scale"),require("./load-image-meta")):e(window.loadImage)}(function(h){"use strict";var t,i,n=h.transform,a=h.requiresCanvas,r=h.requiresMetaData,f=h.transformCoordinates,p=h.getTransformedOptions;function o(e,t){var i=e&&e.orientation;return!0===i&&!h.orientation||1===i&&h.orientation||(!t||h.orientation)&&1<i&&i<9}function A(e,t){return e!==t&&(1===e&&1<t&&t<9||1<e&&e<9)}function b(e,t){if(1<t&&t<9)switch(e){case 2:case 4:return 4<t;case 5:case 7:return t%2==0;case 6:case 8:return 2===t||4===t||5===t||7===t}}(t=h).global.document&&((i=document.createElement("img")).onload=function(){var e;t.orientation=2===i.width&&3===i.height,t.orientation&&((e=t.createCanvas(1,1,!0).getContext("2d")).drawImage(i,1,1,1,1,0,0,1,1),t.orientationCropBug="255,255,255,255"!==e.getImageData(0,0,1,1).data.toString())},i.src="data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAIAAwMBEQACEQEDEQH/xABRAAEAAAAAAAAAAAAAAAAAAAAKEAEBAQADAQEAAAAAAAAAAAAGBQQDCAkCBwEBAAAAAAAAAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AG8T9NfSMEVMhQvoP3fFiRZ+MTHDifa/95OFSZU5OzRzxkyejv8ciEfhSceSXGjS8eSdLnZc2HDm4M3BxcXwH/9k="),h.requiresCanvas=function(e){return o(e)||a.call(h,e)},h.requiresMetaData=function(e){return o(e,!0)||r.call(h,e)},h.transform=function(e,t,r,i,a){n.call(h,e,t,function(e,t){var i,a,n;!t||4<(i=h.orientation&&t.exif&&t.exif.get("Orientation"))&&i<9&&(a=t.originalWidth,n=t.originalHeight,t.originalWidth=n,t.originalHeight=a),r(e,t)},i,a)},h.getTransformedOptions=function(e,t,i){var a=p.call(h,e,t),n=i.exif&&i.exif.get("Orientation"),r=a.orientation,o=h.orientation&&n;if(!0===r&&(r=n),!A(r,o))return a;var s,l,c=a.top,f=a.right,u=a.bottom,d=a.left,g={};for(var m in a)Object.prototype.hasOwnProperty.call(a,m)&&(g[m]=a[m]);if((4<(g.orientation=r)&&!(4<o)||r<5&&4<o)&&(g.maxWidth=a.maxHeight,g.maxHeight=a.maxWidth,g.minWidth=a.minHeight,g.minHeight=a.minWidth,g.sourceWidth=a.sourceHeight,g.sourceHeight=a.sourceWidth),1<o){switch(o){case 2:f=a.left,d=a.right;break;case 3:c=a.bottom,f=a.left,u=a.top,d=a.right;break;case 4:c=a.bottom,u=a.top;break;case 5:c=a.left,f=a.bottom,u=a.right,d=a.top;break;case 6:c=a.left,f=a.top,u=a.right,d=a.bottom;break;case 7:c=a.right,f=a.top,u=a.left,d=a.bottom;break;case 8:c=a.right,f=a.bottom,u=a.left,d=a.top}b(r,o)&&(s=c,l=f,c=u,f=d,u=s,d=l)}switch(g.top=c,g.right=f,g.bottom=u,g.left=d,r){case 2:g.right=d,g.left=f;break;case 3:g.top=u,g.right=d,g.bottom=c,g.left=f;break;case 4:g.top=u,g.bottom=c;break;case 5:g.top=d,g.right=u,g.bottom=f,g.left=c;break;case 6:g.top=f,g.right=u,g.bottom=d,g.left=c;break;case 7:g.top=f,g.right=c,g.bottom=d,g.left=u;break;case 8:g.top=d,g.right=c,g.bottom=f,g.left=u}return g},h.transformCoordinates=function(e,t,i){f.call(h,e,t,i);var a=t.orientation,n=h.orientation&&i.exif&&i.exif.get("Orientation");if(A(a,n)){var r=e.getContext("2d"),o=e.width,s=e.height,l=o,c=s;switch((4<a&&!(4<n)||a<5&&4<n)&&(e.width=s,e.height=o),4<a&&(l=s,c=o),n){case 2:r.translate(l,0),r.scale(-1,1);break;case 3:r.translate(l,c),r.rotate(Math.PI);break;case 4:r.translate(0,c),r.scale(1,-1);break;case 5:r.rotate(-.5*Math.PI),r.scale(-1,1);break;case 6:r.rotate(-.5*Math.PI),r.translate(-l,0);break;case 7:r.rotate(-.5*Math.PI),r.translate(-l,c),r.scale(1,-1);break;case 8:r.rotate(.5*Math.PI),r.translate(0,-c)}switch(b(a,n)&&(r.translate(l,c),r.rotate(Math.PI)),a){case 2:r.translate(o,0),r.scale(-1,1);break;case 3:r.translate(o,s),r.rotate(Math.PI);break;case 4:r.translate(0,s),r.scale(1,-1);break;case 5:r.rotate(.5*Math.PI),r.scale(1,-1);break;case 6:r.rotate(.5*Math.PI),r.translate(0,-s);break;case 7:r.rotate(.5*Math.PI),r.translate(o,-s),r.scale(-1,1);break;case 8:r.rotate(-.5*Math.PI),r.translate(-o,0)}}}}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-meta"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-meta")):e(window.loadImage)}(function(r){"use strict";function h(e){e&&(Object.defineProperty(this,"map",{value:this.ifds[e].map}),Object.defineProperty(this,"tags",{value:this.tags&&this.tags[e]||{}}))}h.prototype.ifds={ifd1:{name:"Thumbnail",map:h.prototype.map={Orientation:274,Thumbnail:"ifd1",Blob:513,Exif:34665,GPSInfo:34853,Interoperability:40965}},34665:{name:"Exif",map:{}},34853:{name:"GPSInfo",map:{}},40965:{name:"Interoperability",map:{}}},h.prototype.get=function(e){return this[e]||this[this.map[e]]};var m={1:{getValue:function(e,t){return e.getUint8(t)},size:1},2:{getValue:function(e,t){return String.fromCharCode(e.getUint8(t))},size:1,ascii:!0},3:{getValue:function(e,t,i){return e.getUint16(t,i)},size:2},4:{getValue:function(e,t,i){return e.getUint32(t,i)},size:4},5:{getValue:function(e,t,i){return e.getUint32(t,i)/e.getUint32(t+4,i)},size:8},9:{getValue:function(e,t,i){return e.getInt32(t,i)},size:4},10:{getValue:function(e,t,i){return e.getInt32(t,i)/e.getInt32(t+4,i)},size:8}};function p(e,t,i){return(!e||e[i])&&(!t||!0!==t[i])}function A(e,t,i,a,n,r,o,s){var l,c,f,u,d,g;if(i+6>e.byteLength)console.log("Invalid Exif data: Invalid directory offset.");else{if(!((c=i+2+12*(l=e.getUint16(i,a)))+4>e.byteLength)){for(f=0;f<l;f+=1)u=i+2+12*f,p(o,s,d=e.getUint16(u,a))&&(g=function(e,t,i,a,n,r){var o,s,l,c,f,u,d=m[a];if(d){if(!((s=4<(o=d.size*n)?t+e.getUint32(i+8,r):i+8)+o>e.byteLength)){if(1===n)return d.getValue(e,s,r);for(l=[],c=0;c<n;c+=1)l[c]=d.getValue(e,s+c*d.size,r);if(d.ascii){for(f="",c=0;c<l.length&&"\0"!==(u=l[c]);c+=1)f+=u;return f}return l}console.log("Invalid Exif data: Invalid data offset.")}else console.log("Invalid Exif data: Invalid tag type.")}(e,t,u,e.getUint16(u+2,a),e.getUint32(u+4,a),a),n[d]=g,r&&(r[d]=u));return e.getUint32(c,a)}console.log("Invalid Exif data: Invalid directory size.")}}m[7]=m[1],r.parseExifData=function(c,e,t,f,i){if(!i.disableExif){var u,a,n,d=i.includeExifTags,g=i.excludeExifTags||{34665:{37500:!0}},m=e+10;if(1165519206===c.getUint32(e+4))if(m+8>c.byteLength)console.log("Invalid Exif data: Invalid segment size.");else if(0===c.getUint16(e+8)){switch(c.getUint16(m)){case 18761:u=!0;break;case 19789:u=!1;break;default:return void console.log("Invalid Exif data: Invalid byte alignment marker.")}42===c.getUint16(m+2,u)?(a=c.getUint32(m+4,u),f.exif=new h,i.disableExifOffsets||(f.exifOffsets=new h,f.exifTiffOffset=m,f.exifLittleEndian=u),(a=A(c,m,m+a,u,f.exif,f.exifOffsets,d,g))&&p(d,g,"ifd1")&&(f.exif.ifd1=a,f.exifOffsets&&(f.exifOffsets.ifd1=m+a)),Object.keys(f.exif.ifds).forEach(function(e){var t,i,a,n,r,o,s,l;i=e,a=c,n=m,r=u,o=d,s=g,(l=(t=f).exif[i])&&(t.exif[i]=new h(i),t.exifOffsets&&(t.exifOffsets[i]=new h(i)),A(a,n,n+l,r,t.exif[i],t.exifOffsets&&t.exifOffsets[i],o&&o[i],s&&s[i]))}),(n=f.exif.ifd1)&&n[513]&&(n[513]=function(e,t,i){if(i){if(!(t+i>e.byteLength))return new Blob([r.bufferSlice.call(e.buffer,t,t+i)],{type:"image/jpeg"});console.log("Invalid Exif data: Invalid thumbnail data.")}}(c,m+n[513],n[514]))):console.log("Invalid Exif data: Missing TIFF marker.")}else console.log("Invalid Exif data: Missing byte alignment offset.")}},r.metaDataParsers.jpeg[65505].push(r.parseExifData),r.exifWriters={274:function(e,t,i){var a=t.exifOffsets[274];return a&&new DataView(e,a+8,2).setUint16(0,i,t.exifLittleEndian),e}},r.writeExifData=function(e,t,i,a){r.exifWriters[t.exif.map[i]](e,t,a)},r.ExifMap=h}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-exif"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-exif")):e(window.loadImage)}(function(e){"use strict";var n=e.ExifMap.prototype;n.tags={256:"ImageWidth",257:"ImageHeight",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright",34665:{36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",42240:"Gamma",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",36880:"OffsetTime",36881:"OffsetTimeOriginal",36882:"OffsetTimeDigitized",37520:"SubSecTime",37521:"SubSecTimeOriginal",37522:"SubSecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"PhotographicSensitivity",34856:"OECF",34864:"SensitivityType",34865:"StandardOutputSensitivity",34866:"RecommendedExposureIndex",34867:"ISOSpeed",34868:"ISOSpeedLatitudeyyy",34869:"ISOSpeedLatitudezzz",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRatio",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",42016:"ImageUniqueID",42032:"CameraOwnerName",42033:"BodySerialNumber",42034:"LensSpecification",42035:"LensMake",42036:"LensModel",42037:"LensSerialNumber"},34853:{0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential",31:"GPSHPositioningError"},40965:{1:"InteroperabilityIndex"}},n.tags.ifd1=n.tags,n.stringValues={ExposureProgram:{0:"Undefined",1:"Manual",2:"Normal program",3:"Aperture priority",4:"Shutter priority",5:"Creative program",6:"Action program",7:"Portrait mode",8:"Landscape mode"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{0:"Unknown",1:"Daylight",2:"Fluorescent",3:"Tungsten (incandescent light)",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 - 5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Undefined",2:"One-chip color area sensor",3:"Two-chip color area sensor",4:"Three-chip color area sensor",5:"Color sequential area sensor",7:"Trilinear sensor",8:"Color sequential linear sensor"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},SceneType:{1:"Directly photographed"},CustomRendered:{0:"Normal process",1:"Custom process"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},GainControl:{0:"None",1:"Low gain up",2:"High gain up",3:"Low gain down",4:"High gain down"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},SubjectDistanceRange:{0:"Unknown",1:"Macro",2:"Close view",3:"Distant view"},FileSource:{3:"DSC"},ComponentsConfiguration:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"},Orientation:{1:"Original",2:"Horizontal flip",3:"Rotate 180° CCW",4:"Vertical flip",5:"Vertical flip + Rotate 90° CW",6:"Rotate 90° CW",7:"Horizontal flip + Rotate 90° CW",8:"Rotate 90° CCW"}},n.getText=function(e){var t=this.get(e);switch(e){case"LightSource":case"Flash":case"MeteringMode":case"ExposureProgram":case"SensingMethod":case"SceneCaptureType":case"SceneType":case"CustomRendered":case"WhiteBalance":case"GainControl":case"Contrast":case"Saturation":case"Sharpness":case"SubjectDistanceRange":case"FileSource":case"Orientation":return this.stringValues[e][t];case"ExifVersion":case"FlashpixVersion":if(!t)return;return String.fromCharCode(t[0],t[1],t[2],t[3]);case"ComponentsConfiguration":if(!t)return;return this.stringValues[e][t[0]]+this.stringValues[e][t[1]]+this.stringValues[e][t[2]]+this.stringValues[e][t[3]];case"GPSVersionID":if(!t)return;return t[0]+"."+t[1]+"."+t[2]+"."+t[3]}return String(t)},n.getAll=function(){var e,t,i,a={};for(e in this)Object.prototype.hasOwnProperty.call(this,e)&&((t=this[e])&&t.getAll?a[this.ifds[e].name]=t.getAll():(i=this.tags[e])&&(a[i]=this.getText(i)));return a},n.getName=function(e){var t=this.tags[e];return"object"==typeof t?this.ifds[e].name:t},function(){var e,t,i,a=n.tags;for(e in a)if(Object.prototype.hasOwnProperty.call(a,e))if(t=n.ifds[e])for(e in i=a[e])Object.prototype.hasOwnProperty.call(i,e)&&(t.map[i[e]]=Number(e));else n.map[a[e]]=Number(e)}()}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-meta"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-meta")):e(window.loadImage)}(function(e){"use strict";function g(){}function m(e,t,i,a,n){return"binary"===t.types[e]?new Blob([i.buffer.slice(a,a+n)]):"Uint16"===t.types[e]?i.getUint16(a):function(e,t,i){for(var a="",n=t+i,r=t;r<n;r+=1)a+=String.fromCharCode(e.getUint8(r));return a}(i,a,n)}function h(e,t,i,a,n,r){for(var o,s,l,c,f,u=t+i,d=t;d<u;)28===e.getUint8(d)&&2===e.getUint8(d+1)&&(l=e.getUint8(d+2),n&&!n[l]||r&&r[l]||(s=e.getInt16(d+3),o=m(l,a.iptc,e,d+5,s),a.iptc[l]=(c=a.iptc[l],f=o,c===undefined?f:c instanceof Array?(c.push(f),c):[c,f]),a.iptcOffsets&&(a.iptcOffsets[l]=d))),d+=1}g.prototype.map={ObjectName:5},g.prototype.types={0:"Uint16",200:"Uint16",201:"Uint16",202:"binary"},g.prototype.get=function(e){return this[e]||this[this.map[e]]},e.parseIptcData=function(e,t,i,a,n){if(!n.disableIptc)for(var r,o,s,l,c=t+i;t+8<c;){if(l=t,943868237===(s=e).getUint32(l)&&1028===s.getUint16(l+4)){var f=(r=t,o=void 0,(o=e.getUint8(r+7))%2!=0&&(o+=1),0===o&&(o=4),o),u=t+8+f;if(c<u){console.log("Invalid IPTC data: Invalid segment offset.");break}var d=e.getUint16(t+6+f);if(c<t+d){console.log("Invalid IPTC data: Invalid segment size.");break}return a.iptc=new g,n.disableIptcOffsets||(a.iptcOffsets=new g),void h(e,u,d,a,n.includeIptcTags,n.excludeIptcTags||{202:!0})}t+=1}},e.metaDataParsers.jpeg[65517].push(e.parseIptcData),e.IptcMap=g}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-iptc"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-iptc")):e(window.loadImage)}(function(e){"use strict";var a=e.IptcMap.prototype;a.tags={0:"ApplicationRecordVersion",3:"ObjectTypeReference",4:"ObjectAttributeReference",5:"ObjectName",7:"EditStatus",8:"EditorialUpdate",10:"Urgency",12:"SubjectReference",15:"Category",20:"SupplementalCategories",22:"FixtureIdentifier",25:"Keywords",26:"ContentLocationCode",27:"ContentLocationName",30:"ReleaseDate",35:"ReleaseTime",37:"ExpirationDate",38:"ExpirationTime",40:"SpecialInstructions",42:"ActionAdvised",45:"ReferenceService",47:"ReferenceDate",50:"ReferenceNumber",55:"DateCreated",60:"TimeCreated",62:"DigitalCreationDate",63:"DigitalCreationTime",65:"OriginatingProgram",70:"ProgramVersion",75:"ObjectCycle",80:"Byline",85:"BylineTitle",90:"City",92:"Sublocation",95:"State",100:"CountryCode",101:"Country",103:"OriginalTransmissionReference",105:"Headline",110:"Credit",115:"Source",116:"CopyrightNotice",118:"Contact",120:"Caption",121:"LocalCaption",122:"Writer",125:"RasterizedCaption",130:"ImageType",131:"ImageOrientation",135:"LanguageIdentifier",150:"AudioType",151:"AudioSamplingRate",152:"AudioSamplingResolution",153:"AudioDuration",154:"AudioOutcue",184:"JobID",185:"MasterDocumentID",186:"ShortDocumentID",187:"UniqueDocumentID",188:"OwnerID",200:"ObjectPreviewFileFormat",201:"ObjectPreviewFileVersion",202:"ObjectPreviewData",221:"Prefs",225:"ClassifyState",228:"SimilarityIndex",230:"DocumentNotes",231:"DocumentHistory",232:"ExifCameraInfo",255:"CatalogSets"},a.stringValues={10:{0:"0 (reserved)",1:"1 (most urgent)",2:"2",3:"3",4:"4",5:"5 (normal urgency)",6:"6",7:"7",8:"8 (least urgent)",9:"9 (user-defined priority)"},75:{a:"Morning",b:"Both Morning and Evening",p:"Evening"},131:{L:"Landscape",P:"Portrait",S:"Square"}},a.getText=function(e){var t=this.get(e),i=this.map[e],a=this.stringValues[i];return a?a[t]:String(t)},a.getAll=function(){var e,t,i={};for(e in this)Object.prototype.hasOwnProperty.call(this,e)&&(t=this.tags[e])&&(i[t]=this.getText(t));return i},a.getName=function(e){return this.tags[e]},function(){var e,t=a.tags,i=a.map||{};for(e in t)Object.prototype.hasOwnProperty.call(t,e)&&(i[t[e]]=Number(e))}()}); +//# sourceMappingURL=load-image.all.min.js.map \ No newline at end of file diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js.map b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js.map new file mode 100644 index 0000000000000..1d6d990b2c58c --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["load-image.js","load-image-scale.js","load-image-meta.js","load-image-fetch.js","load-image-orientation.js","load-image-exif.js","load-image-exif-map.js","load-image-iptc.js","load-image-iptc-map.js"],"names":["$","urlAPI","URL","webkitURL","createObjectURL","blob","revokeObjectURL","url","revokeHelper","options","slice","noRevoke","readFile","file","onload","onerror","method","FileReader","reader","call","this","result","onabort","error","readerMethod","isInstanceOf","type","obj","Object","prototype","toString","loadImage","callback","executor","resolve","reject","img","document","createElement","resolveWrapper","data","Error","image","fetchBlobCallback","err","console","log","crossOrigin","src","event","originalWidth","naturalWidth","width","originalHeight","naturalHeight","height","transform","requiresMetaData","fetchBlob","Promise","meta","global","define","amd","module","exports","window","factory","require","originalTransform","createCanvas","offscreen","OffscreenCanvas","canvas","scale","transformCoordinates","getTransformedOptions","newOptions","i","aspectRatio","hasOwnProperty","crop","maxWidth","maxHeight","drawImage","sourceX","sourceY","sourceWidth","sourceHeight","destWidth","destHeight","ctx","getContext","imageSmoothingEnabled","msImageSmoothingEnabled","imageSmoothingQuality","requiresCanvas","minWidth","minHeight","pixelRatio","downsamplingRatio","tmp","useCanvas","HTMLCanvasElement","scaleUp","Math","max","scaleDown","min","left","top","right","undefined","bottom","contain","cover","style","floor","parseFloat","orientationCropBug","setTransform","blobSlice","Blob","webkitSlice","mozSlice","bufferSlice","ArrayBuffer","begin","end","byteLength","arr1","Uint8Array","arr2","set","buffer","metaDataParsers","jpeg","65505","65517","parseMetaData","that","DataView","size","maxMetaDataSize","dataView","getUint16","markerBytes","markerLength","parsers","offset","maxOffset","headLength","disableMetaDataParsers","length","disableImageHead","imageHead","replaceJPEGHead","oldHead","newHead","apply","arguments","replaceHead","head","then","fetch","Request","Response","responseHandler","response","XMLHttpRequest","responseType","req","open","headers","keys","forEach","key","setRequestHeader","withCredentials","credentials","ontimeout","send","body","originalRequiresCanvas","originalRequiresMetaData","originalTransformCoordinates","originalGetTransformedOptions","requiresCanvasOrientation","withMetaData","orientation","requiresOrientationChange","autoOrientation","requiresRot180","getImageData","exif","get","opts","exifOrientation","tmpTop","tmpRight","translate","rotate","PI","ExifMap","tagCode","defineProperty","value","ifds","map","tags","ifd1","name","Orientation","Thumbnail","Exif","GPSInfo","Interoperability","34665","34853","40965","id","ExifTagTypes","1","getValue","dataOffset","getUint8","2","String","fromCharCode","ascii","3","littleEndian","4","getUint32","5","9","getInt32","10","shouldIncludeTag","includeTags","excludeTags","parseExifTags","tiffOffset","dirOffset","tagOffsets","tagsNumber","dirEndOffset","tagOffset","tagNumber","tagValue","tagSize","values","str","c","tagType","getExifValue","parseExifData","disableExif","thumbnailIFD","includeExifTags","excludeExifTags","37500","disableExifOffsets","exifOffsets","exifTiffOffset","exifLittleEndian","getExifThumbnail","push","exifWriters","274","orientationOffset","setUint16","writeExifData","ExifMapProto","256","257","258","259","262","277","284","530","531","282","283","296","273","278","279","513","514","301","318","319","529","532","306","270","271","272","305","315","33432","36864","40960","40961","40962","40963","42240","37121","37122","37510","40964","36867","36868","36880","36881","36882","37520","37521","37522","33434","33437","34850","34852","34855","34856","34864","34865","34866","34867","34868","34869","37377","37378","37379","37380","37381","37382","37383","37384","37385","37396","37386","41483","41484","41486","41487","41488","41492","41493","41495","41728","41729","41730","41985","41986","41987","41988","41989","41990","41991","41992","41993","41994","41995","41996","42016","42032","42033","42034","42035","42036","42037","0","6","7","8","11","12","13","14","15","16","17","18","19","20","21","22","23","24","25","26","27","28","29","30","31","stringValues","ExposureProgram","MeteringMode","255","LightSource","Flash","32","65","69","71","73","77","79","89","93","95","SensingMethod","SceneCaptureType","SceneType","CustomRendered","WhiteBalance","GainControl","Contrast","Saturation","Sharpness","SubjectDistanceRange","FileSource","ComponentsConfiguration","getText","getAll","prop","getName","ifd","subTags","Number","IptcMap","getTagValue","types","outstr","n","getStringValue","parseIptcTags","segmentOffset","segmentLength","newValue","segmentEnd","getInt16","iptc","Array","iptcOffsets","ObjectName","200","201","202","parseIptcData","disableIptc","headerLength","disableIptcOffsets","includeIptcTags","excludeIptcTags","IptcMapProto","35","37","38","40","42","45","47","50","55","60","62","63","70","75","80","85","90","92","100","101","103","105","110","115","116","118","120","121","122","125","130","131","135","150","151","152","153","154","184","185","186","187","188","221","225","228","230","231","232","a","b","p","L","P","S","stringValue"],"mappings":"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"} \ No newline at end of file diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.js new file mode 100644 index 0000000000000..27387fbd13d76 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.js @@ -0,0 +1,229 @@ +/* + * JavaScript Load Image + * https://github.com/blueimp/JavaScript-Load-Image + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define, module, Promise */ + +;(function ($) { + 'use strict' + + var urlAPI = $.URL || $.webkitURL + + /** + * Creates an object URL for a given File object. + * + * @param {Blob} blob Blob object + * @returns {string|boolean} Returns object URL if API exists, else false. + */ + function createObjectURL(blob) { + return urlAPI ? urlAPI.createObjectURL(blob) : false + } + + /** + * Revokes a given object URL. + * + * @param {string} url Blob object URL + * @returns {undefined|boolean} Returns undefined if API exists, else false. + */ + function revokeObjectURL(url) { + return urlAPI ? urlAPI.revokeObjectURL(url) : false + } + + /** + * Helper function to revoke an object URL + * + * @param {string} url Blob Object URL + * @param {object} [options] Options object + */ + function revokeHelper(url, options) { + if (url && url.slice(0, 5) === 'blob:' && !(options && options.noRevoke)) { + revokeObjectURL(url) + } + } + + /** + * Loads a given File object via FileReader interface. + * + * @param {Blob} file Blob object + * @param {Function} onload Load event callback + * @param {Function} [onerror] Error/Abort event callback + * @param {string} [method=readAsDataURL] FileReader method + * @returns {FileReader|boolean} Returns FileReader if API exists, else false. + */ + function readFile(file, onload, onerror, method) { + if (!$.FileReader) return false + var reader = new FileReader() + reader.onload = function () { + onload.call(reader, this.result) + } + if (onerror) { + reader.onabort = reader.onerror = function () { + onerror.call(reader, this.error) + } + } + var readerMethod = reader[method || 'readAsDataURL'] + if (readerMethod) { + readerMethod.call(reader, file) + return reader + } + } + + /** + * Cross-frame instanceof check. + * + * @param {string} type Instance type + * @param {object} obj Object instance + * @returns {boolean} Returns true if the object is of the given instance. + */ + function isInstanceOf(type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']' + } + + /** + * @typedef { HTMLImageElement|HTMLCanvasElement } Result + */ + + /** + * Loads an image for a given File object. + * + * @param {Blob|string} file Blob object or image URL + * @param {Function|object} [callback] Image load event callback or options + * @param {object} [options] Options object + * @returns {HTMLImageElement|FileReader|Promise<Result>} Object + */ + function loadImage(file, callback, options) { + /** + * Promise executor + * + * @param {Function} resolve Resolution function + * @param {Function} reject Rejection function + * @returns {HTMLImageElement|FileReader} Object + */ + function executor(resolve, reject) { + var img = document.createElement('img') + var url + /** + * Callback for the fetchBlob call. + * + * @param {HTMLImageElement|HTMLCanvasElement} img Error object + * @param {object} data Data object + * @returns {undefined} Undefined + */ + function resolveWrapper(img, data) { + if (resolve === reject) { + // Not using Promises + if (resolve) resolve(img, data) + return + } else if (img instanceof Error) { + reject(img) + return + } + data = data || {} // eslint-disable-line no-param-reassign + data.image = img + resolve(data) + } + /** + * Callback for the fetchBlob call. + * + * @param {Blob} blob Blob object + * @param {Error} err Error object + */ + function fetchBlobCallback(blob, err) { + if (err && $.console) console.log(err) // eslint-disable-line no-console + if (blob && isInstanceOf('Blob', blob)) { + file = blob // eslint-disable-line no-param-reassign + url = createObjectURL(file) + } else { + url = file + if (options && options.crossOrigin) { + img.crossOrigin = options.crossOrigin + } + } + img.src = url + } + img.onerror = function (event) { + revokeHelper(url, options) + if (reject) reject.call(img, event) + } + img.onload = function () { + revokeHelper(url, options) + var data = { + originalWidth: img.naturalWidth || img.width, + originalHeight: img.naturalHeight || img.height + } + try { + loadImage.transform(img, options, resolveWrapper, file, data) + } catch (error) { + if (reject) reject(error) + } + } + if (typeof file === 'string') { + if (loadImage.requiresMetaData(options)) { + loadImage.fetchBlob(file, fetchBlobCallback, options) + } else { + fetchBlobCallback() + } + return img + } else if (isInstanceOf('Blob', file) || isInstanceOf('File', file)) { + url = createObjectURL(file) + if (url) { + img.src = url + return img + } + return readFile( + file, + function (url) { + img.src = url + }, + reject + ) + } + } + if ($.Promise && typeof callback !== 'function') { + options = callback // eslint-disable-line no-param-reassign + return new Promise(executor) + } + return executor(callback, callback) + } + + // Determines if metadata should be loaded automatically. + // Requires the load image meta extension to load metadata. + loadImage.requiresMetaData = function (options) { + return options && options.meta + } + + // If the callback given to this function returns a blob, it is used as image + // source instead of the original url and overrides the file argument used in + // the onload and onerror event callbacks: + loadImage.fetchBlob = function (url, callback) { + callback() + } + + loadImage.transform = function (img, options, callback, file, data) { + callback(img, data) + } + + loadImage.global = $ + loadImage.readFile = readFile + loadImage.isInstanceOf = isInstanceOf + loadImage.createObjectURL = createObjectURL + loadImage.revokeObjectURL = revokeObjectURL + + if (typeof define === 'function' && define.amd) { + define(function () { + return loadImage + }) + } else if (typeof module === 'object' && module.exports) { + module.exports = loadImage + } else { + $.loadImage = loadImage + } +})((typeof window !== 'undefined' && window) || this) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/LICENSE.txt b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/LICENSE.txt new file mode 100644 index 0000000000000..d6a9d74758be3 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/LICENSE.txt @@ -0,0 +1,20 @@ +MIT License + +Copyright © 2011 Sebastian Tschan, https://blueimp.net + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/README.md b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/README.md new file mode 100644 index 0000000000000..d8281b237ca1f --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/README.md @@ -0,0 +1,436 @@ +# JavaScript Templates + +## Contents + +- [Demo](https://blueimp.github.io/JavaScript-Templates/) +- [Description](#description) +- [Usage](#usage) + - [Client-side](#client-side) + - [Server-side](#server-side) +- [Requirements](#requirements) +- [API](#api) + - [tmpl() function](#tmpl-function) + - [Templates cache](#templates-cache) + - [Output encoding](#output-encoding) + - [Local helper variables](#local-helper-variables) + - [Template function argument](#template-function-argument) + - [Template parsing](#template-parsing) +- [Templates syntax](#templates-syntax) + - [Interpolation](#interpolation) + - [Evaluation](#evaluation) +- [Compiled templates](#compiled-templates) +- [Tests](#tests) +- [License](#license) + +## Description + +1KB lightweight, fast & powerful JavaScript templating engine with zero +dependencies. +Compatible with server-side environments like [Node.js](https://nodejs.org/), +module loaders like [RequireJS](https://requirejs.org/) or +[webpack](https://webpack.js.org/) and all web browsers. + +## Usage + +### Client-side + +Install the **blueimp-tmpl** package with [NPM](https://www.npmjs.org/): + +```sh +npm install blueimp-tmpl +``` + +Include the (minified) JavaScript Templates script in your HTML markup: + +```html +<script src="js/tmpl.min.js"></script> +``` + +Add a script section with type **"text/x-tmpl"**, a unique **id** property and +your template definition as content: + +```html +<script type="text/x-tmpl" id="tmpl-demo"> + <h3>{%=o.title%}</h3> + <p>Released under the + <a href="{%=o.license.url%}">{%=o.license.name%}</a>.</p> + <h4>Features</h4> + <ul> + {% for (var i=0; i<o.features.length; i++) { %} + <li>{%=o.features[i]%}</li> + {% } %} + </ul> +</script> +``` + +**"o"** (the lowercase letter) is a reference to the data parameter of the +template function (see the API section on how to modify this identifier). + +In your application code, create a JavaScript object to use as data for the +template: + +```js +var data = { + title: 'JavaScript Templates', + license: { + name: 'MIT license', + url: 'https://opensource.org/licenses/MIT' + }, + features: ['lightweight & fast', 'powerful', 'zero dependencies'] +} +``` + +In a real application, this data could be the result of retrieving a +[JSON](https://json.org/) resource. + +Render the result by calling the **tmpl()** method with the id of the template +and the data object as arguments: + +```js +document.getElementById('result').innerHTML = tmpl('tmpl-demo', data) +``` + +### Server-side + +The following is an example how to use the JavaScript Templates engine on the +server-side with [Node.js](https://nodejs.org/). + +Install the **blueimp-tmpl** package with [NPM](https://www.npmjs.org/): + +```sh +npm install blueimp-tmpl +``` + +Add a file **template.html** with the following content: + +```html +<!DOCTYPE HTML> +<title>{%=o.title%} +

{%=o.title%}

+

Features

+
    +{% for (var i=0; i{%=o.features[i]%} +{% } %} +
+``` + +Add a file **server.js** with the following content: + +```js +require('http') + .createServer(function (req, res) { + var fs = require('fs'), + // The tmpl module exports the tmpl() function: + tmpl = require('./tmpl'), + // Use the following version if you installed the package with npm: + // tmpl = require("blueimp-tmpl"), + // Sample data: + data = { + title: 'JavaScript Templates', + url: 'https://github.com/blueimp/JavaScript-Templates', + features: ['lightweight & fast', 'powerful', 'zero dependencies'] + } + // Override the template loading method: + tmpl.load = function (id) { + var filename = id + '.html' + console.log('Loading ' + filename) + return fs.readFileSync(filename, 'utf8') + } + res.writeHead(200, { 'Content-Type': 'text/x-tmpl' }) + // Render the content: + res.end(tmpl('template', data)) + }) + .listen(8080, 'localhost') +console.log('Server running at http://localhost:8080/') +``` + +Run the application with the following command: + +```sh +node server.js +``` + +## Requirements + +The JavaScript Templates script has zero dependencies. + +## API + +### tmpl() function + +The **tmpl()** function is added to the global **window** object and can be +called as global function: + +```js +var result = tmpl('tmpl-demo', data) +``` + +The **tmpl()** function can be called with the id of a template, or with a +template string: + +```js +var result = tmpl('

{%=o.title%}

', data) +``` + +If called without second argument, **tmpl()** returns a reusable template +function: + +```js +var func = tmpl('

{%=o.title%}

') +document.getElementById('result').innerHTML = func(data) +``` + +### Templates cache + +Templates loaded by id are cached in the map **tmpl.cache**: + +```js +var func = tmpl('tmpl-demo'), // Loads and parses the template + cached = typeof tmpl.cache['tmpl-demo'] === 'function', // true + result = tmpl('tmpl-demo', data) // Uses cached template function + +tmpl.cache['tmpl-demo'] = null +result = tmpl('tmpl-demo', data) // Loads and parses the template again +``` + +### Output encoding + +The method **tmpl.encode** is used to escape HTML special characters in the +template output: + +```js +var output = tmpl.encode('<>&"\'\x00') // Renders "<>&"'" +``` + +**tmpl.encode** makes use of the regular expression **tmpl.encReg** and the +encoding map **tmpl.encMap** to match and replace special characters, which can +be modified to change the behavior of the output encoding. +Strings matched by the regular expression, but not found in the encoding map are +removed from the output. This allows for example to automatically trim input +values (removing whitespace from the start and end of the string): + +```js +tmpl.encReg = /(^\s+)|(\s+$)|[<>&"'\x00]/g +var output = tmpl.encode(' Banana! ') // Renders "Banana" (without whitespace) +``` + +### Local helper variables + +The local variables available inside the templates are the following: + +- **o**: The data object given as parameter to the template function (see the + next section on how to modify the parameter name). +- **tmpl**: A reference to the **tmpl** function object. +- **\_s**: The string for the rendered result content. +- **\_e**: A reference to the **tmpl.encode** method. +- **print**: Helper function to add content to the rendered result string. +- **include**: Helper function to include the return value of a different + template in the result. + +To introduce additional local helper variables, the string **tmpl.helper** can +be extended. The following adds a convenience function for _console.log_ and a +streaming function, that streams the template rendering result back to the +callback argument (note the comma at the beginning of each variable +declaration): + +```js +tmpl.helper += + ',log=function(){console.log.apply(console, arguments)}' + + ",st='',stream=function(cb){var l=st.length;st=_s;cb( _s.slice(l));}" +``` + +Those new helper functions could be used to stream the template contents to the +console output: + +```html + +``` + +### Template function argument + +The generated template functions accept one argument, which is the data object +given to the **tmpl(id, data)** function. This argument is available inside the +template definitions as parameter **o** (the lowercase letter). + +The argument name can be modified by overriding **tmpl.arg**: + +```js +tmpl.arg = 'p' + +// Renders "

JavaScript Templates

": +var result = tmpl('

{%=p.title%}

', { title: 'JavaScript Templates' }) +``` + +### Template parsing + +The template contents are matched and replaced using the regular expression +**tmpl.regexp** and the replacement function **tmpl.func**. The replacement +function operates based on the +[parenthesized submatch strings](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_function_as_a_parameter). + +To use different tags for the template syntax, override **tmpl.regexp** with a +modified regular expression, by exchanging all occurrences of "{%" and "%}", +e.g. with "[%" and "%]": + +```js +tmpl.regexp = /([\s'\\])(?!(?:[^[]|\[(?!%))*%\])|(?:\[%(=|#)([\s\S]+?)%\])|(\[%)|(%\])/g +``` + +By default, the plugin preserves whitespace (newlines, carriage returns, tabs +and spaces). To strip unnecessary whitespace, you can override the **tmpl.func** +function, e.g. with the following code: + +```js +var originalFunc = tmpl.func +tmpl.func = function (s, p1, p2, p3, p4, p5, offset, str) { + if (p1 && /\s/.test(p1)) { + if ( + !offset || + /\s/.test(str.charAt(offset - 1)) || + /^\s+$/g.test(str.slice(offset)) + ) { + return '' + } + return ' ' + } + return originalFunc.apply(tmpl, arguments) +} +``` + +## Templates syntax + +### Interpolation + +Print variable with HTML special characters escaped: + +```html +

{%=o.title%}

+``` + +Print variable without escaping: + +```html +

{%#o.user_id%}

+``` + +Print output of function calls: + +```html +Website +``` + +Use dot notation to print nested properties: + +```html +{%=o.author.name%} +``` + +### Evaluation + +Use **print(str)** to add escaped content to the output: + +```html +Year: {% var d=new Date(); print(d.getFullYear()); %} +``` + +Use **print(str, true)** to add unescaped content to the output: + +```html +{% print("Fast & powerful", true); %} +``` + +Use **include(str, obj)** to include content from a different template: + +```html +
+ {% include('tmpl-link', {name: "Website", url: "https://example.org"}); %} +
+``` + +**If else condition**: + +```html +{% if (o.author.url) { %} +{%=o.author.name%} +{% } else { %} +No author url. +{% } %} +``` + +**For loop**: + +```html +
    +{% for (var i=0; i{%=o.features[i]%} +{% } %} +
+``` + +## Compiled templates + +The JavaScript Templates project comes with a compilation script, that allows +you to compile your templates into JavaScript code and combine them with a +minimal Templates runtime into one combined JavaScript file. + +The compilation script is built for [Node.js](https://nodejs.org/). +To use it, first install the JavaScript Templates project via +[NPM](https://www.npmjs.org/): + +```sh +npm install blueimp-tmpl +``` + +This will put the executable **tmpl.js** into the folder **node_modules/.bin**. +It will also make it available on your PATH if you install the package globally +(by adding the **-g** flag to the install command). + +The **tmpl.js** executable accepts the paths to one or multiple template files +as command line arguments and prints the generated JavaScript code to the +console output. The following command line shows you how to store the generated +code in a new JavaScript file that can be included in your project: + +```sh +tmpl.js index.html > tmpl.js +``` + +The files given as command line arguments to **tmpl.js** can either be pure +template files or HTML documents with embedded template script sections. For the +pure template files, the file names (without extension) serve as template ids. +The generated file can be included in your project as a replacement for the +original **tmpl.js** runtime. It provides you with the same API and provides a +**tmpl(id, data)** function that accepts the id of one of your templates as +first and a data object as optional second parameter. + +## Tests + +The JavaScript Templates project comes with +[Unit Tests](https://en.wikipedia.org/wiki/Unit_testing). +There are two different ways to run the tests: + +- Open test/index.html in your browser or +- run `npm test` in the Terminal in the root path of the repository package. + +The first one tests the browser integration, the second one the +[Node.js](https://nodejs.org/) integration. + +## License + +The JavaScript Templates script is released under the +[MIT license](https://opensource.org/licenses/MIT). diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/compile.js b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/compile.js new file mode 100755 index 0000000000000..122d034eaa8ea --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/compile.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node +/* + * JavaScript Templates Compiler + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* eslint-disable strict */ +/* eslint-disable no-console */ + +;(function () { + 'use strict' + var path = require('path') + var tmpl = require(path.join(__dirname, 'tmpl.js')) + var fs = require('fs') + // Retrieve the content of the minimal runtime: + var runtime = fs.readFileSync(path.join(__dirname, 'runtime.js'), 'utf8') + // A regular expression to parse templates from script tags in a HTML page: + var regexp = /([\s\S]+?)<\/script>/gi + // A regular expression to match the helper function names: + var helperRegexp = new RegExp( + tmpl.helper.match(/\w+(?=\s*=\s*function\s*\()/g).join('\\s*\\(|') + + '\\s*\\(' + ) + // A list to store the function bodies: + var list = [] + var code + // Extend the Templating engine with a print method for the generated functions: + tmpl.print = function (str) { + // Only add helper functions if they are used inside of the template: + var helper = helperRegexp.test(str) ? tmpl.helper : '' + var body = str.replace(tmpl.regexp, tmpl.func) + if (helper || /_e\s*\(/.test(body)) { + helper = '_e=tmpl.encode' + helper + ',' + } + return ( + 'function(' + + tmpl.arg + + ',tmpl){' + + ('var ' + helper + "_s='" + body + "';return _s;") + .split("_s+='';") + .join('') + + '}' + ) + } + // Loop through the command line arguments: + process.argv.forEach(function (file, index) { + var listLength = list.length + var stats + var content + var result + var id + // Skip the first two arguments, which are "node" and the script: + if (index > 1) { + stats = fs.statSync(file) + if (!stats.isFile()) { + console.error(file + ' is not a file.') + return + } + content = fs.readFileSync(file, 'utf8') + // eslint-disable-next-line no-constant-condition + while (true) { + // Find templates in script tags: + result = regexp.exec(content) + if (!result) { + break + } + id = result[2] || result[4] + list.push("'" + id + "':" + tmpl.print(result[5])) + } + if (listLength === list.length) { + // No template script tags found, use the complete content: + id = path.basename(file, path.extname(file)) + list.push("'" + id + "':" + tmpl.print(content)) + } + } + }) + if (!list.length) { + console.error('Missing input file.') + return + } + // Combine the generated functions as cache of the minimal runtime: + code = runtime.replace('{}', '{' + list.join(',') + '}') + // Print the resulting code to the console output: + console.log(code) +})() diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/runtime.js b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/runtime.js new file mode 100644 index 0000000000000..1a3a716c51bc0 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/runtime.js @@ -0,0 +1,50 @@ +/* + * JavaScript Templates Runtime + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +/* global define */ + +/* eslint-disable strict */ + +;(function ($) { + 'use strict' + var tmpl = function (id, data) { + var f = tmpl.cache[id] + return data + ? f(data, tmpl) + : function (data) { + return f(data, tmpl) + } + } + tmpl.cache = {} + tmpl.encReg = /[<>&"'\x00]/g // eslint-disable-line no-control-regex + tmpl.encMap = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' + } + tmpl.encode = function (s) { + // eslint-disable-next-line eqeqeq + return (s == null ? '' : '' + s).replace(tmpl.encReg, function (c) { + return tmpl.encMap[c] || '' + }) + } + if (typeof define === 'function' && define.amd) { + define(function () { + return tmpl + }) + } else if (typeof module === 'object' && module.exports) { + module.exports = tmpl + } else { + $.tmpl = tmpl + } +})(this) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.js b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.js new file mode 100644 index 0000000000000..63eb927cb0d4d --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.js @@ -0,0 +1,98 @@ +/* + * JavaScript Templates + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Inspired by John Resig's JavaScript Micro-Templating: + * http://ejohn.org/blog/javascript-micro-templating/ + */ + +/* global define */ + +/* eslint-disable strict */ + +;(function ($) { + 'use strict' + var tmpl = function (str, data) { + var f = !/[^\w\-.:]/.test(str) + ? (tmpl.cache[str] = tmpl.cache[str] || tmpl(tmpl.load(str))) + : new Function( // eslint-disable-line no-new-func + tmpl.arg + ',tmpl', + 'var _e=tmpl.encode' + + tmpl.helper + + ",_s='" + + str.replace(tmpl.regexp, tmpl.func) + + "';return _s;" + ) + return data + ? f(data, tmpl) + : function (data) { + return f(data, tmpl) + } + } + tmpl.cache = {} + tmpl.load = function (id) { + return document.getElementById(id).innerHTML + } + tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g + tmpl.func = function (s, p1, p2, p3, p4, p5) { + if (p1) { + // whitespace, quote and backspace in HTML context + return ( + { + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + ' ': ' ' + }[p1] || '\\' + p1 + ) + } + if (p2) { + // interpolation: {%=prop%}, or unescaped: {%#prop%} + if (p2 === '=') { + return "'+_e(" + p3 + ")+'" + } + return "'+(" + p3 + "==null?'':" + p3 + ")+'" + } + if (p4) { + // evaluation start tag: {% + return "';" + } + if (p5) { + // evaluation end tag: %} + return "_s+='" + } + } + tmpl.encReg = /[<>&"'\x00]/g // eslint-disable-line no-control-regex + tmpl.encMap = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' + } + tmpl.encode = function (s) { + // eslint-disable-next-line eqeqeq + return (s == null ? '' : '' + s).replace(tmpl.encReg, function (c) { + return tmpl.encMap[c] || '' + }) + } + tmpl.arg = 'o' + tmpl.helper = + ",print=function(s,e){_s+=e?(s==null?'':s):_e(s);}" + + ',include=function(s,d){_s+=tmpl(s,d);}' + if (typeof define === 'function' && define.amd) { + define(function () { + return tmpl + }) + } else if (typeof module === 'object' && module.exports) { + module.exports = tmpl + } else { + $.tmpl = tmpl + } +})(this) diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js new file mode 100644 index 0000000000000..f8fec2a542ef5 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js @@ -0,0 +1,2 @@ +!function(e){"use strict";var r=function(e,n){var t=/[^\w\-.:]/.test(e)?new Function(r.arg+",tmpl","var _e=tmpl.encode"+r.helper+",_s='"+e.replace(r.regexp,r.func)+"';return _s;"):r.cache[e]=r.cache[e]||r(r.load(e));return n?t(n,r):function(e){return t(e,r)}};r.cache={},r.load=function(e){return document.getElementById(e).innerHTML},r.regexp=/([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g,r.func=function(e,n,t,r,c,u){return n?{"\n":"\\n","\r":"\\r","\t":"\\t"," ":" "}[n]||"\\"+n:t?"="===t?"'+_e("+r+")+'":"'+("+r+"==null?'':"+r+")+'":c?"';":u?"_s+='":void 0},r.encReg=/[<>&"'\x00]/g,r.encMap={"<":"<",">":">","&":"&",'"':""","'":"'"},r.encode=function(e){return(null==e?"":""+e).replace(r.encReg,function(e){return r.encMap[e]||""})},r.arg="o",r.helper=",print=function(s,e){_s+=e?(s==null?'':s):_e(s);},include=function(s,d){_s+=tmpl(s,d);}","function"==typeof define&&define.amd?define(function(){return r}):"object"==typeof module&&module.exports?module.exports=r:e.tmpl=r}(this); +//# sourceMappingURL=tmpl.min.js.map \ No newline at end of file diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map new file mode 100644 index 0000000000000..1c55780228b23 --- /dev/null +++ b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["tmpl.js"],"names":["$","tmpl","str","data","f","test","Function","arg","helper","replace","regexp","func","cache","load","id","document","getElementById","innerHTML","s","p1","p2","p3","p4","p5","\n","\r","\t"," ","encReg","encMap","<",">","&","\"","'","encode","c","define","amd","module","exports","this"],"mappings":"CAkBC,SAAWA,gBAEV,IAAIC,EAAO,SAAUC,EAAKC,GACxB,IAAIC,EAAK,YAAYC,KAAKH,GAEtB,IAAII,SACFL,EAAKM,IAAM,QACX,qBACEN,EAAKO,OACL,QACAN,EAAIO,QAAQR,EAAKS,OAAQT,EAAKU,MAC9B,gBAPHV,EAAKW,MAAMV,GAAOD,EAAKW,MAAMV,IAAQD,EAAKA,EAAKY,KAAKX,IASzD,OAAOC,EACHC,EAAED,EAAMF,GACR,SAAUE,GACR,OAAOC,EAAED,EAAMF,KAGvBA,EAAKW,MAAQ,GACbX,EAAKY,KAAO,SAAUC,GACpB,OAAOC,SAASC,eAAeF,GAAIG,WAErChB,EAAKS,OAAS,2EACdT,EAAKU,KAAO,SAAUO,EAAGC,EAAIC,EAAIC,EAAIC,EAAIC,GACvC,OAAIJ,EAGA,CACEK,KAAM,MACNC,KAAM,MACNC,KAAM,MACNC,IAAK,KACLR,IAAO,KAAOA,EAGhBC,EAES,MAAPA,EACK,QAAUC,EAAK,MAEjB,MAAQA,EAAK,aAAeA,EAAK,MAEtCC,EAEK,KAELC,EAEK,aAFT,GAKFtB,EAAK2B,OAAS,eACd3B,EAAK4B,OAAS,CACZC,IAAK,OACLC,IAAK,OACLC,IAAK,QACLC,IAAK,SACLC,IAAK,SAEPjC,EAAKkC,OAAS,SAAUjB,GAEtB,OAAa,MAALA,EAAY,GAAK,GAAKA,GAAGT,QAAQR,EAAK2B,OAAQ,SAAUQ,GAC9D,OAAOnC,EAAK4B,OAAOO,IAAM,MAG7BnC,EAAKM,IAAM,IACXN,EAAKO,OACH,0FAEoB,mBAAX6B,QAAyBA,OAAOC,IACzCD,OAAO,WACL,OAAOpC,IAEkB,iBAAXsC,QAAuBA,OAAOC,QAC9CD,OAAOC,QAAUvC,EAEjBD,EAAEC,KAAOA,EA7EZ,CA+EEwC"} \ No newline at end of file From 1acef34f46af395e2ab868d05c50ed7c5fe37009 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Wed, 10 Jun 2020 21:19:45 -0500 Subject: [PATCH 0073/1013] MC-34467: Updated jQuery File Upload plugin - Updated file-uploader dependency - Fixed static test failure --- .../view/adminhtml/web/js/new-video-dialog.js | 7 ++++++- lib/web/jquery/fileUploader/jquery.fileupload-ui.js | 8 +++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js index 9cc731dde4b0c..066cfb88ad959 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js @@ -580,7 +580,12 @@ define([ * @private */ _onImageLoaded: function (result, file, oldFile, callback) { - var data = JSON.parse(result); + var data; + try { + data = JSON.parse(result); + } catch (e) { + data = result; + } if (this.element.find('#video_url').parent().find('.image-upload-error').length > 0) { this.element.find('.image-upload-error').remove(); diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js index caacf95c507bb..8d56075e2ee15 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js @@ -21,7 +21,8 @@ 'jquery/fileUploader/jquery.fileupload-image', 'jquery/fileUploader/jquery.fileupload-audio', 'jquery/fileUploader/jquery.fileupload-video', - 'jquery/fileUploader/jquery.fileupload-validate' + 'jquery/fileUploader/jquery.fileupload-validate', + 'jquery/fileUploader/jquery.iframe-transport', ], factory); } else if (typeof exports === 'object') { // Node/CommonJS: @@ -31,7 +32,8 @@ require('jquery/fileUploader/jquery.fileupload-image'), require('jquery/fileUploader/jquery.fileupload-audio'), require('jquery/fileUploader/jquery.fileupload-video'), - require('jquery/fileUploader/jquery.fileupload-validate') + require('jquery/fileUploader/jquery.fileupload-validate'), + require('jquery/fileUploader/jquery.iframe-transport') ); } else { // Browser globals: @@ -725,7 +727,7 @@ _initSpecialOptions: function () { this._super(); this._initFilesContainer(); - this._initTemplates(); + // this._initTemplates(); }, _create: function () { From 810fcb5d3d2d7e15f9200925a1de179e6651561b Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Wed, 10 Jun 2020 23:22:28 -0500 Subject: [PATCH 0074/1013] MC-34467: Updated jQuery File Upload plugin - Updated file-uploader dependency - Fixed static test failure --- .../ProductVideo/view/adminhtml/web/js/new-video-dialog.js | 1 + app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js | 2 +- app/code/Magento/Theme/view/base/requirejs-config.js | 2 +- app/code/Magento/User/view/adminhtml/web/app-config.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js index 066cfb88ad959..562bff2e1d472 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js +++ b/app/code/Magento/ProductVideo/view/adminhtml/web/js/new-video-dialog.js @@ -581,6 +581,7 @@ define([ */ _onImageLoaded: function (result, file, oldFile, callback) { var data; + try { data = JSON.parse(result); } catch (e) { diff --git a/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js b/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js index 216d68b29e1fd..bbcb8d0efaaf9 100644 --- a/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js +++ b/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js @@ -5,7 +5,7 @@ */ require([ - 'jquery/fileUploader/jquery.fileupload-ui', + 'jquery/fileUploader/jquery.fileupload-image', 'mage/adminhtml/browser', 'Magento_Theme/js/form' ]); diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index cba1b3307407b..80a0eedacd0ea 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -31,7 +31,7 @@ var config = { 'paths': { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-ui', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-image', 'prototype': 'legacy-build.min', 'jquery/jquery-storageapi': 'jquery/jquery.storageapi.min', 'text': 'mage/requirejs/text', diff --git a/app/code/Magento/User/view/adminhtml/web/app-config.js b/app/code/Magento/User/view/adminhtml/web/app-config.js index f5c8cb9dd19c8..4eb16c587ef16 100644 --- a/app/code/Magento/User/view/adminhtml/web/app-config.js +++ b/app/code/Magento/User/view/adminhtml/web/app-config.js @@ -26,7 +26,7 @@ require.config({ 'jquery/ui': 'jquery/jquery-ui-1.9.2', 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-ui', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-image', 'prototype': 'prototype/prototype-amd', 'text': 'requirejs/text', 'domReady': 'requirejs/domReady', From 615f251b01f244d4b81ee30a53e62fa00a96ebec Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Wed, 10 Jun 2020 23:25:23 -0500 Subject: [PATCH 0075/1013] MC-34467: Updated jQuery File Upload plugin - Reverted incorrect bootstrap dependency --- app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js b/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js index bbcb8d0efaaf9..216d68b29e1fd 100644 --- a/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js +++ b/app/code/Magento/Theme/view/adminhtml/web/js/bootstrap.js @@ -5,7 +5,7 @@ */ require([ - 'jquery/fileUploader/jquery.fileupload-image', + 'jquery/fileUploader/jquery.fileupload-ui', 'mage/adminhtml/browser', 'Magento_Theme/js/form' ]); From a6d197634e84277ee375a5c9668395a18828b39e Mon Sep 17 00:00:00 2001 From: Barny Shergold Date: Thu, 11 Jun 2020 14:21:31 +0100 Subject: [PATCH 0076/1013] Correction from Code Sniffer --- .../CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index 693084b7aba18..02a00748442be 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -21,6 +21,9 @@ /** * Class ProductScopeRewriteGenerator + * + * Generates Product/Category URLs for different scopes + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductScopeRewriteGenerator @@ -235,7 +238,6 @@ public function isCategoryProperForGenerating(Category $category, $storeId) return false; } - /** * Check config value of generate_category_product_rewrites * From 0c4bf0216d485e2eb32a6bd5e704b41b287c78da Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Thu, 11 Jun 2020 13:02:19 -0500 Subject: [PATCH 0077/1013] MC-34467: Updated jQuery File Upload plugin - Updated customer fileuploader script - Updated media-uploader to correctly use the new format --- .../view/adminhtml/web/js/media-uploader.js | 12 +++---- .../Theme/view/base/requirejs-config.js | 2 +- .../User/view/adminhtml/web/app-config.js | 2 +- .../fileUploader/jquery.fileupload-ui.js | 6 ++-- .../fileUploader/jquery.fileuploader.js | 33 +++++++++++++++++++ 5 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 lib/web/jquery/fileUploader/jquery.fileuploader.js diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js index 119e7a35747cb..4667d550c2e44 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js @@ -37,14 +37,14 @@ define([ progressTmpl = mageTemplate('[data-template="uploader"]'), isResizeEnabled = this.options.isResizeEnabled, resizeConfiguration = { - action: 'resize', + action: 'resizeImage', maxWidth: this.options.maxWidth, maxHeight: this.options.maxHeight }; if (!isResizeEnabled) { resizeConfiguration = { - action: 'resize' + action: 'resizeImage', }; } @@ -131,13 +131,13 @@ define([ }); this.element.find('input[type=file]').fileupload('option', { - process: [{ - action: 'load', - fileTypes: /^image\/(gif|jpeg|png)$/ + processQueue: [{ + action: 'loadImage', + fileTypes: /^image\/(gif|jpeg|png)$/, }, resizeConfiguration, { - action: 'save' + action: 'saveImage', }] }); } diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index 80a0eedacd0ea..13fc1dc5882ba 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -31,7 +31,7 @@ var config = { 'paths': { 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-image', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileuploader', 'prototype': 'legacy-build.min', 'jquery/jquery-storageapi': 'jquery/jquery.storageapi.min', 'text': 'mage/requirejs/text', diff --git a/app/code/Magento/User/view/adminhtml/web/app-config.js b/app/code/Magento/User/view/adminhtml/web/app-config.js index 4eb16c587ef16..0567e95fcc6e9 100644 --- a/app/code/Magento/User/view/adminhtml/web/app-config.js +++ b/app/code/Magento/User/view/adminhtml/web/app-config.js @@ -26,7 +26,7 @@ require.config({ 'jquery/ui': 'jquery/jquery-ui-1.9.2', 'jquery/validate': 'jquery/jquery.validate', 'jquery/hover-intent': 'jquery/jquery.hoverIntent', - 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileupload-image', + 'jquery/file-uploader': 'jquery/fileUploader/jquery.fileuploader', 'prototype': 'prototype/prototype-amd', 'text': 'requirejs/text', 'domReady': 'requirejs/domReady', diff --git a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js index 8d56075e2ee15..a4665f8392fe0 100644 --- a/lib/web/jquery/fileUploader/jquery.fileupload-ui.js +++ b/lib/web/jquery/fileUploader/jquery.fileupload-ui.js @@ -21,8 +21,7 @@ 'jquery/fileUploader/jquery.fileupload-image', 'jquery/fileUploader/jquery.fileupload-audio', 'jquery/fileUploader/jquery.fileupload-video', - 'jquery/fileUploader/jquery.fileupload-validate', - 'jquery/fileUploader/jquery.iframe-transport', + 'jquery/fileUploader/jquery.fileupload-validate' ], factory); } else if (typeof exports === 'object') { // Node/CommonJS: @@ -32,8 +31,7 @@ require('jquery/fileUploader/jquery.fileupload-image'), require('jquery/fileUploader/jquery.fileupload-audio'), require('jquery/fileUploader/jquery.fileupload-video'), - require('jquery/fileUploader/jquery.fileupload-validate'), - require('jquery/fileUploader/jquery.iframe-transport') + require('jquery/fileUploader/jquery.fileupload-validate') ); } else { // Browser globals: diff --git a/lib/web/jquery/fileUploader/jquery.fileuploader.js b/lib/web/jquery/fileUploader/jquery.fileuploader.js new file mode 100644 index 0000000000000..4ec869ab4f470 --- /dev/null +++ b/lib/web/jquery/fileUploader/jquery.fileuploader.js @@ -0,0 +1,33 @@ +/** + * Custom Uploader + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global define, require */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery/fileUploader/jquery.fileupload-image', + 'jquery/fileUploader/jquery.fileupload-audio', + 'jquery/fileUploader/jquery.fileupload-video', + 'jquery/fileUploader/jquery.iframe-transport', + ], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS: + factory( + require('jquery'), + require('jquery/fileUploader/jquery.fileupload-image'), + require('jquery/fileUploader/jquery.fileupload-audio'), + require('jquery/fileUploader/jquery.fileupload-video'), + require('jquery/fileUploader/jquery.iframe-transport') + ); + } else { + // Browser globals: + factory(window.jQuery); + } +})(); From f3efb74485a7299566892ada3f4f0734f1228a36 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Thu, 11 Jun 2020 14:56:14 -0500 Subject: [PATCH 0078/1013] MC-34467: Updated jQuery File Upload plugin - Fixed static test failures --- .../Magento/Backend/view/adminhtml/web/js/media-uploader.js | 2 +- .../vendor/blueimp-load-image/js/load-image.all.min.js | 2 -- .../vendor/blueimp-load-image/js/load-image.all.min.js.map | 1 - lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js | 2 -- .../jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map | 1 - 5 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js delete mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js.map delete mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js delete mode 100644 lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js index 4667d550c2e44..11cb80501d9c8 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js @@ -44,7 +44,7 @@ define([ if (!isResizeEnabled) { resizeConfiguration = { - action: 'resizeImage', + action: 'resizeImage' }; } diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js b/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js deleted file mode 100644 index 8651c3489378a..0000000000000 --- a/lib/web/jquery/fileUploader/vendor/blueimp-load-image/js/load-image.all.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(c){"use strict";var t=c.URL||c.webkitURL;function f(e){return!!t&&t.createObjectURL(e)}function i(e){return!!t&&t.revokeObjectURL(e)}function u(e,t){!e||"blob:"!==e.slice(0,5)||t&&t.noRevoke||i(e)}function d(e,t,i,a){if(!c.FileReader)return!1;var n=new FileReader;n.onload=function(){t.call(n,this.result)},i&&(n.onabort=n.onerror=function(){i.call(n,this.error)});var r=n[a||"readAsDataURL"];return r?(r.call(n,e),n):void 0}function g(e,t){return Object.prototype.toString.call(t)==="[object "+e+"]"}function m(s,e,l){function t(i,a){var n,r=document.createElement("img");function o(e,t){i!==a?e instanceof Error?a(e):((t=t||{}).image=e,i(t)):i&&i(e,t)}function e(e,t){t&&c.console&&console.log(t),e&&g("Blob",e)?n=f(s=e):(n=s,l&&l.crossOrigin&&(r.crossOrigin=l.crossOrigin)),r.src=n}return r.onerror=function(e){u(n,l),a&&a.call(r,e)},r.onload=function(){u(n,l);var e={originalWidth:r.naturalWidth||r.width,originalHeight:r.naturalHeight||r.height};try{m.transform(r,l,o,s,e)}catch(t){a&&a(t)}},"string"==typeof s?(m.requiresMetaData(l)?m.fetchBlob(s,e,l):e(),r):g("Blob",s)||g("File",s)?(n=f(s))?(r.src=n,r):d(s,function(e){r.src=e},a):void 0}return c.Promise&&"function"!=typeof e?(l=e,new Promise(t)):t(e,e)}m.requiresMetaData=function(e){return e&&e.meta},m.fetchBlob=function(e,t){t()},m.transform=function(e,t,i,a,n){i(e,n)},m.global=c,m.readFile=d,m.isInstanceOf=g,m.createObjectURL=f,m.revokeObjectURL=i,"function"==typeof define&&define.amd?define(function(){return m}):"object"==typeof module&&module.exports?module.exports=m:c.loadImage=m}("undefined"!=typeof window&&window||this),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image"],e):"object"==typeof module&&module.exports?e(require("./load-image")):e(window.loadImage)}(function(E){"use strict";var r=E.transform;E.createCanvas=function(e,t,i){if(i&&E.global.OffscreenCanvas)return new OffscreenCanvas(e,t);var a=document.createElement("canvas");return a.width=e,a.height=t,a},E.transform=function(e,t,i,a,n){r.call(E,E.scale(e,t,n),t,i,a,n)},E.transformCoordinates=function(){},E.getTransformedOptions=function(e,t){var i,a,n,r,o=t.aspectRatio;if(!o)return t;for(a in i={},t)Object.prototype.hasOwnProperty.call(t,a)&&(i[a]=t[a]);return i.crop=!0,o<(n=e.naturalWidth||e.width)/(r=e.naturalHeight||e.height)?(i.maxWidth=r*o,i.maxHeight=r):(i.maxWidth=n,i.maxHeight=n/o),i},E.drawImage=function(e,t,i,a,n,r,o,s,l){var c=t.getContext("2d");return!1===l.imageSmoothingEnabled?(c.msImageSmoothingEnabled=!1,c.imageSmoothingEnabled=!1):l.imageSmoothingQuality&&(c.imageSmoothingQuality=l.imageSmoothingQuality),c.drawImage(e,i,a,n,r,0,0,o,s),c},E.requiresCanvas=function(e){return e.canvas||e.crop||!!e.aspectRatio},E.scale=function(e,t,i){t=t||{},i=i||{};var a,n,r,o,s,l,c,f,u,d,g,m,h=e.getContext||E.requiresCanvas(t)&&!!E.global.HTMLCanvasElement,p=e.naturalWidth||e.width,A=e.naturalHeight||e.height,b=p,y=A;function S(){var e=Math.max((r||b)/b,(o||y)/y);1t.byteLength){console.log("Invalid JPEG metadata: Invalid segment size.");break}if((n=h.jpeg[i])&&!u.disableMetaDataParsers)for(r=0;re.byteLength)console.log("Invalid Exif data: Invalid directory offset.");else{if(!((c=i+2+12*(l=e.getUint16(i,a)))+4>e.byteLength)){for(f=0;fe.byteLength)){if(1===n)return d.getValue(e,s,r);for(l=[],c=0;cc.byteLength)console.log("Invalid Exif data: Invalid segment size.");else if(0===c.getUint16(e+8)){switch(c.getUint16(m)){case 18761:u=!0;break;case 19789:u=!1;break;default:return void console.log("Invalid Exif data: Invalid byte alignment marker.")}42===c.getUint16(m+2,u)?(a=c.getUint32(m+4,u),f.exif=new h,i.disableExifOffsets||(f.exifOffsets=new h,f.exifTiffOffset=m,f.exifLittleEndian=u),(a=A(c,m,m+a,u,f.exif,f.exifOffsets,d,g))&&p(d,g,"ifd1")&&(f.exif.ifd1=a,f.exifOffsets&&(f.exifOffsets.ifd1=m+a)),Object.keys(f.exif.ifds).forEach(function(e){var t,i,a,n,r,o,s,l;i=e,a=c,n=m,r=u,o=d,s=g,(l=(t=f).exif[i])&&(t.exif[i]=new h(i),t.exifOffsets&&(t.exifOffsets[i]=new h(i)),A(a,n,n+l,r,t.exif[i],t.exifOffsets&&t.exifOffsets[i],o&&o[i],s&&s[i]))}),(n=f.exif.ifd1)&&n[513]&&(n[513]=function(e,t,i){if(i){if(!(t+i>e.byteLength))return new Blob([r.bufferSlice.call(e.buffer,t,t+i)],{type:"image/jpeg"});console.log("Invalid Exif data: Invalid thumbnail data.")}}(c,m+n[513],n[514]))):console.log("Invalid Exif data: Missing TIFF marker.")}else console.log("Invalid Exif data: Missing byte alignment offset.")}},r.metaDataParsers.jpeg[65505].push(r.parseExifData),r.exifWriters={274:function(e,t,i){var a=t.exifOffsets[274];return a&&new DataView(e,a+8,2).setUint16(0,i,t.exifLittleEndian),e}},r.writeExifData=function(e,t,i,a){r.exifWriters[t.exif.map[i]](e,t,a)},r.ExifMap=h}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-exif"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-exif")):e(window.loadImage)}(function(e){"use strict";var n=e.ExifMap.prototype;n.tags={256:"ImageWidth",257:"ImageHeight",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright",34665:{36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",42240:"Gamma",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",36880:"OffsetTime",36881:"OffsetTimeOriginal",36882:"OffsetTimeDigitized",37520:"SubSecTime",37521:"SubSecTimeOriginal",37522:"SubSecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"PhotographicSensitivity",34856:"OECF",34864:"SensitivityType",34865:"StandardOutputSensitivity",34866:"RecommendedExposureIndex",34867:"ISOSpeed",34868:"ISOSpeedLatitudeyyy",34869:"ISOSpeedLatitudezzz",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRatio",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",42016:"ImageUniqueID",42032:"CameraOwnerName",42033:"BodySerialNumber",42034:"LensSpecification",42035:"LensMake",42036:"LensModel",42037:"LensSerialNumber"},34853:{0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential",31:"GPSHPositioningError"},40965:{1:"InteroperabilityIndex"}},n.tags.ifd1=n.tags,n.stringValues={ExposureProgram:{0:"Undefined",1:"Manual",2:"Normal program",3:"Aperture priority",4:"Shutter priority",5:"Creative program",6:"Action program",7:"Portrait mode",8:"Landscape mode"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{0:"Unknown",1:"Daylight",2:"Fluorescent",3:"Tungsten (incandescent light)",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 - 5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Undefined",2:"One-chip color area sensor",3:"Two-chip color area sensor",4:"Three-chip color area sensor",5:"Color sequential area sensor",7:"Trilinear sensor",8:"Color sequential linear sensor"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},SceneType:{1:"Directly photographed"},CustomRendered:{0:"Normal process",1:"Custom process"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},GainControl:{0:"None",1:"Low gain up",2:"High gain up",3:"Low gain down",4:"High gain down"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},SubjectDistanceRange:{0:"Unknown",1:"Macro",2:"Close view",3:"Distant view"},FileSource:{3:"DSC"},ComponentsConfiguration:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"},Orientation:{1:"Original",2:"Horizontal flip",3:"Rotate 180° CCW",4:"Vertical flip",5:"Vertical flip + Rotate 90° CW",6:"Rotate 90° CW",7:"Horizontal flip + Rotate 90° CW",8:"Rotate 90° CCW"}},n.getText=function(e){var t=this.get(e);switch(e){case"LightSource":case"Flash":case"MeteringMode":case"ExposureProgram":case"SensingMethod":case"SceneCaptureType":case"SceneType":case"CustomRendered":case"WhiteBalance":case"GainControl":case"Contrast":case"Saturation":case"Sharpness":case"SubjectDistanceRange":case"FileSource":case"Orientation":return this.stringValues[e][t];case"ExifVersion":case"FlashpixVersion":if(!t)return;return String.fromCharCode(t[0],t[1],t[2],t[3]);case"ComponentsConfiguration":if(!t)return;return this.stringValues[e][t[0]]+this.stringValues[e][t[1]]+this.stringValues[e][t[2]]+this.stringValues[e][t[3]];case"GPSVersionID":if(!t)return;return t[0]+"."+t[1]+"."+t[2]+"."+t[3]}return String(t)},n.getAll=function(){var e,t,i,a={};for(e in this)Object.prototype.hasOwnProperty.call(this,e)&&((t=this[e])&&t.getAll?a[this.ifds[e].name]=t.getAll():(i=this.tags[e])&&(a[i]=this.getText(i)));return a},n.getName=function(e){var t=this.tags[e];return"object"==typeof t?this.ifds[e].name:t},function(){var e,t,i,a=n.tags;for(e in a)if(Object.prototype.hasOwnProperty.call(a,e))if(t=n.ifds[e])for(e in i=a[e])Object.prototype.hasOwnProperty.call(i,e)&&(t.map[i[e]]=Number(e));else n.map[a[e]]=Number(e)}()}),function(e){"use strict";"function"==typeof define&&define.amd?define(["./load-image","./load-image-meta"],e):"object"==typeof module&&module.exports?e(require("./load-image"),require("./load-image-meta")):e(window.loadImage)}(function(e){"use strict";function g(){}function m(e,t,i,a,n){return"binary"===t.types[e]?new Blob([i.buffer.slice(a,a+n)]):"Uint16"===t.types[e]?i.getUint16(a):function(e,t,i){for(var a="",n=t+i,r=t;r&"'\x00]/g,r.encMap={"<":"<",">":">","&":"&",'"':""","'":"'"},r.encode=function(e){return(null==e?"":""+e).replace(r.encReg,function(e){return r.encMap[e]||""})},r.arg="o",r.helper=",print=function(s,e){_s+=e?(s==null?'':s):_e(s);},include=function(s,d){_s+=tmpl(s,d);}","function"==typeof define&&define.amd?define(function(){return r}):"object"==typeof module&&module.exports?module.exports=r:e.tmpl=r}(this); -//# sourceMappingURL=tmpl.min.js.map \ No newline at end of file diff --git a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map b/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map deleted file mode 100644 index 1c55780228b23..0000000000000 --- a/lib/web/jquery/fileUploader/vendor/blueimp-tmpl/js/tmpl.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["tmpl.js"],"names":["$","tmpl","str","data","f","test","Function","arg","helper","replace","regexp","func","cache","load","id","document","getElementById","innerHTML","s","p1","p2","p3","p4","p5","\n","\r","\t"," ","encReg","encMap","<",">","&","\"","'","encode","c","define","amd","module","exports","this"],"mappings":"CAkBC,SAAWA,gBAEV,IAAIC,EAAO,SAAUC,EAAKC,GACxB,IAAIC,EAAK,YAAYC,KAAKH,GAEtB,IAAII,SACFL,EAAKM,IAAM,QACX,qBACEN,EAAKO,OACL,QACAN,EAAIO,QAAQR,EAAKS,OAAQT,EAAKU,MAC9B,gBAPHV,EAAKW,MAAMV,GAAOD,EAAKW,MAAMV,IAAQD,EAAKA,EAAKY,KAAKX,IASzD,OAAOC,EACHC,EAAED,EAAMF,GACR,SAAUE,GACR,OAAOC,EAAED,EAAMF,KAGvBA,EAAKW,MAAQ,GACbX,EAAKY,KAAO,SAAUC,GACpB,OAAOC,SAASC,eAAeF,GAAIG,WAErChB,EAAKS,OAAS,2EACdT,EAAKU,KAAO,SAAUO,EAAGC,EAAIC,EAAIC,EAAIC,EAAIC,GACvC,OAAIJ,EAGA,CACEK,KAAM,MACNC,KAAM,MACNC,KAAM,MACNC,IAAK,KACLR,IAAO,KAAOA,EAGhBC,EAES,MAAPA,EACK,QAAUC,EAAK,MAEjB,MAAQA,EAAK,aAAeA,EAAK,MAEtCC,EAEK,KAELC,EAEK,aAFT,GAKFtB,EAAK2B,OAAS,eACd3B,EAAK4B,OAAS,CACZC,IAAK,OACLC,IAAK,OACLC,IAAK,QACLC,IAAK,SACLC,IAAK,SAEPjC,EAAKkC,OAAS,SAAUjB,GAEtB,OAAa,MAALA,EAAY,GAAK,GAAKA,GAAGT,QAAQR,EAAK2B,OAAQ,SAAUQ,GAC9D,OAAOnC,EAAK4B,OAAOO,IAAM,MAG7BnC,EAAKM,IAAM,IACXN,EAAKO,OACH,0FAEoB,mBAAX6B,QAAyBA,OAAOC,IACzCD,OAAO,WACL,OAAOpC,IAEkB,iBAAXsC,QAAuBA,OAAOC,QAC9CD,OAAOC,QAAUvC,EAEjBD,EAAEC,KAAOA,EA7EZ,CA+EEwC"} \ No newline at end of file From 1bfe3563de41541094fd62dfc0cba743d5e29c3f Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Thu, 11 Jun 2020 16:47:20 -0500 Subject: [PATCH 0079/1013] MC-34467: Updated jQuery File Upload plugin - Fixed static test failures --- .../Magento/Backend/view/adminhtml/web/js/media-uploader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js index 11cb80501d9c8..c22c1788cdd29 100644 --- a/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js +++ b/app/code/Magento/Backend/view/adminhtml/web/js/media-uploader.js @@ -133,11 +133,11 @@ define([ this.element.find('input[type=file]').fileupload('option', { processQueue: [{ action: 'loadImage', - fileTypes: /^image\/(gif|jpeg|png)$/, + fileTypes: /^image\/(gif|jpeg|png)$/ }, resizeConfiguration, { - action: 'saveImage', + action: 'saveImage' }] }); } From 7e9ce4123551d95744aae702c3c119a7aba85336 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 12 Jun 2020 15:21:10 +0300 Subject: [PATCH 0080/1013] MC-34197: Admin user improvement --- app/code/Magento/Authorization/Model/Role.php | 7 +++++++ app/code/Magento/User/Model/User.php | 14 +++++++++++--- .../testsuite/Magento/User/Model/UserTest.php | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php index fc32fbcaa2e98..c47e1d85bac16 100644 --- a/app/code/Magento/Authorization/Model/Role.php +++ b/app/code/Magento/Authorization/Model/Role.php @@ -5,6 +5,8 @@ */ namespace Magento\Authorization\Model; +use Magento\User\Model\User; + /** * Admin Role Model * @@ -33,6 +35,11 @@ class Role extends \Magento\Framework\Model\AbstractModel */ protected $_eventPrefix = 'authorization_roles'; + /** + * @var string + */ + protected $_cacheTag = User::CACHE_TAG; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index cd969bab27840..313f0cf0f2b90 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -65,6 +65,11 @@ class User extends AbstractModel implements StorageInterface, UserInterface const MESSAGE_ID_PASSWORD_EXPIRED = 'magento_user_password_expired'; + /** + * Tag to use for user assigned role caching. + */ + const CACHE_TAG = 'user_assigned_role'; + /** * Model event prefix * @@ -150,9 +155,12 @@ class User extends AbstractModel implements StorageInterface, UserInterface private $deploymentConfig; /** - * @var string + * @var array */ - protected $_cacheTag = \Magento\Backend\Block\Menu::CACHE_TAGS; + protected $_cacheTag = [ + \Magento\Backend\Block\Menu::CACHE_TAGS, + self::CACHE_TAG, + ]; /** * @param \Magento\Framework\Model\Context $context @@ -703,7 +711,7 @@ public function hasAssigned2Role($user) $this->_cacheManager->save( $this->serializer->serialize($data), 'assigned_role_' . $userId, - [\Magento\Backend\Block\Menu::CACHE_TAGS] + [self::CACHE_TAG] ); } else { $data = $this->serializer->unserialize($data); diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php index feb50b60a8e4a..784dd6752da4c 100644 --- a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php @@ -332,7 +332,7 @@ public function testHasAssigned2Role() $this->assertArrayHasKey('role_id', $role[0]); $roles = $this->_model->getRoles(); $this->_model->setRoleId(reset($roles))->deleteFromRole(); - $this->cache->clean([\Magento\Backend\Block\Menu::CACHE_TAGS]); + $this->cache->clean([UserModel::CACHE_TAG]); $this->assertEmpty($this->_model->hasAssigned2Role($this->_model)); } From d28b1797c35729ee7f3c0c9f8e6cbf1e7c19981c Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Fri, 12 Jun 2020 16:13:09 +0300 Subject: [PATCH 0081/1013] MC-34197: Admin user improvement --- app/code/Magento/Authorization/Model/Role.php | 2 +- app/code/Magento/User/Model/User.php | 2 +- dev/tests/integration/testsuite/Magento/User/Model/UserTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php index c47e1d85bac16..df644ed024a05 100644 --- a/app/code/Magento/Authorization/Model/Role.php +++ b/app/code/Magento/Authorization/Model/Role.php @@ -38,7 +38,7 @@ class Role extends \Magento\Framework\Model\AbstractModel /** * @var string */ - protected $_cacheTag = User::CACHE_TAG; + protected $_cacheTag = 'user_assigned_role'; /** * @param \Magento\Framework\Model\Context $context diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index 313f0cf0f2b90..829410a5c52be 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -68,7 +68,7 @@ class User extends AbstractModel implements StorageInterface, UserInterface /** * Tag to use for user assigned role caching. */ - const CACHE_TAG = 'user_assigned_role'; + private const CACHE_TAG = 'user_assigned_role'; /** * Model event prefix diff --git a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php index 784dd6752da4c..90b1706ed4e22 100644 --- a/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php +++ b/dev/tests/integration/testsuite/Magento/User/Model/UserTest.php @@ -332,7 +332,7 @@ public function testHasAssigned2Role() $this->assertArrayHasKey('role_id', $role[0]); $roles = $this->_model->getRoles(); $this->_model->setRoleId(reset($roles))->deleteFromRole(); - $this->cache->clean([UserModel::CACHE_TAG]); + $this->cache->clean(['user_assigned_role']); $this->assertEmpty($this->_model->hasAssigned2Role($this->_model)); } From 18c2882b49a68026f6fbd7281f4ce34cb2ca753c Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Mon, 15 Jun 2020 08:33:38 +0300 Subject: [PATCH 0082/1013] MC-34197: Admin user improvement --- app/code/Magento/Authorization/Model/Role.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/code/Magento/Authorization/Model/Role.php b/app/code/Magento/Authorization/Model/Role.php index df644ed024a05..96cf956afd1bc 100644 --- a/app/code/Magento/Authorization/Model/Role.php +++ b/app/code/Magento/Authorization/Model/Role.php @@ -5,8 +5,6 @@ */ namespace Magento\Authorization\Model; -use Magento\User\Model\User; - /** * Admin Role Model * From 19220c2d03325f1e5273bda8b483e432f4a1efad Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 17 Jun 2020 16:54:36 +0300 Subject: [PATCH 0083/1013] MC-35207: Improve customer custom attribute value validation --- app/code/Magento/Store/etc/config.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml index 07e4c8b0b6529..d4dddbb6a7dfa 100644 --- a/app/code/Magento/Store/etc/config.xml +++ b/app/code/Magento/Store/etc/config.xml @@ -132,6 +132,8 @@ shtml phpt pht + svg + xml From b7cc7f76979bf1ca9386b529f63a5082e0e4dec9 Mon Sep 17 00:00:00 2001 From: Oleksandr Gorkun Date: Wed, 17 Jun 2020 11:44:21 -0500 Subject: [PATCH 0084/1013] MC-35233: Distance between buttons isn't present on the edit Shopping Cart page in the backend. --- .../Magento_AdvancedCheckout/web/css/source/_module.less | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/design/adminhtml/Magento/backend/Magento_AdvancedCheckout/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_AdvancedCheckout/web/css/source/_module.less index fbc429d3afa50..1a54ba92dc66a 100644 --- a/app/design/adminhtml/Magento/backend/Magento_AdvancedCheckout/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_AdvancedCheckout/web/css/source/_module.less @@ -28,10 +28,8 @@ } .order-discounts { - .action-secondary { - + .action-secondary { - margin-right: @indent__s; - } + .action-secondary:not(:first-of-type) { + margin-right: @indent__s; } } From 5fd973325272198f1644c2f4be15490acc0d5518 Mon Sep 17 00:00:00 2001 From: Timon de Groot Date: Fri, 19 Jun 2020 11:59:35 +0200 Subject: [PATCH 0085/1013] Add Imagick CMYK to SRGB conversion --- .../Framework/Image/Adapter/ImageMagick.php | 64 ++++++++++++++++-- .../Test/Unit/Adapter/ImageMagickTest.php | 43 ++++++++++-- .../Test/Unit/Adapter/_files/cmyk_image.jpg | Bin 0 -> 362387 bytes .../Test/Unit/Adapter/_files/srgb_image.jpg | Bin 0 -> 1692 bytes 4 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/cmyk_image.jpg create mode 100644 lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/srgb_image.jpg diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 24e036c02e718..14292adff005f 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Framework\Image\Adapter; use Magento\Framework\Exception\LocalizedException; /** - * Image adapter from ImageMagick + * Image adapter from ImageMagick. + * + * @property \Imagick $_imageHandler */ class ImageMagick extends AbstractAdapter { @@ -35,6 +38,19 @@ class ImageMagick extends AbstractAdapter 'sharpen' => ['radius' => 4, 'deviation' => 1], ]; + /** + * Colorspace of the image + * + * @var int + */ + protected $colorspace = -1; + /** + * Original colorspace of the image + * + * @var int + */ + protected $originalColorspace = -1; + /** * Set/get background color. Check Imagick::COLOR_* constants * @@ -89,6 +105,8 @@ public function open($filename) ); } + $this->getColorspace(); + $this->maybeConvertColorspace(); $this->backgroundColor(); $this->getMimeType(); } @@ -136,8 +154,8 @@ protected function _applyOptions() /** * Render image and return its binary contents * - * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage * @return string + * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage */ public function getImage() { @@ -265,7 +283,7 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $this->_checkCanProcess(); $opacity = $this->getWatermarkImageOpacity() ? $this->getWatermarkImageOpacity() : $opacity; - $opacity = (double)number_format($opacity / 100, 1); + $opacity = (double) number_format($opacity / 100, 1); $watermark = new \Imagick($imagePath); @@ -395,8 +413,8 @@ public function getColorAt($x, $y) /** * Check whether the adapter can work with the image * - * @throws \LogicException * @return true + * @throws \LogicException */ protected function _checkCanProcess() { @@ -562,4 +580,42 @@ private function addSingleWatermark($positionX, int $positionY, \Imagick $waterm $compositeChannels ); } + + /** + * Get and store the image colorspace. + * + * @return int + */ + public function getColorspace(): int + { + if ($this->colorspace === -1) { + $this->originalColorspace = $this->colorspace = $this->_imageHandler->getImageColorspace(); + } + + return $this->colorspace; + } + + /** + * Get the original image colorspace. + * + * @return int + */ + public function getOriginalColorspace(): int + { + return $this->originalColorspace; + } + + /** + * Convert colorspace to SRGB if current colorspace + * is COLORSPACE_CMYK or COLORSPACE_UNDEFINED. + * + * @return void + */ + private function maybeConvertColorspace(): void + { + if ($this->colorspace === \Imagick::COLORSPACE_CMYK || $this->colorspace === \Imagick::COLORSPACE_UNDEFINED) { + $this->_imageHandler->transformImageColorspace(\Imagick::COLORSPACE_SRGB); + $this->colorspace = \Imagick::COLORSPACE_SRGB; + } + } } 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..d109b023d5fdf 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -73,10 +73,7 @@ public function testWatermark($imagePath, $expectedMessage) $this->imageMagic->watermark($imagePath); } - /** - * @return array - */ - public function watermarkDataProvider() + public function watermarkDataProvider(): array { return [ ['', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], @@ -88,6 +85,44 @@ public function watermarkDataProvider() ]; } + /** + * @param string $imagePath + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider cmykDataProvider + */ + public function testCmyk(string $imagePath) + { + $this->imageMagic->open($imagePath); + $this->assertEquals(\Imagick::COLORSPACE_CMYK, $this->imageMagic->getOriginalColorspace()); + $this->assertEquals(\Imagick::COLORSPACE_SRGB, $this->imageMagic->getColorspace()); + } + + public function cmykDataProvider(): array + { + return [ + [__DIR__ . '/_files/cmyk_image.jpg'] + ]; + } + + /** + * @param string $imagePath + * @throws \Magento\Framework\Exception\LocalizedException + * @dataProvider srgbDataProvider + */ + public function testSrgb(string $imagePath) + { + $this->imageMagic->open($imagePath); + $this->assertEquals(\Imagick::COLORSPACE_SRGB, $this->imageMagic->getOriginalColorspace()); + $this->assertEquals(\Imagick::COLORSPACE_SRGB, $this->imageMagic->getColorspace()); + } + + public function srgbDataProvider(): array + { + return [ + [__DIR__ . '/_files/srgb_image.jpg'] + ]; + } + public function testSaveWithException() { $this->expectException('Exception'); diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/cmyk_image.jpg b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/cmyk_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..27f7230d053ba470380a3b325c23c28ebbe23007 GIT binary patch literal 362387 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 r4eYQE%W} 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 EFEaw=ng9R* literal 0 HcmV?d00001 diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/srgb_image.jpg b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/srgb_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3526efa91f55a94f0fa8807609fdd84f759882a9 GIT binary patch literal 1692 zcmex=28ygoVmoP7{u$Um9pcq_&SjPWD41yetY>aHo zjDieIf{e_9jQ@`?NHQ>j0RsZCFfuc7{J+J(!N>>_{Ed)dw)_g@uv>n$P^GNK z1)#qEQU)d4@)tJ0Y<|i7sLXBP=yq<`mQ>F>tPEoN)!4l4>3!9o)4Pp9Nmt42m|JcS z4VfyRwshQOTL`t+kNKGj`rO=Haq6;?%Fmn zis6>yGR>Tjd#sBTvs#TW%LIgeXmX>6mHin-(qpD$`v!nU0A zTFyJAFEqX|^M%jbWpfh33)|;7$-P(j&|vK*7vb-7{lzlxME*(+lcRb{TNqC#K23h0 zD|mVCvaLV0QaB3e+H!mdAmi6^X_H1 zJt;k&SpI2Z-YzHm(43qfubxK#Se+tgY1?~>x$xx`-rJ(LzOVhh`vaHtwCt_#gNv7J z6VK$m-fh0&?`rGZ@AuZXo-^3^Xsy7H7cRTMh8;RC5%D?xM$LzPVwt>?yCVyKJDAH) zb$Z+Nw))cQ-8PJ|-COT{U9s^b%gvp!@1tBc+px+WPwU*;KYvo-ug5hx0n+S8wco9u zRQYHD?=-s%=XdLS?-}*pyS2@Hfw`P|Vfv?uIkOH?d0?7aBpJr(R#&?tNBcu)jd4tMid^HY_;KWgi%S$ z%&YA(oW``2sL9yo-vY9(QLQxb*b;#acYj>)om{m0-qvl`Dkr6f$rPxZTVt^xJn3!u z-VCNAHJg^-Hv&4jk7ef7y(@T|mw{v-c+Q*KeW&8VwV1lgoA>=^2+h&S;s$B3>b&_m z{-({ReP%XS6u)1cTrDrVQgl Date: Mon, 22 Jun 2020 10:59:01 +0300 Subject: [PATCH 0086/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store. --- .../Controller/Adminhtml/Login/Login.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 7ccdcfe45e482..97efddaffb3a0 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -11,6 +11,7 @@ use Magento\Backend\App\Action\Context; use Magento\Backend\Model\Auth\Session; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Config\Share; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; @@ -80,6 +81,11 @@ class Login extends Action implements HttpGetActionInterface */ private $url; + /** + * @var Share + */ + private $share; + /** * @param Context $context * @param Session $authSession @@ -90,6 +96,7 @@ class Login extends Action implements HttpGetActionInterface * @param SaveAuthenticationDataInterface $saveAuthenticationData , * @param DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser * @param Url $url + * @param Share $share */ public function __construct( Context $context, @@ -100,7 +107,8 @@ public function __construct( AuthenticationDataInterfaceFactory $authenticationDataFactory, SaveAuthenticationDataInterface $saveAuthenticationData, DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, - Url $url + Url $url, + Share $share ) { parent::__construct($context); @@ -112,6 +120,7 @@ public function __construct( $this->saveAuthenticationData = $saveAuthenticationData; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; $this->url = $url; + $this->share = $share; } /** @@ -149,6 +158,8 @@ public function execute(): ResultInterface $this->messageManager->addNoticeMessage(__('Please select a Store View to login in.')); return $resultRedirect->setPath('customer/index/edit', ['id' => $customerId]); } + } elseif ($this->share->isGlobalScope()) { + $storeId = (int)$this->storeManager->getDefaultStoreView()->getId(); } else { $storeId = (int)$customer->getStoreId(); } From ac1ac0632f8c7e9a93d25d5a498dbce1e2170c1d Mon Sep 17 00:00:00 2001 From: Barny Shergold Date: Mon, 22 Jun 2020 13:55:55 +0100 Subject: [PATCH 0087/1013] Corrected issue causing integration test to fail when category move is tested --- .../Model/CategoryUrlPathGenerator.php | 5 +-- .../Model/ProductScopeRewriteGenerator.php | 33 +++++++++++++++++-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php index cba9218ce7c72..118bd17ddd604 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php @@ -73,10 +73,7 @@ public function getUrlPath($category, $parentCategory = null) if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) { return ''; } - $path = $category->getUrlPath(); - if ($path !== null && !$category->dataHasChangedFor('url_key') && !$category->dataHasChangedFor('parent_id')) { - return $path; - } + $path = $category->getUrlKey(); if ($path === false) { return $category->getUrlPath(); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index 02a00748442be..a958ad4db0c8f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -177,9 +177,7 @@ public function generateForSpecificStoreView($storeId, $productCategories, Produ continue; } - // Category should be loaded per appropriate store at all times. This is because whilst the URL key on the - // category in focus might be unchanged, parent category URL keys might be - $categories[] = $this->categoryRepository->get($category->getEntityId(), $storeId); + $categories[] = $this->getCategoryWithOverriddenUrlKey($storeId, $category); } $productCategories = $this->objectRegistryFactory->create(['entities' => $categories]); @@ -238,6 +236,35 @@ public function isCategoryProperForGenerating(Category $category, $storeId) return false; } + /** + * Check if URL key has been changed + * + * Checks if URL key has been changed for provided category and returns reloaded category, + * in other case - returns provided category. + * + * @param int $storeId + * @param Category $category + * @return Category + */ + private function getCategoryWithOverriddenUrlKey($storeId, Category $category) + { + $isUrlKeyOverridden = $this->storeViewService->doesEntityHaveOverriddenUrlKeyForStore( + $storeId, + $category->getEntityId(), + Category::ENTITY + ); + + // Category should be loaded per appropriate store at all times. This is because whilst the URL key on the + // category in focus might be unchanged, parent category URL keys might be. If the category store ID + // and passed store ID are the same then return current category as it is correct but may have changed in memory + + if (!$isUrlKeyOverridden && $storeId == $category->getStoreId()) { + return $category; + } + + return $this->categoryRepository->get($category->getEntityId(), $storeId); + } + /** * Check config value of generate_category_product_rewrites * From e100822f5858d5c218cfb72a76478ff3db194a26 Mon Sep 17 00:00:00 2001 From: Barny Shergold Date: Mon, 22 Jun 2020 15:21:22 +0100 Subject: [PATCH 0088/1013] Reversed change --- .../CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php index 118bd17ddd604..cba9218ce7c72 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlPathGenerator.php @@ -73,7 +73,10 @@ public function getUrlPath($category, $parentCategory = null) if (in_array($category->getParentId(), [Category::ROOT_CATEGORY_ID, Category::TREE_ROOT_ID])) { return ''; } - + $path = $category->getUrlPath(); + if ($path !== null && !$category->dataHasChangedFor('url_key') && !$category->dataHasChangedFor('parent_id')) { + return $path; + } $path = $category->getUrlKey(); if ($path === false) { return $category->getUrlPath(); From 80adcee1647860d788372b5c1714236fc338b244 Mon Sep 17 00:00:00 2001 From: Mark Berube Date: Mon, 22 Jun 2020 15:09:39 -0500 Subject: [PATCH 0089/1013] MC-34174: adding sorting validation --- .../Listing/Columns/AttributeSetId.php | 3 +- .../Ui/Component/Listing/Columns/Websites.php | 1 + .../Ui/Component/Listing/Columns/Column.php | 13 +- .../Listing/Columns/AttributeSetIdTest.php | 55 +++++++ .../Component/Listing/Columns/ColumnTest.php | 147 ++++++++++++++---- .../Listing/Columns/WebsitesTest.php | 85 ++++++++++ 6 files changed, 269 insertions(+), 35 deletions(-) create mode 100644 app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/AttributeSetIdTest.php create mode 100644 app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/WebsitesTest.php diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/AttributeSetId.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/AttributeSetId.php index 5e9f7ba065be7..9dc5704673f01 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/AttributeSetId.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/AttributeSetId.php @@ -8,7 +8,7 @@ namespace Magento\Catalog\Ui\Component\Listing\Columns; /** - * Attribute set listing column component + * AttributeSetId listing column component. */ class AttributeSetId extends \Magento\Ui\Component\Listing\Columns\Column { @@ -23,6 +23,7 @@ protected function applySorting() && !empty($sorting['field']) && !empty($sorting['direction']) && $sorting['field'] === $this->getName() + && in_array(strtoupper($sorting['direction']), ['ASC', 'DESC'], true) ) { $collection = $this->getContext()->getDataProvider()->getCollection(); $collection->joinField( 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..eb3c63635b291 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php @@ -118,6 +118,7 @@ protected function applySorting() && !empty($sorting['field']) && !empty($sorting['direction']) && $sorting['field'] === $this->getName() + && in_array(strtoupper($sorting['direction']), ['ASC', 'DESC'], true) ) { /** @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection $collection */ $collection = $this->getContext()->getDataProvider()->getCollection(); diff --git a/app/code/Magento/Ui/Component/Listing/Columns/Column.php b/app/code/Magento/Ui/Component/Listing/Columns/Column.php index e69658540c51f..a4abf7551c5ce 100644 --- a/app/code/Magento/Ui/Component/Listing/Columns/Column.php +++ b/app/code/Magento/Ui/Component/Listing/Columns/Column.php @@ -5,10 +5,10 @@ */ namespace Magento\Ui\Component\Listing\Columns; -use Magento\Ui\Component\AbstractComponent; +use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponentInterface; -use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\AbstractComponent; /** * @api @@ -64,6 +64,7 @@ public function getComponentName() * Prepare component configuration * * @return void + * @throws \Magento\Framework\Exception\LocalizedException */ public function prepare() { @@ -97,18 +98,19 @@ public function prepare() } /** - * To prepare items of a column + * Prepares items of a column * * @param array $items * @return array */ - public function prepareItems(array & $items) + public function prepareItems(array &$items) { return $items; } /** - * Add field to select + * Adds additional field to select object + * * @return void */ protected function addFieldToSelect() @@ -131,6 +133,7 @@ protected function applySorting() && !empty($sorting['field']) && !empty($sorting['direction']) && $sorting['field'] === $this->getName() + && in_array(strtoupper($sorting['direction']), ['ASC', 'DESC'], true) ) { $this->getContext()->getDataProvider()->addOrder( $this->getName(), diff --git a/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/AttributeSetIdTest.php b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/AttributeSetIdTest.php new file mode 100644 index 0000000000000..50515105e82ae --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/AttributeSetIdTest.php @@ -0,0 +1,55 @@ +getMockBuilder(AbstractCollection::class) + ->disableOriginalConstructor() + ->getMock(); + + $selectMock = $this->createMock(Select::class); + + $selectMock->expects($this->once()) + ->method('order') + ->with('attribute_set_name asc'); + + $this->dataProviderMock = $this->getMockBuilder(DataProviderInterface::class) + ->addMethods(['getCollection', 'getSelect']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->dataProviderMock->expects($this->once()) + ->method('getCollection') + ->willReturn($collectionMock); + + $collectionMock->expects($this->once()) + ->method('getSelect') + ->willReturn($selectMock); + + parent::testPrepare(); + } +} diff --git a/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/ColumnTest.php b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/ColumnTest.php index 0bade901361a3..fb0f9f215163b 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/ColumnTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/ColumnTest.php @@ -14,9 +14,11 @@ use Magento\Framework\View\Element\UiComponentFactory; use Magento\Framework\View\Element\UiComponentInterface; use Magento\Ui\Component\Listing\Columns\Column; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Testing for generic UI column classes & for custom ones such as Websites + */ class ColumnTest extends TestCase { /** @@ -29,6 +31,23 @@ class ColumnTest extends TestCase */ protected $objectManager; + /** + * @var UiComponentFactory + */ + protected $uiComponentFactoryMock; + + protected $dataProviderMock; + + /** + * @var string + */ + protected $columnClass = Column::class; + + /** + * @var string + */ + protected $columnName = Column::NAME; + /** * Set up */ @@ -45,6 +64,8 @@ protected function setUp(): void true, [] ); + + $this->uiComponentFactoryMock = $this->createMock(UiComponentFactory::class); } /** @@ -56,7 +77,7 @@ public function testGetComponentName() { $this->contextMock->expects($this->never())->method('getProcessor'); $column = $this->objectManager->getObject( - Column::class, + $this->columnClass, [ 'context' => $this->contextMock, 'data' => [ @@ -70,7 +91,7 @@ public function testGetComponentName() ] ); - $this->assertEquals($column->getComponentName(), Column::NAME . '.testType'); + $this->assertEquals($column->getComponentName(), $this->columnName . '.testType'); } /** @@ -82,7 +103,7 @@ public function testPrepareItems() { $testItems = ['item1','item2', 'item3']; $column = $this->objectManager->getObject( - Column::class, + $this->columnClass, ['context' => $this->contextMock] ); @@ -92,57 +113,70 @@ public function testPrepareItems() /** * Run test prepare method * + * @param null $dataProviderMock * @return void */ public function testPrepare() { - $processor = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); - $this->contextMock->expects($this->atLeastOnce())->method('getProcessor')->willReturn($processor); $data = [ 'name' => 'test_name', 'js_config' => ['extends' => 'test_config_extends'], 'config' => ['dataType' => 'test_type', 'sortable' => true] ]; - /** @var UiComponentFactory|MockObject $uiComponentFactoryMock */ - $uiComponentFactoryMock = $this->createMock(UiComponentFactory::class); + /** @var Column $column */ + $column = $this->objectManager->getObject( + $this->columnClass, + [ + 'context' => $this->contextMock, + 'uiComponentFactory' => $this->uiComponentFactoryMock, + 'data' => $data + ] + ); - /** @var UiComponentInterface|MockObject $wrappedComponentMock */ + /** @var UiComponentInterface|PHPUnit\Framework\MockObject\MockObject $wrappedComponentMock */ $wrappedComponentMock = $this->getMockForAbstractClass( UiComponentInterface::class, [], '', false ); - /** @var DataProviderInterface|MockObject $dataProviderMock */ - $dataProviderMock = $this->getMockForAbstractClass( - DataProviderInterface::class, - [], - '', - false - ); + + if ($this->dataProviderMock === null) { + $this->dataProviderMock = $this->getMockForAbstractClass( + DataProviderInterface::class, + [], + '', + false + ); + + $this->dataProviderMock->expects($this->once()) + ->method('addOrder') + ->with('test_name', 'ASC'); + } + + $processor = $this->getMockBuilder(Processor::class) + ->disableOriginalConstructor() + ->getMock(); + $this->contextMock->expects($this->atLeastOnce()) + ->method('getProcessor') + ->willReturn($processor); $this->contextMock->expects($this->atLeastOnce()) ->method('getNamespace') ->willReturn('test_namespace'); $this->contextMock->expects($this->atLeastOnce()) ->method('getDataProvider') - ->willReturn($dataProviderMock); + ->willReturn($this->dataProviderMock); $this->contextMock->expects($this->atLeastOnce()) ->method('getRequestParam') ->with('sorting') ->willReturn(['field' => 'test_name', 'direction' => 'asc']); $this->contextMock->expects($this->atLeastOnce()) ->method('addComponentDefinition') - ->with(Column::NAME . '.test_type', ['extends' => 'test_config_extends']); + ->with($this->columnName . '.test_type', ['extends' => 'test_config_extends']); - $dataProviderMock->expects($this->once()) - ->method('addOrder') - ->with('test_name', 'ASC'); - - $uiComponentFactoryMock->expects($this->once()) + $this->uiComponentFactoryMock->expects($this->once()) ->method('create') ->with('test_name', 'test_type', array_merge(['context' => $this->contextMock], $data)) ->willReturn($wrappedComponentMock); @@ -153,16 +187,71 @@ public function testPrepare() $wrappedComponentMock->expects($this->once()) ->method('prepare'); - /** @var Column $column */ + $column->prepare(); + } + + /** + * Run a test on sorting function + * + * @param array $config + * @param string $direction + * @param int $numOfProviderCalls + * @throws \ReflectionException + * + * @dataProvider sortingDataProvider + */ + public function testSorting(array $config, string $direction, int $numOfProviderCalls) + { + $data = [ + 'name' => 'test_name', + 'config' => $config + ]; + + $this->dataProviderMock = $this->getMockForAbstractClass( + DataProviderInterface::class, + [], + '', + false + ); + + $this->dataProviderMock->expects($this->exactly($numOfProviderCalls)) + ->method('addOrder') + ->with('test_name', $direction); + + $this->contextMock->expects($this->atLeastOnce()) + ->method('getRequestParam') + ->with('sorting') + ->willReturn(['field' => 'test_name', 'direction' => $direction]); + + $this->contextMock->expects($this->exactly($numOfProviderCalls)) + ->method('getDataProvider') + ->willReturn($this->dataProviderMock); + $column = $this->objectManager->getObject( - Column::class, + $this->columnClass, [ 'context' => $this->contextMock, - 'uiComponentFactory' => $uiComponentFactoryMock, + 'uiComponentFactory' => $this->uiComponentFactoryMock, 'data' => $data ] ); - $column->prepare(); + // get access to the method + $method = new \ReflectionMethod( + Column::class, + 'applySorting' + ); + $method->setAccessible(true); + + $method->invokeArgs($column, []); + } + + public function sortingDataProvider() + { + return [ + [['dataType' => 'test_type', 'sortable' => true], 'ASC', 1], + [['dataType' => 'test_type', 'sortable' => false], 'ASC', 0], + [['dataType' => 'test_type', 'sortable' => true], 'foobar', 0] + ]; } } diff --git a/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/WebsitesTest.php b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/WebsitesTest.php new file mode 100644 index 0000000000000..ba842f330b3b8 --- /dev/null +++ b/app/code/Magento/Ui/Test/Unit/Component/Listing/Columns/WebsitesTest.php @@ -0,0 +1,85 @@ +getMockBuilder(AbstractCollection::class) + ->disableOriginalConstructor() + ->getMock(); + + $selectMock = $this->createMock(Select::class); + + $selectMock->expects($this->once()) + ->method('order'); + + $selectMock->expects($this->once()) + ->method('from') + ->willReturn($selectMock); + + $selectMock->expects($this->atLeastOnce()) + ->method('joinLeft') + ->willReturn($selectMock); + + $selectMock->expects($this->once()) + ->method('group'); + + $connectionMock = $this->createMock(AdapterInterface::class); + + $connectionMock->expects($this->once()) + ->method('select') + ->willReturn($selectMock); + + $this->dataProviderMock = $this->getMockBuilder(DataProviderInterface::class) + ->addMethods(['getCollection', 'getSelect']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->dataProviderMock->expects($this->once()) + ->method('getCollection') + ->willReturn($collectionMock); + + $collectionMock->expects($this->once()) + ->method('getConnection') + ->willReturn($connectionMock); + + $collectionMock->expects($this->atLeastOnce()) + ->method('getTable') + ->willReturn('test_table'); + + $collectionMock->expects($this->once()) + ->method('getSelect') + ->willReturn($selectMock); + + parent::testPrepare(); + } +} From 3c1b606c230a0b709bb641369e423eb6fcdab081 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 23 Jun 2020 18:09:24 +0300 Subject: [PATCH 0090/1013] Added hashing for Login as Customer secret. --- .../GetAuthenticationDataBySecret.php | 15 +++++++++++++-- .../ResourceModel/SaveAuthenticationData.php | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php index 078eb93405299..0b2577053acc6 100644 --- a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php @@ -8,8 +8,9 @@ namespace Magento\LoginAsCustomer\Model\ResourceModel; use Magento\Framework\App\ResourceConnection; -use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\LoginAsCustomerApi\Api\ConfigInterface; use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterfaceFactory; @@ -20,6 +21,11 @@ */ class GetAuthenticationDataBySecret implements GetAuthenticationDataBySecretInterface { + /** + * @var EncryptorInterface + */ + private $encryptor; + /** * @var ResourceConnection */ @@ -41,17 +47,20 @@ class GetAuthenticationDataBySecret implements GetAuthenticationDataBySecretInte private $authenticationDataFactory; /** + * @param EncryptorInterface $encryptor * @param ResourceConnection $resourceConnection * @param DateTime $dateTime * @param ConfigInterface $config * @param AuthenticationDataInterfaceFactory $authenticationDataFactory */ public function __construct( + EncryptorInterface $encryptor, ResourceConnection $resourceConnection, DateTime $dateTime, ConfigInterface $config, AuthenticationDataInterfaceFactory $authenticationDataFactory ) { + $this->encryptor = $encryptor; $this->resourceConnection = $resourceConnection; $this->dateTime = $dateTime; $this->config = $config; @@ -71,9 +80,11 @@ public function execute(string $secret): AuthenticationDataInterface $this->dateTime->gmtTimestamp() - $this->config->getAuthenticationDataExpirationTime() ); + $hash = $this->encryptor->hash($secret); + $select = $connection->select() ->from(['main_table' => $tableName]) - ->where('main_table.secret = ?', $secret) + ->where('main_table.secret = ?', $hash) ->where('main_table.created_at > ?', $timePoint); $data = $connection->fetchRow($select); diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php index d120b0eae392e..8351441038641 100644 --- a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php @@ -8,8 +8,9 @@ namespace Magento\LoginAsCustomer\Model\ResourceModel; use Magento\Framework\App\ResourceConnection; -use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Math\Random; +use Magento\Framework\Stdlib\DateTime\DateTime; use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; use Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface; @@ -18,6 +19,11 @@ */ class SaveAuthenticationData implements SaveAuthenticationDataInterface { + /** + * @var EncryptorInterface + */ + private $encryptor; + /** * @var ResourceConnection */ @@ -34,15 +40,18 @@ class SaveAuthenticationData implements SaveAuthenticationDataInterface private $random; /** + * @param EncryptorInterface $encryptor * @param ResourceConnection $resourceConnection * @param DateTime $dateTime * @param Random $random */ public function __construct( + EncryptorInterface $encryptor, ResourceConnection $resourceConnection, DateTime $dateTime, Random $random ) { + $this->encryptor = $encryptor; $this->resourceConnection = $resourceConnection; $this->dateTime = $dateTime; $this->random = $random; @@ -57,16 +66,18 @@ public function execute(AuthenticationDataInterface $authenticationData): string $tableName = $this->resourceConnection->getTableName('login_as_customer'); $secret = $this->random->getRandomString(64); + $hash = $this->encryptor->hash($secret); $connection->insert( $tableName, [ 'customer_id' => $authenticationData->getCustomerId(), 'admin_id' => $authenticationData->getAdminId(), - 'secret' => $secret, + 'secret' => $hash, 'created_at' => $this->dateTime->gmtDate(), ] ); + return $secret; } } From f51a7f656e438145b95e63dc1dd8176e2c3d6654 Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Wed, 24 Jun 2020 14:49:34 -0500 Subject: [PATCH 0091/1013] MC-35076: Catalog Event Update. --- .../Block/Adminhtml/Import/Frame/Result.php | 5 +- .../Controller/Adminhtml/ImportResult.php | 14 ++- .../Adminhtml/Import/Frame/ResultTest.php | 110 ++++++++++++++++++ 3 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Frame/ResultTest.php diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Frame/Result.php b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Frame/Result.php index acca62b4cb72e..0b9857edc53eb 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Import/Frame/Result.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Import/Frame/Result.php @@ -102,7 +102,7 @@ public function addError($message) $this->addError($row); } } else { - $this->_messages['error'][] = $message; + $this->_messages['error'][] = $this->escapeHtml($message); } return $this; } @@ -140,7 +140,8 @@ public function addSuccess($message, $appendImportButton = false) $this->addSuccess($row); } } else { - $this->_messages['success'][] = $message . ($appendImportButton ? $this->getImportButtonHtml() : ''); + $escapedMessage = $this->escapeHtml($message); + $this->_messages['success'][] = $escapedMessage . ($appendImportButton ? $this->getImportButtonHtml() : ''); } return $this; } diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php index 47210dd9805e5..6da90efa4592c 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/ImportResult.php @@ -56,6 +56,8 @@ public function __construct( } /** + * Add Error Messages for Import + * * @param \Magento\Framework\View\Element\AbstractBlock $resultBlock * @param ProcessingErrorAggregatorInterface $errorAggregator * @return $this @@ -68,7 +70,7 @@ protected function addErrorMessages( $message = ''; $counter = 0; foreach ($this->getErrorMessages($errorAggregator) as $error) { - $message .= ++$counter . '. ' . $error . '
'; + $message .= (++$counter) . '. ' . $error . '
'; if ($counter >= self::LIMIT_ERRORS_MESSAGE) { break; } @@ -88,7 +90,7 @@ protected function addErrorMessages( . '
' . __('Download full report') . '
' - . '

' . $message . '
' + . '
' . $resultBlock->escapeHtml($message) . '
' ); } catch (\Exception $e) { foreach ($this->getErrorMessages($errorAggregator) as $errorMessage) { @@ -101,6 +103,8 @@ protected function addErrorMessages( } /** + * Get all Error Messages from Import Results + * * @param \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface $errorAggregator * @return array */ @@ -115,6 +119,8 @@ protected function getErrorMessages(ProcessingErrorAggregatorInterface $errorAgg } /** + * Get System Generated Exception + * * @param ProcessingErrorAggregatorInterface $errorAggregator * @return \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError[] */ @@ -124,6 +130,8 @@ protected function getSystemExceptions(ProcessingErrorAggregatorInterface $error } /** + * Generate Error Report File + * * @param ProcessingErrorAggregatorInterface $errorAggregator * @return string */ @@ -141,6 +149,8 @@ protected function createErrorReport(ProcessingErrorAggregatorInterface $errorAg } /** + * Get Import History Url + * * @param string $fileName * @return string */ diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Frame/ResultTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Frame/ResultTest.php new file mode 100644 index 0000000000000..218babc2b48f0 --- /dev/null +++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Import/Frame/ResultTest.php @@ -0,0 +1,110 @@ +contextMock = $this->createMock(Context::class); + $this->encoderMock = $this->getMockBuilder(EncoderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->escaperMock = $this->createPartialMock(Escaper::class, ['escapeHtml']); + $this->contextMock->expects($this->once())->method('getEscaper')->willReturn($this->escaperMock); + $this->result = new Result( + $this->contextMock, + $this->encoderMock + ); + } + + /** + * Test error message + * + * @return void + */ + public function testAddError(): void + { + $errors = ['first error', 'second error','third error']; + $this->escaperMock + ->expects($this->exactly(count($errors))) + ->method('escapeHtml') + ->willReturnOnConsecutiveCalls(...array_values($errors)); + + $this->result->addError($errors); + $this->assertEquals(count($errors), count($this->result->getMessages()['error'])); + } + + /** + * Test success message + * + * @return void + */ + public function testAddSuccess(): void + { + $success = ['first message', 'second message','third message']; + $this->escaperMock + ->expects($this->exactly(count($success))) + ->method('escapeHtml') + ->willReturnOnConsecutiveCalls(...array_values($success)); + + $this->result->addSuccess($success); + $this->assertEquals(count($success), count($this->result->getMessages()['success'])); + } + + /** + * Test Add Notice message + * + * @return void + */ + public function testAddNotice(): void + { + $notice = ['notice 1', 'notice 2','notice 3']; + + $this->result->addNotice($notice); + $this->assertEquals(count($notice), count($this->result->getMessages()['notice'])); + } +} From 0ec2c916c5c8cb6464b3ddd5f3b20b4e27e1f331 Mon Sep 17 00:00:00 2001 From: Johan Lindahl Date: Mon, 29 Jun 2020 11:07:20 +0200 Subject: [PATCH 0092/1013] AppState emulateAreaCode was not respected by file collector --- lib/internal/Magento/Framework/View/File/Collector/Base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/View/File/Collector/Base.php b/lib/internal/Magento/Framework/View/File/Collector/Base.php index a5824b7321e84..f82ba6b0d7fab 100644 --- a/lib/internal/Magento/Framework/View/File/Collector/Base.php +++ b/lib/internal/Magento/Framework/View/File/Collector/Base.php @@ -65,7 +65,7 @@ public function getFiles(ThemeInterface $theme, $filePath) foreach ($sharedFiles as $file) { $result[] = $this->fileFactory->create($file->getFullPath(), $file->getComponentName(), null, true); } - $area = $theme->getData('area'); + $area = $theme->getArea(); $themeFiles = $this->componentDirSearch->collectFilesWithContext( ComponentRegistrar::MODULE, "view/{$area}/{$this->subDir}{$filePath}" From 7660725eddb33e837a6286bfe131c0a96e209144 Mon Sep 17 00:00:00 2001 From: Guillaume Quintard Date: Mon, 29 Jun 2020 19:01:56 -0700 Subject: [PATCH 0093/1013] [vcl] don't explicitly hash the host header Hashing `req.http.host`/`client.ip` is already handled by the [built-in vcl](https://github.com/varnishcache/varnish-cache/blob/6.0/bin/varnishd/builtin.vcl#L86) so there's no need to repeat it explicitly. It's also a bit confusing as `req.url` is not explicitly handled, even though it's a more important hash input than the host. note: all versions have been changed for the sake of consistency but both the 4.x and 5.x series have been EOL'd a (long) while ago and users should be encouraged to upgraded as soon as possible. --- app/code/Magento/PageCache/etc/varnish4.vcl | 7 ------- app/code/Magento/PageCache/etc/varnish5.vcl | 7 ------- app/code/Magento/PageCache/etc/varnish6.vcl | 7 ------- 3 files changed, 21 deletions(-) diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index f5e25ce36e973..7ae857c54e67c 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -121,13 +121,6 @@ sub vcl_hash { hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); } - # For multi site configurations to not cache each other's content - if (req.http.host) { - hash_data(req.http.host); - } else { - hash_data(server.ip); - } - if (req.url ~ "/graphql") { call process_graphql_headers; } diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 92bb3394486fc..7daa56c59fe63 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -122,13 +122,6 @@ sub vcl_hash { hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); } - # For multi site configurations to not cache each other's content - if (req.http.host) { - hash_data(req.http.host); - } else { - hash_data(server.ip); - } - # To make sure http users don't see ssl warning if (req.http./* {{ ssl_offloaded_header }} */) { hash_data(req.http./* {{ ssl_offloaded_header }} */); diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl index eef5e99862538..d603a8fed3cea 100644 --- a/app/code/Magento/PageCache/etc/varnish6.vcl +++ b/app/code/Magento/PageCache/etc/varnish6.vcl @@ -122,13 +122,6 @@ sub vcl_hash { hash_data(regsub(req.http.cookie, "^.*?X-Magento-Vary=([^;]+);*.*$", "\1")); } - # For multi site configurations to not cache each other's content - if (req.http.host) { - hash_data(req.http.host); - } else { - hash_data(server.ip); - } - # To make sure http users don't see ssl warning if (req.http./* {{ ssl_offloaded_header }} */) { hash_data(req.http./* {{ ssl_offloaded_header }} */); From e72478890ecbd2296789551fd2f46927d69750fc Mon Sep 17 00:00:00 2001 From: Johan Lindahl Date: Wed, 1 Jul 2020 17:29:56 +0200 Subject: [PATCH 0094/1013] Fixed broken unit test --- .../Framework/View/Test/Unit/File/Collector/BaseTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/View/Test/Unit/File/Collector/BaseTest.php b/lib/internal/Magento/Framework/View/Test/Unit/File/Collector/BaseTest.php index 0edafcb125dd3..ea0ef1cc69e87 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/File/Collector/BaseTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/File/Collector/BaseTest.php @@ -85,8 +85,7 @@ public function testGetFiles() ->method('create') ->willReturn($this->createFileMock()); $this->themeMock->expects($this->once()) - ->method('getData') - ->with('area') + ->method('getArea') ->willReturn('frontend'); $result = $this->fileCollector->getFiles($this->themeMock, '*.xml'); From 54a5d1bd9634f055927516bb950a97da4e223cb4 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Thu, 2 Jul 2020 14:11:04 +0300 Subject: [PATCH 0095/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - updated. --- .../Controller/Adminhtml/Login/Login.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 97efddaffb3a0..77eb63c59eaee 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -13,6 +13,7 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\Config\Share; 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; @@ -108,7 +109,7 @@ public function __construct( SaveAuthenticationDataInterface $saveAuthenticationData, DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, Url $url, - Share $share + Share $share = null ) { parent::__construct($context); @@ -120,7 +121,7 @@ public function __construct( $this->saveAuthenticationData = $saveAuthenticationData; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; $this->url = $url; - $this->share = $share; + $this->share = $share ?? ObjectManager::getInstance()->get(Share::class); } /** From 27720600a0eca514bcce711e24db02be70f4e485 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Fri, 3 Jul 2020 08:58:19 +0300 Subject: [PATCH 0096/1013] Added hashing for Login as Customer secret - updated. --- .../GetAuthenticationDataBySecret.php | 19 ++++++++++--------- .../ResourceModel/SaveAuthenticationData.php | 7 ++++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php index 0b2577053acc6..0c417f78800a2 100644 --- a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/GetAuthenticationDataBySecret.php @@ -7,6 +7,7 @@ namespace Magento\LoginAsCustomer\Model\ResourceModel; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Exception\LocalizedException; @@ -21,11 +22,6 @@ */ class GetAuthenticationDataBySecret implements GetAuthenticationDataBySecretInterface { - /** - * @var EncryptorInterface - */ - private $encryptor; - /** * @var ResourceConnection */ @@ -47,24 +43,29 @@ class GetAuthenticationDataBySecret implements GetAuthenticationDataBySecretInte private $authenticationDataFactory; /** - * @param EncryptorInterface $encryptor + * @var EncryptorInterface + */ + private $encryptor; + + /** * @param ResourceConnection $resourceConnection * @param DateTime $dateTime * @param ConfigInterface $config * @param AuthenticationDataInterfaceFactory $authenticationDataFactory + * @param EncryptorInterface|null $encryptor */ public function __construct( - EncryptorInterface $encryptor, ResourceConnection $resourceConnection, DateTime $dateTime, ConfigInterface $config, - AuthenticationDataInterfaceFactory $authenticationDataFactory + AuthenticationDataInterfaceFactory $authenticationDataFactory, + ?EncryptorInterface $encryptor = null ) { - $this->encryptor = $encryptor; $this->resourceConnection = $resourceConnection; $this->dateTime = $dateTime; $this->config = $config; $this->authenticationDataFactory = $authenticationDataFactory; + $this->encryptor = $encryptor ?? ObjectManager::getInstance()->get(EncryptorInterface::class); } /** diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php index 8351441038641..23d707d151487 100644 --- a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php @@ -7,6 +7,7 @@ namespace Magento\LoginAsCustomer\Model\ResourceModel; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Encryption\EncryptorInterface; use Magento\Framework\Math\Random; @@ -46,15 +47,15 @@ class SaveAuthenticationData implements SaveAuthenticationDataInterface * @param Random $random */ public function __construct( - EncryptorInterface $encryptor, ResourceConnection $resourceConnection, DateTime $dateTime, - Random $random + Random $random, + ?EncryptorInterface $encryptor = null ) { - $this->encryptor = $encryptor; $this->resourceConnection = $resourceConnection; $this->dateTime = $dateTime; $this->random = $random; + $this->encryptor = $encryptor ?? ObjectManager::getInstance()->get(EncryptorInterface::class); } /** From 8782b6c1ef0c72793dd731112fc0eb6c1bbc3735 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 7 Jul 2020 17:46:20 +0300 Subject: [PATCH 0097/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - updated to use Store Groups. --- .../Block/Adminhtml/ConfirmationPopup.php | 2 +- .../Component/ConfirmationPopup/Options.php | 150 ++++++++++++++++++ .../confirmation-popup/store-view-ptions.html | 8 +- 3 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php index e2d11b2c8cb80..aaec06e1f4a90 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php @@ -10,7 +10,7 @@ use Magento\Backend\Block\Template; use Magento\Framework\Serialize\Serializer\Json; use Magento\LoginAsCustomerApi\Api\ConfigInterface; -use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\ConfirmationPopup\Options as StoreOptions; /** * Login confirmation pop-up diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php new file mode 100644 index 0000000000000..424fbc3faa2fe --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -0,0 +1,150 @@ +customerRepository = $customerRepository; + $this->escaper = $escaper; + $this->request = $request; + $this->share = $share; + $this->systemStore = $systemStore; + } + + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + if ($this->options !== null) { + return $this->options; + } + + $customerId = (int)$this->request->getParam('id'); + $this->options = $this->generateCurrentOptions($customerId); + + return $this->options; + } + + /** + * Sanitize website/store option name. + * + * @param string $name + * + * @return string + */ + private function sanitizeName(string $name): string + { + $matches = []; + preg_match('/\$[:]*{(.)*}/', $name, $matches); + if (count($matches) > 0) { + $name = $this->escaper->escapeHtml($this->escaper->escapeJs($name)); + } else { + $name = $this->escaper->escapeHtml($name); + } + + return $name; + } + + /** + * Generate current options. + * + * @param int $customerId + * @return array + */ + private function generateCurrentOptions(int $customerId): array + { + $options = []; + if ($customerId) { + $customer = $this->customerRepository->getById($customerId); + $customerWebsiteId = $customer->getWebsiteId(); + $customerStoreId = $customer->getStoreId(); + $isGlobalScope = $this->share->isGlobalScope(); + $websiteCollection = $this->systemStore->getWebsiteCollection(); + $groupCollection = $this->systemStore->getGroupCollection(); + /** @var \Magento\Store\Model\Website $website */ + foreach ($websiteCollection as $website) { + $groups = []; + /** @var \Magento\Store\Model\Group $group */ + foreach ($groupCollection as $group) { + if ($group->getWebsiteId() == $website->getId()) { + $storeViewIds = $group->getStoreIds(); + if (!empty($storeViewIds)) { + $name = $this->sanitizeName($group->getName()); + $groups[$name]['label'] = str_repeat(' ', 4) . $name; + $groups[$name]['value'] = array_values($storeViewIds)[0]; + $groups[$name]['disabled'] = !$isGlobalScope && $customerWebsiteId !== $website->getId(); + $groups[$name]['selected'] = in_array($customerStoreId, $storeViewIds) ? true : false; + } + } + } + if (!empty($groups)) { + $name = $this->sanitizeName($website->getName()); + $options[$name]['label'] = $name; + $options[$name]['value'] = array_values($groups); + } + } + } + + return $options; + } +} diff --git a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html index ed1f991245e70..916a5583abe57 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html +++ b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html @@ -18,10 +18,10 @@ <% _.each(data.storeViewOptions, function(website) { %> <% _.each(website.value, function(group) { %> - - <% _.each(group.value, function(storeview) { %> - - <% }); %> + <% }); %> <% }); %> From 095606e0c884c66c28cc501af1189348a7fff6fb Mon Sep 17 00:00:00 2001 From: Dmytro Voskoboinikov Date: Tue, 7 Jul 2020 14:28:50 -0500 Subject: [PATCH 0098/1013] MC-34174: adding sorting validation --- .../testsuite/Magento/Customer/Api/CustomerRepositoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d0ca88ae237f..75e7fea036486 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -296,7 +296,7 @@ public function testDeleteCustomerNonAuthorized(): void $this->assertEquals(HTTPExceptionCodes::HTTP_UNAUTHORIZED, $e->getCode()); } /** @var Customer $data */ - $data = $this->_getCustomerData($customerData[Customer::ID]); + $data = $this->getCustomerData($customerData[Customer::ID]); $this->assertNotNull($data->getId()); } From d59d740c6d4424ca479a19581f1cfa56579e5096 Mon Sep 17 00:00:00 2001 From: Barny Shergold Date: Tue, 7 Jul 2020 22:32:18 +0100 Subject: [PATCH 0099/1013] Updated code as per reviewer --- .../Model/ProductScopeRewriteGenerator.php | 20 +++++++++---------- .../ProductScopeRewriteGeneratorTest.php | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index a958ad4db0c8f..5879a62a85954 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -6,6 +6,7 @@ namespace Magento\CatalogUrlRewrite\Model; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\CatalogUrlRewrite\Model\Product\AnchorUrlRewriteGenerator; @@ -15,13 +16,12 @@ use Magento\CatalogUrlRewrite\Service\V1\StoreViewService; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\UrlRewrite\Model\MergeDataProviderFactory; /** - * Class ProductScopeRewriteGenerator - * * Generates Product/Category URLs for different scopes * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -237,14 +237,18 @@ public function isCategoryProperForGenerating(Category $category, $storeId) } /** - * Check if URL key has been changed - * * Checks if URL key has been changed for provided category and returns reloaded category, * in other case - returns provided category. * + * Category should be loaded per appropriate store at all times. This is because whilst the URL key on the + * category in focus might be unchanged, parent category URL keys might be. If the category store ID + * and passed store ID are the same then return current category as it is correct but may have changed in memory + * * @param int $storeId * @param Category $category - * @return Category + * + * @return CategoryInterface + * @throws NoSuchEntityException */ private function getCategoryWithOverriddenUrlKey($storeId, Category $category) { @@ -254,11 +258,7 @@ private function getCategoryWithOverriddenUrlKey($storeId, Category $category) Category::ENTITY ); - // Category should be loaded per appropriate store at all times. This is because whilst the URL key on the - // category in focus might be unchanged, parent category URL keys might be. If the category store ID - // and passed store ID are the same then return current category as it is correct but may have changed in memory - - if (!$isUrlKeyOverridden && $storeId == $category->getStoreId()) { + if (!$isUrlKeyOverridden && $storeId === $category->getStoreId()) { return $category; } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php index f54805158cfb2..d9c6adce9661f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Model/ProductScopeRewriteGeneratorTest.php @@ -71,7 +71,7 @@ class ProductScopeRewriteGeneratorTest extends TestCase private $configMock; /** @var CategoryRepositoryInterface|MockObject */ - private $categoryRepository; + private $categoryRepositoryMock; protected function setUp(): void { @@ -130,7 +130,7 @@ function ($value) { $this->configMock = $this->getMockBuilder(ScopeConfigInterface::class) ->getMock(); - $this->categoryRepository = $this->getMockForAbstractClass(CategoryRepositoryInterface::class); + $this->categoryRepositoryMock = $this->getMockForAbstractClass(CategoryRepositoryInterface::class); $this->productScopeGenerator = (new ObjectManager($this))->getObject( ProductScopeRewriteGenerator::class, @@ -144,7 +144,7 @@ function ($value) { 'storeManager' => $this->storeManager, 'mergeDataProviderFactory' => $mergeDataProviderFactory, 'config' => $this->configMock, - 'categoryRepository' => $this->categoryRepository + 'categoryRepository' => $this->categoryRepositoryMock ] ); $this->categoryMock = $this->getMockBuilder(Category::class) @@ -222,7 +222,7 @@ public function testGenerationForSpecificStore() $this->anchorUrlRewriteGenerator->expects($this->any())->method('generate') ->willReturn([]); - $this->categoryRepository->expects($this->once())->method('get')->willReturn($this->categoryMock); + $this->categoryRepositoryMock->expects($this->once())->method('get')->willReturn($this->categoryMock); $this->assertEquals( ['category-1_1' => $canonical], From 009762500fc1a45f9deac4e2628bdfbcaf61c62c Mon Sep 17 00:00:00 2001 From: Barny Shergold Date: Wed, 8 Jul 2020 01:49:09 +0100 Subject: [PATCH 0100/1013] Updated code as per failed test --- .../CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index 5879a62a85954..cf01c825e9231 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -237,8 +237,7 @@ public function isCategoryProperForGenerating(Category $category, $storeId) } /** - * Checks if URL key has been changed for provided category and returns reloaded category, - * in other case - returns provided category. + * Checks if URL key has been changed for provided category and returns reloaded category in other case - returns provided category. * * Category should be loaded per appropriate store at all times. This is because whilst the URL key on the * category in focus might be unchanged, parent category URL keys might be. If the category store ID From 7ee8e09fdf8d2d88eca262688b5bf14170b124c6 Mon Sep 17 00:00:00 2001 From: Barny Shergold Date: Wed, 8 Jul 2020 09:42:42 +0100 Subject: [PATCH 0101/1013] Updated code as per failed test --- .../CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index cf01c825e9231..165f9c07127e7 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -237,8 +237,11 @@ public function isCategoryProperForGenerating(Category $category, $storeId) } /** - * Checks if URL key has been changed for provided category and returns reloaded category in other case - returns provided category. + * Check if URL key has been changed * + * Checks if URL key has been changed for provided category and returns reloaded category, + * in other case - returns provided category. + * * Category should be loaded per appropriate store at all times. This is because whilst the URL key on the * category in focus might be unchanged, parent category URL keys might be. If the category store ID * and passed store ID are the same then return current category as it is correct but may have changed in memory From 54457b42ba1c7304a794598b26cadd829fa6cbe0 Mon Sep 17 00:00:00 2001 From: Barny Shergold Date: Wed, 8 Jul 2020 10:23:05 +0100 Subject: [PATCH 0102/1013] Updated code as per failed test --- .../CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php index 165f9c07127e7..7bf1da2b814e3 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductScopeRewriteGenerator.php @@ -241,7 +241,7 @@ public function isCategoryProperForGenerating(Category $category, $storeId) * * Checks if URL key has been changed for provided category and returns reloaded category, * in other case - returns provided category. - * + * * Category should be loaded per appropriate store at all times. This is because whilst the URL key on the * category in focus might be unchanged, parent category URL keys might be. If the category store ID * and passed store ID are the same then return current category as it is correct but may have changed in memory From ad85143801deb724aed15c484d735fe83c8ec274 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 8 Jul 2020 12:40:15 +0300 Subject: [PATCH 0103/1013] MC-35207: Improve customer custom attribute value validation --- .../testsuite/Magento/Customer/Api/CustomerRepositoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d0ca88ae237f..75e7fea036486 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -296,7 +296,7 @@ public function testDeleteCustomerNonAuthorized(): void $this->assertEquals(HTTPExceptionCodes::HTTP_UNAUTHORIZED, $e->getCode()); } /** @var Customer $data */ - $data = $this->_getCustomerData($customerData[Customer::ID]); + $data = $this->getCustomerData($customerData[Customer::ID]); $this->assertNotNull($data->getId()); } From 62171192a3bb5256c1e5f4e0917415cd4c7f9b33 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra Date: Thu, 9 Jul 2020 13:15:13 +0300 Subject: [PATCH 0104/1013] MC-35758: Fix static test Magento.Test.Integrity.ComposerTest.testValidComposerJson after merging 2.4-develop into 2.4.1-develop --- composer.lock | 195 +------------------------------------------------- 1 file changed, 2 insertions(+), 193 deletions(-) diff --git a/composer.lock b/composer.lock index e5614cfd0ac99..b587f2a362e0c 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": "f3674961f96b48fdd025a6c94610c8eb", + "content-hash": "92dbe431360d97af80030834b46dd77d", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -206,16 +206,6 @@ "ssl", "tls" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], "time": "2020-04-08T08:27:21+00:00" }, { @@ -462,12 +452,6 @@ "Xdebug", "performance" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - } - ], "time": "2020-03-01T12:26:26+00:00" }, { @@ -3924,20 +3908,6 @@ "x.509", "x509" ], - "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-04-04T23:17:33+00:00" }, { @@ -4474,20 +4444,6 @@ ], "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" }, { @@ -4666,20 +4622,6 @@ ], "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" }, { @@ -4729,20 +4671,6 @@ ], "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" }, { @@ -5939,12 +5867,6 @@ "functional testing", "unit testing" ], - "funding": [ - { - "url": "https://opencollective.com/codeception", - "type": "open_collective" - } - ], "time": "2020-05-24T13:58:47+00:00" }, { @@ -6517,20 +6439,6 @@ "redis", "xcache" ], - "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-05-27T16:24:54+00:00" }, { @@ -6654,20 +6562,6 @@ "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" }, { @@ -6730,20 +6624,6 @@ "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" }, { @@ -9855,20 +9735,6 @@ ], "description": "A generic function and convention to trigger deprecation notices", "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-27T08:34:37+00:00" }, { @@ -9930,20 +9796,6 @@ ], "description": "Symfony HttpFoundation 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-24T12:18:07+00:00" }, { @@ -10007,20 +9859,6 @@ "mime", "mime-type" ], - "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-25T12:33:44+00:00" }, { @@ -10196,20 +10034,6 @@ "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-05-12T16:47:27+00:00" }, { @@ -10323,20 +10147,6 @@ ], "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" }, { @@ -10727,6 +10537,5 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } From de13944bfce410f7c65ab77900839e4997a3d5a9 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Thu, 9 Jul 2020 11:52:05 +0300 Subject: [PATCH 0105/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - login to correct Store View. --- .../Controller/Adminhtml/Login/Login.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 77eb63c59eaee..e3ef361d11743 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -26,6 +26,7 @@ use Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface; use Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\StoreSwitcher\ManageStoreCookie; /** * Login as customer action @@ -87,6 +88,11 @@ class Login extends Action implements HttpGetActionInterface */ private $share; + /** + * @var ManageStoreCookie + */ + private $manageStoreCookie; + /** * @param Context $context * @param Session $authSession @@ -98,6 +104,8 @@ class Login extends Action implements HttpGetActionInterface * @param DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser * @param Url $url * @param Share $share + * @param ManageStoreCookie $manageStoreCookie + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, @@ -109,7 +117,8 @@ public function __construct( SaveAuthenticationDataInterface $saveAuthenticationData, DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, Url $url, - Share $share = null + ?Share $share = null, + ?ManageStoreCookie $manageStoreCookie = null ) { parent::__construct($context); @@ -122,6 +131,7 @@ public function __construct( $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; $this->url = $url; $this->share = $share ?? ObjectManager::getInstance()->get(Share::class); + $this->manageStoreCookie = $manageStoreCookie ?? ObjectManager::getInstance()->get(ManageStoreCookie::class); } /** @@ -195,10 +205,17 @@ public function execute(): ResultInterface */ private function getLoginProceedRedirectUrl(string $secret, int $storeId): string { - $store = $this->storeManager->getStore($storeId); + $targetStore = $this->storeManager->getStore($storeId); - return $this->url - ->setScope($store) + $redirectUrl = $this->url + ->setScope($targetStore) ->getUrl('loginascustomer/login/index', ['secret' => $secret, '_nosid' => true]); + + if (!$targetStore->isUseStoreInUrl()) { + $fromStore = $this->storeManager->getStore(); + $redirectUrl = $this->manageStoreCookie->switch($fromStore, $targetStore, $redirectUrl); + } + + return $redirectUrl; } } From 3bd5fa090b9c4a597220af07281b894b38372314 Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Mon, 13 Jul 2020 18:50:46 +0100 Subject: [PATCH 0106/1013] Fix unit test --- .../Test/Unit/Model/Category/DataProviderTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php index e2c37c904ee82..b824929de0733 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php @@ -20,6 +20,8 @@ use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\RequestInterface; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Config\Data; +use Magento\Framework\Config\DataInterfaceFactory; use Magento\Framework\Registry; use Magento\Framework\Stdlib\ArrayUtils; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -151,6 +153,15 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); + $dataMock = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiConfigFactory = $this->getMockBuilder(DataInterfaceFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->uiConfigFactory->method('create') + ->willReturn($dataMock); + $this->fileInfo = $this->getMockBuilder(FileInfo::class) ->disableOriginalConstructor() ->getMock(); @@ -198,6 +209,7 @@ private function getModel() 'eavConfig' => $this->eavConfig, 'request' => $this->request, 'categoryFactory' => $this->categoryFactory, + 'uiConfigFactory' => $this->uiConfigFactory, 'pool' => $this->modifierPool, 'auth' => $this->auth, 'arrayUtils' => $this->arrayUtils, From de823ab06ab0fd65d9d831ae60442beba88d81dd Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 14 Jul 2020 10:54:00 +0300 Subject: [PATCH 0107/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - remove disabled Store Groups. --- .../Component/ConfirmationPopup/Options.php | 56 ++++++++++++------- .../confirmation-popup/store-view-ptions.html | 1 - 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 424fbc3faa2fe..8b0928c25678c 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -8,11 +8,14 @@ namespace Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\ConfirmationPopup; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Config\Share; use Magento\Framework\App\RequestInterface; use Magento\Framework\Data\OptionSourceInterface; use Magento\Framework\Escaper; +use Magento\Store\Model\Group; use Magento\Store\Model\System\Store as SystemStore; +use Magento\Store\Model\Website; /** * Store group options for Login As Customer confirmation pop-up. @@ -116,27 +119,10 @@ private function generateCurrentOptions(int $customerId): array $options = []; if ($customerId) { $customer = $this->customerRepository->getById($customerId); - $customerWebsiteId = $customer->getWebsiteId(); - $customerStoreId = $customer->getStoreId(); - $isGlobalScope = $this->share->isGlobalScope(); $websiteCollection = $this->systemStore->getWebsiteCollection(); - $groupCollection = $this->systemStore->getGroupCollection(); - /** @var \Magento\Store\Model\Website $website */ + /** @var Website $website */ foreach ($websiteCollection as $website) { - $groups = []; - /** @var \Magento\Store\Model\Group $group */ - foreach ($groupCollection as $group) { - if ($group->getWebsiteId() == $website->getId()) { - $storeViewIds = $group->getStoreIds(); - if (!empty($storeViewIds)) { - $name = $this->sanitizeName($group->getName()); - $groups[$name]['label'] = str_repeat(' ', 4) . $name; - $groups[$name]['value'] = array_values($storeViewIds)[0]; - $groups[$name]['disabled'] = !$isGlobalScope && $customerWebsiteId !== $website->getId(); - $groups[$name]['selected'] = in_array($customerStoreId, $storeViewIds) ? true : false; - } - } - } + $groups = $this->fillStoreGroupOptions($website, $customer); if (!empty($groups)) { $name = $this->sanitizeName($website->getName()); $options[$name]['label'] = $name; @@ -147,4 +133,36 @@ private function generateCurrentOptions(int $customerId): array return $options; } + + /** + * Fill Store Group options array. + * + * @param Website $website + * @param CustomerInterface $customer + * @return array + */ + private function fillStoreGroupOptions(Website $website, CustomerInterface $customer): array + { + $groups = []; + $groupCollection = $this->systemStore->getGroupCollection(); + $isGlobalScope = $this->share->isGlobalScope(); + $customerWebsiteId = $customer->getWebsiteId(); + $customerStoreId = $customer->getStoreId(); + /** @var Group $group */ + foreach ($groupCollection as $group) { + if ($group->getWebsiteId() == $website->getId()) { + $storeViewIds = $group->getStoreIds(); + if (!empty($storeViewIds) + && ($customerWebsiteId === $website->getId() || $isGlobalScope) + ) { + $name = $this->sanitizeName($group->getName()); + $groups[$name]['label'] = str_repeat(' ', 4) . $name; + $groups[$name]['value'] = array_values($storeViewIds)[0]; + $groups[$name]['selected'] = in_array($customerStoreId, $storeViewIds) ? true : false; + } + } + } + + return $groups; + } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html index 916a5583abe57..b7074798b80f5 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html +++ b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html @@ -19,7 +19,6 @@ <% _.each(website.value, function(group) { %> <% }); %> From 4d801c57e0c3774a28017b034066be5bf5965976 Mon Sep 17 00:00:00 2001 From: Timon de Groot Date: Wed, 15 Jul 2020 09:54:43 +0200 Subject: [PATCH 0108/1013] Fetch image color space after conversion --- lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 14292adff005f..5505ad8dc09e3 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -615,7 +615,7 @@ private function maybeConvertColorspace(): void { if ($this->colorspace === \Imagick::COLORSPACE_CMYK || $this->colorspace === \Imagick::COLORSPACE_UNDEFINED) { $this->_imageHandler->transformImageColorspace(\Imagick::COLORSPACE_SRGB); - $this->colorspace = \Imagick::COLORSPACE_SRGB; + $this->colorspace = $this->_imageHandler->getImageColorspace(); } } } From 94006bd7156346f68031480efa1bf54c3e25d8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= Date: Wed, 15 Jul 2020 14:57:40 +0200 Subject: [PATCH 0109/1013] Update ImageResizeCommandTest.php test if order of tests changes behavior here. --- .../Command/ImageResizeCommandTest.php | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php index 62dae6ba1c5e9..9df470c68e6e8 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php @@ -65,6 +65,20 @@ protected function setUp(): void $this->filesystem = $this->objectManager->get(Filesystem::class); $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); } + + + /** + * Test that catalog:image:resize command executes successfully in database storage mode + * with file missing from local folder + * + * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php + * @magentoDataFixture Magento/MediaStorage/_files/product_with_missed_image.php + */ + public function testDatabaseStorageMissingFile() + { + $this->tester->execute([]); + $this->assertStringContainsString('Product images resized successfully', $this->tester->getDisplay()); + } /** * Test that catalog:image:resize command executed successfully with missing image file @@ -109,17 +123,4 @@ public function testExecuteWithZeroByteImage() $this->assertStringContainsString('Wrong file', $this->tester->getDisplay()); $this->mediaDirectory->getDriver()->deleteFile($this->mediaDirectory->getAbsolutePath($this->fileName)); } - - /** - * Test that catalog:image:resize command executes successfully in database storage mode - * with file missing from local folder - * - * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php - * @magentoDataFixture Magento/MediaStorage/_files/product_with_missed_image.php - */ - public function testDatabaseStorageMissingFile() - { - $this->tester->execute([]); - $this->assertStringContainsString('Product images resized successfully', $this->tester->getDisplay()); - } } From 3f6d8c42a08cdf82f852d2c4e50903ad276488ee Mon Sep 17 00:00:00 2001 From: ogorkun Date: Wed, 15 Jul 2020 14:32:40 -0500 Subject: [PATCH 0110/1013] MC-34385: Filter fields allowing HTML --- .../Attribute/Backend/DefaultBackend.php | 94 +++++++++++ .../Model/ResourceModel/Eav/Attribute.php | 15 ++ .../Attribute/Backend/DefaultBackendTest.php | 111 ++++++++++++ app/etc/di.xml | 17 ++ .../HTML/ConfigurableWYSIWYGValidatorTest.php | 113 +++++++++++++ .../HTML/ConfigurableWYSIWYGValidator.php | 158 ++++++++++++++++++ .../HTML/WYSIWYGValidatorInterface.php | 25 +++ 7 files changed, 533 insertions(+) create mode 100644 app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/DefaultBackendTest.php create mode 100644 lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php create mode 100644 lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php create mode 100644 lib/internal/Magento/Framework/Validator/HTML/WYSIWYGValidatorInterface.php diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php b/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php new file mode 100644 index 0000000000000..e3b38bf7a578a --- /dev/null +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php @@ -0,0 +1,94 @@ +wysiwygValidator = $wysiwygValidator; + } + + /** + * Validate user HTML value. + * + * @param DataObject $object + * @return void + * @throws LocalizedException + */ + private function validateHtml(DataObject $object): void + { + $attribute = $this->getAttribute(); + $code = $attribute->getAttributeCode(); + if ($attribute instanceof Attribute && $attribute->getIsHtmlAllowedOnFront()) { + if ($object->getData($code) + && (!($object instanceof AbstractModel) || $object->getData($code) !== $object->getOrigData($code)) + ) { + try { + $this->wysiwygValidator->validate($object->getData($code)); + } catch (ValidationException $exception) { + $attributeException = new Exception( + __( + 'Using restricted HTML elements for "%1". %2', + $attribute->getName(), + $exception->getMessage() + ), + $exception + ); + $attributeException->setAttributeCode($code)->setPart('backend'); + throw $attributeException; + } + } + } + } + + /** + * @inheritDoc + */ + public function beforeSave($object) + { + parent::beforeSave($object); + $this->validateHtml($object); + + return $this; + } + + /** + * @inheritDoc + */ + public function validate($object) + { + $isValid = parent::validate($object); + if ($isValid) { + $this->validateHtml($object); + } + + return $isValid; + } +} diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index e1c90017327cd..b803695a94702 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -6,7 +6,9 @@ namespace Magento\Catalog\Model\ResourceModel\Eav; +use Magento\Catalog\Model\Attribute\Backend\DefaultBackend; use Magento\Catalog\Model\Attribute\LockValidatorInterface; +use Magento\Eav\Model\Entity; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; @@ -901,4 +903,17 @@ public function setIsFilterableInGrid($isFilterableInGrid) $this->setData(self::IS_FILTERABLE_IN_GRID, $isFilterableInGrid); return $this; } + + /** + * @inheritDoc + */ + protected function _getDefaultBackendModel() + { + $backend = parent::_getDefaultBackendModel(); + if ($backend === Entity::DEFAULT_BACKEND_MODEL) { + $backend = DefaultBackend::class; + } + + return $backend; + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/DefaultBackendTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/DefaultBackendTest.php new file mode 100644 index 0000000000000..36ec38841b7cc --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Attribute/Backend/DefaultBackendTest.php @@ -0,0 +1,111 @@ + [true, false, true, 'basic', 'value', false, true, false], + 'non-html-attribute' => [false, false, false, 'non-html', 'value', false, false, false], + 'empty-html-attribute' => [false, false, true, 'html', null, false, true, false], + 'invalid-html-attribute' => [false, false, false, 'html', 'value', false, true, true], + 'valid-html-attribute' => [false, true, false, 'html', 'value', false, true, false], + 'changed-invalid-html-attribute' => [false, false, true, 'html', 'value', true, true, true], + 'changed-valid-html-attribute' => [false, true, true, 'html', 'value', true, true, false] + ]; + } + + /** + * Test attribute validation. + * + * @param bool $isBasic + * @param bool $isValidated + * @param bool $isCatalogEntity + * @param string $code + * @param mixed $value + * @param bool $isChanged + * @param bool $isHtmlAttribute + * @param bool $exceptionThrown + * @dataProvider getAttributeConfigurations + */ + public function testValidate( + bool $isBasic, + bool $isValidated, + bool $isCatalogEntity, + string $code, + $value, + bool $isChanged, + bool $isHtmlAttribute, + bool $exceptionThrown + ): void { + if ($isBasic) { + $attributeMock = $this->createMock(BasicAttribute::class); + } else { + $attributeMock = $this->createMock(Attribute::class); + $attributeMock->expects($this->any()) + ->method('getIsHtmlAllowedOnFront') + ->willReturn($isHtmlAttribute); + } + $attributeMock->expects($this->any())->method('getAttributeCode')->willReturn($code); + + $validatorMock = $this->getMockForAbstractClass(WYSIWYGValidatorInterface::class); + if (!$isValidated) { + $validatorMock->expects($this->any()) + ->method('validate') + ->willThrowException(new ValidationException(__('HTML is invalid'))); + } else { + $validatorMock->expects($this->any())->method('validate'); + } + + if ($isCatalogEntity) { + $objectMock = $this->createMock(AbstractModel::class); + $objectMock->expects($this->any()) + ->method('getOrigData') + ->willReturn($isChanged ? $value .'-OLD' : $value); + } else { + $objectMock = $this->createMock(DataObject::class); + } + $objectMock->expects($this->any())->method('getData')->with($code)->willReturn($value); + + $model = new DefaultBackend($validatorMock); + $model->setAttribute($attributeMock); + + $actuallyThrownForSave = false; + try { + $model->beforeSave($objectMock); + } catch (AttributeException $exception) { + $actuallyThrownForSave = true; + } + $actuallyThrownForValidate = false; + try { + $model->validate($objectMock); + } catch (AttributeException $exception) { + $actuallyThrownForValidate = true; + } + $this->assertEquals($actuallyThrownForSave, $actuallyThrownForValidate); + $this->assertEquals($actuallyThrownForSave, $exceptionThrown); + } +} diff --git a/app/etc/di.xml b/app/etc/di.xml index 31cc5caf3ba67..9b85e09ac9611 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1832,4 +1832,21 @@

P(|c04#!yYy;_E9a|d) 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

+ + + + div + a + + + class + + + + href + + + + + diff --git a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php new file mode 100644 index 0000000000000..aef019b20f519 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php @@ -0,0 +1,113 @@ + [['div'], [], [], 'just text', true], + 'allowed-tag' => [['div'], [], [], 'just text and
a div
', true], + 'restricted-tag' => [['div', 'p'], [], [], 'text and

a p

,
a div
, a tr', false], + 'restricted-tag-wtih-attr' => [['div'], [], [], 'just text and

a p

', false], + 'allowed-tag-with-attr' => [['div'], [], [], 'just text and
a div
', false], + 'multiple-tags' => [['div', 'p'], [], [], 'just text and
a div
and

a p

', true], + 'tags-with-attrs' => [ + ['div', 'p'], + ['class', 'style'], + [], + 'text and
a div
and

a p

', + true + ], + 'tags-with-restricted-attrs' => [ + ['div', 'p'], + ['class', 'align'], + [], + 'text and
a div
and

a p

', + false + ], + 'tags-with-specific-attrs' => [ + ['div', 'a', 'p'], + ['class'], + ['a' => ['href'], 'div' => ['style']], + '
a div
, an a' + .',

a p

', + true + ], + 'tags-with-specific-restricted-attrs' => [ + ['div', 'a'], + ['class'], + ['a' => ['href']], + 'text and
a div
and an a', + false + ], + 'invalid-tag-with-full-config' => [ + ['div', 'a', 'p'], + ['class', 'src'], + ['a' => ['href'], 'div' => ['style']], + '
a div
, an a' + .',

a p

, ', + false + ], + 'invalid-html' => [ + ['div', 'a', 'p'], + ['class', 'src'], + ['a' => ['href'], 'div' => ['style']], + 'some ', + true + ], + 'invalid-html-with-violations' => [ + ['div', 'a', 'p'], + ['class', 'src'], + ['a' => ['href'], 'div' => ['style']], + 'some some trs', + false + ] + ]; + } + + /** + * Test different configurations and content. + * + * @param array $allowedTags + * @param array $allowedAttr + * @param array $allowedTagAttrs + * @param string $html + * @param bool $isValid + * @return void + * @dataProvider getConfigurations + */ + public function testConfigurations( + array $allowedTags, + array $allowedAttr, + array $allowedTagAttrs, + string $html, + bool $isValid + ): void { + $validator = new ConfigurableWYSIWYGValidator($allowedTags, $allowedAttr, $allowedTagAttrs); + $valid = true; + try { + $validator->validate($html); + } catch (ValidationException $exception) { + $valid = false; + } + + self::assertEquals($isValid, $valid); + } +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php new file mode 100644 index 0000000000000..0b1993c044f6f --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php @@ -0,0 +1,158 @@ +allowedTags = array_unique($allowedTags); + $this->allowedAttributes = array_unique($allowedAttributes); + $this->attributesAllowedByTags = array_filter( + $attributesAllowedByTags, + function (string $tag) use ($allowedTags): bool { + return in_array($tag, $allowedTags, true); + }, + ARRAY_FILTER_USE_KEY + ); + } + + /** + * @inheritDoc + */ + public function validate(string $content): void + { + $dom = $this->loadHtml($content); + $xpath = new \DOMXPath($dom); + + //Validating tags + $found = $xpath->query( + $query='//*[' + . implode( + ' and ', + array_map( + function (string $tag): string { + return "name() != '$tag'"; + }, + array_merge($this->allowedTags, ['body', 'html']) + ) + ) + .']' + ); + if (count($found)) { + throw new ValidationException( + __('Allowed HTML tags are: %1', implode(', ', $this->allowedTags)) + ); + } + + //Validating attributes + if ($this->attributesAllowedByTags) { + foreach ($this->allowedTags as $tag) { + $allowed = $this->allowedAttributes; + if (!empty($this->attributesAllowedByTags[$tag])) { + $allowed = array_unique(array_merge($allowed, $this->attributesAllowedByTags[$tag])); + } + $allowedQuery = ''; + if ($allowed) { + $allowedQuery = '[' + . implode( + ' and ', + array_map( + function (string $attribute): string { + return "name() != '$attribute'"; + }, + $allowed + ) + ) + .']'; + } + $found = $xpath->query("//$tag/@*$allowedQuery"); + if (count($found)) { + throw new ValidationException( + __('Allowed HTML attributes for tag "%1" are: %2', $tag, implode(',', $allowed)) + ); + } + } + } else { + $allowed = ''; + if ($this->allowedAttributes) { + $allowed = '[' + . implode( + ' and ', + array_map( + function (string $attribute): string { + return "name() != '$attribute'"; + }, + $this->allowedAttributes + ) + ) + .']'; + } + $found = $xpath->query("//@*$allowed"); + if (count($found)) { + throw new ValidationException( + __('Allowed HTML attributes are: %1', implode(',', $this->allowedAttributes)) + ); + } + } + } + + /** + * Load DOM. + * + * @param string $content + * @return \DOMDocument + * @throws ValidationException + */ + private function loadHtml(string $content): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $loaded = true; + set_error_handler( + function () use (&$loaded) { + $loaded = false; + } + ); + $loaded = $dom->loadHTML("$content"); + restore_error_handler(); + if (!$loaded) { + throw new ValidationException(__('Invalid HTML content provided')); + } + + return $dom; + } +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/WYSIWYGValidatorInterface.php b/lib/internal/Magento/Framework/Validator/HTML/WYSIWYGValidatorInterface.php new file mode 100644 index 0000000000000..8045bc6a86c0b --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/WYSIWYGValidatorInterface.php @@ -0,0 +1,25 @@ + Date: Wed, 15 Jul 2020 16:23:21 -0500 Subject: [PATCH 0111/1013] MC-34385: Filter fields allowing HTML --- .../Cms/Command/WysiwygRestrictCommand.php | 70 +++++++++++++++ .../Magento/Cms/Model/Wysiwyg/Validator.php | 87 +++++++++++++++++++ .../Test/Unit/Model/Wysiwyg/ValidatorTest.php | 78 +++++++++++++++++ app/code/Magento/Cms/etc/config.xml | 1 + app/code/Magento/Cms/etc/di.xml | 6 ++ .../Command/WysiwygRestrictCommandTest.php | 78 +++++++++++++++++ 6 files changed, 320 insertions(+) create mode 100644 app/code/Magento/Cms/Command/WysiwygRestrictCommand.php create mode 100644 app/code/Magento/Cms/Model/Wysiwyg/Validator.php create mode 100644 app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Cms/Command/WysiwygRestrictCommandTest.php diff --git a/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php b/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php new file mode 100644 index 0000000000000..bafe98ad377f5 --- /dev/null +++ b/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php @@ -0,0 +1,70 @@ +configWriter = $configWriter; + $this->cache = $cache; + } + + /** + * @inheritDoc + */ + protected function configure() + { + $this->setName('cms:wysiwyg:restrict'); + $this->setDescription('Set whether to enforce user HTML content validation or show a warning instead'); + $this->setDefinition([new InputArgument('restrict', InputArgument::REQUIRED, 'y\n')]); + + parent::configure(); + } + + /** + * @inheritDoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $restrictArg = mb_strtolower((string)$input->getArgument('restrict')); + $restrict = $restrictArg === 'y' ? '1' : '0'; + $this->configWriter->saveConfig(Validator::CONFIG_PATH_THROW_EXCEPTION, $restrict); + $this->cache->cleanType('config'); + + $output->writeln('HTML user content validation is now ' .($restrictArg === 'y' ? 'enforced' : 'suggested')); + } +} diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Validator.php b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php new file mode 100644 index 0000000000000..c3eb14082ee98 --- /dev/null +++ b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php @@ -0,0 +1,87 @@ +validator = $validator; + $this->messages = $messages; + $this->config = $config; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function validate(string $content): void + { + $throwException = $this->config->isSetFlag(self::CONFIG_PATH_THROW_EXCEPTION); + try { + $this->validator->validate($content); + } catch (ValidationException $exception) { + if ($throwException) { + throw $exception; + } else { + $this->messages->addWarningMessage( + __('Temporarily allowed to save restricted HTML value. %1', $exception->getMessage()) + ); + } + } catch (\Throwable $exception) { + if ($throwException) { + throw $exception; + } else { + $this->messages->addWarningMessage(__('Invalid HTML provided')->render()); + $this->logger->error($exception); + } + } + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php new file mode 100644 index 0000000000000..b14ad81aa2c1a --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php @@ -0,0 +1,78 @@ + [true, new ValidationException(__('Invalid html')), true, false], + 'invalid-warning' => [false, new \RuntimeException('Invalid html'), false, true], + 'valid' => [false, null, false, false] + ]; + } + + /** + * Test validation. + * + * @param bool $isFlagSet + * @param \Throwable|null $thrown + * @param bool $exceptionThrown + * @param bool $warned + * @dataProvider getValidationCases + */ + public function testValidate(bool $isFlagSet, ?\Throwable $thrown, bool $exceptionThrown, bool $warned): void + { + $actuallyWarned = false; + + $configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $configMock->method('isSetFlag') + ->with(Validator::CONFIG_PATH_THROW_EXCEPTION) + ->willReturn($isFlagSet); + + $backendMock = $this->getMockForAbstractClass(WYSIWYGValidatorInterface::class); + if ($thrown) { + $backendMock->method('validate')->willThrowException($thrown); + } + + $messagesMock = $this->getMockForAbstractClass(ManagerInterface::class); + $messagesMock->method('addWarningMessage') + ->willReturnCallback( + function () use (&$actuallyWarned): void { + $actuallyWarned = true; + } + ); + + $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + + $validator = new Validator($backendMock, $messagesMock, $configMock, $loggerMock); + try { + $validator->validate('content'); + $actuallyThrown = false; + } catch (\Throwable $exception) { + $actuallyThrown = true; + } + $this->assertEquals($exceptionThrown, $actuallyThrown); + $this->assertEquals($warned, $actuallyWarned); + } +} diff --git a/app/code/Magento/Cms/etc/config.xml b/app/code/Magento/Cms/etc/config.xml index 7090bb7a1fd25..d7a9e172f59a6 100644 --- a/app/code/Magento/Cms/etc/config.xml +++ b/app/code/Magento/Cms/etc/config.xml @@ -24,6 +24,7 @@ enabled mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter + 0 diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 7fc8268eea5e0..67f88605a3e11 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -243,4 +243,10 @@ + + + DefaultWYSIWYGValidator + + + diff --git a/dev/tests/integration/testsuite/Magento/Cms/Command/WysiwygRestrictCommandTest.php b/dev/tests/integration/testsuite/Magento/Cms/Command/WysiwygRestrictCommandTest.php new file mode 100644 index 0000000000000..cd9844dc98811 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Command/WysiwygRestrictCommandTest.php @@ -0,0 +1,78 @@ +config = $objectManager->get(ReinitableConfigInterface::class); + $this->factory = $objectManager->get(WysiwygRestrictCommandFactory::class); + } + + /** + * "Execute" method cases. + * + * @return array + */ + public function getExecuteCases(): array + { + return [ + 'yes' => ['y', true], + 'no' => ['n', false], + 'no-but-different' => ['what', false] + ]; + } + + /** + * Test the command. + * + * @param string $argument + * @param bool $expectedFlag + * @return void + * @dataProvider getExecuteCases + * @magentoConfigFixture default_store cms/wysiwyg/force_valid 0 + */ + public function testExecute(string $argument, bool $expectedFlag): void + { + /** @var WysiwygRestrictCommand $model */ + $model = $this->factory->create(); + $tester = new CommandTester($model); + $tester->execute(['restrict' => $argument]); + + $this->config->reinit(); + $this->assertEquals($expectedFlag, $this->config->isSetFlag(Validator::CONFIG_PATH_THROW_EXCEPTION)); + } +} From 2047a293727a482a190bc26a008c5bb8b57efdfd Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Thu, 16 Jul 2020 10:51:26 +0300 Subject: [PATCH 0112/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - add disabled Store Groups. --- .../Ui/Customer/Component/ConfirmationPopup/Options.php | 8 ++++---- .../template/confirmation-popup/store-view-ptions.html | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 8b0928c25678c..c11337bbc5fe8 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -148,16 +148,16 @@ private function fillStoreGroupOptions(Website $website, CustomerInterface $cust $isGlobalScope = $this->share->isGlobalScope(); $customerWebsiteId = $customer->getWebsiteId(); $customerStoreId = $customer->getStoreId(); + $websiteId = $website->getId(); /** @var Group $group */ foreach ($groupCollection as $group) { - if ($group->getWebsiteId() == $website->getId()) { + if ($group->getWebsiteId() == $websiteId) { $storeViewIds = $group->getStoreIds(); - if (!empty($storeViewIds) - && ($customerWebsiteId === $website->getId() || $isGlobalScope) - ) { + if (!empty($storeViewIds)) { $name = $this->sanitizeName($group->getName()); $groups[$name]['label'] = str_repeat(' ', 4) . $name; $groups[$name]['value'] = array_values($storeViewIds)[0]; + $groups[$name]['disabled'] = !$isGlobalScope && $customerWebsiteId !== $websiteId; $groups[$name]['selected'] = in_array($customerStoreId, $storeViewIds) ? true : false; } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html index b7074798b80f5..916a5583abe57 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html +++ b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/template/confirmation-popup/store-view-ptions.html @@ -19,6 +19,7 @@ <% _.each(website.value, function(group) { %> <% }); %> From 2c1e0363e4d3064e4f766071f31587bb05a83b7d Mon Sep 17 00:00:00 2001 From: ogorkun Date: Thu, 16 Jul 2020 12:35:34 -0500 Subject: [PATCH 0113/1013] MC-34385: Filter fields allowing HTML --- .../Magento/Cms/Model/BlockRepository.php | 49 +++++++++++++++- app/code/Magento/Cms/Model/PageRepository.php | 18 ++++-- .../PageRepository/ValidationComposite.php | 15 ++++- .../Validator/ContentValidator.php | 57 +++++++++++++++++++ app/code/Magento/Cms/etc/di.xml | 13 +++++ .../HTML/ConfigurableWYSIWYGValidator.php | 3 + 6 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php diff --git a/app/code/Magento/Cms/Model/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index fa29cc9ff7631..317c3eeb6dcfb 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -12,10 +12,13 @@ use Magento\Cms\Model\ResourceModel\Block\CollectionFactory as BlockCollectionFactory; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -69,6 +72,11 @@ class BlockRepository implements BlockRepositoryInterface */ private $collectionProcessor; + /** + * @var WYSIWYGValidatorInterface + */ + private $wysiwygValidator; + /** * @param ResourceBlock $resource * @param BlockFactory $blockFactory @@ -79,6 +87,7 @@ class BlockRepository implements BlockRepositoryInterface * @param DataObjectProcessor $dataObjectProcessor * @param StoreManagerInterface $storeManager * @param CollectionProcessorInterface $collectionProcessor + * @param WYSIWYGValidatorInterface|null $wysiwygValidator */ public function __construct( ResourceBlock $resource, @@ -89,7 +98,8 @@ public function __construct( DataObjectHelper $dataObjectHelper, DataObjectProcessor $dataObjectProcessor, StoreManagerInterface $storeManager, - CollectionProcessorInterface $collectionProcessor = null + CollectionProcessorInterface $collectionProcessor = null, + ?WYSIWYGValidatorInterface $wysiwygValidator = null ) { $this->resource = $resource; $this->blockFactory = $blockFactory; @@ -100,13 +110,46 @@ public function __construct( $this->dataObjectProcessor = $dataObjectProcessor; $this->storeManager = $storeManager; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); + $this->wysiwygValidator = $wysiwygValidator + ?? ObjectManager::getInstance()->get(WYSIWYGValidatorInterface::class); + } + + /** + * Validate block's content. + * + * @param Data\BlockInterface|Block $block + * @throws CouldNotSaveException + * @return void + */ + private function validateHtml(Data\BlockInterface $block): void + { + $oldContent = null; + if ($block->getId()) { + if ($block instanceof Block && $block->getOrigData()) { + $oldContent = $block->getOrigData(Data\BlockInterface::CONTENT); + } else { + $oldBlock = $this->getById($block->getId()); + $oldContent = $oldBlock->getContent(); + } + } + if ($block->getContent() && $block->getContent() !== $oldContent) { + //Validate HTML content. + try { + $this->wysiwygValidator->validate($block->getContent()); + } catch (ValidationException $exception) { + throw new CouldNotSaveException( + __('Content HTML has restricted elements. %1', $exception->getMessage()), + $exception + ); + } + } } /** * Save Block data * * @param \Magento\Cms\Api\Data\BlockInterface $block - * @return Block + * @return Block|Data\BlockInterface * @throws CouldNotSaveException */ public function save(Data\BlockInterface $block) @@ -115,6 +158,8 @@ public function save(Data\BlockInterface $block) $block->setStoreId($this->storeManager->getStore()->getId()); } + $this->validateHtml($block); + try { $this->resource->save($block); } catch (\Exception $exception) { diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index 2de44b6691274..b09e9283870bc 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -133,15 +133,21 @@ public function __construct( private function validateLayoutUpdate(Data\PageInterface $page): void { //Persisted data - $savedPage = $page->getId() ? $this->getById($page->getId()) : null; + $oldData = null; + if ($page->getId() && $page instanceof Page) { + $oldData = $page->getOrigData(); + } //Custom layout update can be removed or kept as is. if ($page->getCustomLayoutUpdateXml() - && (!$savedPage || $page->getCustomLayoutUpdateXml() !== $savedPage->getCustomLayoutUpdateXml()) + && ( + !$oldData + || $page->getCustomLayoutUpdateXml() !== $oldData[Data\PageInterface::CUSTOM_LAYOUT_UPDATE_XML] + ) ) { throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); } if ($page->getLayoutUpdateXml() - && (!$savedPage || $page->getLayoutUpdateXml() !== $savedPage->getLayoutUpdateXml()) + && (!$oldData || $page->getLayoutUpdateXml() !== $oldData[Data\PageInterface::LAYOUT_UPDATE_XML]) ) { throw new \InvalidArgumentException('Custom layout updates must be selected from a file'); } @@ -161,12 +167,12 @@ public function save(\Magento\Cms\Api\Data\PageInterface $page) $page->setStoreId($storeId); } $pageId = $page->getId(); + if ($pageId && !($page instanceof Page && $page->getOrigData())) { + $page = $this->hydrator->hydrate($this->getById($pageId), $this->hydrator->extract($page)); + } try { $this->validateLayoutUpdate($page); - if ($pageId) { - $page = $this->hydrator->hydrate($this->getById($pageId), $this->hydrator->extract($page)); - } $this->resource->save($page); $this->identityMap->add($page); } catch (\Exception $exception) { diff --git a/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php index 9fd94d4c11e1c..fe8817f5f40b4 100644 --- a/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php +++ b/app/code/Magento/Cms/Model/PageRepository/ValidationComposite.php @@ -11,6 +11,8 @@ use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\PageRepositoryInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\HydratorInterface; /** * Validates and saves a page @@ -27,13 +29,20 @@ class ValidationComposite implements PageRepositoryInterface */ private $validators; + /** + * @var HydratorInterface + */ + private $hydrator; + /** * @param PageRepositoryInterface $repository * @param ValidatorInterface[] $validators + * @param HydratorInterface|null $hydrator */ public function __construct( PageRepositoryInterface $repository, - array $validators = [] + array $validators = [], + ?HydratorInterface $hydrator = null ) { foreach ($validators as $validator) { if (!$validator instanceof ValidatorInterface) { @@ -44,6 +53,7 @@ public function __construct( } $this->repository = $repository; $this->validators = $validators; + $this->hydrator = $hydrator ?? ObjectManager::getInstance()->get(HydratorInterface::class); } /** @@ -51,6 +61,9 @@ public function __construct( */ public function save(PageInterface $page) { + if ($page->getId()) { + $page = $this->hydrator->hydrate($this->getById($page->getId()), $this->hydrator->extract($page)); + } foreach ($this->validators as $validator) { $validator->validate($page); } diff --git a/app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php b/app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php new file mode 100644 index 0000000000000..6bca6103863fb --- /dev/null +++ b/app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php @@ -0,0 +1,57 @@ +wysiwygValidator = $wysiwygValidator; + } + + /** + * @inheritDoc + */ + public function validate(PageInterface $page): void + { + $oldValue = null; + if ($page->getId() && $page instanceof Page && $page->getOrigData()) { + $oldValue = $page->getOrigData(PageInterface::CONTENT); + } + + if ($page->getContent() && $page->getContent() !== $oldValue) { + try { + $this->wysiwygValidator->validate($page->getContent()); + } catch (ValidationException $exception) { + throw new ValidationException( + __('Content HTML contains restricted elements. %1', $exception->getMessage()), + $exception + ); + } + } + } +} diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 67f88605a3e11..1f2067a6e525b 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -233,6 +233,7 @@ Magento\Cms\Model\PageRepository Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator + Magento\Cms\Model\PageRepository\Validator\ContentValidator @@ -249,4 +250,16 @@ + + + + Magento\Cms\Command\WysiwygRestrictCommand + + + + + + Magento\Framework\EntityManager\AbstractModelHydrator + + diff --git a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php index 0b1993c044f6f..0e317f071ab39 100644 --- a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php +++ b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php @@ -56,6 +56,9 @@ function (string $tag) use ($allowedTags): bool { */ public function validate(string $content): void { + if (mb_strlen($content) === 0) { + return; + } $dom = $this->loadHtml($content); $xpath = new \DOMXPath($dom); From 1e50290e8fd249a7163cf1f5bc381a2674275b82 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Fri, 17 Jul 2020 10:45:31 -0500 Subject: [PATCH 0114/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/Model/Page.php | 33 ++++++++++- .../Validator/ContentValidator.php | 57 ------------------- app/code/Magento/Cms/etc/di.xml | 7 +-- 3 files changed, 33 insertions(+), 64 deletions(-) delete mode 100644 app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php diff --git a/app/code/Magento/Cms/Model/Page.php b/app/code/Magento/Cms/Model/Page.php index 28d013f45f1fa..35e049caea203 100644 --- a/app/code/Magento/Cms/Model/Page.php +++ b/app/code/Magento/Cms/Model/Page.php @@ -13,6 +13,8 @@ use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; /** * Cms Page Model @@ -64,6 +66,11 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface */ private $customLayoutRepository; + /** + * @var WYSIWYGValidatorInterface + */ + private $wysiwygValidator; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -71,6 +78,7 @@ class Page extends AbstractModel implements PageInterface, IdentityInterface * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection * @param array $data * @param CustomLayoutRepository|null $customLayoutRepository + * @param WYSIWYGValidatorInterface|null $wysiwygValidator */ public function __construct( \Magento\Framework\Model\Context $context, @@ -78,11 +86,14 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - ?CustomLayoutRepository $customLayoutRepository = null + ?CustomLayoutRepository $customLayoutRepository = null, + ?WYSIWYGValidatorInterface $wysiwygValidator = null ) { parent::__construct($context, $registry, $resource, $resourceCollection, $data); $this->customLayoutRepository = $customLayoutRepository ?? ObjectManager::getInstance()->get(CustomLayoutRepository::class); + $this->wysiwygValidator = $wysiwygValidator + ?? ObjectManager::getInstance()->get(WYSIWYGValidatorInterface::class); } /** @@ -615,6 +626,26 @@ public function beforeSave() $this->setData('layout_update_selected', $layoutUpdate); $this->customLayoutRepository->validateLayoutSelectedFor($this); + //Validating Content HTML. + $oldValue = null; + if ($this->getId()) { + if ($this->getOrigData()) { + $oldValue = $this->getOrigData(self::CONTENT); + } elseif (array_key_exists(self::CONTENT, $this->getStoredData())) { + $oldValue = $this->getStoredData()[self::CONTENT]; + } + } + if ($this->getContent() && $this->getContent() !== $oldValue) { + try { + $this->wysiwygValidator->validate($this->getContent()); + } catch (ValidationException $exception) { + throw new ValidationException( + __('Content HTML contains restricted elements. %1', $exception->getMessage()), + $exception + ); + } + } + return parent::beforeSave(); } diff --git a/app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php b/app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php deleted file mode 100644 index 6bca6103863fb..0000000000000 --- a/app/code/Magento/Cms/Model/PageRepository/Validator/ContentValidator.php +++ /dev/null @@ -1,57 +0,0 @@ -wysiwygValidator = $wysiwygValidator; - } - - /** - * @inheritDoc - */ - public function validate(PageInterface $page): void - { - $oldValue = null; - if ($page->getId() && $page instanceof Page && $page->getOrigData()) { - $oldValue = $page->getOrigData(PageInterface::CONTENT); - } - - if ($page->getContent() && $page->getContent() !== $oldValue) { - try { - $this->wysiwygValidator->validate($page->getContent()); - } catch (ValidationException $exception) { - throw new ValidationException( - __('Content HTML contains restricted elements. %1', $exception->getMessage()), - $exception - ); - } - } - } -} diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 1f2067a6e525b..1837aaca74789 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -233,8 +233,8 @@ Magento\Cms\Model\PageRepository Magento\Cms\Model\PageRepository\Validator\LayoutUpdateValidator - Magento\Cms\Model\PageRepository\Validator\ContentValidator + Magento\Framework\EntityManager\AbstractModelHydrator @@ -257,9 +257,4 @@ - - - Magento\Framework\EntityManager\AbstractModelHydrator - - From 9b094232f14e1677fac4898b6fff1d0e53f032eb Mon Sep 17 00:00:00 2001 From: ogorkun Date: Fri, 17 Jul 2020 15:53:44 -0500 Subject: [PATCH 0115/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/Model/Block.php | 57 +++++++++++++++++-- .../Magento/Cms/Model/BlockRepository.php | 51 ++++------------- app/code/Magento/Cms/etc/di.xml | 1 + 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/app/code/Magento/Cms/Model/Block.php b/app/code/Magento/Cms/Model/Block.php index 9da444c72e80c..ab8d65399f37c 100644 --- a/app/code/Magento/Cms/Model/Block.php +++ b/app/code/Magento/Cms/Model/Block.php @@ -6,8 +6,15 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Validation\ValidationException; +use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; +use Magento\Framework\Model\Context; +use Magento\Framework\Registry; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Data\Collection\AbstractDb; /** * CMS block model @@ -40,6 +47,32 @@ class Block extends AbstractModel implements BlockInterface, IdentityInterface */ protected $_eventPrefix = 'cms_block'; + /** + * @var WYSIWYGValidatorInterface + */ + private $wysiwygValidator; + + /** + * @param Context $context + * @param Registry $registry + * @param AbstractResource|null $resource + * @param AbstractDb|null $resourceCollection + * @param array $data + * @param WYSIWYGValidatorInterface|null $wysiwygValidator + */ + public function __construct( + Context $context, + Registry $registry, + AbstractResource $resource = null, + AbstractDb $resourceCollection = null, + array $data = [], + ?WYSIWYGValidatorInterface $wysiwygValidator = null + ) { + parent::__construct($context, $registry, $resource, $resourceCollection, $data); + $this->wysiwygValidator = $wysiwygValidator + ?? ObjectManager::getInstance()->get(WYSIWYGValidatorInterface::class); + } + /** * Construct. * @@ -63,12 +96,26 @@ public function beforeSave() } $needle = 'block_id="' . $this->getId() . '"'; - if (false == strstr($this->getContent(), (string) $needle)) { - return parent::beforeSave(); + if (strstr($this->getContent(), (string) $needle) !== false) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Make sure that static block content does not reference the block itself.') + ); } - throw new \Magento\Framework\Exception\LocalizedException( - __('Make sure that static block content does not reference the block itself.') - ); + parent::beforeSave(); + + //Validating HTML content. + if ($this->getContent() && $this->getContent() !== $this->getOrigData(self::CONTENT)) { + try { + $this->wysiwygValidator->validate($this->getContent()); + } catch (ValidationException $exception) { + throw new ValidationException( + __('Content field contains restricted HTML elements. %1', $exception->getMessage()), + $exception + ); + } + } + + return $this; } /** diff --git a/app/code/Magento/Cms/Model/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index 317c3eeb6dcfb..f8129ca4a2961 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -17,9 +17,8 @@ use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; -use Magento\Framework\Validation\ValidationException; -use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\EntityManager\HydratorInterface; /** * Class BlockRepository @@ -73,9 +72,9 @@ class BlockRepository implements BlockRepositoryInterface private $collectionProcessor; /** - * @var WYSIWYGValidatorInterface + * @var HydratorInterface */ - private $wysiwygValidator; + private $hydrator; /** * @param ResourceBlock $resource @@ -87,7 +86,7 @@ class BlockRepository implements BlockRepositoryInterface * @param DataObjectProcessor $dataObjectProcessor * @param StoreManagerInterface $storeManager * @param CollectionProcessorInterface $collectionProcessor - * @param WYSIWYGValidatorInterface|null $wysiwygValidator + * @param HydratorInterface|null $hydrator */ public function __construct( ResourceBlock $resource, @@ -99,7 +98,7 @@ public function __construct( DataObjectProcessor $dataObjectProcessor, StoreManagerInterface $storeManager, CollectionProcessorInterface $collectionProcessor = null, - ?WYSIWYGValidatorInterface $wysiwygValidator = null + ?HydratorInterface $hydrator = null ) { $this->resource = $resource; $this->blockFactory = $blockFactory; @@ -110,46 +109,14 @@ public function __construct( $this->dataObjectProcessor = $dataObjectProcessor; $this->storeManager = $storeManager; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); - $this->wysiwygValidator = $wysiwygValidator - ?? ObjectManager::getInstance()->get(WYSIWYGValidatorInterface::class); - } - - /** - * Validate block's content. - * - * @param Data\BlockInterface|Block $block - * @throws CouldNotSaveException - * @return void - */ - private function validateHtml(Data\BlockInterface $block): void - { - $oldContent = null; - if ($block->getId()) { - if ($block instanceof Block && $block->getOrigData()) { - $oldContent = $block->getOrigData(Data\BlockInterface::CONTENT); - } else { - $oldBlock = $this->getById($block->getId()); - $oldContent = $oldBlock->getContent(); - } - } - if ($block->getContent() && $block->getContent() !== $oldContent) { - //Validate HTML content. - try { - $this->wysiwygValidator->validate($block->getContent()); - } catch (ValidationException $exception) { - throw new CouldNotSaveException( - __('Content HTML has restricted elements. %1', $exception->getMessage()), - $exception - ); - } - } + $this->hydrator = $hydrator ?? ObjectManager::getInstance()->get(HydratorInterface::class); } /** * Save Block data * * @param \Magento\Cms\Api\Data\BlockInterface $block - * @return Block|Data\BlockInterface + * @return Block * @throws CouldNotSaveException */ public function save(Data\BlockInterface $block) @@ -158,7 +125,9 @@ public function save(Data\BlockInterface $block) $block->setStoreId($this->storeManager->getStore()->getId()); } - $this->validateHtml($block); + if ($block->getId() && $block instanceof Block && !$block->getOrigData()) { + $block = $this->hydrator->hydrate($this->getById($block->getId()), $this->hydrator->extract($block)); + } try { $this->resource->save($block); diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 1837aaca74789..d79e805e25890 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -215,6 +215,7 @@ Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor + Magento\Framework\EntityManager\AbstractModelHydrator From 1abdd4794847e779253487a50f04b711dbf801cd Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Fri, 17 Jul 2020 16:09:03 -0500 Subject: [PATCH 0116/1013] MC-35389: Set Same Site attribute --- .../Stdlib/Cookie/CookieMetadata.php | 34 ++++++++- .../Stdlib/Cookie/PhpCookieManager.php | 18 +++-- .../Test/Unit/Cookie/PhpCookieManagerTest.php | 69 +++++++++++++++---- .../Unit/Cookie/PublicCookieMetadataTest.php | 1 + .../Unit/Cookie/_files/setcookie_mock.php | 19 +++-- 5 files changed, 114 insertions(+), 27 deletions(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index 2b4cddf242113..99c32a1121f82 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Stdlib\Cookie; /** - * Class CookieMetadata + * Cookie Attributes * @api */ class CookieMetadata @@ -19,6 +19,12 @@ class CookieMetadata const KEY_SECURE = 'secure'; const KEY_HTTP_ONLY = 'http_only'; const KEY_DURATION = 'duration'; + const KEY_SAME_SITE = 'samesite'; + const SAME_SITE_ALLOWED_VALUES = [ + 'strict' => 'Strict', + 'lax' => 'Lax', + 'none' => 'None', + ]; /**#@-*/ /**#@-*/ @@ -135,4 +141,30 @@ public function getSecure() { return $this->get(self::KEY_SECURE); } + + /** + * Setter for Cookie SameSite attribute + * + * @param string|null $sameSite + * @return $this + */ + public function setSameSite($sameSite) + { + if (! array_key_exists(strtolower($sameSite), self::SAME_SITE_ALLOWED_VALUES)) { + throw new \InvalidArgumentException( + 'Invalid argument provided for SameSite directive expected one of: Strict, Lax or None' + ); + } + return $this->set(self::KEY_SAME_SITE, $sameSite); + } + + /** + * Get Same Site Flag + * + * @return bool|null + */ + public function getSameSite() + { + return $this->get(self::KEY_SAME_SITE); + } } diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php index dff31a897e1ac..b456208fd41f5 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php @@ -141,11 +141,14 @@ protected function setCookie($name, $value, array $metadataArray) $phpSetcookieSuccess = setcookie( $name, $value, - $expire, - $this->extractValue(CookieMetadata::KEY_PATH, $metadataArray, ''), - $this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, ''), - $this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false), - $this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false) + [ + 'expires' => $expire, + 'path' => $this->extractValue(CookieMetadata::KEY_PATH, $metadataArray, ''), + 'domain' => $this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, ''), + 'secure' => $this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false), + 'httponly' => $this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false), + 'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, 'None') + ] ); if (!$phpSetcookieSuccess) { @@ -164,6 +167,7 @@ protected function setCookie($name, $value, array $metadataArray) /** * Retrieve the size of a cookie. + * * The size of a cookie is determined by the length of 'name=value' portion of the cookie. * * @param string $name @@ -177,8 +181,7 @@ private function sizeOfCookie($name, $value) } /** - * Determines whether or not it is possible to send the cookie, based on the number of cookies that already - * exist and the size of the cookie. + * Determines ability to send cookies, based on the number of existing cookies and cookie size * * @param string $name * @param string|null $value @@ -249,6 +252,7 @@ private function computeExpirationTime(array $metadataArray) /** * Determines the value to be used as a $parameter. + * * If $metadataArray[$parameter] is not set, returns the $defaultValue. * * @param string $parameter diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php index 84e6911266276..ffefdfdf9af62 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php @@ -196,6 +196,7 @@ public function testDeleteCookie() 'metadata' => [ 'domain' => 'magento.url', 'path' => '/backend', + 'samesite' => 'Strict' ] ] ); @@ -346,6 +347,7 @@ public function testSetSensitiveCookieWithPathAndDomain() ], ] ); + $sensitiveCookieMetadata->setSameSite('Strict'); $this->scopeMock->expects($this->once()) ->method('getSensitiveCookieMetadata') @@ -402,6 +404,7 @@ public function testSetPublicCookieDefaultValues() ], ] ); + $publicCookieMetadata->setSameSite('Lax'); $this->scopeMock->expects($this->once()) ->method('getPublicCookieMetadata') @@ -430,6 +433,7 @@ public function testSetPublicCookieSomeFieldsSet() 'domain' => 'magento.url', 'path' => '/backend', 'http_only' => true, + 'samesite' => 'Lax' ], ] ); @@ -501,6 +505,7 @@ public function testSetCookieSizeTooLarge() 'secure' => false, 'http_only' => false, 'duration' => 3600, + 'samesite' => 'Strict' ], ] ); @@ -580,7 +585,7 @@ public function testSetTooManyCookies() * Suppressing UnusedFormalParameter, since PHPMD doesn't detect the callback call. * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public static function assertCookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly) + public static function assertCookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly, $sameSite) { if (self::EXCEPTION_COOKIE_NAME == $name) { return false; @@ -605,7 +610,8 @@ private static function assertDeleteCookie( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::DELETE_COOKIE_NAME, $name); self::assertEquals('', $value); @@ -614,6 +620,7 @@ private static function assertDeleteCookie( self::assertFalse($httpOnly); self::assertEquals('magento.url', $domain); self::assertEquals('/backend', $path); + self::assertEquals('Strict', $sameSite); } /** @@ -629,7 +636,8 @@ private static function assertDeleteCookieWithNoMetadata( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::DELETE_COOKIE_NAME_NO_METADATA, $name); self::assertEquals('', $value); @@ -638,6 +646,7 @@ private static function assertDeleteCookieWithNoMetadata( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); + self::assertEquals('None', $sameSite); } /** @@ -653,7 +662,8 @@ private static function assertSensitiveCookieWithNoMetaDataHttps( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::SENSITIVE_COOKIE_NAME_NO_METADATA_HTTPS, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -662,6 +672,7 @@ private static function assertSensitiveCookieWithNoMetaDataHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); + self::assertEquals('None', $sameSite); } /** @@ -677,7 +688,8 @@ private static function assertSensitiveCookieWithNoMetaDataNotHttps( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::SENSITIVE_COOKIE_NAME_NO_METADATA_NOT_HTTPS, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -686,6 +698,7 @@ private static function assertSensitiveCookieWithNoMetaDataNotHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); + self::assertEquals('None', $sameSite); } /** @@ -701,7 +714,8 @@ private static function assertSensitiveCookieNoDomainNoPath( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::SENSITIVE_COOKIE_NAME_NO_DOMAIN_NO_PATH, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -710,6 +724,7 @@ private static function assertSensitiveCookieNoDomainNoPath( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); + self::assertEquals('None', $sameSite); } /** @@ -725,7 +740,8 @@ private static function assertSensitiveCookieWithDomainAndPath( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::SENSITIVE_COOKIE_NAME_WITH_DOMAIN_AND_PATH, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -734,6 +750,7 @@ private static function assertSensitiveCookieWithDomainAndPath( self::assertTrue($httpOnly); self::assertEquals('magento.url', $domain); self::assertEquals('/backend', $path); + self::assertEquals('Strict', $sameSite); } /** @@ -749,7 +766,8 @@ private static function assertPublicCookieWithNoMetaData( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::PUBLIC_COOKIE_NAME_NO_METADATA, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -758,6 +776,7 @@ private static function assertPublicCookieWithNoMetaData( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); + self::assertEquals('None', $sameSite); } /** @@ -773,7 +792,8 @@ private static function assertPublicCookieWithNoDomainNoPath( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::PUBLIC_COOKIE_NAME_NO_METADATA, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -782,6 +802,7 @@ private static function assertPublicCookieWithNoDomainNoPath( self::assertTrue($httpOnly); self::assertEquals('magento.url', $domain); self::assertEquals('/backend', $path); + self::assertEquals('None', $sameSite); } /** @@ -797,7 +818,8 @@ private static function assertPublicCookieWithDefaultValues( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::PUBLIC_COOKIE_NAME_DEFAULT_VALUES, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -806,6 +828,7 @@ private static function assertPublicCookieWithDefaultValues( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); + self::assertEquals('Lax', $sameSite); } /** @@ -821,7 +844,8 @@ private static function assertPublicCookieWithSomeFieldSet( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::PUBLIC_COOKIE_NAME_SOME_FIELDS_SET, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -830,6 +854,8 @@ private static function assertPublicCookieWithSomeFieldSet( self::assertTrue($httpOnly); self::assertEquals('magento.url', $domain); self::assertEquals('/backend', $path); + self::assertEquals('/backend', $path); + self::assertEquals('Lax', $sameSite); } /** @@ -845,7 +871,8 @@ private static function assertCookieSize( $path, $domain, $secure, - $httpOnly + $httpOnly, + $sameSite ) { self::assertEquals(self::MAX_COOKIE_SIZE_TEST_NAME, $name); self::assertEquals(self::COOKIE_VALUE, $value); @@ -854,6 +881,7 @@ private static function assertCookieSize( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); + self::assertEquals('None', $sameSite); } /** @@ -868,5 +896,22 @@ protected function stubGetCookie($get, $default, $return) ->with($get, $default) ->willReturn($return); } + + public function testSetCookieInvalidSameSiteValue() + { + /** @var \Magento\Framework\Stdlib\Cookie\PublicCookieMetadata $cookieMetadata */ + $cookieMetadata = $this->objectManager->getObject( + CookieMetadata::class + ); + + try { + $cookieMetadata->setSameSite('default value'); + } catch (\InvalidArgumentException $e) { + $this->assertEquals( + 'Invalid argument provided for SameSite directive expected one of: Strict, Lax or None', + $e->getMessage() + ); + } + } } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php index f6fee3377f256..5dc13e7727e76 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php @@ -53,6 +53,7 @@ public function getMethodData() "getHttpOnly" => ["setHttpOnly", 'getHttpOnly', true], "getSecure" => ["setSecure", 'getSecure', true], "getDurationOneYear" => ["setDurationOneYear", 'getDuration', (3600*24*365)], + "getSameSite" => ["setSameSite", 'getSameSite', 'Strict'] ]; } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php index a6d2b53495ec0..a767c1eac9ad0 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php @@ -14,20 +14,25 @@ * * @param string $name * @param string $value - * @param int $expiry - * @param string $path - * @param string $domain - * @param bool $secure - * @param bool $httpOnly + * @param array $options * @return bool */ -function setcookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly) +function setcookie($name, $value, $options) { global $mockTranslateSetCookie; if (isset($mockTranslateSetCookie) && $mockTranslateSetCookie === true) { PhpCookieManagerTest::$isSetCookieInvoked = true; - return PhpCookieManagerTest::assertCookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly); + return PhpCookieManagerTest::assertCookie( + $name, + $value, + $options['expires'], + $options['path'], + $options['domain'], + $options['secure'], + $options['httponly'], + $options['samesite'] + ); } else { return call_user_func_array(__FUNCTION__, func_get_args()); } From 7c8a2b166947dd2a280007e880d9d1db7d8da99a Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Sat, 18 Jul 2020 13:09:59 -0500 Subject: [PATCH 0117/1013] MC-35389: Set same site attribute. Static fixes --- .../Framework/Stdlib/Cookie/CookieMetadata.php | 8 ++++++-- .../Framework/Stdlib/Cookie/PhpCookieManager.php | 4 +++- .../Test/Unit/Cookie/PhpCookieManagerTest.php | 15 +++++++-------- .../Test/Unit/Cookie/_files/setcookie_mock.php | 1 + 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index 99c32a1121f82..7ef1d816a163a 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\Stdlib\Cookie; +// phpcs:ignore Magento2.Functions.MethodDoubleUnderscore + /** * Cookie Attributes * @api @@ -59,7 +63,7 @@ public function __toArray() * @param string $domain * @return $this */ - public function setDomain($domain) + public function setDomain($domain): CookieMetadata { return $this->set(self::KEY_DOMAIN, $domain); } @@ -148,7 +152,7 @@ public function getSecure() * @param string|null $sameSite * @return $this */ - public function setSameSite($sameSite) + public function setSameSite($sameSite): CookieMetadata { if (! array_key_exists(strtolower($sameSite), self::SAME_SITE_ALLOWED_VALUES)) { throw new \InvalidArgumentException( diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php index b456208fd41f5..97b07d8813d1c 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Framework\Stdlib\Cookie; @@ -21,6 +22,7 @@ * stores the cookie. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class PhpCookieManager implements CookieManagerInterface { @@ -147,7 +149,7 @@ protected function setCookie($name, $value, array $metadataArray) 'domain' => $this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, ''), 'secure' => $this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false), 'httponly' => $this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false), - 'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, 'None') + 'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, 'Lax') ] ); diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php index ffefdfdf9af62..e5b9973d216e8 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php @@ -73,8 +73,6 @@ class PhpCookieManagerTest extends TestCase self::SENSITIVE_COOKIE_NAME_WITH_DOMAIN_AND_PATH => 'self::assertSensitiveCookieWithDomainAndPath', self::PUBLIC_COOKIE_NAME_NO_METADATA => 'self::assertPublicCookieWithNoMetaData', self::PUBLIC_COOKIE_NAME_DEFAULT_VALUES => 'self::assertPublicCookieWithDefaultValues', - self::PUBLIC_COOKIE_NAME_NO_METADATA => 'self::assertPublicCookieWithNoMetaData', - self::PUBLIC_COOKIE_NAME_DEFAULT_VALUES => 'self::assertPublicCookieWithDefaultValues', self::PUBLIC_COOKIE_NAME_SOME_FIELDS_SET => 'self::assertPublicCookieWithSomeFieldSet', self::MAX_COOKIE_SIZE_TEST_NAME => 'self::assertCookieSize', ]; @@ -590,6 +588,7 @@ public static function assertCookie($name, $value, $expiry, $path, $domain, $sec if (self::EXCEPTION_COOKIE_NAME == $name) { return false; } elseif (isset(self::$functionTestAssertionMapping[$name])) { + // phpcs:ignore call_user_func_array(self::$functionTestAssertionMapping[$name], func_get_args()); } else { self::fail('Non-tested case in mock setcookie()'); @@ -646,7 +645,7 @@ private static function assertDeleteCookieWithNoMetadata( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('None', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -672,7 +671,7 @@ private static function assertSensitiveCookieWithNoMetaDataHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('None', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -698,7 +697,7 @@ private static function assertSensitiveCookieWithNoMetaDataNotHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('None', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -724,7 +723,7 @@ private static function assertSensitiveCookieNoDomainNoPath( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('None', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -776,7 +775,7 @@ private static function assertPublicCookieWithNoMetaData( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('None', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -881,7 +880,7 @@ private static function assertCookieSize( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('None', $sameSite); + self::assertEquals('Lax', $sameSite); } /** diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php index a767c1eac9ad0..1c01631a94ab2 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/_files/setcookie_mock.php @@ -34,6 +34,7 @@ function setcookie($name, $value, $options) $options['samesite'] ); } else { + // phpcs:ignore return call_user_func_array(__FUNCTION__, func_get_args()); } } From f39fe6f756537d9577d5943d4d0cea992d34f18a Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Sat, 18 Jul 2020 20:26:59 -0500 Subject: [PATCH 0118/1013] MC-35389: Set same site attribute. Fix strict type error. --- .../Magento/Framework/Stdlib/Cookie/PhpCookieManager.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php index 97b07d8813d1c..a5fe6f6c61506 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php @@ -100,7 +100,7 @@ public function __construct( public function setSensitiveCookie($name, $value, SensitiveCookieMetadata $metadata = null) { $metadataArray = $this->scope->getSensitiveCookieMetadata($metadata)->__toArray(); - $this->setCookie($name, $value, $metadataArray); + $this->setCookie((string)$name, (string)$value, $metadataArray); } /** @@ -120,7 +120,7 @@ public function setSensitiveCookie($name, $value, SensitiveCookieMetadata $metad public function setPublicCookie($name, $value, PublicCookieMetadata $metadata = null) { $metadataArray = $this->scope->getPublicCookieMetadata($metadata)->__toArray(); - $this->setCookie($name, $value, $metadataArray); + $this->setCookie((string)$name, (string)$value, $metadataArray); } /** From f0c645307e031f9c6b8beb31f18c801dc62c02e3 Mon Sep 17 00:00:00 2001 From: Pieter Hoste Date: Sun, 19 Jul 2020 11:07:26 +0200 Subject: [PATCH 0119/1013] Avoids indefinite loop of indexers being marked as invalid. --- app/code/Magento/Indexer/Model/Processor.php | 80 ++++++++++++++++--- .../Indexer/Test/Unit/Model/ProcessorTest.php | 8 ++ 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 534ea805bb8fc..01f530488fbe7 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -15,6 +15,11 @@ */ class Processor { + /** + * @var array + */ + private $sharedIndexesComplete = []; + /** * @var ConfigInterface */ @@ -60,32 +65,83 @@ public function __construct( */ public function reindexAllInvalid() { - $sharedIndexesComplete = []; foreach (array_keys($this->config->getIndexers()) as $indexerId) { /** @var Indexer $indexer */ $indexer = $this->indexerFactory->create(); $indexer->load($indexerId); $indexerConfig = $this->config->getIndexer($indexerId); + $sharedIndex = $indexerConfig['shared_index']; + if ($indexer->isInvalid()) { // Skip indexers having shared index that was already complete $sharedIndex = $indexerConfig['shared_index'] ?? null; - if (!in_array($sharedIndex, $sharedIndexesComplete)) { + if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { $indexer->reindexAll(); - } else { - /** @var \Magento\Indexer\Model\Indexer\State $state */ - $state = $indexer->getState(); - $state->setStatus(StateInterface::STATUS_WORKING); - $state->save(); - $state->setStatus(StateInterface::STATUS_VALID); - $state->save(); - } - if ($sharedIndex) { - $sharedIndexesComplete[] = $sharedIndex; + + if ($sharedIndex) { + $this->validateSharedIndex($sharedIndex); + } } } } } + /** + * Get indexer ids that have common shared index + * + * @param string $sharedIndex + * @return array + */ + private function getIndexerIdsBySharedIndex(string $sharedIndex): array + { + $indexers = $this->config->getIndexers(); + + $result = []; + foreach ($indexers as $indexerConfig) { + if ($indexerConfig['shared_index'] == $sharedIndex) { + $result[] = $indexerConfig['indexer_id']; + } + } + + return $result; + } + + /** + * Validate indexers by shared index ID + * + * @param string $sharedIndex + * @return $this + */ + private function validateSharedIndex(string $sharedIndex): self + { + if (empty($sharedIndex)) { + throw new \InvalidArgumentException( + 'The sharedIndex is an invalid shared index identifier. Verify the identifier and try again.' + ); + } + + $indexerIds = $this->getIndexerIdsBySharedIndex($sharedIndex); + if (empty($indexerIds)) { + return $this; + } + + foreach ($indexerIds as $indexerId) { + /** @var \Magento\Indexer\Model\Indexer $indexer */ + $indexer = $this->indexerFactory->create(); + $indexer->load($indexerId); + /** @var \Magento\Indexer\Model\Indexer\State $state */ + $state = $indexer->getState(); + $state->setStatus(StateInterface::STATUS_WORKING); + $state->save(); + $state->setStatus(StateInterface::STATUS_VALID); + $state->save(); + } + + $this->sharedIndexesComplete[] = $sharedIndex; + + return $this; + } + /** * Regenerate indexes for all indexers * diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index 7a06fb745ba89..9cc0277997289 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -85,6 +85,14 @@ public function testReindexAllInvalid() $this->configMock->expects($this->once())->method('getIndexers')->willReturn($indexers); + $this->configMock->expects($this->exactly(2)) + ->method('getIndexer') + ->willReturn( + [ + 'shared_index' => null + ] + ); + $state1Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); $state1Mock->expects( $this->once() From 5307bad69e56266eedf6fe80cd9d6da4c7ec0344 Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Mon, 20 Jul 2020 09:03:00 +0200 Subject: [PATCH 0120/1013] #29174 Add save_rewrites_history to categories via API --- .../Controller/Rest/InputParamsResolver.php | 43 ++++++++++++--- .../Rest/InputParamsResolverTest.php | 52 +++++++++++++++++-- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php index 4e8e3840693a5..cf849af0facc0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php @@ -8,6 +8,7 @@ namespace Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Webapi\Rest\Request as RestRequest; @@ -18,6 +19,8 @@ */ class InputParamsResolver { + const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; + /** * @var RestRequest */ @@ -32,7 +35,7 @@ public function __construct(RestRequest $request) } /** - * Add 'save_rewrites_history' param to the product data + * Add 'save_rewrites_history' param to the product and category data * * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject @@ -47,12 +50,27 @@ public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $requestBodyParams = $this->request->getBodyParams(); if ($this->isProductSaveCalled($serviceClassName, $serviceMethodName) - && $this->isCustomAttributesExists($requestBodyParams)) { + && $this->isCustomAttributesExists($requestBodyParams, 'product')) { foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { - if ($attribute['attribute_code'] === 'save_rewrites_history') { + if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { foreach ($result as $resultItem) { if ($resultItem instanceof \Magento\Catalog\Model\Product) { - $resultItem->setData('save_rewrites_history', (bool)$attribute['value']); + $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); + break 2; + } + } + break; + } + } + } + + if ($this->isCategorySaveCalled($serviceClassName, $serviceMethodName) + && $this->isCustomAttributesExists($requestBodyParams, 'category')) { + foreach ($requestBodyParams['category']['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { + foreach ($result as $resultItem) { + if ($resultItem instanceof \Magento\Catalog\Model\Category) { + $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); break 2; } } @@ -75,14 +93,27 @@ private function isProductSaveCalled(string $serviceClassName, string $serviceMe return $serviceClassName === ProductRepositoryInterface::class && $serviceMethodName === 'save'; } + /** + * Check that category save method called + * + * @param string $serviceClassName + * @param string $serviceMethodName + * @return bool + */ + private function isCategorySaveCalled(string $serviceClassName, string $serviceMethodName): bool + { + return $serviceClassName === CategoryRepositoryInterface::class && $serviceMethodName === 'save'; + } + /** * Check is any custom options exists in product data * * @param array $requestBodyParams + * @param string $entityCode * @return bool */ - private function isCustomAttributesExists(array $requestBodyParams): bool + private function isCustomAttributesExists(array $requestBodyParams, string $entityCode): bool { - return !empty($requestBodyParams['product']['custom_attributes']); + return !empty($requestBodyParams[$entityCode]['custom_attributes']); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php index 5edc463ac49f3..66210c95ff2cc 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php @@ -8,7 +8,9 @@ namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Webapi\Controller\Rest; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; use Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver as InputParamsResolverPlugin; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; @@ -74,6 +76,10 @@ class InputParamsResolverTest extends TestCase protected function setUp(): void { $this->saveRewritesHistory = 'save_rewrites_history'; + } + + public function testAfterResolveWithProduct() + { $this->requestBodyParams = [ 'product' => [ 'sku' => 'test', @@ -99,10 +105,7 @@ protected function setUp(): void 'request' => $this->request ] ); - } - public function testAfterResolve() - { $this->route->expects($this->once()) ->method('getServiceClass') ->willReturn(ProductRepositoryInterface::class); @@ -115,4 +118,47 @@ public function testAfterResolve() $this->plugin->afterResolve($this->subject, $this->result); } + + + public function testAfterResolveWithCategory() + { + $this->requestBodyParams = [ + 'category' => [ + 'name' => 'new name', + 'custom_attributes' => [ + ['attribute_code' => $this->saveRewritesHistory, 'value' => 1], + ['attribute_code' => 'url_key', 'value' => 'new name'] + ] + ] + ]; + + $this->route = $this->createPartialMock(Route::class, ['getServiceMethod', 'getServiceClass']); + $this->request = $this->createPartialMock(RestRequest::class, ['getBodyParams']); + $this->request->expects($this->any())->method('getBodyParams')->willReturn($this->requestBodyParams); + $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); + $this->subject->expects($this->any())->method('getRoute')->willReturn($this->route); + $this->category = $this->createPartialMock(Category::class, ['setData']); + + $this->result = [false, $this->category, 'test']; + + $this->objectManager = new ObjectManager($this); + $this->plugin = $this->objectManager->getObject( + InputParamsResolverPlugin::class, + [ + 'request' => $this->request + ] + ); + + $this->route->expects($this->once()) + ->method('getServiceClass') + ->willReturn(CategoryRepositoryInterface::class); + $this->route->expects($this->once()) + ->method('getServiceMethod') + ->willReturn('save'); + $this->category->expects($this->once()) + ->method('setData') + ->with($this->saveRewritesHistory, true); + + $this->plugin->afterResolve($this->subject, $this->result); + } } From 00176f5a3a0c6671556ff28b66f437ffb1970060 Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Mon, 20 Jul 2020 10:22:49 +0200 Subject: [PATCH 0121/1013] #29174 Transport save_rewrites_history in save * Support save_rewrites_history in save --- app/code/Magento/Catalog/Model/CategoryRepository.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 0ce52b966c32c..4a59676450677 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -82,6 +82,10 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); $existingData['store_id'] = $storeId; + if ($category->getData('save_rewrites_history', null) !== null) { + $existingData['save_rewrites_history'] = $category->getData('save_rewrites_history'); + } + if ($category->getId()) { $metadata = $this->getMetadataPool()->getMetadata( CategoryInterface::class From 3dcf9110e7147beebf1529880315dfc1daccedc7 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Mon, 20 Jul 2020 13:32:27 -0500 Subject: [PATCH 0122/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/Model/Wysiwyg/Validator.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Validator.php b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php index c3eb14082ee98..eb17a0f3127ea 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Validator.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php @@ -72,7 +72,10 @@ public function validate(string $content): void throw $exception; } else { $this->messages->addWarningMessage( - __('Temporarily allowed to save restricted HTML value. %1', $exception->getMessage()) + __( + 'Temporarily allowed to save HTML value that contains restricted elements. %1', + $exception->getMessage() + ) ); } } catch (\Throwable $exception) { From a962cced8cf519a90483dfcebcb98af29c8a269f Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Mon, 20 Jul 2020 16:00:47 -0500 Subject: [PATCH 0123/1013] MC-35389: Set same site attribute. Fix default value for same site. Static fixes. --- .../Framework/Stdlib/Cookie/CookieMetadata.php | 8 +++----- .../Framework/Stdlib/Cookie/PhpCookieManager.php | 2 +- .../Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php | 12 ++++++------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index 7ef1d816a163a..2c757691a6c91 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -7,8 +7,6 @@ namespace Magento\Framework\Stdlib\Cookie; -// phpcs:ignore Magento2.Functions.MethodDoubleUnderscore - /** * Cookie Attributes * @api @@ -48,11 +46,11 @@ public function __construct($metadata = []) /** * Returns an array representation of this metadata. * - * If a value has not yet been set then the key will not show up in the array. + * If a value has not yet been set then the key will not show up in the array * * @return array */ - public function __toArray() + public function __toArray() //phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames { return $this->metadata; } @@ -63,7 +61,7 @@ public function __toArray() * @param string $domain * @return $this */ - public function setDomain($domain): CookieMetadata + public function setDomain($domain) { return $this->set(self::KEY_DOMAIN, $domain); } diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php index a5fe6f6c61506..5cfd38e258145 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php @@ -149,7 +149,7 @@ protected function setCookie($name, $value, array $metadataArray) 'domain' => $this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, ''), 'secure' => $this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false), 'httponly' => $this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false), - 'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, 'Lax') + 'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, '') ] ); diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php index e5b9973d216e8..1a4c1af07ec2f 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php @@ -645,7 +645,7 @@ private static function assertDeleteCookieWithNoMetadata( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Lax', $sameSite); + self::assertEquals('', $sameSite); } /** @@ -671,7 +671,7 @@ private static function assertSensitiveCookieWithNoMetaDataHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Lax', $sameSite); + self::assertEquals('', $sameSite); } /** @@ -697,7 +697,7 @@ private static function assertSensitiveCookieWithNoMetaDataNotHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Lax', $sameSite); + self::assertEquals('', $sameSite); } /** @@ -723,7 +723,7 @@ private static function assertSensitiveCookieNoDomainNoPath( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Lax', $sameSite); + self::assertEquals('', $sameSite); } /** @@ -775,7 +775,7 @@ private static function assertPublicCookieWithNoMetaData( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Lax', $sameSite); + self::assertEquals('', $sameSite); } /** @@ -880,7 +880,7 @@ private static function assertCookieSize( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Lax', $sameSite); + self::assertEquals('', $sameSite); } /** From 4c99f0d4dc714de6fc49e2b7d2b5f9a28a243ec1 Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Tue, 21 Jul 2020 14:40:00 +0200 Subject: [PATCH 0124/1013] #29174 Make const private --- .../Plugin/Webapi/Controller/Rest/InputParamsResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php index cf849af0facc0..6d76005de5073 100644 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php @@ -19,7 +19,7 @@ */ class InputParamsResolver { - const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; + private const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; /** * @var RestRequest From f0ba72a4ba585e0f53f7d4bbb9de35f1a2ed49e1 Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Tue, 21 Jul 2020 15:05:32 +0200 Subject: [PATCH 0125/1013] #29174 Add API functional test for redirect generation for category updates --- .../Catalog/Api/CategoryRepositoryTest.php | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) 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..5df939b9d4fcb 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -218,6 +218,83 @@ public function testUpdate() $this->deleteCategory($categoryId); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testUpdateUrlKey() + { + $categoryId = 333; + $categoryData = [ + 'name' => 'Update Category Test Old Name', + 'custom_attributes' => [ + [ + 'attribute_code' => 'url_key', + 'value' => "Update Category Test Old Name", + ], + ], + ]; + $result = $this->updateCategory($categoryId, $categoryData); + $this->assertEquals($categoryId, $result['id']); + + + $categoryData = [ + 'name' => 'Update Category Test New Name', + 'custom_attributes' => [ + [ + 'attribute_code' => 'url_key', + 'value' => "Update Category Test New Name", + ], + [ + 'attribute_code' => 'save_rewrites_history', + 'value' => 1, + ], + ], + ]; + $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->assertEquals("Update Category Test New Name", $category->getName()); + // delete category to clean up auto-generated url rewrites + + + $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $data = [ + UrlRewrite::ENTITY_ID => $categoryId, + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::REDIRECT_TYPE => 0, + ]; + + $urlRewrite = $storage->findOneByData($data); + + // Assert that a url rewrite is auto-generated for the category created from the data fixture + $this->assertNotNull($urlRewrite); + $this->assertEquals(1, $urlRewrite->getIsAutogenerated()); + $this->assertEquals($categoryId, $urlRewrite->getEntityId()); + $this->assertEquals(CategoryUrlRewriteGenerator::ENTITY_TYPE, $urlRewrite->getEntityType()); + $this->assertEquals('update-category-test-new-name.html', $urlRewrite->getRequestPath()); + + + // check for the forward + $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $data = [ + UrlRewrite::ENTITY_ID => $categoryId, + UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, + UrlRewrite::REDIRECT_TYPE => 301, + ]; + + $urlRewrite = $storage->findOneByData($data); + + $this->assertNotNull($urlRewrite); + $this->assertEquals(0, $urlRewrite->getIsAutogenerated()); + $this->assertEquals($categoryId, $urlRewrite->getEntityId()); + $this->assertEquals(CategoryUrlRewriteGenerator::ENTITY_TYPE, $urlRewrite->getEntityType()); + $this->assertEquals('update-category-test-old-name.html', $urlRewrite->getRequestPath()); + + $this->deleteCategory($categoryId); + } + protected function getSimpleCategoryData($categoryData = []) { return [ From 0f983b209a5df48040dd9aa744847b19ce091bd2 Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Tue, 21 Jul 2020 15:12:15 +0200 Subject: [PATCH 0126/1013] #29174 Add API functional test for redirect generation for category updates --- .../Magento/Catalog/Api/CategoryRepositoryTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 5df939b9d4fcb..81e51cebc24c7 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -236,7 +236,6 @@ public function testUpdateUrlKey() $result = $this->updateCategory($categoryId, $categoryData); $this->assertEquals($categoryId, $result['id']); - $categoryData = [ 'name' => 'Update Category Test New Name', 'custom_attributes' => [ @@ -256,9 +255,8 @@ public function testUpdateUrlKey() $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); $category = $model->load($categoryId); $this->assertEquals("Update Category Test New Name", $category->getName()); - // delete category to clean up auto-generated url rewrites - + // check for the url rewrite for the new name $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); $data = [ UrlRewrite::ENTITY_ID => $categoryId, @@ -276,7 +274,7 @@ public function testUpdateUrlKey() $this->assertEquals('update-category-test-new-name.html', $urlRewrite->getRequestPath()); - // check for the forward + // check for the forward from the old name to the new name $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); $data = [ UrlRewrite::ENTITY_ID => $categoryId, From 1a9fc92f48a84dc8744e6254180e33743406de96 Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Tue, 21 Jul 2020 10:03:24 -0500 Subject: [PATCH 0127/1013] MC-35389: Set same site attribute. Fix punctuation in method signature. --- lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index 2c757691a6c91..83e1630f939d7 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -46,7 +46,7 @@ public function __construct($metadata = []) /** * Returns an array representation of this metadata. * - * If a value has not yet been set then the key will not show up in the array + * If a value has not yet been set then the key will not show up in the array. * * @return array */ From fdd36dea96bb8085a67509a3a492e013f0ffae44 Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Tue, 21 Jul 2020 15:35:09 -0500 Subject: [PATCH 0128/1013] MC-35389: Set same site attribute. Cookie manager default value updates. Fixes to unit tests. Cookie Metadata constant visibility updates. --- .../Stdlib/Cookie/CookieMetadata.php | 23 ++++----- .../Stdlib/Cookie/PhpCookieManager.php | 2 +- .../Stdlib/Cookie/PublicCookieMetadata.php | 13 ++++- .../Stdlib/Cookie/SensitiveCookieMetadata.php | 9 ++-- .../Test/Unit/Cookie/PhpCookieManagerTest.php | 21 ++++---- .../Unit/Cookie/PublicCookieMetadataTest.php | 48 ++++++++++++++++++- .../Cookie/SensitiveCookieMetadataTest.php | 3 ++ 7 files changed, 92 insertions(+), 27 deletions(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index 83e1630f939d7..8cec82d64199c 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -16,13 +16,13 @@ class CookieMetadata /**#@+ * Constant for metadata value key. */ - const KEY_DOMAIN = 'domain'; - const KEY_PATH = 'path'; - const KEY_SECURE = 'secure'; - const KEY_HTTP_ONLY = 'http_only'; - const KEY_DURATION = 'duration'; - const KEY_SAME_SITE = 'samesite'; - const SAME_SITE_ALLOWED_VALUES = [ + public const KEY_DOMAIN = 'domain'; + public const KEY_PATH = 'path'; + public const KEY_SECURE = 'secure'; + public const KEY_HTTP_ONLY = 'http_only'; + public const KEY_DURATION = 'duration'; + public const KEY_SAME_SITE = 'samesite'; + private const SAME_SITE_ALLOWED_VALUES = [ 'strict' => 'Strict', 'lax' => 'Lax', 'none' => 'None', @@ -150,22 +150,23 @@ public function getSecure() * @param string|null $sameSite * @return $this */ - public function setSameSite($sameSite): CookieMetadata + public function setSameSite(?string $sameSite): CookieMetadata { - if (! array_key_exists(strtolower($sameSite), self::SAME_SITE_ALLOWED_VALUES)) { + if (!array_key_exists(strtolower($sameSite), self::SAME_SITE_ALLOWED_VALUES)) { throw new \InvalidArgumentException( 'Invalid argument provided for SameSite directive expected one of: Strict, Lax or None' ); } + $sameSite = self::SAME_SITE_ALLOWED_VALUES[strtolower($sameSite)]; return $this->set(self::KEY_SAME_SITE, $sameSite); } /** * Get Same Site Flag * - * @return bool|null + * @return string|null */ - public function getSameSite() + public function getSameSite(): ?string { return $this->get(self::KEY_SAME_SITE); } diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php index 5cfd38e258145..a5fe6f6c61506 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PhpCookieManager.php @@ -149,7 +149,7 @@ protected function setCookie($name, $value, array $metadataArray) 'domain' => $this->extractValue(CookieMetadata::KEY_DOMAIN, $metadataArray, ''), 'secure' => $this->extractValue(CookieMetadata::KEY_SECURE, $metadataArray, false), 'httponly' => $this->extractValue(CookieMetadata::KEY_HTTP_ONLY, $metadataArray, false), - 'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, '') + 'samesite' => $this->extractValue(CookieMetadata::KEY_SAME_SITE, $metadataArray, 'Lax') ] ); diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php index ef40ea94a6d08..6e5e174e4e9f9 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php @@ -7,12 +7,23 @@ namespace Magento\Framework\Stdlib\Cookie; /** - * Class PublicCookieMetadata + * Public Cookie Attributes * * @api */ class PublicCookieMetadata extends CookieMetadata { + /** + * @param array $metadata + */ + public function __construct($metadata = []) + { + if (!isset($metadata[self::KEY_SAME_SITE])) { + $metadata[self::KEY_SAME_SITE] = 'Lax'; + } + parent::__construct($metadata); + } + /** * Set the number of seconds until the cookie expires * diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php index aab8e93160c8d..b913e49e77716 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php @@ -9,7 +9,7 @@ use Magento\Framework\App\RequestInterface; /** - * Class SensitiveCookieMetadata + * Sensitive Cookie Attributes * * The class has only methods extended from CookieMetadata * as path and domain are only data to be exposed by SensitiveCookieMetadata @@ -32,12 +32,15 @@ public function __construct(RequestInterface $request, $metadata = []) if (!isset($metadata[self::KEY_HTTP_ONLY])) { $metadata[self::KEY_HTTP_ONLY] = true; } + if (!isset($metadata[self::KEY_SAME_SITE])) { + $metadata[self::KEY_SAME_SITE] = 'Strict'; + } $this->request = $request; parent::__construct($metadata); } /** - * {@inheritdoc} + * @inheritdoc */ public function getSecure() { @@ -46,7 +49,7 @@ public function getSecure() } /** - * {@inheritdoc} + * @inheritdoc */ public function __toArray() { diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php index 1a4c1af07ec2f..d55a4200a5750 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php @@ -345,7 +345,6 @@ public function testSetSensitiveCookieWithPathAndDomain() ], ] ); - $sensitiveCookieMetadata->setSameSite('Strict'); $this->scopeMock->expects($this->once()) ->method('getSensitiveCookieMetadata') @@ -402,7 +401,6 @@ public function testSetPublicCookieDefaultValues() ], ] ); - $publicCookieMetadata->setSameSite('Lax'); $this->scopeMock->expects($this->once()) ->method('getPublicCookieMetadata') @@ -645,7 +643,7 @@ private static function assertDeleteCookieWithNoMetadata( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -671,7 +669,7 @@ private static function assertSensitiveCookieWithNoMetaDataHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('', $sameSite); + self::assertEquals('Strict', $sameSite); } /** @@ -697,7 +695,7 @@ private static function assertSensitiveCookieWithNoMetaDataNotHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('', $sameSite); + self::assertEquals('Strict', $sameSite); } /** @@ -723,7 +721,7 @@ private static function assertSensitiveCookieNoDomainNoPath( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('', $sameSite); + self::assertEquals('Strict', $sameSite); } /** @@ -775,7 +773,7 @@ private static function assertPublicCookieWithNoMetaData( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -880,7 +878,7 @@ private static function assertCookieSize( self::assertFalse($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -896,7 +894,12 @@ protected function stubGetCookie($get, $default, $return) ->willReturn($return); } - public function testSetCookieInvalidSameSiteValue() + /** + * Test Set Invalid Same Site Cookie + * + * @return void + */ + public function testSetCookieInvalidSameSiteValue(): void { /** @var \Magento\Framework\Stdlib\Cookie\PublicCookieMetadata $cookieMetadata */ $cookieMetadata = $this->objectManager->getObject( diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php index 5dc13e7727e76..a6b5e43b44bbe 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php @@ -20,11 +20,13 @@ class PublicCookieMetadataTest extends TestCase { /** @var PublicCookieMetadata */ private $publicCookieMetadata; + /** @var ObjectManager */ + private $objectManager; protected function setUp(): void { - $objectManager = new ObjectManager($this); - $this->publicCookieMetadata = $objectManager->getObject( + $this->objectManager = new ObjectManager($this); + $this->publicCookieMetadata = $this->objectManager->getObject( PublicCookieMetadata::class ); } @@ -56,4 +58,46 @@ public function getMethodData() "getSameSite" => ["setSameSite", 'getSameSite', 'Strict'] ]; } + + /** + * @return array + */ + public function toArrayDataProvider(): array + { + return [ + [ + [ + PublicCookieMetadata::KEY_SECURE => false, + PublicCookieMetadata::KEY_DOMAIN => 'domain', + PublicCookieMetadata::KEY_PATH => 'path', + ], + [ + PublicCookieMetadata::KEY_SECURE => false, + PublicCookieMetadata::KEY_DOMAIN => 'domain', + PublicCookieMetadata::KEY_PATH => 'path', + PublicCookieMetadata::KEY_SAME_SITE => 'Lax', + ], + ] + ]; + } + + /** + * Test To Array + * + * @param array $metadata + * @param array $expected + * @dataProvider toArrayDataProvider + * @return void + */ + public function testToArray(array $metadata, array $expected): void + { + /** @var \Magento\Framework\Stdlib\Cookie\PublicCookieMetadata $object */ + $object = $this->objectManager->getObject( + PublicCookieMetadata::class, + [ + 'metadata' => $metadata, + ] + ); + $this->assertEquals($expected, $object->__toArray()); + } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php index 864c71ca2cc86..e93e0703d7e04 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php @@ -189,6 +189,7 @@ public function toArrayDataProvider() SensitiveCookieMetadata::KEY_DOMAIN => 'domain', SensitiveCookieMetadata::KEY_PATH => 'path', SensitiveCookieMetadata::KEY_HTTP_ONLY => 1, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', ], 0, ], @@ -203,6 +204,7 @@ public function toArrayDataProvider() SensitiveCookieMetadata::KEY_DOMAIN => 'domain', SensitiveCookieMetadata::KEY_PATH => 'path', SensitiveCookieMetadata::KEY_HTTP_ONLY => 1, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', ], ], 'without secure 2' => [ @@ -216,6 +218,7 @@ public function toArrayDataProvider() SensitiveCookieMetadata::KEY_DOMAIN => 'domain', SensitiveCookieMetadata::KEY_PATH => 'path', SensitiveCookieMetadata::KEY_HTTP_ONLY => 1, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', ], ], ]; From 79f9defaaa00bfafd4411a3900e1f28896fab303 Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Tue, 21 Jul 2020 20:24:49 -0500 Subject: [PATCH 0129/1013] MC-35389: Set same site attribute. Unit and Integration test fixes. --- .../Stdlib/Cookie/CookieScopeTest.php | 9 ++++++-- .../Stdlib/Cookie/SensitiveCookieMetadata.php | 2 +- .../Test/Unit/Cookie/CookieScopeTest.php | 21 ++++++++++++------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php b/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php index 5670c54e1fbd2..e10fae226a0be 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php @@ -43,6 +43,7 @@ public function testGetSensitiveCookieMetadataEmpty() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -50,11 +51,11 @@ public function testGetSensitiveCookieMetadataEmpty() $this->request->setServer(new Parameters($serverVal)); } - public function testGetPublicCookieMetadataEmpty() + public function testGetPublicCookieMetadataNotEmpty() { $cookieScope = $this->createCookieScope(); - $this->assertEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); + $this->assertNotEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); } public function testGetSensitiveCookieMetadataDefaults() @@ -77,6 +78,7 @@ public function testGetSensitiveCookieMetadataDefaults() SensitiveCookieMetadata::KEY_DOMAIN => 'default domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => false, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -90,6 +92,7 @@ public function testGetPublicCookieMetadataDefaults() PublicCookieMetadata::KEY_DURATION => 'default duration', PublicCookieMetadata::KEY_HTTP_ONLY => 'default http', PublicCookieMetadata::KEY_SECURE => 'default secure', + PublicCookieMetadata::KEY_SAME_SITE => 'Lax' ]; $public = $this->createPublicMetadata($defaultValues); $cookieScope = $this->createCookieScope( @@ -139,6 +142,7 @@ public function testGetSensitiveCookieMetadataOverrides() SensitiveCookieMetadata::KEY_DOMAIN => 'override domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => false, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' ], $cookieScope->getSensitiveCookieMetadata($override)->__toArray() ); @@ -159,6 +163,7 @@ public function testGetPublicCookieMetadataOverrides() PublicCookieMetadata::KEY_DURATION => 'override duration', PublicCookieMetadata::KEY_HTTP_ONLY => 'override http', PublicCookieMetadata::KEY_SECURE => 'override secure', + PublicCookieMetadata::KEY_SAME_SITE => 'Lax' ]; $public = $this->createPublicMetadata($defaultValues); $cookieScope = $this->createCookieScope( diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php index b913e49e77716..1b184ab979790 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php @@ -51,7 +51,7 @@ public function getSecure() /** * @inheritdoc */ - public function __toArray() + public function __toArray() //phpcs:ignore PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames { $this->updateSecureValue(); return parent::__toArray(); diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php index ccaa20103652a..4ae3336a40d7d 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php @@ -68,6 +68,7 @@ public function testGetSensitiveCookieMetadataEmpty() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -76,21 +77,21 @@ public function testGetSensitiveCookieMetadataEmpty() /** * @covers ::getPublicCookieMetadata */ - public function testGetPublicCookieMetadataEmpty() + public function testGetPublicCookieMetadataNotEmpty() { $cookieScope = $this->createCookieScope(); - $this->assertEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); + $this->assertNotEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); } /** * @covers ::getCookieMetadata */ - public function testGetCookieMetadataEmpty() + public function testGetCookieMetadataNotEmpty() { $cookieScope = $this->createCookieScope(); - $this->assertEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); + $this->assertNotEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); } /** @@ -111,7 +112,7 @@ public function testGetSensitiveCookieMetadataDefaults() ] ); - $this->assertEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); + $this->assertNotEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); $this->assertEmpty($cookieScope->getCookieMetadata()->__toArray()); $this->assertEquals( [ @@ -119,6 +120,7 @@ public function testGetSensitiveCookieMetadataDefaults() SensitiveCookieMetadata::KEY_DOMAIN => 'default domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -135,6 +137,7 @@ public function testGetPublicCookieMetadataDefaults() PublicCookieMetadata::KEY_DURATION => 'default duration', PublicCookieMetadata::KEY_HTTP_ONLY => 'default http', PublicCookieMetadata::KEY_SECURE => 'default secure', + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ]; $public = $this->createPublicMetadata($defaultValues); $cookieScope = $this->createCookieScope( @@ -149,6 +152,7 @@ public function testGetPublicCookieMetadataDefaults() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -200,7 +204,7 @@ public function testGetSensitiveCookieMetadataOverrides() ); $override = $this->createSensitiveMetadata($overrideValues); - $this->assertEmpty($cookieScope->getPublicCookieMetadata($this->createPublicMetadata())->__toArray()); + $this->assertNotEmpty($cookieScope->getPublicCookieMetadata($this->createPublicMetadata())->__toArray()); $this->assertEmpty($cookieScope->getCookieMetadata($this->createCookieMetadata())->__toArray()); $this->assertEquals( [ @@ -208,6 +212,7 @@ public function testGetSensitiveCookieMetadataOverrides() SensitiveCookieMetadata::KEY_DOMAIN => 'override domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' ], $cookieScope->getSensitiveCookieMetadata($override)->__toArray() ); @@ -231,6 +236,7 @@ public function testGetPublicCookieMetadataOverrides() PublicCookieMetadata::KEY_DURATION => 'override duration', PublicCookieMetadata::KEY_HTTP_ONLY => 'override http', PublicCookieMetadata::KEY_SECURE => 'override secure', + PublicCookieMetadata::KEY_SAME_SITE => 'Strict' ]; $public = $this->createPublicMetadata($defaultValues); $cookieScope = $this->createCookieScope( @@ -271,11 +277,12 @@ public function testGetCookieMetadataOverrides() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, + SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' ], $cookieScope->getSensitiveCookieMetadata($this->createSensitiveMetadata())->__toArray() ); $this->assertEquals( - [], + ['samesite' => 'Lax'], $cookieScope->getPublicCookieMetadata($this->createPublicMetadata())->__toArray() ); $this->assertEquals($overrideValues, $cookieScope->getCookieMetadata($override)->__toArray()); From 3dd1811cd5e4dad3f05b3881a8a1b63490086be3 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Wed, 22 Jul 2020 16:41:14 -0500 Subject: [PATCH 0130/1013] MC-34385: Filter fields allowing HTML --- app/etc/di.xml | 31 +++++++ .../HTML/ConfigurableWYSIWYGValidatorTest.php | 89 +++++++++++++++---- .../HTML/StyleAttributeValidatorTest.php | 57 ++++++++++++ .../HTML/AttributeValidatorInterface.php | 28 ++++++ .../HTML/ConfigurableWYSIWYGValidator.php | 32 +++++-- .../HTML/StyleAttributeValidator.php | 31 +++++++ 6 files changed, 248 insertions(+), 20 deletions(-) create mode 100644 lib/internal/Magento/Framework/Test/Unit/Validator/HTML/StyleAttributeValidatorTest.php create mode 100644 lib/internal/Magento/Framework/Validator/HTML/AttributeValidatorInterface.php create mode 100644 lib/internal/Magento/Framework/Validator/HTML/StyleAttributeValidator.php diff --git a/app/etc/di.xml b/app/etc/di.xml index 9b85e09ac9611..fa1887cbe1372 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1837,14 +1837,45 @@ div a + p + span + em + strong + ul + li + ol + h5 + h4 + h3 + h2 + h1 + table + tbody + tr + td + th + tfoot + img class + width + height + style + alt + title + border href + + src + + + + Magento\Framework\Validator\HTML\StyleAttributeValidator diff --git a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php index aef019b20f519..43ff2ae1377b0 100644 --- a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php @@ -10,6 +10,7 @@ use Magento\Framework\Validation\ValidationException; use Magento\Framework\Validator\HTML\ConfigurableWYSIWYGValidator; +use Magento\Framework\Validator\HTML\AttributeValidatorInterface; use PHPUnit\Framework\TestCase; class ConfigurableWYSIWYGValidatorTest extends TestCase @@ -22,25 +23,34 @@ class ConfigurableWYSIWYGValidatorTest extends TestCase public function getConfigurations(): array { return [ - 'no-html' => [['div'], [], [], 'just text', true], - 'allowed-tag' => [['div'], [], [], 'just text and
a div
', true], - 'restricted-tag' => [['div', 'p'], [], [], 'text and

a p

,
a div
, a tr', false], - 'restricted-tag-wtih-attr' => [['div'], [], [], 'just text and

a p

', false], - 'allowed-tag-with-attr' => [['div'], [], [], 'just text and
a div
', false], - 'multiple-tags' => [['div', 'p'], [], [], 'just text and
a div
and

a p

', true], + 'no-html' => [['div'], [], [], 'just text', true, []], + 'allowed-tag' => [['div'], [], [], 'just text and
a div
', true, []], + 'restricted-tag' => [ + ['div', 'p'], + [], + [], + 'text and

a p

,
a div
, a tr', + false, + [] + ], + 'restricted-tag-wtih-attr' => [['div'], [], [], 'just text and

a p

', false, []], + 'allowed-tag-with-attr' => [['div'], [], [], 'just text and
a div
', false, []], + 'multiple-tags' => [['div', 'p'], [], [], 'just text and
a div
and

a p

', true, []], 'tags-with-attrs' => [ ['div', 'p'], ['class', 'style'], [], 'text and
a div
and

a p

', - true + true, + [] ], 'tags-with-restricted-attrs' => [ ['div', 'p'], ['class', 'align'], [], 'text and
a div
and

a p

', - false + false, + [] ], 'tags-with-specific-attrs' => [ ['div', 'a', 'p'], @@ -48,14 +58,16 @@ public function getConfigurations(): array ['a' => ['href'], 'div' => ['style']], '
a div
, an a' .',

a p

', - true + true, + [] ], 'tags-with-specific-restricted-attrs' => [ ['div', 'a'], ['class'], ['a' => ['href']], 'text and
a div
and an a', - false + false, + [] ], 'invalid-tag-with-full-config' => [ ['div', 'a', 'p'], @@ -63,21 +75,48 @@ public function getConfigurations(): array ['a' => ['href'], 'div' => ['style']], '
a div
, an a' .',

a p

, ', - false + false, + [] ], 'invalid-html' => [ ['div', 'a', 'p'], ['class', 'src'], ['a' => ['href'], 'div' => ['style']], 'some ', - true + true, + [] ], 'invalid-html-with-violations' => [ ['div', 'a', 'p'], ['class', 'src'], ['a' => ['href'], 'div' => ['style']], 'some some trs', - false + false, + [] + ], + 'invalid-html-attributes' => [ + ['div', 'a', 'p'], + ['class', 'src'], + [], + 'some
DIV
', + false, + ['class' => false] + ], + 'ignored-html-attributes' => [ + ['div', 'a', 'p'], + ['class', 'src'], + [], + 'some
DIV
', + true, + ['src' => false, 'class' => true] + ], + 'valid-html-attributes' => [ + ['div', 'a', 'p'], + ['class', 'src'], + [], + 'some
DIV
', + true, + ['src' => true, 'class' => true] ] ]; } @@ -90,6 +129,7 @@ public function getConfigurations(): array * @param array $allowedTagAttrs * @param string $html * @param bool $isValid + * @param array $attributeValidityMap * @return void * @dataProvider getConfigurations */ @@ -98,9 +138,28 @@ public function testConfigurations( array $allowedAttr, array $allowedTagAttrs, string $html, - bool $isValid + bool $isValid, + array $attributeValidityMap ): void { - $validator = new ConfigurableWYSIWYGValidator($allowedTags, $allowedAttr, $allowedTagAttrs); + $attributeValidator = $this->getMockForAbstractClass(AttributeValidatorInterface::class); + $attributeValidator->method('validate') + ->willReturnCallback( + function (string $tag, string $attribute, string $content) use ($attributeValidityMap): void { + if (array_key_exists($attribute, $attributeValidityMap) && !$attributeValidityMap[$attribute]) { + throw new ValidationException(__('Invalid attribute for %1', $tag)); + } + } + ); + $attrValidators = []; + foreach (array_keys($attributeValidityMap) as $attr) { + $attrValidators[$attr] = $attributeValidator; + } + $validator = new ConfigurableWYSIWYGValidator( + $allowedTags, + $allowedAttr, + $allowedTagAttrs, + $attrValidators + ); $valid = true; try { $validator->validate($html); diff --git a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/StyleAttributeValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/StyleAttributeValidatorTest.php new file mode 100644 index 0000000000000..b705939feec16 --- /dev/null +++ b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/StyleAttributeValidatorTest.php @@ -0,0 +1,57 @@ + ['class', 'value', true], + 'valid style' => ['style', 'color: blue', true], + 'invalid position style' => ['style', 'color: blue; position: absolute; width: 100%', false], + 'another invalid position style' => ['style', 'position: fixed; width: 100%', false], + 'valid position style' => ['style', 'color: blue; position: inherit; width: 100%', true], + 'valid background style' => ['style', 'color: blue; background-position: left; width: 100%', true], + 'invalid opacity style' => ['style', 'color: blue; width: 100%; opacity: 0.5', false], + 'invalid z-index style' => ['style', 'color: blue; width: 100%; z-index: 11', false] + ]; + } + + /** + * Test "validate" method. + * + * @param string $attr + * @param string $value + * @param bool $expectedValid + * @return void + * @dataProvider getAttributes + */ + public function testValidate(string $attr, string $value, bool $expectedValid): void + { + $validator = new StyleAttributeValidator(); + + try { + $validator->validate('does not matter', $attr, $value); + $actuallyValid = true; + } catch (ValidationException $exception) { + $actuallyValid = false; + } + $this->assertEquals($expectedValid, $actuallyValid); + } +} diff --git a/lib/internal/Magento/Framework/Validator/HTML/AttributeValidatorInterface.php b/lib/internal/Magento/Framework/Validator/HTML/AttributeValidatorInterface.php new file mode 100644 index 0000000000000..6426e19a537da --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/AttributeValidatorInterface.php @@ -0,0 +1,28 @@ +attributeValidators = $attributeValidators; } /** @@ -132,6 +143,17 @@ function (string $attribute): string { ); } } + + //Validating allowed attributes. + if ($this->attributeValidators) { + foreach ($this->attributeValidators as $attr => $validator) { + $found = $xpath->query("//@*[name() = '$attr']"); + foreach ($found as $attribute) { + $validator->validate($attribute->parentNode->tagName, $attribute->name, $attribute->value); + } + } + } + } /** diff --git a/lib/internal/Magento/Framework/Validator/HTML/StyleAttributeValidator.php b/lib/internal/Magento/Framework/Validator/HTML/StyleAttributeValidator.php new file mode 100644 index 0000000000000..4b5ccc9e32863 --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/StyleAttributeValidator.php @@ -0,0 +1,31 @@ + Date: Wed, 22 Jul 2020 16:49:37 -0500 Subject: [PATCH 0131/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/etc/di.xml | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index d79e805e25890..74ba239bca587 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -258,4 +258,51 @@ + + + + div + a + p + span + em + strong + ul + li + ol + h5 + h4 + h3 + h2 + h1 + table + tbody + tr + td + th + tfoot + img + + + class + width + height + style + alt + title + border + + + + href + + + src + + + + Magento\Framework\Validator\HTML\StyleAttributeValidator + + + From 62ccdbe6226d7d3c7a30ac4b909f89f258470203 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Thu, 23 Jul 2020 16:17:26 -0500 Subject: [PATCH 0132/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/etc/di.xml | 2 + app/etc/di.xml | 4 +- .../HTML/ConfigurableWYSIWYGValidatorTest.php | 97 ++++++++++++++++--- .../HTML/ConfigurableWYSIWYGValidator.php | 83 +++++++++++++--- .../Validator/HTML/TagValidatorInterface.php | 34 +++++++ 5 files changed, 188 insertions(+), 32 deletions(-) create mode 100644 lib/internal/Magento/Framework/Validator/HTML/TagValidatorInterface.php diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 74ba239bca587..18d45980c6328 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -282,6 +282,8 @@ th tfoot img + hr + figure class diff --git a/app/etc/di.xml b/app/etc/di.xml index fa1887cbe1372..16fb4b65700cb 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1875,7 +1875,9 @@ - Magento\Framework\Validator\HTML\StyleAttributeValidator + + Magento\Framework\Validator\HTML\StyleAttributeValidator + diff --git a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php index 43ff2ae1377b0..029098a4252c6 100644 --- a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php @@ -11,6 +11,7 @@ use Magento\Framework\Validation\ValidationException; use Magento\Framework\Validator\HTML\ConfigurableWYSIWYGValidator; use Magento\Framework\Validator\HTML\AttributeValidatorInterface; +use Magento\Framework\Validator\HTML\TagValidatorInterface; use PHPUnit\Framework\TestCase; class ConfigurableWYSIWYGValidatorTest extends TestCase @@ -23,25 +24,43 @@ class ConfigurableWYSIWYGValidatorTest extends TestCase public function getConfigurations(): array { return [ - 'no-html' => [['div'], [], [], 'just text', true, []], - 'allowed-tag' => [['div'], [], [], 'just text and
a div
', true, []], + 'no-html' => [['div'], [], [], 'just text', true, [], []], + 'allowed-tag' => [['div'], [], [], 'just text and
a div
', true, [], []], 'restricted-tag' => [ ['div', 'p'], [], [], 'text and

a p

,
a div
, a tr', false, + [], + [] + ], + 'restricted-tag-wtih-attr' => [ + ['div'], + [], + [], + 'just text and

a p

', + false, + [], + [] + ], + 'allowed-tag-with-attr' => [ + ['div'], + [], + [], + 'just text and
a div
', + false, + [], [] ], - 'restricted-tag-wtih-attr' => [['div'], [], [], 'just text and

a p

', false, []], - 'allowed-tag-with-attr' => [['div'], [], [], 'just text and
a div
', false, []], - 'multiple-tags' => [['div', 'p'], [], [], 'just text and
a div
and

a p

', true, []], + 'multiple-tags' => [['div', 'p'], [], [], 'just text and
a div
and

a p

', true, [], []], 'tags-with-attrs' => [ ['div', 'p'], ['class', 'style'], [], 'text and
a div
and

a p

', true, + [], [] ], 'tags-with-restricted-attrs' => [ @@ -50,6 +69,7 @@ public function getConfigurations(): array [], 'text and
a div
and

a p

', false, + [], [] ], 'tags-with-specific-attrs' => [ @@ -59,6 +79,7 @@ public function getConfigurations(): array '
a div
, an a' .',

a p

', true, + [], [] ], 'tags-with-specific-restricted-attrs' => [ @@ -67,6 +88,7 @@ public function getConfigurations(): array ['a' => ['href']], 'text and
a div
and an a', false, + [], [] ], 'invalid-tag-with-full-config' => [ @@ -76,6 +98,7 @@ public function getConfigurations(): array '
a div
, an a' .',

a p

, ', false, + [], [] ], 'invalid-html' => [ @@ -84,6 +107,7 @@ public function getConfigurations(): array ['a' => ['href'], 'div' => ['style']], 'some ', true, + [], [] ], 'invalid-html-with-violations' => [ @@ -92,6 +116,7 @@ public function getConfigurations(): array ['a' => ['href'], 'div' => ['style']], 'some some trs', false, + [], [] ], 'invalid-html-attributes' => [ @@ -100,7 +125,8 @@ public function getConfigurations(): array [], 'some
DIV
', false, - ['class' => false] + ['class' => false], + [] ], 'ignored-html-attributes' => [ ['div', 'a', 'p'], @@ -108,7 +134,8 @@ public function getConfigurations(): array [], 'some
DIV
', true, - ['src' => false, 'class' => true] + ['src' => false, 'class' => true], + [] ], 'valid-html-attributes' => [ ['div', 'a', 'p'], @@ -116,7 +143,26 @@ public function getConfigurations(): array [], 'some
DIV
', true, - ['src' => true, 'class' => true] + ['src' => true, 'class' => true], + [] + ], + 'invalid-allowed-tag' => [ + ['div'], + ['class', 'src'], + [], + '
IS A DIV
', + false, + [], + ['div' => ['class' => false]] + ], + 'valid-allowed-tag' => [ + ['div'], + ['class', 'src'], + [], + '
IS A DIV
', + true, + [], + ['div' => ['src' => false]] ] ]; } @@ -124,12 +170,13 @@ public function getConfigurations(): array /** * Test different configurations and content. * - * @param array $allowedTags - * @param array $allowedAttr - * @param array $allowedTagAttrs + * @param string[] $allowedTags + * @param string[] $allowedAttr + * @param string[][] $allowedTagAttrs * @param string $html * @param bool $isValid - * @param array $attributeValidityMap + * @param bool[] $attributeValidityMap + * @param bool[][] $tagValidators * @return void * @dataProvider getConfigurations */ @@ -139,7 +186,8 @@ public function testConfigurations( array $allowedTagAttrs, string $html, bool $isValid, - array $attributeValidityMap + array $attributeValidityMap, + array $tagValidators ): void { $attributeValidator = $this->getMockForAbstractClass(AttributeValidatorInterface::class); $attributeValidator->method('validate') @@ -152,13 +200,32 @@ function (string $tag, string $attribute, string $content) use ($attributeValidi ); $attrValidators = []; foreach (array_keys($attributeValidityMap) as $attr) { - $attrValidators[$attr] = $attributeValidator; + $attrValidators[$attr] = [$attributeValidator]; + } + $tagValidatorsMocks = []; + foreach ($tagValidators as $tag => $allowedAttributes) { + $mock = $this->getMockForAbstractClass(TagValidatorInterface::class); + $mock->method('validate') + ->willReturnCallback( + function (string $givenTag, array $attrs, string $value) use($tag, $allowedAttributes): void { + if ($givenTag !== $tag) { + throw new \RuntimeException(); + } + foreach (array_keys($attrs) as $attr) { + if (array_key_exists($attr, $allowedAttributes) && !$allowedAttributes[$attr]) { + throw new ValidationException(__('Invalid tag')); + } + } + } + ); + $tagValidatorsMocks[$tag] = [$mock]; } $validator = new ConfigurableWYSIWYGValidator( $allowedTags, $allowedAttr, $allowedTagAttrs, - $attrValidators + $attrValidators, + $tagValidatorsMocks ); $valid = true; try { diff --git a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php index 5b9a73a5f2570..caa32be4abc55 100644 --- a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php +++ b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php @@ -31,21 +31,28 @@ class ConfigurableWYSIWYGValidator implements WYSIWYGValidatorInterface private $attributesAllowedByTags; /** - * @var AttributeValidatorInterface[] + * @var AttributeValidatorInterface[][] */ private $attributeValidators; + /** + * @var TagValidatorInterface[][] + */ + private $tagValidators; + /** * @param string[] $allowedTags * @param string[] $allowedAttributes - * @param string[] $attributesAllowedByTags - * @param AttributeValidatorInterface[] $attributeValidators + * @param string[][] $attributesAllowedByTags + * @param AttributeValidatorInterface[][] $attributeValidators + * @param TagValidatorInterface[][] $tagValidators */ public function __construct( array $allowedTags, array $allowedAttributes = [], array $attributesAllowedByTags = [], - array $attributeValidators = [] + array $attributeValidators = [], + array $tagValidators = [] ) { if (empty(array_filter($allowedTags))) { throw new \InvalidArgumentException('List of allowed HTML tags cannot be empty'); @@ -60,6 +67,7 @@ function (string $tag) use ($allowedTags): bool { ARRAY_FILTER_USE_KEY ); $this->attributeValidators = $attributeValidators; + $this->tagValidators = $tagValidators; } /** @@ -73,19 +81,32 @@ public function validate(string $content): void $dom = $this->loadHtml($content); $xpath = new \DOMXPath($dom); + $this->validateConfigured($xpath); + $this->callDynamicValidators($xpath); + } + + /** + * Check declarative restrictions + * + * @param \DOMXPath $xpath + * @return void + * @throws ValidationException + */ + private function validateConfigured(\DOMXPath $xpath): void + { //Validating tags $found = $xpath->query( $query='//*[' - . implode( - ' and ', - array_map( - function (string $tag): string { - return "name() != '$tag'"; - }, - array_merge($this->allowedTags, ['body', 'html']) + . implode( + ' and ', + array_map( + function (string $tag): string { + return "name() != '$tag'"; + }, + array_merge($this->allowedTags, ['body', 'html']) + ) ) - ) - .']' + .']' ); if (count($found)) { throw new ValidationException( @@ -143,17 +164,48 @@ function (string $attribute): string { ); } } + } + /** + * Cycle dynamic validators. + * + * @param \DOMXPath $xpath + * @return void + * @throws ValidationException + */ + private function callDynamicValidators(\DOMXPath $xpath): void + { //Validating allowed attributes. if ($this->attributeValidators) { - foreach ($this->attributeValidators as $attr => $validator) { + foreach ($this->attributeValidators as $attr => $validators) { $found = $xpath->query("//@*[name() = '$attr']"); foreach ($found as $attribute) { - $validator->validate($attribute->parentNode->tagName, $attribute->name, $attribute->value); + foreach ($validators as $validator) { + $validator->validate($attribute->parentNode->tagName, $attribute->name, $attribute->value); + } } } } + //Validating allowed tags + if ($this->tagValidators) { + foreach ($this->tagValidators as $tag => $validators) { + $found = $xpath->query("//*[name() = '$tag']"); + /** @var \DOMElement $tagNode */ + foreach ($found as $tagNode) { + $attributes = []; + if ($tagNode->hasAttributes()) { + /** @var \DOMAttr $attributeNode */ + foreach ($tagNode->attributes as $attributeNode) { + $attributes[$attributeNode->name] = $attributeNode->value; + } + } + foreach ($validators as $validator) { + $validator->validate($tagNode->tagName, $attributes, $tagNode->textContent, $this); + } + } + } + } } /** @@ -166,7 +218,6 @@ function (string $attribute): string { private function loadHtml(string $content): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); - $loaded = true; set_error_handler( function () use (&$loaded) { $loaded = false; diff --git a/lib/internal/Magento/Framework/Validator/HTML/TagValidatorInterface.php b/lib/internal/Magento/Framework/Validator/HTML/TagValidatorInterface.php new file mode 100644 index 0000000000000..d81172edc87c9 --- /dev/null +++ b/lib/internal/Magento/Framework/Validator/HTML/TagValidatorInterface.php @@ -0,0 +1,34 @@ + Date: Sat, 25 Jul 2020 13:24:25 +0200 Subject: [PATCH 0133/1013] #29174 Category save: transport all existing data to the save function --- app/code/Magento/Catalog/Model/CategoryRepository.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 4a59676450677..e2cd6c7ef6a4a 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -82,9 +82,7 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); $existingData['store_id'] = $storeId; - if ($category->getData('save_rewrites_history', null) !== null) { - $existingData['save_rewrites_history'] = $category->getData('save_rewrites_history'); - } + $existingData = array_replace($existingData, $category->getData()); if ($category->getId()) { $metadata = $this->getMetadataPool()->getMetadata( From 79c54aecb267aa5ce9dea5e6bb4e96e0dcc7f803 Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Sat, 25 Jul 2020 15:53:14 +0200 Subject: [PATCH 0134/1013] #29174 Fix unit tests --- app/code/Magento/Catalog/Model/CategoryRepository.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index e2cd6c7ef6a4a..6865798052e58 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -7,10 +7,10 @@ namespace Magento\Catalog\Model; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; -use Magento\Catalog\Api\Data\CategoryInterface; /** * Repository for categories. @@ -82,7 +82,9 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); $existingData['store_id'] = $storeId; - $existingData = array_replace($existingData, $category->getData()); + if (is_array($category->getData())) { + $existingData = array_replace($existingData, $category->getData()); + } if ($category->getId()) { $metadata = $this->getMetadataPool()->getMetadata( From 99da072f8cef3250be8c54f236306d6fbc51ae0f Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Sat, 25 Jul 2020 15:53:49 +0200 Subject: [PATCH 0135/1013] #29174 Fix complexity --- .../Controller/Rest/InputParamsResolver.php | 101 +++++++++++------- 1 file changed, 62 insertions(+), 39 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php index 6d76005de5073..cff868f1ecceb 100644 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php @@ -11,6 +11,7 @@ use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Webapi\Controller\Rest\Router\Route; /** * Plugin for InputParamsResolver @@ -44,64 +45,37 @@ public function __construct(RestRequest $request) */ public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array { - $route = $subject->getRoute(); - $serviceMethodName = $route->getServiceMethod(); - $serviceClassName = $route->getServiceClass(); - $requestBodyParams = $this->request->getBodyParams(); + $this->processProductCall($subject, $result); + $this->processCategoryCall($subject, $result); - if ($this->isProductSaveCalled($serviceClassName, $serviceMethodName) - && $this->isCustomAttributesExists($requestBodyParams, 'product')) { - foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { - if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { - foreach ($result as $resultItem) { - if ($resultItem instanceof \Magento\Catalog\Model\Product) { - $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); - break 2; - } - } - break; - } - } - } - - if ($this->isCategorySaveCalled($serviceClassName, $serviceMethodName) - && $this->isCustomAttributesExists($requestBodyParams, 'category')) { - foreach ($requestBodyParams['category']['custom_attributes'] as $attribute) { - if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { - foreach ($result as $resultItem) { - if ($resultItem instanceof \Magento\Catalog\Model\Category) { - $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); - break 2; - } - } - break; - } - } - } return $result; } /** * Check that product save method called * - * @param string $serviceClassName - * @param string $serviceMethodName + * @param Route $route * @return bool */ - private function isProductSaveCalled(string $serviceClassName, string $serviceMethodName): bool + private function isProductSaveCalled(Route $route): bool { + $serviceMethodName = $route->getServiceMethod(); + $serviceClassName = $route->getServiceClass(); + return $serviceClassName === ProductRepositoryInterface::class && $serviceMethodName === 'save'; } /** * Check that category save method called * - * @param string $serviceClassName - * @param string $serviceMethodName + * @param Route $route * @return bool */ - private function isCategorySaveCalled(string $serviceClassName, string $serviceMethodName): bool + private function isCategorySaveCalled(Route $route): bool { + $serviceMethodName = $route->getServiceMethod(); + $serviceClassName = $route->getServiceClass(); + return $serviceClassName === CategoryRepositoryInterface::class && $serviceMethodName === 'save'; } @@ -116,4 +90,53 @@ private function isCustomAttributesExists(array $requestBodyParams, string $enti { return !empty($requestBodyParams[$entityCode]['custom_attributes']); } + + /** + * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject + * @param array $result + * @return array + */ + private function processProductCall(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): void + { + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isProductSaveCalled($subject->getRoute()) + && $this->isCustomAttributesExists($requestBodyParams, 'product')) { + foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { + foreach ($result as $resultItem) { + if ($resultItem instanceof \Magento\Catalog\Model\Product) { + $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); + break 2; + } + } + break; + } + } + } + } + + /** + * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject + * @param array $result + */ + private function processCategoryCall(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): void + { + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isCategorySaveCalled($subject->getRoute()) + && $this->isCustomAttributesExists($requestBodyParams, 'category')) { + foreach ($requestBodyParams['category']['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { + foreach ($result as $resultItem) { + if ($resultItem instanceof \Magento\Catalog\Model\Category) { + $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); + break 2; + } + } + break; + } + } + } + } } From f61a27af1429effd65f3706663223e1e295d707c Mon Sep 17 00:00:00 2001 From: Alexander Menk Date: Sat, 25 Jul 2020 15:55:13 +0200 Subject: [PATCH 0136/1013] #29174 Fix PHPStan Test --- .../Webapi/Controller/Rest/InputParamsResolverTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php index 66210c95ff2cc..7cc88020b694b 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php @@ -137,9 +137,9 @@ public function testAfterResolveWithCategory() $this->request->expects($this->any())->method('getBodyParams')->willReturn($this->requestBodyParams); $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); $this->subject->expects($this->any())->method('getRoute')->willReturn($this->route); - $this->category = $this->createPartialMock(Category::class, ['setData']); + $category = $this->createPartialMock(Category::class, ['setData']); - $this->result = [false, $this->category, 'test']; + $this->result = [false, $category, 'test']; $this->objectManager = new ObjectManager($this); $this->plugin = $this->objectManager->getObject( @@ -155,7 +155,7 @@ public function testAfterResolveWithCategory() $this->route->expects($this->once()) ->method('getServiceMethod') ->willReturn('save'); - $this->category->expects($this->once()) + $category->expects($this->once()) ->method('setData') ->with($this->saveRewritesHistory, true); From 471dc6b18b03b615729c51f22fc061a209e7e05a Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Mon, 27 Jul 2020 18:46:48 +0100 Subject: [PATCH 0137/1013] Add special handling of config nodes --- .../Catalog/Model/Category/DataProvider.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index d9e655fff2cef..5d2dcf07f68bc 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -659,14 +659,31 @@ protected function getFieldsMap() ->create(['componentName' => $referenceName]) ->get($referenceName); + if (empty($config)) { + return []; + } + $fieldsMap = []; - if (isset($config['children']) && !empty($config['children'])) { - foreach ($config['children'] as $name => $child) { - if (isset($child['children']) && !empty($child['children'])) { - $fieldsMap[$name] = array_keys($child['children']); + foreach ($config['children'] as $group => $node) { + $fieldsMap[$group] = []; + + foreach ($node['children'] as $childName => $childNode) { + if (!empty($childNode['children'])) { + // nodes need special handling + $fieldsMap[$group] = array_merge( + $fieldsMap[$group], + array_keys($childNode['children']) + ); + } else { + $fieldsMap[$group][] = $childName; } } + + // Remove empty groups + if (empty($fieldsMap[$group])) { + unset($fieldsMap[$group]); + } } return $fieldsMap; From d3e011ca213a94d631c7d9c1a0966225c289b4ec Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Mon, 27 Jul 2020 19:36:47 +0100 Subject: [PATCH 0138/1013] Skip disabled components --- app/code/Magento/Catalog/Model/Category/DataProvider.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 5d2dcf07f68bc..fe2fdaf53686b 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -666,6 +666,11 @@ protected function getFieldsMap() $fieldsMap = []; foreach ($config['children'] as $group => $node) { + // Skip disabled components (required for Commerce Edition) + if ($node['arguments']['data']['config']['componentDisabled'] ?? false) { + continue; + } + $fieldsMap[$group] = []; foreach ($node['children'] as $childName => $childNode) { From ff9026000f545497efeb87033e9adbea426fb3e0 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Mon, 27 Jul 2020 15:07:55 -0500 Subject: [PATCH 0139/1013] MC-34385: Filter fields allowing HTML --- .../Cms/Command/WysiwygRestrictCommand.php | 2 ++ .../Magento/Cms/Model/BlockRepository.php | 6 ++-- app/code/Magento/Cms/Model/Page.php | 5 +++- .../HTML/ConfigurableWYSIWYGValidatorTest.php | 8 ++++-- .../HTML/ConfigurableWYSIWYGValidator.php | 28 +++++++++++++------ 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php b/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php index bafe98ad377f5..e676cb1fe0ee5 100644 --- a/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php +++ b/app/code/Magento/Cms/Command/WysiwygRestrictCommand.php @@ -66,5 +66,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->cache->cleanType('config'); $output->writeln('HTML user content validation is now ' .($restrictArg === 'y' ? 'enforced' : 'suggested')); + + return 0; } } diff --git a/app/code/Magento/Cms/Model/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index f8129ca4a2961..5d5a0b9f6bed9 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -21,7 +21,7 @@ use Magento\Framework\EntityManager\HydratorInterface; /** - * Class BlockRepository + * Default block repo impl. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class BlockRepository implements BlockRepositoryInterface @@ -87,6 +87,8 @@ class BlockRepository implements BlockRepositoryInterface * @param StoreManagerInterface $storeManager * @param CollectionProcessorInterface $collectionProcessor * @param HydratorInterface|null $hydrator + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ResourceBlock $resource, @@ -217,7 +219,7 @@ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - 'Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor' + \Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor::class ); } return $this->collectionProcessor; diff --git a/app/code/Magento/Cms/Model/Page.php b/app/code/Magento/Cms/Model/Page.php index 35e049caea203..7e3e3ff44cfa0 100644 --- a/app/code/Magento/Cms/Model/Page.php +++ b/app/code/Magento/Cms/Model/Page.php @@ -23,12 +23,13 @@ * @method Page setStoreId(int $storeId) * @method int getStoreId() * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ class Page extends AbstractModel implements PageInterface, IdentityInterface { /** - * No route page id + * Page ID for the 404 page. */ const NOROUTE_PAGE_ID = 'no-route'; @@ -605,6 +606,8 @@ private function validateNewIdentifier(): void /** * @inheritdoc * @since 101.0.0 + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function beforeSave() { diff --git a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php index 029098a4252c6..3c703c9f037d7 100644 --- a/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/Validator/HTML/ConfigurableWYSIWYGValidatorTest.php @@ -20,6 +20,8 @@ class ConfigurableWYSIWYGValidatorTest extends TestCase * Configurations to test. * * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function getConfigurations(): array { @@ -178,7 +180,9 @@ public function getConfigurations(): array * @param bool[] $attributeValidityMap * @param bool[][] $tagValidators * @return void + * * @dataProvider getConfigurations + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function testConfigurations( array $allowedTags, @@ -192,7 +196,7 @@ public function testConfigurations( $attributeValidator = $this->getMockForAbstractClass(AttributeValidatorInterface::class); $attributeValidator->method('validate') ->willReturnCallback( - function (string $tag, string $attribute, string $content) use ($attributeValidityMap): void { + function (string $tag, string $attribute) use ($attributeValidityMap): void { if (array_key_exists($attribute, $attributeValidityMap) && !$attributeValidityMap[$attribute]) { throw new ValidationException(__('Invalid attribute for %1', $tag)); } @@ -207,7 +211,7 @@ function (string $tag, string $attribute, string $content) use ($attributeValidi $mock = $this->getMockForAbstractClass(TagValidatorInterface::class); $mock->method('validate') ->willReturnCallback( - function (string $givenTag, array $attrs, string $value) use($tag, $allowedAttributes): void { + function (string $givenTag, array $attrs) use ($tag, $allowedAttributes): void { if ($givenTag !== $tag) { throw new \RuntimeException(); } diff --git a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php index caa32be4abc55..f436fddf26e8d 100644 --- a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php +++ b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php @@ -82,7 +82,8 @@ public function validate(string $content): void $xpath = new \DOMXPath($dom); $this->validateConfigured($xpath); - $this->callDynamicValidators($xpath); + $this->callAttributeValidators($xpath); + $this->callTagValidators($xpath); } /** @@ -96,7 +97,7 @@ private function validateConfigured(\DOMXPath $xpath): void { //Validating tags $found = $xpath->query( - $query='//*[' + '//*[' . implode( ' and ', array_map( @@ -117,10 +118,11 @@ function (string $tag): string { //Validating attributes if ($this->attributesAllowedByTags) { foreach ($this->allowedTags as $tag) { - $allowed = $this->allowedAttributes; + $allowed = [$this->allowedAttributes]; if (!empty($this->attributesAllowedByTags[$tag])) { - $allowed = array_unique(array_merge($allowed, $this->attributesAllowedByTags[$tag])); + $allowed[] = $this->attributesAllowedByTags[$tag]; } + $allowed = array_unique(array_merge(...$allowed)); $allowedQuery = ''; if ($allowed) { $allowedQuery = '[' @@ -167,15 +169,14 @@ function (string $attribute): string { } /** - * Cycle dynamic validators. + * Validate allowed HTML attributes' content. * * @param \DOMXPath $xpath - * @return void * @throws ValidationException + * @return void */ - private function callDynamicValidators(\DOMXPath $xpath): void + private function callAttributeValidators(\DOMXPath $xpath): void { - //Validating allowed attributes. if ($this->attributeValidators) { foreach ($this->attributeValidators as $attr => $validators) { $found = $xpath->query("//@*[name() = '$attr']"); @@ -186,8 +187,17 @@ private function callDynamicValidators(\DOMXPath $xpath): void } } } + } - //Validating allowed tags + /** + * Validate allowed tags. + * + * @param \DOMXPath $xpath + * @return void + * @throws ValidationException + */ + private function callTagValidators(\DOMXPath $xpath): void + { if ($this->tagValidators) { foreach ($this->tagValidators as $tag => $validators) { $found = $xpath->query("//*[name() = '$tag']"); From 8d899e19c5414414cf037fdfa27f9aed3e95d209 Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Mon, 27 Jul 2020 21:20:55 +0100 Subject: [PATCH 0140/1013] Avoid use of array_merge() to appease code linter --- app/code/Magento/Catalog/Model/Category/DataProvider.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index fe2fdaf53686b..613205ee10e22 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -676,10 +676,9 @@ protected function getFieldsMap() foreach ($node['children'] as $childName => $childNode) { if (!empty($childNode['children'])) { // nodes need special handling - $fieldsMap[$group] = array_merge( - $fieldsMap[$group], - array_keys($childNode['children']) - ); + foreach ($childNode['children'] as $grandchildName => $grandchildNode) { + $fieldsMap[$group][] = $grandchildName; + } } else { $fieldsMap[$group][] = $childName; } From d62b0b5d36854065a420492684be0681793f3071 Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Mon, 27 Jul 2020 21:23:02 +0100 Subject: [PATCH 0141/1013] Declare missing property --- .../Catalog/Test/Unit/Model/Category/DataProviderTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php index b824929de0733..5b2334cd55f05 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/DataProviderTest.php @@ -72,6 +72,11 @@ class DataProviderTest extends TestCase */ private $categoryFactory; + /** + * @var DataInterfaceFactory|MockObject + */ + private $uiConfigFactory; + /** * @var Collection|MockObject */ From 49f7dae9013c1be18f61433973459f51567e59c4 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Mon, 27 Jul 2020 15:36:03 -0500 Subject: [PATCH 0142/1013] MC-34385: Filter fields allowing HTML --- .../Catalog/Model/Attribute/Backend/DefaultBackend.php | 4 +++- app/code/Magento/Cms/etc/di.xml | 1 + app/etc/di.xml | 7 ++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php b/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php index e3b38bf7a578a..a02d589fae055 100644 --- a/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/DefaultBackend.php @@ -47,7 +47,9 @@ private function validateHtml(DataObject $object): void $attribute = $this->getAttribute(); $code = $attribute->getAttributeCode(); if ($attribute instanceof Attribute && $attribute->getIsHtmlAllowedOnFront()) { - if ($object->getData($code) + $value = $object->getData($code); + if ($value + && is_string($value) && (!($object instanceof AbstractModel) || $object->getData($code) !== $object->getOrigData($code)) ) { try { diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 18d45980c6328..c18aadd3f6a80 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -293,6 +293,7 @@ alt title border + id diff --git a/app/etc/di.xml b/app/etc/di.xml index 887ed6d96d7ad..f3dac922b5a2d 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1856,6 +1856,8 @@ th tfoot img + hr + figure class @@ -1865,6 +1867,7 @@ alt title border + id @@ -1875,9 +1878,7 @@ - - Magento\Framework\Validator\HTML\StyleAttributeValidator - + Magento\Framework\Validator\HTML\StyleAttributeValidator From f3c4042fa82d2bc8700a57c59bce0328d31726d8 Mon Sep 17 00:00:00 2001 From: Konstantin Dubovik Date: Tue, 28 Jul 2020 17:18:43 +0300 Subject: [PATCH 0143/1013] Guest print order form fixes --- app/code/Magento/Sales/Helper/Guest.php | 1 + ...OrdersAndReturnsFormTypeZipActionGroup.xml | 22 +++++ .../StorefrontGuestOrderSearchSection.xml | 1 + ...StorefrontPrintOrderFindByZipGuestTest.xml | 30 ++++++ .../Sales/Test/Unit/Helper/GuestTest.php | 94 ++++++++++++++++--- 5 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontFillOrdersAndReturnsFormTypeZipActionGroup.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml diff --git a/app/code/Magento/Sales/Helper/Guest.php b/app/code/Magento/Sales/Helper/Guest.php index a3f2ac6ba3556..119a8aa810443 100644 --- a/app/code/Magento/Sales/Helper/Guest.php +++ b/app/code/Magento/Sales/Helper/Guest.php @@ -151,6 +151,7 @@ public function loadValidOrder(App\RequestInterface $request) return $this->resultRedirectFactory->create()->setPath('sales/order/history'); } $post = $request->getPostValue(); + $post = filter_var($post, FILTER_CALLBACK, ['options' => 'trim']); $fromCookie = $this->cookieManager->getCookie(self::COOKIE_NAME); if (empty($post) && !$fromCookie) { return $this->resultRedirectFactory->create()->setPath('sales/guest/form'); diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontFillOrdersAndReturnsFormTypeZipActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontFillOrdersAndReturnsFormTypeZipActionGroup.xml new file mode 100644 index 0000000000000..ad7f5011af954 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/StorefrontFillOrdersAndReturnsFormTypeZipActionGroup.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml index 5e420ee03bf75..efee68f2bd25f 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderSearchSection.xml @@ -13,6 +13,7 @@ + diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml new file mode 100644 index 0000000000000..85d2a229c5446 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml @@ -0,0 +1,30 @@ + + + + + + + + + <description value="Print Order from Guest on Frontend"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-16225"/> + <group value="sales"/> + </annotations> + + <remove keyForRemoval="fillOrder"/> + + <!-- Fill the form with correspondent Order data using search by Zip --> + <actionGroup ref="StorefrontFillOrdersAndReturnsFormTypeZipActionGroup" stepKey="fillOrderZip" before="clickContinue"> + <argument name="orderNumber" value="{$getOrderId}"/> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php b/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php index 0ee1e4249e27d..236da3de2c63d 100644 --- a/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php +++ b/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php @@ -112,7 +112,6 @@ protected function setUp(): void ->setMethods(['getTotalCount', 'getItems']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->searchCriteriaBuilder->method('addFilter')->willReturnSelf(); $resultRedirectFactory = $this->getMockBuilder(RedirectFactory::class) ->setMethods(['create']) @@ -148,29 +147,44 @@ protected function setUp(): void ); } - public function testLoadValidOrderNotEmptyPost() + /** + * Test load valid order with non empty post data. + * + * @param array $post + * @dataProvider loadValidOrderNotEmptyPostDataProvider + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Stdlib\Cookie\CookieSizeLimitReachedException + * @throws \Magento\Framework\Stdlib\Cookie\FailureToSendException + */ + public function testLoadValidOrderNotEmptyPost($post) { - $post = [ - 'oar_order_id' => 1, - 'oar_type' => 'email', - 'oar_billing_lastname' => 'oar_billing_lastname', - 'oar_email' => 'oar_email', - 'oar_zip' => 'oar_zip', - - ]; $incrementId = $post['oar_order_id']; $protectedCode = 'protectedCode'; $this->sessionMock->expects($this->once())->method('isLoggedIn')->willReturn(false); $requestMock = $this->createMock(Http::class); $requestMock->expects($this->once())->method('getPostValue')->willReturn($post); + + $this->searchCriteriaBuilder + ->expects($this->at(0)) + ->method('addFilter') + ->with('increment_id', trim($incrementId)) + ->willReturnSelf(); + + $this->searchCriteriaBuilder + ->expects($this->at(1)) + ->method('addFilter') + ->with('store_id', $this->storeModelMock->getId()) + ->willReturnSelf(); + $this->salesOrderMock->expects($this->any())->method('getId')->willReturn($incrementId); $billingAddressMock = $this->createPartialMock( Address::class, - ['getLastname', 'getEmail'] + ['getLastname', 'getEmail', 'getPostcode'] ); - $billingAddressMock->expects($this->once())->method('getLastname')->willReturn(($post['oar_billing_lastname'])); - $billingAddressMock->expects($this->once())->method('getEmail')->willReturn(($post['oar_email'])); + $billingAddressMock->expects($this->once())->method('getLastname')->willReturn(trim($post['oar_billing_lastname'])); + $billingAddressMock->expects($this->any())->method('getEmail')->willReturn(trim($post['oar_email'])); + $billingAddressMock->expects($this->any())->method('getPostcode')->willReturn(trim($post['oar_zip'])); $this->salesOrderMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); $this->salesOrderMock->expects($this->once())->method('getProtectCode')->willReturn($protectedCode); $metaDataMock = $this->createMock(PublicCookieMetadata::class); @@ -190,10 +204,49 @@ public function testLoadValidOrderNotEmptyPost() $this->assertTrue($this->guest->loadValidOrder($requestMock)); } + /** + * Load valid order with non empty post data provider. + * + * @return array + */ + public function loadValidOrderNotEmptyPostDataProvider() + { + return [ + [ + [ + 'oar_order_id' => '1', + 'oar_type' => 'email', + 'oar_billing_lastname' => 'White', + 'oar_email' => 'test@magento-test.com', + 'oar_zip' => '', + + ] + ], + [ + [ + 'oar_order_id' => ' 14 ', + 'oar_type' => 'email', + 'oar_billing_lastname' => 'Black ', + 'oar_email' => ' test1@magento-test.com ', + 'oar_zip' => '', + ] + ], + [ + [ + 'oar_order_id' => ' 14 ', + 'oar_type' => 'zip', + 'oar_billing_lastname' => 'Black ', + 'oar_email' => ' test1@magento-test.com ', + 'oar_zip' => '123456 ', + ] + ] + ]; + } + public function testLoadValidOrderStoredCookie() { $protectedCode = 'protectedCode'; - $incrementId = 1; + $incrementId = '1'; $cookieData = $protectedCode . ':' . $incrementId; $cookieDataHash = base64_encode($cookieData); $this->sessionMock->expects($this->once())->method('isLoggedIn')->willReturn(false); @@ -201,6 +254,19 @@ public function testLoadValidOrderStoredCookie() ->method('getCookie') ->with(Guest::COOKIE_NAME) ->willReturn($cookieDataHash); + + $this->searchCriteriaBuilder + ->expects($this->at(0)) + ->method('addFilter') + ->with('increment_id', trim($incrementId)) + ->willReturnSelf(); + + $this->searchCriteriaBuilder + ->expects($this->at(1)) + ->method('addFilter') + ->with('store_id', $this->storeModelMock->getId()) + ->willReturnSelf(); + $this->salesOrderMock->expects($this->any())->method('getId')->willReturn($incrementId); $this->salesOrderMock->expects($this->once())->method('getProtectCode')->willReturn($protectedCode); $metaDataMock = $this->createMock(PublicCookieMetadata::class); From efc25d74e4c8f34f11115add6c0eb5f0f60bbc1c Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 28 Jul 2020 10:48:00 -0500 Subject: [PATCH 0144/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/Model/BlockRepository.php | 2 +- .../Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Model/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index 7502b017584df..c26e2d809d996 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -219,7 +219,7 @@ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - \Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor::class + 'Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor' ); } return $this->collectionProcessor; diff --git a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php index f436fddf26e8d..bfa6bc37600bf 100644 --- a/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php +++ b/lib/internal/Magento/Framework/Validator/HTML/ConfigurableWYSIWYGValidator.php @@ -122,6 +122,7 @@ function (string $tag): string { if (!empty($this->attributesAllowedByTags[$tag])) { $allowed[] = $this->attributesAllowedByTags[$tag]; } + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $allowed = array_unique(array_merge(...$allowed)); $allowedQuery = ''; if ($allowed) { From 420fb3cb7dcb2393bf83065a2871df1adc26dafe Mon Sep 17 00:00:00 2001 From: Sachin Admane <sadmane@adobe.com> Date: Tue, 28 Jul 2020 12:10:16 -0500 Subject: [PATCH 0145/1013] MC-35389: Set same site attribute. Add validation in setters for directive "None". Fix unit and integration tests. --- .../Magento/Framework/Stdlib/Cookie/CookieMetadata.php | 8 ++++++++ .../Framework/Stdlib/Cookie/PublicCookieMetadata.php | 5 +++++ .../Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php | 2 +- .../Test/Unit/Cookie/SensitiveCookieMetadataTest.php | 3 ++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index 8cec82d64199c..8ae35837cebf8 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -41,6 +41,9 @@ public function __construct($metadata = []) $metadata = []; } $this->metadata = $metadata; + if (isset($metadata[self::KEY_SAME_SITE])) { + $this->setSameSite($metadata[self::KEY_SAME_SITE]); + } } /** @@ -157,6 +160,11 @@ public function setSameSite(?string $sameSite): CookieMetadata 'Invalid argument provided for SameSite directive expected one of: Strict, Lax or None' ); } + if (!$this->getSecure() && strtolower($sameSite) === 'none') { + throw new \InvalidArgumentException( + 'Cookie must be secure in order to use the Same Site None directive.' + ); + } $sameSite = self::SAME_SITE_ALLOWED_VALUES[strtolower($sameSite)]; return $this->set(self::KEY_SAME_SITE, $sameSite); } diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php index 6e5e174e4e9f9..b6707d14c6c85 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php @@ -78,6 +78,11 @@ public function setHttpOnly($httpOnly) */ public function setSecure($secure) { + if (!$secure && strtolower(self::KEY_SAME_SITE) === 'none') { + throw new \InvalidArgumentException( + 'Cookie must be secure in order to use the Same Site None directive.' + ); + } return $this->set(self::KEY_SECURE, $secure); } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php index a6b5e43b44bbe..2cf56b661fe27 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php @@ -55,7 +55,7 @@ public function getMethodData() "getHttpOnly" => ["setHttpOnly", 'getHttpOnly', true], "getSecure" => ["setSecure", 'getSecure', true], "getDurationOneYear" => ["setDurationOneYear", 'getDuration', (3600*24*365)], - "getSameSite" => ["setSameSite", 'getSameSite', 'Strict'] + "getSameSite" => ["setSameSite", 'getSameSite', 'Lax'] ]; } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php index e93e0703d7e04..b113944299a7e 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php @@ -243,7 +243,8 @@ public function getMethodData() { return [ "getDomain" => ["setDomain", 'getDomain', "example.com"], - "getPath" => ["setPath", 'getPath', "path"] + "getPath" => ["setPath", 'getPath', "path"], + "getSameSite" => ["setSameSite", 'getSameSite', 'Strict'] ]; } } From d2d7e8d1f44928e6642a78b1bcb8c8a860cf8d97 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Tue, 28 Jul 2020 22:19:30 -0500 Subject: [PATCH 0146/1013] MC-32659: Order Details by Order Number with additional different product types --- .../InvoiceItemTypeResolver.php | 29 +++++++ .../Resolver/DownloadableOrderItem/Links.php | 76 +++++++++++++++++++ .../OrderItemTypeResolver.php | 29 +++++++ .../Magento/DownloadableGraphQl/composer.json | 2 + .../DownloadableGraphQl/etc/graphql/di.xml | 14 ++++ .../DownloadableGraphQl/etc/schema.graphqls | 11 +++ .../Query/Resolver/TypeResolverInterface.php | 2 +- 7 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php create mode 100644 app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Links.php create mode 100644 app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php new file mode 100644 index 0000000000000..35120e4c0917d --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Leaf for composite class to resolve invoice item type + */ +class InvoiceItemTypeResolver implements TypeResolverInterface +{ + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (isset($data['product_type'])) { + if ($data['product_type'] == 'downloadable') { + return 'DownloadableInvoiceItem'; + } + } + return ''; + } +} diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Links.php b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Links.php new file mode 100644 index 0000000000000..97da31a6af912 --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Links.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; + +use Magento\Downloadable\Model\ResourceModel\Link\Collection; +use Magento\Downloadable\Model\ResourceModel\Link\CollectionFactory; +use Magento\DownloadableGraphQl\Model\ConvertLinksToArray; +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\Item as OrderItem; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Resolver fetches downloadable order item links and formats it according to the GraphQL schema. + */ +class Links implements ResolverInterface +{ + /** + * @var ConvertLinksToArray + */ + private $convertLinksToArray; + + /** + * @var CollectionFactory + */ + private $linkCollectionFactory; + + /** + * @param ConvertLinksToArray $convertLinksToArray + * @param CollectionFactory $linkCollectionFactory + */ + public function __construct( + ConvertLinksToArray $convertLinksToArray, + CollectionFactory $linkCollectionFactory + ) { + $this->convertLinksToArray = $convertLinksToArray; + $this->linkCollectionFactory = $linkCollectionFactory; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + /** @var OrderItem $orderItem */ + $orderItem = $value['model']; + + $orderLinks = $orderItem->getProductOptionByCode('links'); + + /** @var Collection */ + $linksCollection = $this->linkCollectionFactory->create(); + $linksCollection->addTitleToResult($store->getStoreId()) + ->addPriceToResult($store->getWebsiteId()) + ->addFieldToFilter('main_table.link_id', ['in' => $orderLinks]); + + return $this->convertLinksToArray->execute($linksCollection->getItems()); + } +} diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php new file mode 100644 index 0000000000000..2f835e790d0db --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Leaf for composite class to resolve order item type + */ +class OrderItemTypeResolver implements TypeResolverInterface +{ + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (isset($data['product_type'])) { + if ($data['product_type'] == 'downloadable') { + return 'DownloadableOrderItem'; + } + } + return ''; + } +} diff --git a/app/code/Magento/DownloadableGraphQl/composer.json b/app/code/Magento/DownloadableGraphQl/composer.json index 185a50f77cc15..d4f506d886bfb 100644 --- a/app/code/Magento/DownloadableGraphQl/composer.json +++ b/app/code/Magento/DownloadableGraphQl/composer.json @@ -7,7 +7,9 @@ "magento/module-catalog": "*", "magento/module-downloadable": "*", "magento/module-quote": "*", + "magento/module-sales": "*", "magento/module-quote-graph-ql": "*", + "magento/module-sales-graph-ql": "*", "magento/framework": "*" }, "suggest": { diff --git a/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml b/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml index c95667de15ac3..b54d88b0f1ae6 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml @@ -39,4 +39,18 @@ </argument> </arguments> </type> + <type name="Magento\SalesGraphQl\Model\OrderItemInterfaceTypeResolverComposite"> + <arguments> + <argument name="orderItemTypeResolvers" xsi:type="array"> + <item name="downloadable_order_catalog_item_type_resolver" xsi:type="object">Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\OrderItemTypeResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\InvoiceItemInterfaceTypeResolverComposite"> + <arguments> + <argument name="invoiceItemTypeResolvers" xsi:type="array"> + <item name="downloadable_invoice_catalog_item_type_resolver" xsi:type="object">Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\InvoiceItemTypeResolver</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 5863e62e81b1b..5fe6914ee3903 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -64,3 +64,14 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de 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") } + +type DownloadableOrderItem implements OrderItemInterface { + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are assigned to the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Links") +} + +type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { + title: String @doc(description: "The display name of the link") + sort_order: Int @doc(description: "A number indicating the sort order") + price: Float @doc(description: "The price of the downloadable product") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") # A Base64 string that encodes option details. +} diff --git a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/TypeResolverInterface.php b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/TypeResolverInterface.php index 34e9c0796b5a5..fc078a0ab184f 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/Resolver/TypeResolverInterface.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/Resolver/TypeResolverInterface.php @@ -21,5 +21,5 @@ interface TypeResolverInterface * @return string * @throws GraphQlInputException */ - public function resolveType(array $data) : string; + public function resolveType(array $data): string; } From 43a721a1dd74eb9cb0bb12f07d96749d6688fa8c Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Tue, 28 Jul 2020 23:18:47 -0500 Subject: [PATCH 0147/1013] MC-36015: Update in Order Status Does Not Reflect in Email - fixed - modified unit test --- .../Controller/Adminhtml/Order/AddComment.php | 3 +++ app/code/Magento/Sales/Model/Order/Config.php | 2 +- .../Magento/Sales/Model/Order/ConfigTest.php | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php index e85083a50d725..492d2d71df8d9 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AddComment.php @@ -10,6 +10,8 @@ /** * Class AddComment + * + * Controller responsible for addition of the order comment to the order */ class AddComment extends \Magento\Sales\Controller\Adminhtml\Order implements HttpPostActionInterface { @@ -42,6 +44,7 @@ public function execute() ); } + $order->setStatus($data['status']); $notify = $data['is_customer_notified'] ?? false; $visible = $data['is_visible_on_front'] ?? false; diff --git a/app/code/Magento/Sales/Model/Order/Config.php b/app/code/Magento/Sales/Model/Order/Config.php index 32b9298be2b5f..20aee5c76cc1f 100644 --- a/app/code/Magento/Sales/Model/Order/Config.php +++ b/app/code/Magento/Sales/Model/Order/Config.php @@ -52,7 +52,7 @@ class Config */ protected $maskStatusesMapping = [ \Magento\Framework\App\Area::AREA_FRONTEND => [ - \Magento\Sales\Model\Order::STATUS_FRAUD => \Magento\Sales\Model\Order::STATE_PROCESSING, + \Magento\Sales\Model\Order::STATUS_FRAUD => \Magento\Sales\Model\Order::STATUS_FRAUD, \Magento\Sales\Model\Order::STATE_PAYMENT_REVIEW => \Magento\Sales\Model\Order::STATE_PROCESSING ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ConfigTest.php index 5f1ac868c5f4e..9e43fb16eac11 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/ConfigTest.php @@ -50,4 +50,28 @@ public function testCorrectCompleteStatusInStatesList() $this->assertEquals($completeStatus->getLabel(), $completeState->getText()); } + + /** + * Test Mask Status For Area + * + * @param string $code + * @param string $expected + * @dataProvider dataProviderForTestMaskStatusForArea + */ + public function testMaskStatusForArea(string $code, string $expected) + { + $result = $this->orderConfig->getStatusFrontendLabel($code); + $this->assertEquals($expected, $result); + } + + /** + * @return array + */ + public function dataProviderForTestMaskStatusForArea() + { + return [ + ['fraud', 'Suspected Fraud'], + ['processing', 'Processing'], + ]; + } } From 475d7a087ffd31dbf758b9162629637141714f54 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Wed, 29 Jul 2020 14:35:24 +0300 Subject: [PATCH 0148/1013] MC-35480: Bug with Company Structure Page --- app/code/Magento/Cms/Model/PageRepository.php | 79 +++++++++++++------ .../Magento/Cms/Model/PageRepositoryTest.php | 22 +++++- 2 files changed, 74 insertions(+), 27 deletions(-) diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index 0439fbcd2f799..56b9e639cd14e 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -7,14 +7,20 @@ namespace Magento\Cms\Model; use Magento\Cms\Api\Data; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\Data\PageInterfaceFactory; +use Magento\Cms\Api\Data\PageSearchResultsInterface; use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor; use Magento\Cms\Model\Page\IdentityMap; use Magento\Cms\Model\ResourceModel\Page as ResourcePage; use Magento\Cms\Model\ResourceModel\Page\CollectionFactory as PageCollectionFactory; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\HydratorInterface; +use Magento\Framework\App\Route\Config; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; @@ -22,7 +28,7 @@ use Magento\Store\Model\StoreManagerInterface; /** - * @inheritdoc + * Cms page repository * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -59,12 +65,12 @@ class PageRepository implements PageRepositoryInterface protected $dataObjectProcessor; /** - * @var \Magento\Cms\Api\Data\PageInterfaceFactory + * @var PageInterfaceFactory */ protected $dataPageFactory; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; @@ -83,10 +89,15 @@ class PageRepository implements PageRepositoryInterface */ private $hydrator; + /** + * @var Config + */ + private $routeConfig; + /** * @param ResourcePage $resource * @param PageFactory $pageFactory - * @param Data\PageInterfaceFactory $dataPageFactory + * @param PageInterfaceFactory $dataPageFactory * @param PageCollectionFactory $pageCollectionFactory * @param Data\PageSearchResultsInterfaceFactory $searchResultsFactory * @param DataObjectHelper $dataObjectHelper @@ -95,12 +106,13 @@ class PageRepository implements PageRepositoryInterface * @param CollectionProcessorInterface $collectionProcessor * @param IdentityMap|null $identityMap * @param HydratorInterface|null $hydrator + * @param Config|null $routeConfig * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ResourcePage $resource, PageFactory $pageFactory, - Data\PageInterfaceFactory $dataPageFactory, + PageInterfaceFactory $dataPageFactory, PageCollectionFactory $pageCollectionFactory, Data\PageSearchResultsInterfaceFactory $searchResultsFactory, DataObjectHelper $dataObjectHelper, @@ -108,7 +120,8 @@ public function __construct( StoreManagerInterface $storeManager, CollectionProcessorInterface $collectionProcessor = null, ?IdentityMap $identityMap = null, - ?HydratorInterface $hydrator = null + ?HydratorInterface $hydrator = null, + ?Config $routeConfig = null ) { $this->resource = $resource; $this->pageFactory = $pageFactory; @@ -119,18 +132,22 @@ public function __construct( $this->dataObjectProcessor = $dataObjectProcessor; $this->storeManager = $storeManager; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); - $this->identityMap = $identityMap ?? ObjectManager::getInstance()->get(IdentityMap::class); - $this->hydrator = $hydrator ?: ObjectManager::getInstance()->get(HydratorInterface::class); + $this->identityMap = $identityMap ?? ObjectManager::getInstance() + ->get(IdentityMap::class); + $this->hydrator = $hydrator ?: ObjectManager::getInstance() + ->get(HydratorInterface::class); + $this->routeConfig = $routeConfig ?? ObjectManager::getInstance() + ->get(Config::class); } /** * Validate new layout update values. * - * @param Data\PageInterface $page + * @param PageInterface $page * @return void * @throws \InvalidArgumentException */ - private function validateLayoutUpdate(Data\PageInterface $page): void + private function validateLayoutUpdate(PageInterface $page): void { //Persisted data $savedPage = $page->getId() ? $this->getById($page->getId()) : null; @@ -150,11 +167,11 @@ private function validateLayoutUpdate(Data\PageInterface $page): void /** * Save Page data * - * @param \Magento\Cms\Api\Data\PageInterface|Page $page + * @param PageInterface|Page $page * @return Page * @throws CouldNotSaveException */ - public function save(\Magento\Cms\Api\Data\PageInterface $page) + public function save(PageInterface $page) { if ($page->getStoreId() === null) { $storeId = $this->storeManager->getStore()->getId(); @@ -167,6 +184,7 @@ public function save(\Magento\Cms\Api\Data\PageInterface $page) if ($pageId) { $page = $this->hydrator->hydrate($this->getById($pageId), $this->hydrator->extract($page)); } + $this->validateRoutesDuplication($page); $this->resource->save($page); $this->identityMap->add($page); } catch (\Exception $exception) { @@ -183,7 +201,7 @@ public function save(\Magento\Cms\Api\Data\PageInterface $page) * * @param string $pageId * @return Page - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function getById($pageId) { @@ -202,17 +220,15 @@ public function getById($pageId) * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @param \Magento\Framework\Api\SearchCriteriaInterface $criteria - * @return \Magento\Cms\Api\Data\PageSearchResultsInterface + * @param SearchCriteriaInterface $criteria + * @return PageSearchResultsInterface */ - public function getList(\Magento\Framework\Api\SearchCriteriaInterface $criteria) + public function getList(SearchCriteriaInterface $criteria) { - /** @var \Magento\Cms\Model\ResourceModel\Page\Collection $collection */ $collection = $this->pageCollectionFactory->create(); $this->collectionProcessor->process($criteria, $collection); - /** @var Data\PageSearchResultsInterface $searchResults */ $searchResults = $this->searchResultsFactory->create(); $searchResults->setSearchCriteria($criteria); $searchResults->setItems($collection->getItems()); @@ -223,11 +239,11 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $criteria /** * Delete Page * - * @param \Magento\Cms\Api\Data\PageInterface $page + * @param PageInterface $page * @return bool * @throws CouldNotDeleteException */ - public function delete(\Magento\Cms\Api\Data\PageInterface $page) + public function delete(PageInterface $page) { try { $this->resource->delete($page); @@ -262,11 +278,26 @@ public function deleteById($pageId) private function getCollectionProcessor() { if (!$this->collectionProcessor) { - $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( - // phpstan:ignore "Class Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor not found." - \Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor::class - ); + // phpstan:ignore "Class Magento\Cms\Model\Api\SearchCriteria\PageCollectionProcessor not found." + $this->collectionProcessor = ObjectManager::getInstance() + ->get(PageCollectionProcessor::class); } return $this->collectionProcessor; } + + /** + * Checks that page identifier doesn't duplicate existed routes + * + * @param PageInterface $page + * @return void + * @throws CouldNotSaveException + */ + private function validateRoutesDuplication($page): void + { + if ($this->routeConfig->getRouteByFrontName($page->getIdentifier(), 'frontend')) { + throw new CouldNotSaveException( + __('The value specified in the URL Key field would generate a URL that already exists.') + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php index 53e514083d6ba..42845c0d8ac73 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php @@ -40,8 +40,9 @@ protected function setUp(): void \Magento\TestFramework\Cms\Model\CustomLayoutManager::class ] ]); - $this->repo = Bootstrap::getObjectManager()->get(PageRepositoryInterface::class); - $this->retriever = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); + $objectManager = Bootstrap::getObjectManager(); + $this->repo = $objectManager->get(PageRepositoryInterface::class); + $this->retriever = $objectManager->get(GetPageByIdentifierInterface::class); } /** @@ -54,7 +55,7 @@ protected function setUp(): void public function testSaveUpdateXml(): void { $page = $this->retriever->execute('test_custom_layout_page_1', 0); - $page->setTitle($page->getTitle() .'TEST'); + $page->setTitle($page->getTitle() . 'TEST'); //Is successfully saved without changes to the custom layout xml. $page = $this->repo->save($page); @@ -86,4 +87,19 @@ public function testSaveUpdateXml(): void $this->assertEmpty($page->getCustomLayoutUpdateXml()); $this->assertEmpty($page->getLayoutUpdateXml()); } + + /** + * Verifies that cms page with identifier which duplicates existed route shouldn't be saved + * + * @return void + * @throws \Throwable + * @magentoDataFixture Magento/Cms/_files/pages.php + */ + public function testSaveWithRouteDuplicate(): void + { + $page = $this->retriever->execute('page100', 0); + $page->setIdentifier('customer'); + $this->expectException(CouldNotSaveException::class); + $this->repo->save($page); + } } From 0c79ecf00d7e2c88c833ad2325dfd6dfd5af9c27 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Wed, 29 Jul 2020 11:19:21 -0500 Subject: [PATCH 0149/1013] MC-32659: Order Details by Order Number with additional different product types --- .../Resolver/DownloadableOrderItem/Title.php | 31 +++++++++++++++++++ .../DownloadableGraphQl/etc/schema.graphqls | 4 +-- .../Magento/SalesGraphQl/etc/schema.graphqls | 4 +-- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php new file mode 100644 index 0000000000000..42e7b94e51515 --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver fetches title for links and formats it according to the GraphQL schema. + */ +class Title implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return __('Links'); + } +} diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 5fe6914ee3903..31b93f3b2779d 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -66,12 +66,12 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de } type DownloadableOrderItem implements OrderItemInterface { - downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are assigned to the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Links") + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are ordered from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Links") + downloadable_links_title: String @doc(description: "The label title for the links for the downloadable products") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Title") } type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") - price: Float @doc(description: "The price of the downloadable product") uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") # A Base64 string that encodes option details. } diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index 218619e0ced34..ebda2b5a829b2 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -77,7 +77,7 @@ type OrderAddress @doc(description: "OrderAddress contains detailed information 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\\OrderItemTypeResolver") { +interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\OrderItemInterfaceTypeResolverComposite") { 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") @@ -147,7 +147,7 @@ type Invoice @doc(description: "Invoice details") { comments: [CommentItem] @doc(description: "Comments on the invoice") } -interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\InvoiceItemTypeResolver") { +interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\InvoiceItemInterfaceTypeResolverComposite") { 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") From b1c86405a1ebffd47d0b8854b9613bc8db5c6818 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Wed, 29 Jul 2020 12:36:06 -0500 Subject: [PATCH 0150/1013] MC-32659: Order Details by Order Number with additional different product types --- .../Resolver/DownloadableOrderItem/Title.php | 31 ------------------- .../DownloadableGraphQl/etc/schema.graphqls | 1 - 2 files changed, 32 deletions(-) delete mode 100644 app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php deleted file mode 100644 index 42e7b94e51515..0000000000000 --- a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Title.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; - -use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; - -/** - * Resolver fetches title for links and formats it according to the GraphQL schema. - */ -class Title implements ResolverInterface -{ - /** - * @inheritdoc - */ - public function resolve( - Field $field, - $context, - ResolveInfo $info, - array $value = null, - array $args = null - ) { - return __('Links'); - } -} diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 31b93f3b2779d..9c9dd438746f0 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -67,7 +67,6 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de type DownloadableOrderItem implements OrderItemInterface { downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are ordered from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Links") - downloadable_links_title: String @doc(description: "The label title for the links for the downloadable products") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Title") } type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { From 4c7afa57eb00d7d356eb93c1df578d743ac27ac7 Mon Sep 17 00:00:00 2001 From: Sachin Admane <sadmane@adobe.com> Date: Wed, 29 Jul 2020 14:57:12 -0500 Subject: [PATCH 0151/1013] MC-35389: Set same site attribute. CookieManager default value fixes in unit and integration tests. --- .../Stdlib/Cookie/CookieScopeTest.php | 14 +++++----- .../Stdlib/Cookie/CookieMetadata.php | 6 ++--- .../Stdlib/Cookie/PublicCookieMetadata.php | 4 +-- .../Stdlib/Cookie/SensitiveCookieMetadata.php | 2 +- .../Test/Unit/Cookie/CookieScopeTest.php | 26 +++++++++++-------- .../Test/Unit/Cookie/PhpCookieManagerTest.php | 22 ++++++---------- .../Unit/Cookie/PublicCookieMetadataTest.php | 17 ++++++++++++ .../Cookie/SensitiveCookieMetadataTest.php | 8 +++--- 8 files changed, 58 insertions(+), 41 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php b/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php index e10fae226a0be..3fa318e6cc98e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Stdlib/Cookie/CookieScopeTest.php @@ -43,7 +43,7 @@ public function testGetSensitiveCookieMetadataEmpty() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -51,11 +51,13 @@ public function testGetSensitiveCookieMetadataEmpty() $this->request->setServer(new Parameters($serverVal)); } - public function testGetPublicCookieMetadataNotEmpty() + public function testGetPublicCookieDefaultMetadata() { $cookieScope = $this->createCookieScope(); - - $this->assertNotEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); + $expected = [ + PublicCookieMetadata::KEY_SAME_SITE => 'Lax' + ]; + $this->assertEquals($expected, $cookieScope->getPublicCookieMetadata()->__toArray()); } public function testGetSensitiveCookieMetadataDefaults() @@ -78,7 +80,7 @@ public function testGetSensitiveCookieMetadataDefaults() SensitiveCookieMetadata::KEY_DOMAIN => 'default domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => false, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -142,7 +144,7 @@ public function testGetSensitiveCookieMetadataOverrides() SensitiveCookieMetadata::KEY_DOMAIN => 'override domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => false, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ], $cookieScope->getSensitiveCookieMetadata($override)->__toArray() ); diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index ccba535ebb2a7..c799821519c60 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -151,10 +151,10 @@ public function getSecure() /** * Setter for Cookie SameSite attribute * - * @param string|null $sameSite + * @param string $sameSite * @return $this */ - public function setSameSite(?string $sameSite): CookieMetadata + public function setSameSite(string $sameSite): CookieMetadata { if (!array_key_exists(strtolower($sameSite), self::SAME_SITE_ALLOWED_VALUES)) { throw new \InvalidArgumentException( @@ -163,7 +163,7 @@ public function setSameSite(?string $sameSite): CookieMetadata } if (!$this->getSecure() && strtolower($sameSite) === 'none') { throw new \InvalidArgumentException( - 'Cookie must be secure in order to use the Same Site None directive.' + 'Cookie must be secure in order to use the SameSite None directive.' ); } $sameSite = self::SAME_SITE_ALLOWED_VALUES[strtolower($sameSite)]; diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php index 41201a3f4cbf1..2978177586ad3 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php @@ -79,9 +79,9 @@ public function setHttpOnly($httpOnly) */ public function setSecure($secure) { - if (!$secure && strtolower(self::KEY_SAME_SITE) === 'none') { + if (!$secure && $this->get(self::KEY_SAME_SITE) === 'None') { throw new \InvalidArgumentException( - 'Cookie must be secure in order to use the Same Site None directive.' + 'Cookie must be secure in order to use the SameSite None directive.' ); } return $this->set(self::KEY_SECURE, $secure); diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php index 1d787b1a6a488..24bfabaebf08c 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php @@ -34,7 +34,7 @@ public function __construct(RequestInterface $request, $metadata = []) $metadata[self::KEY_HTTP_ONLY] = true; } if (!isset($metadata[self::KEY_SAME_SITE])) { - $metadata[self::KEY_SAME_SITE] = 'Strict'; + $metadata[self::KEY_SAME_SITE] = 'Lax'; } $this->request = $request; parent::__construct($metadata); diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php index 4ae3336a40d7d..6baa1fca5f2e2 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/CookieScopeTest.php @@ -68,7 +68,7 @@ public function testGetSensitiveCookieMetadataEmpty() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax', ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -77,21 +77,25 @@ public function testGetSensitiveCookieMetadataEmpty() /** * @covers ::getPublicCookieMetadata */ - public function testGetPublicCookieMetadataNotEmpty() + public function testGetPublicCookieDefaultMetadata() { $cookieScope = $this->createCookieScope(); - - $this->assertNotEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); + $expected = [ + PublicCookieMetadata::KEY_SAME_SITE => 'Lax' + ]; + $this->assertEquals($expected, $cookieScope->getPublicCookieMetadata()->__toArray()); } /** * @covers ::getCookieMetadata */ - public function testGetCookieMetadataNotEmpty() + public function testGetCookieDefaultMetadata() { $cookieScope = $this->createCookieScope(); - - $this->assertNotEmpty($cookieScope->getPublicCookieMetadata()->__toArray()); + $expected = [ + CookieMetadata::KEY_SAME_SITE => 'Lax' + ]; + $this->assertEquals($expected, $cookieScope->getPublicCookieMetadata()->__toArray()); } /** @@ -120,7 +124,7 @@ public function testGetSensitiveCookieMetadataDefaults() SensitiveCookieMetadata::KEY_DOMAIN => 'default domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -152,7 +156,7 @@ public function testGetPublicCookieMetadataDefaults() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ], $cookieScope->getSensitiveCookieMetadata()->__toArray() ); @@ -212,7 +216,7 @@ public function testGetSensitiveCookieMetadataOverrides() SensitiveCookieMetadata::KEY_DOMAIN => 'override domain', SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ], $cookieScope->getSensitiveCookieMetadata($override)->__toArray() ); @@ -277,7 +281,7 @@ public function testGetCookieMetadataOverrides() [ SensitiveCookieMetadata::KEY_HTTP_ONLY => true, SensitiveCookieMetadata::KEY_SECURE => true, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict' + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax' ], $cookieScope->getSensitiveCookieMetadata($this->createSensitiveMetadata())->__toArray() ); diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php index d55a4200a5750..9b4e07092751c 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php @@ -669,7 +669,7 @@ private static function assertSensitiveCookieWithNoMetaDataHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Strict', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -695,7 +695,7 @@ private static function assertSensitiveCookieWithNoMetaDataNotHttps( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Strict', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -721,7 +721,7 @@ private static function assertSensitiveCookieNoDomainNoPath( self::assertTrue($httpOnly); self::assertEquals('', $domain); self::assertEquals('', $path); - self::assertEquals('Strict', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -747,7 +747,7 @@ private static function assertSensitiveCookieWithDomainAndPath( self::assertTrue($httpOnly); self::assertEquals('magento.url', $domain); self::assertEquals('/backend', $path); - self::assertEquals('Strict', $sameSite); + self::assertEquals('Lax', $sameSite); } /** @@ -901,19 +901,13 @@ protected function stubGetCookie($get, $default, $return) */ public function testSetCookieInvalidSameSiteValue(): void { - /** @var \Magento\Framework\Stdlib\Cookie\PublicCookieMetadata $cookieMetadata */ + /** @var \Magento\Framework\Stdlib\Cookie\CookieMetadata $cookieMetadata */ $cookieMetadata = $this->objectManager->getObject( CookieMetadata::class ); - - try { - $cookieMetadata->setSameSite('default value'); - } catch (\InvalidArgumentException $e) { - $this->assertEquals( - 'Invalid argument provided for SameSite directive expected one of: Strict, Lax or None', - $e->getMessage() - ); - } + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Invalid argument provided for SameSite directive expected one of: Strict, Lax or None'); + $cookieMetadata->setSameSite('default value'); } } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php index 2cf56b661fe27..a94377e686409 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PublicCookieMetadataTest.php @@ -100,4 +100,21 @@ public function testToArray(array $metadata, array $expected): void ); $this->assertEquals($expected, $object->__toArray()); } + + /** + * Test Set SameSite None With Insecure Cookies + * + * @return void + */ + public function testSetSecureWithSameSiteNone(): void + { + /** @var \Magento\Framework\Stdlib\Cookie\PublicCookieMetadata $publicCookieMetadata */ + $publicCookieMetadata = $this->objectManager->getObject( + PublicCookieMetadata::class + ); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage('Cookie must be secure in order to use the SameSite None directive.'); + $publicCookieMetadata->setSameSite('None'); + $publicCookieMetadata->setSecure(false); + } } diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php index b113944299a7e..b71a70c687897 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/SensitiveCookieMetadataTest.php @@ -189,7 +189,7 @@ public function toArrayDataProvider() SensitiveCookieMetadata::KEY_DOMAIN => 'domain', SensitiveCookieMetadata::KEY_PATH => 'path', SensitiveCookieMetadata::KEY_HTTP_ONLY => 1, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax', ], 0, ], @@ -204,7 +204,7 @@ public function toArrayDataProvider() SensitiveCookieMetadata::KEY_DOMAIN => 'domain', SensitiveCookieMetadata::KEY_PATH => 'path', SensitiveCookieMetadata::KEY_HTTP_ONLY => 1, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax', ], ], 'without secure 2' => [ @@ -218,7 +218,7 @@ public function toArrayDataProvider() SensitiveCookieMetadata::KEY_DOMAIN => 'domain', SensitiveCookieMetadata::KEY_PATH => 'path', SensitiveCookieMetadata::KEY_HTTP_ONLY => 1, - SensitiveCookieMetadata::KEY_SAME_SITE => 'Strict', + SensitiveCookieMetadata::KEY_SAME_SITE => 'Lax', ], ], ]; @@ -244,7 +244,7 @@ public function getMethodData() return [ "getDomain" => ["setDomain", 'getDomain', "example.com"], "getPath" => ["setPath", 'getPath', "path"], - "getSameSite" => ["setSameSite", 'getSameSite', 'Strict'] + "getSameSite" => ["setSameSite", 'getSameSite', 'Lax'] ]; } } From 98aaeab27f4b26c9ba898f5afdefdf75788cec60 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Wed, 29 Jul 2020 15:47:11 -0500 Subject: [PATCH 0152/1013] MC-32659: Order Details by Order Number with additional different product types --- app/code/Magento/DownloadableGraphQl/composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/DownloadableGraphQl/composer.json b/app/code/Magento/DownloadableGraphQl/composer.json index d4f506d886bfb..36f06b7ba8284 100644 --- a/app/code/Magento/DownloadableGraphQl/composer.json +++ b/app/code/Magento/DownloadableGraphQl/composer.json @@ -4,6 +4,7 @@ "type": "magento2-module", "require": { "php": "~7.3.0||~7.4.0", + "magento/module-store": "*", "magento/module-catalog": "*", "magento/module-downloadable": "*", "magento/module-quote": "*", From cacc407d356f0c0018e82f6e014402de791ea44c Mon Sep 17 00:00:00 2001 From: Sachin Admane <sadmane@adobe.com> Date: Wed, 29 Jul 2020 19:15:39 -0500 Subject: [PATCH 0153/1013] MC-35389: Set same site attribute. Static fix. --- .../Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php index 9b4e07092751c..e41cbdfe51638 100644 --- a/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php +++ b/lib/internal/Magento/Framework/Stdlib/Test/Unit/Cookie/PhpCookieManagerTest.php @@ -906,7 +906,8 @@ public function testSetCookieInvalidSameSiteValue(): void CookieMetadata::class ); $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('Invalid argument provided for SameSite directive expected one of: Strict, Lax or None'); + $exceptionMessage = 'Invalid argument provided for SameSite directive expected one of: Strict, Lax or None'; + $this->expectExceptionMessage($exceptionMessage); $cookieMetadata->setSameSite('default value'); } } From 72dc2dd8f02340be0517c39b461bc0a208346d4f Mon Sep 17 00:00:00 2001 From: Alexander Steshuk <grp-engcom-vendorworker-Kilo@adobe.com> Date: Thu, 30 Jul 2020 12:21:42 +0300 Subject: [PATCH 0154/1013] Code refactoring. --- .../CatalogSearch/Model/Search/Category.php | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Search/Category.php b/app/code/Magento/CatalogSearch/Model/Search/Category.php index 2deee1027da74..1f304d0c920dc 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/Category.php +++ b/app/code/Magento/CatalogSearch/Model/Search/Category.php @@ -3,22 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogSearch\Model\Search; /** * Search model for backend search - * - * @method Category setQuery(string $query) - * @method string|null getQuery() - * @method bool hasQuery() - * @method Category setStart(int $startPosition) - * @method int|null getStart() - * @method bool hasStart() - * @method Category setLimit(int $limit) - * @method int|null getLimit() - * @method bool hasLimit() - * @method Category setResults(array $results) - * @method array getResults() - * @api */ class Category extends \Magento\Framework\DataObject { @@ -106,7 +95,7 @@ public function load() $searchResults = $this->categoryRepository->getList($searchCriteria); foreach ($searchResults->getItems() as $category) { - $description = strip_tags($category->getDescription()); + $description = $category->getDescription() ? strip_tags($category->getDescription()) : ''; $result[] = [ 'id' => 'category/1/' . $category->getId(), 'type' => __('Category'), From 20d6fdf19bbcc6d224709468dcd3ff1dd26af4c9 Mon Sep 17 00:00:00 2001 From: Alexander Steshuk <grp-engcom-vendorworker-Kilo@adobe.com> Date: Thu, 30 Jul 2020 12:26:07 +0300 Subject: [PATCH 0155/1013] Code refactoring. --- .../CatalogSearch/Model/Search/Category.php | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Search/Category.php b/app/code/Magento/CatalogSearch/Model/Search/Category.php index 1f304d0c920dc..13e15dbf0c0f0 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/Category.php +++ b/app/code/Magento/CatalogSearch/Model/Search/Category.php @@ -6,60 +6,70 @@ declare(strict_types=1); namespace Magento\CatalogSearch\Model\Search; + +use Magento\Backend\Helper\Data; +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Api\SearchCriteriaBuilderFactory; +use Magento\Framework\DataObject; +use Magento\Framework\Stdlib\StringUtils; + /** * Search model for backend search */ -class Category extends \Magento\Framework\DataObject +class Category extends DataObject { /** - * Adminhtml data - * - * @var \Magento\Backend\Helper\Data + * @var Data */ - protected $_adminhtmlData = null; + private $adminhtmlData = null; /** - * @var \Magento\Catalog\Api\CategoryListInterface + * @var CategoryListInterface */ - protected $categoryRepository; + private $categoryRepository; /** - * @var \Magento\Framework\Api\SearchCriteriaBuilder + * @var SearchCriteriaBuilder */ - protected $searchCriteriaBuilder; + private $searchCriteriaBuilder; /** - * @var \Magento\Framework\Api\FilterBuilder + * @var FilterBuilder */ - protected $filterBuilder; + private $filterBuilder; /** - * Magento string lib - * - * @var \Magento\Framework\Stdlib\StringUtils + * @var SearchCriteriaBuilderFactory */ - protected $string; + private $searchCriteriaBuilderFactory; /** - * Initialize dependencies. - * - * @param \Magento\Backend\Helper\Data $adminhtmlData - * @param \Magento\Catalog\Api\CategoryListInterface $categoryRepository - * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder - * @param \Magento\Framework\Api\FilterBuilder $filterBuilder - * @param \Magento\Framework\Stdlib\StringUtils $string + * @var StringUtils + */ + private $string; + + /** + * @param Data $adminhtmlData + * @param CategoryListInterface $categoryRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory + * @param FilterBuilder $filterBuilder + * @param StringUtils $string */ public function __construct( - \Magento\Backend\Helper\Data $adminhtmlData, - \Magento\Catalog\Api\CategoryListInterface $categoryRepository, - \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, - \Magento\Framework\Api\FilterBuilder $filterBuilder, - \Magento\Framework\Stdlib\StringUtils $string - ) - { - $this->_adminhtmlData = $adminhtmlData; + Data $adminhtmlData, + CategoryListInterface $categoryRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory, + FilterBuilder $filterBuilder, + StringUtils $string + ) { + $this->adminhtmlData = $adminhtmlData; $this->categoryRepository = $categoryRepository; $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; $this->filterBuilder = $filterBuilder; $this->string = $string; } @@ -76,7 +86,7 @@ public function load() $this->setResults($result); return $this; } - + $this->searchCriteriaBuilder = $this->searchCriteriaBuilderFactory->create(); $this->searchCriteriaBuilder->setCurrentPage($this->getStart()); $this->searchCriteriaBuilder->setPageSize($this->getLimit()); $searchFields = ['name']; @@ -86,7 +96,7 @@ public function load() $filters[] = $this->filterBuilder ->setField($field) ->setConditionType('like') - ->setValue('%' . $this->getQuery() . '%') + ->setValue(sprintf("%%%s%%", $this->getQuery())) ->create(); } $this->searchCriteriaBuilder->addFilters($filters); @@ -101,7 +111,7 @@ public function load() 'type' => __('Category'), 'name' => $category->getName(), 'description' => $this->string->substr($description, 0, 30), - 'url' => $this->_adminhtmlData->getUrl('catalog/category/edit', ['id' => $category->getId()]), + 'url' => $this->adminhtmlData->getUrl('catalog/category/edit', ['id' => $category->getId()]), ]; } $this->setResults($result); From 3f37b18c808acc8941ebf07a7ee785b1f3d29f54 Mon Sep 17 00:00:00 2001 From: Alexander Steshuk <grp-engcom-vendorworker-Kilo@adobe.com> Date: Thu, 30 Jul 2020 12:34:03 +0300 Subject: [PATCH 0156/1013] MFTF test --- .../Mftf/Test/AdminCategorySearchTest.xml | 51 +++++++++++++++++++ .../AdminSetGlobalSearchValueActionGroup.xml | 20 ++++++++ .../Mftf/Section/AdminGlobalSearchSection.xml | 3 ++ 3 files changed, 74 insertions(+) create mode 100644 app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml create mode 100644 app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSetGlobalSearchValueActionGroup.xml diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml new file mode 100644 index 0000000000000..4c473f06a8884 --- /dev/null +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.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="AdminCategorySearchTest"> + <annotations> + <features value="Search Category"/> + <stories value="Search categories in admin panel"/> + <title value="Search for categories"/> + <description value="Global search in backend can search into Categories."/> + <severity value="MINOR"/> + <group value="Search"/> + </annotations> + <before> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Create Simple Category --> + <createData entity="SimpleSubCategory" stepKey="createSimpleCategory"/> + </before> + <after> + <!-- Delete created category --> + <deleteData createDataKey="createSimpleCategory" stepKey="deleteCreatedCategory"/> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Add created category name in the search field--> + <actionGroup ref="AdminSetGlobalSearchValueActionGroup" stepKey="setSearch"> + <argument name="textSearch" value="$$createSimpleCategory.name$$"/> + </actionGroup> + + <!-- Wait for suggested results--> + <waitForElementVisible selector="{{AdminGlobalSearchSection.globalSearchSuggestedCategoryText}}" stepKey="waitForSuggestions"/> + + <!-- Click on suggested result in category URL--> + <click selector="{{AdminGlobalSearchSection.globalSearchSuggestedCategoryLink}}" stepKey="openCategory"/> + + <!-- Wait for suggested results--> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Loaded page should be edit page of created category --> + <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="$$createSimpleCategory.name$$" stepKey="checkCategoryName"/> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSetGlobalSearchValueActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSetGlobalSearchValueActionGroup.xml new file mode 100644 index 0000000000000..5bc63bf730de0 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminSetGlobalSearchValueActionGroup.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="AdminSetGlobalSearchValueActionGroup"> + <arguments> + <argument name="textSearch" type="string" defaultValue=""/> + </arguments> + + <click selector="{{AdminGlobalSearchSection.globalSearch}}" stepKey="clickSearchBtn"/> + <waitForElementVisible selector="{{AdminGlobalSearchSection.globalSearchActive}}" stepKey="waitForSearchInputVisible"/> + <fillField selector="{{AdminGlobalSearchSection.globalSearchInput}}" userInput="{{textSearch}}" stepKey="fillSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml b/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml index 0ba61283548cf..a529000e20923 100644 --- a/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml +++ b/app/code/Magento/Search/Test/Mftf/Section/AdminGlobalSearchSection.xml @@ -11,5 +11,8 @@ <section name="AdminGlobalSearchSection"> <element name="globalSearch" type="button" selector=".search-global-label"/> <element name="globalSearchActive" type="block" selector=".search-global-field._active"/> + <element name="globalSearchInput" type="input" selector=".search-global-input"/> + <element name="globalSearchSuggestedCategoryText" type="text" selector="//span[contains(text(), 'Category')]"/> + <element name="globalSearchSuggestedCategoryLink" type="text" selector="//span[contains(text(), 'Category')]/preceding-sibling::a"/> </section> </sections> From dae1dd8d1bbab88eb7f6ee2d12c7049f9dcb930b Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Fri, 31 Jul 2020 15:31:27 -0500 Subject: [PATCH 0157/1013] MC-36403: DB setup patch update - Added clean up patch to quote, sales, and wishlist module --- .../Setup/Patch/Data/WishlistDataCleanUp.php | 154 +++++++++++++++++ .../Setup/Patch/Data/WishlistDataCleanUp.php | 157 ++++++++++++++++++ .../Setup/Patch/Data/WishlistDataCleanUp.php | 153 +++++++++++++++++ 3 files changed, 464 insertions(+) create mode 100644 app/code/Magento/Quote/Setup/Patch/Data/WishlistDataCleanUp.php create mode 100644 app/code/Magento/Sales/Setup/Patch/Data/WishlistDataCleanUp.php create mode 100644 app/code/Magento/Wishlist/Setup/Patch/Data/WishlistDataCleanUp.php diff --git a/app/code/Magento/Quote/Setup/Patch/Data/WishlistDataCleanUp.php b/app/code/Magento/Quote/Setup/Patch/Data/WishlistDataCleanUp.php new file mode 100644 index 0000000000000..3d66f53ff6c6d --- /dev/null +++ b/app/code/Magento/Quote/Setup/Patch/Data/WishlistDataCleanUp.php @@ -0,0 +1,154 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); +namespace Magento\Quote\Setup\Patch\Data; + +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Quote\Setup\QuoteSetupFactory; +use Psr\Log\LoggerInterface; + +/** + * Class Clean Up Data Removes unused data + */ +class WishlistDataCleanUp implements DataPatchInterface +{ + /** + * Batch size for query + */ + private const BATCH_SIZE = 1000; + + /** + * @var QuoteSetupFactory + */ + private $quoteSetupFactory; + + /** + * @var Generator + */ + private $queryGenerator; + + /** + * @var Json + */ + private $json; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * RemoveData constructor. + * @param Json $json + * @param Generator $queryGenerator + * @param QuoteSetupFactory $quoteSetupFactory + * @param LoggerInterface $logger + */ + public function __construct( + Json $json, + Generator $queryGenerator, + QuoteSetupFactory $quoteSetupFactory, + LoggerInterface $logger + ) { + $this->json = $json; + $this->queryGenerator = $queryGenerator; + $this->quoteSetupFactory = $quoteSetupFactory; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function apply() + { + try { + $this->cleanQuoteItemOptionTable(); + } catch (\Throwable $e) { + $this->logger->warning( + 'Quote module WishlistDataCleanUp patch experienced an error and could not be completed.' + . ' Please submit a support ticket or email us at security@magento.com.' + ); + + return $this; + } + + return $this; + } + + /** + * Remove login data from quote_item_option table. + * + * @throws LocalizedException + */ + private function cleanQuoteItemOptionTable() + { + $quoteSetup = $this->quoteSetupFactory->create(); + $tableName = $quoteSetup->getTable('quote_item_option'); + $select = $quoteSetup + ->getConnection() + ->select() + ->from( + $tableName, + ['option_id', 'value'] + ) + ->where( + 'value LIKE ?', + '%login%' + ); + $iterator = $this->queryGenerator->generate('option_id', $select, self::BATCH_SIZE); + $rowErrorFlag = false; + foreach ($iterator as $selectByRange) { + $optionRows = $quoteSetup->getConnection()->fetchAll($selectByRange); + foreach ($optionRows as $optionRow) { + try { + $rowValue = $this->json->unserialize($optionRow['value']); + if (is_array($rowValue) + && array_key_exists('login', $rowValue) + ) { + unset($rowValue['login']); + } + $rowValue = $this->json->serialize($rowValue); + $quoteSetup->getConnection()->update( + $tableName, + ['value' => $rowValue], + ['option_id = ?' => $optionRow['option_id']] + ); + } catch (\Throwable $e) { + $rowErrorFlag = true; + continue; + } + } + } + if ($rowErrorFlag) { + $this->logger->warning( + 'Data clean up could not be completed due to unexpected data format in the table "' + . $tableName + . '". Please submit a support ticket or email us at security@magento.com.' + ); + } + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + ConvertSerializedDataToJson::class + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Sales/Setup/Patch/Data/WishlistDataCleanUp.php b/app/code/Magento/Sales/Setup/Patch/Data/WishlistDataCleanUp.php new file mode 100644 index 0000000000000..7a10d5cd191bf --- /dev/null +++ b/app/code/Magento/Sales/Setup/Patch/Data/WishlistDataCleanUp.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); +namespace Magento\Sales\Setup\Patch\Data; + +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Sales\Setup\SalesSetupFactory; +use Psr\Log\LoggerInterface; + +/** + * Class Clean Up Data Removes unused data + */ +class WishlistDataCleanUp implements DataPatchInterface +{ + /** + * Batch size for query + */ + private const BATCH_SIZE = 1000; + + /** + * @var SalesSetupFactory + */ + private $salesSetupFactory; + + /** + * @var Generator + */ + private $queryGenerator; + + /** + * @var Json + */ + private $json; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * RemoveData constructor. + * @param Json $json + * @param Generator $queryGenerator + * @param SalesSetupFactory $salesSetupFactory + * @param LoggerInterface $logger + */ + public function __construct( + Json $json, + Generator $queryGenerator, + SalesSetupFactory $salesSetupFactory, + LoggerInterface $logger + ) { + $this->json = $json; + $this->queryGenerator = $queryGenerator; + $this->salesSetupFactory = $salesSetupFactory; + $this->logger = $logger; + } + + /** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function apply() + { + try { + $this->cleanSalesOrderItemTable(); + } catch (\Throwable $e) { + $this->logger->warning( + 'Sales module WishlistDataCleanUp patch experienced an error and could not be completed.' + . ' Please submit a support ticket or email us at security@magento.com.' + ); + + return $this; + } + + return $this; + } + + /** + * Remove login data from sales_order_item table. + * + * @throws LocalizedException + */ + private function cleanSalesOrderItemTable() + { + $salesSetup = $this->salesSetupFactory->create(); + $tableName = $salesSetup->getTable('sales_order_item'); + $select = $salesSetup + ->getConnection() + ->select() + ->from( + $tableName, + ['item_id', 'product_options'] + ) + ->where( + 'product_options LIKE ?', + '%login%' + ); + $iterator = $this->queryGenerator->generate('item_id', $select, self::BATCH_SIZE); + $rowErrorFlag = false; + foreach ($iterator as $selectByRange) { + $itemRows = $salesSetup->getConnection()->fetchAll($selectByRange); + foreach ($itemRows as $itemRow) { + try { + $rowValue = $this->json->unserialize($itemRow['product_options']); + if (is_array($rowValue) + && array_key_exists('info_buyRequest', $rowValue) + && array_key_exists('login', $rowValue['info_buyRequest']) + ) { + unset($rowValue['info_buyRequest']['login']); + } + $rowValue = $this->json->serialize($rowValue); + $salesSetup->getConnection()->update( + $tableName, + ['product_options' => $rowValue], + ['item_id = ?' => $itemRow['item_id']] + ); + } catch (\Throwable $e) { + $rowErrorFlag = true; + continue; + } + } + } + if ($rowErrorFlag) { + $this->logger->warning( + 'Data clean up could not be completed due to unexpected data format in the table "' + . $tableName + . '". Please submit a support ticket or email us at security@magento.com.' + ); + } + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + ConvertSerializedDataToJson::class + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Wishlist/Setup/Patch/Data/WishlistDataCleanUp.php b/app/code/Magento/Wishlist/Setup/Patch/Data/WishlistDataCleanUp.php new file mode 100644 index 0000000000000..df0aed7daf2c0 --- /dev/null +++ b/app/code/Magento/Wishlist/Setup/Patch/Data/WishlistDataCleanUp.php @@ -0,0 +1,153 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); +namespace Magento\Wishlist\Setup\Patch\Data; + +use Magento\Framework\DB\Query\Generator; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Psr\Log\LoggerInterface; + +/** + * Class Clean Up Data Removes unused data + */ +class WishlistDataCleanUp implements DataPatchInterface +{ + /** + * Batch size for query + */ + private const BATCH_SIZE = 1000; + + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var Generator + */ + private $queryGenerator; + + /** + * @var Json + */ + private $json; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * RemoveData constructor. + * @param Json $json + * @param Generator $queryGenerator + * @param ModuleDataSetupInterface $moduleDataSetup + * @param LoggerInterface $logger + */ + public function __construct( + Json $json, + Generator $queryGenerator, + ModuleDataSetupInterface $moduleDataSetup, + LoggerInterface $logger + ) { + $this->json = $json; + $this->queryGenerator = $queryGenerator; + $this->moduleDataSetup = $moduleDataSetup; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function apply() + { + try { + $this->cleanWishlistItemOptionTable(); + } catch (\Throwable $e) { + $this->logger->warning( + 'Wishlist module WishlistDataCleanUp patch experienced an error and could not be completed.' + . ' Please submit a support ticket or email us at security@magento.com.' + ); + + return $this; + } + + return $this; + } + + /** + * Remove login data from wishlist_item_option table. + * + * @throws LocalizedException + */ + private function cleanWishlistItemOptionTable() + { + $tableName = $this->moduleDataSetup->getTable('wishlist_item_option'); + $select = $this->moduleDataSetup + ->getConnection() + ->select() + ->from( + $tableName, + ['option_id', 'value'] + ) + ->where( + 'value LIKE ?', + '%login%' + ); + $iterator = $this->queryGenerator->generate('option_id', $select, self::BATCH_SIZE); + $rowErrorFlag = false; + foreach ($iterator as $selectByRange) { + $optionRows = $this->moduleDataSetup->getConnection()->fetchAll($selectByRange); + foreach ($optionRows as $optionRow) { + try { + $rowValue = $this->json->unserialize($optionRow['value']); + if (is_array($rowValue) + && array_key_exists('login', $rowValue) + ) { + unset($rowValue['login']); + } + $rowValue = $this->json->serialize($rowValue); + $this->moduleDataSetup->getConnection()->update( + $tableName, + ['value' => $rowValue], + ['option_id = ?' => $optionRow['option_id']] + ); + } catch (\Throwable $e) { + $rowErrorFlag = true; + continue; + } + } + } + if ($rowErrorFlag) { + $this->logger->warning( + 'Data clean up could not be completed due to unexpected data format in the table "' + . $tableName + . '". Please submit a support ticket or email us at security@magento.com.' + ); + } + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + ConvertSerializedData::class + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} From efeff4cce3904d1042614c1a956ecdd7cbdff69f Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Fri, 31 Jul 2020 16:08:16 -0500 Subject: [PATCH 0158/1013] MC-35230: Password lifetime is not honored, it's possible to use the old password --- .../Backend/TrackAdminNewPasswordObserver.php | 33 +++++++++++-------- .../TrackAdminNewPasswordObserverTest.php | 7 ++-- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/User/Observer/Backend/TrackAdminNewPasswordObserver.php b/app/code/Magento/User/Observer/Backend/TrackAdminNewPasswordObserver.php index 059879ab9613f..9573aaa1547ab 100644 --- a/app/code/Magento/User/Observer/Backend/TrackAdminNewPasswordObserver.php +++ b/app/code/Magento/User/Observer/Backend/TrackAdminNewPasswordObserver.php @@ -8,52 +8,57 @@ use Magento\Framework\Event\Observer as EventObserver; use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Message\ManagerInterface; use Magento\User\Model\User; +use Magento\User\Model\Backend\Config\ObserverConfig; +use Magento\User\Model\ResourceModel\User as UserResource; +use Magento\Backend\Model\Auth\Session as AuthSession; /** * User backend observer model for passwords + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class TrackAdminNewPasswordObserver implements ObserverInterface { /** * Backend configuration interface * - * @var \Magento\User\Model\Backend\Config\ObserverConfig + * @var ObserverConfig */ protected $observerConfig; /** * Admin user resource model * - * @var \Magento\User\Model\ResourceModel\User + * @var UserResource */ protected $userResource; /** * Backend authorization session * - * @var \Magento\Backend\Model\Auth\Session + * @var AuthSession */ protected $authSession; /** * Message manager interface * - * @var \Magento\Framework\Message\ManagerInterface + * @var ManagerInterface */ protected $messageManager; /** - * @param \Magento\User\Model\Backend\Config\ObserverConfig $observerConfig - * @param \Magento\User\Model\ResourceModel\User $userResource - * @param \Magento\Backend\Model\Auth\Session $authSession - * @param \Magento\Framework\Message\ManagerInterface $messageManager + * @param ObserverConfig $observerConfig + * @param UserResource $userResource + * @param AuthSession $authSession + * @param ManagerInterface $messageManager */ public function __construct( - \Magento\User\Model\Backend\Config\ObserverConfig $observerConfig, - \Magento\User\Model\ResourceModel\User $userResource, - \Magento\Backend\Model\Auth\Session $authSession, - \Magento\Framework\Message\ManagerInterface $messageManager + ObserverConfig $observerConfig, + UserResource $userResource, + AuthSession $authSession, + ManagerInterface $messageManager ) { $this->observerConfig = $observerConfig; $this->userResource = $userResource; @@ -69,11 +74,11 @@ public function __construct( */ public function execute(EventObserver $observer) { - /* @var $user \Magento\User\Model\User */ + /* @var $user User */ $user = $observer->getEvent()->getObject(); if ($user->getId()) { $passwordHash = $user->getPassword(); - if ($passwordHash && !$user->getForceNewPassword()) { + if ($passwordHash && $user->dataHasChangedFor('password')) { $this->userResource->trackPassword($user, $passwordHash); $this->messageManager->getMessages()->deleteMessageByIdentifier(User::MESSAGE_ID_PASSWORD_EXPIRED); $this->authSession->unsPciAdminUserIsPasswordExpired(); diff --git a/app/code/Magento/User/Test/Unit/Observer/Backend/TrackAdminNewPasswordObserverTest.php b/app/code/Magento/User/Test/Unit/Observer/Backend/TrackAdminNewPasswordObserverTest.php index 10477bdf80303..90e5f04b9c73e 100644 --- a/app/code/Magento/User/Test/Unit/Observer/Backend/TrackAdminNewPasswordObserverTest.php +++ b/app/code/Magento/User/Test/Unit/Observer/Backend/TrackAdminNewPasswordObserverTest.php @@ -112,14 +112,17 @@ public function testTrackAdminPassword() /** @var \Magento\User\Model\User|MockObject $userMock */ $userMock = $this->getMockBuilder(\Magento\User\Model\User::class) ->disableOriginalConstructor() - ->setMethods(['getId', 'getPassword', 'getForceNewPassword']) + ->setMethods(['getId', 'getPassword', 'dataHasChangedFor']) ->getMock(); $eventObserverMock->expects($this->once())->method('getEvent')->willReturn($eventMock); $eventMock->expects($this->once())->method('getObject')->willReturn($userMock); $userMock->expects($this->once())->method('getId')->willReturn($uid); $userMock->expects($this->once())->method('getPassword')->willReturn($newPW); - $userMock->expects($this->once())->method('getForceNewPassword')->willReturn(false); + $userMock->expects($this->once()) + ->method('dataHasChangedFor') + ->with('password') + ->willReturn(true); /** @var Collection|MockObject $collectionMock */ $collectionMock = $this->getMockBuilder(Collection::class) From 5c111dd8ecfa1d7a2c5dc1216f44a6455cd36d58 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Mon, 3 Aug 2020 09:11:57 -0500 Subject: [PATCH 0159/1013] MC-20639: MyAccount :: Order Details :: Order Details by Order Number with additional different product types - test for downloadable product --- ...rieveOrdersWithDownloadableProductTest.php | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php new file mode 100644 index 0000000000000..143e0bb1cddb6 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -0,0 +1,157 @@ +<?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\Downloadable\Api\Data\LinkInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class RetrieveOrdersTest + */ +class RetrieveOrdersWithDownloadableProductTest 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/Downloadable/_files/order_with_customer_and_downloadable_product.php + */ + public function testGetCustomerOrdersDownloadableProduct() + { + $orderNumber = '100000001'; + $response = $this->getCustomerOrderQuery($orderNumber); + $customerOrderItemsInResponse = $response[0]['items']; + + $this->assertNotEmpty($customerOrderItemsInResponse); + $downloadableItemInTheOrder = $customerOrderItemsInResponse[0]; + $this->assertEquals( + 'downloadable-product', + $downloadableItemInTheOrder['product_sku'] + ); + $priceOfDownloadableItemInOrder = $downloadableItemInTheOrder['product_sale_price']['value']; + $this->assertEquals(10, $priceOfDownloadableItemInOrder); + $this->assertArrayHasKey('downloadable_links', $downloadableItemInTheOrder); + $downloadableLinksFromResponse = $downloadableItemInTheOrder['downloadable_links']; + $this->assertNotEmpty($downloadableLinksFromResponse); + + $downloadableProduct = $this->productRepository->get('downloadable-product'); + /** @var LinkInterface $downloadableProductLinks */ + $downloadableProductLinks = $downloadableProduct->getExtensionAttributes()->getDownloadableProductLinks(); + $linkId = $downloadableProductLinks[0]->getId(); + $expectedDownloadableLinksData = + [ + [ + 'title' =>'Downloadable Product Link', + 'sort_order' => 1, + 'uid'=> base64_encode("downloadable/{$linkId}") + ] + ]; + $this->assertResponseFields($expectedDownloadableLinksData,$downloadableLinksFromResponse); + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerOrderQuery($orderNumber): array + { + $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 DownloadableOrderItem{ + downloadable_links{ + title + sort_order + uid + } + entered_options{value id} + product_sku + product_name + quantity_ordered + } + } + 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} label} + 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']); + $this->assertNotEmpty($response['customer']['orders']['items']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + return $customerOrderItemsInResponse; + } +} From 5512263dca239bc415648ce5605ab07b58f2d784 Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Mon, 3 Aug 2020 12:32:46 -0500 Subject: [PATCH 0160/1013] MC-34939: Not able to export customers from backend --- .../VersionControl/AbstractCollection.php | 11 +++++++++++ .../ResourceModel/Db/VersionControl/Snapshot.php | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php index 631bfa3c2d2b5..5ecbf70c246d1 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/VersionControl/AbstractCollection.php @@ -87,4 +87,15 @@ protected function beforeAddLoadedItem(\Magento\Framework\DataObject $item) $this->entitySnapshot->registerSnapshot($item); return $item; } + + /** + * Clear collection + * + * @return $this + */ + public function clear() + { + $this->entitySnapshot->clear($this->getNewEmptyItem()); + return parent::clear(); + } } diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php index 095b5accda7c3..a287fa5e1af42 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/VersionControl/Snapshot.php @@ -72,4 +72,18 @@ public function isModified(\Magento\Framework\DataObject $entity) return false; } + + /** + * Clear snapshot data + * + * @param \Magento\Framework\DataObject|null $entity + */ + public function clear(\Magento\Framework\DataObject $entity = null) + { + if ($entity !== null) { + $this->snapshotData[get_class($entity)] = []; + } else { + $this->snapshotData = []; + } + } } From 582cff8e13e3ebd6ff721faee2520f63301d1c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= <huenig@t-online.de> Date: Mon, 3 Aug 2020 22:25:26 +0200 Subject: [PATCH 0161/1013] Revert "Update ImageResizeCommandTest.php" This reverts commit 94006bd7156346f68031480efa1bf54c3e25d8c5. --- .../Command/ImageResizeCommandTest.php | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php index 9df470c68e6e8..62dae6ba1c5e9 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php @@ -65,20 +65,6 @@ protected function setUp(): void $this->filesystem = $this->objectManager->get(Filesystem::class); $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); } - - - /** - * Test that catalog:image:resize command executes successfully in database storage mode - * with file missing from local folder - * - * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php - * @magentoDataFixture Magento/MediaStorage/_files/product_with_missed_image.php - */ - public function testDatabaseStorageMissingFile() - { - $this->tester->execute([]); - $this->assertStringContainsString('Product images resized successfully', $this->tester->getDisplay()); - } /** * Test that catalog:image:resize command executed successfully with missing image file @@ -123,4 +109,17 @@ public function testExecuteWithZeroByteImage() $this->assertStringContainsString('Wrong file', $this->tester->getDisplay()); $this->mediaDirectory->getDriver()->deleteFile($this->mediaDirectory->getAbsolutePath($this->fileName)); } + + /** + * Test that catalog:image:resize command executes successfully in database storage mode + * with file missing from local folder + * + * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php + * @magentoDataFixture Magento/MediaStorage/_files/product_with_missed_image.php + */ + public function testDatabaseStorageMissingFile() + { + $this->tester->execute([]); + $this->assertStringContainsString('Product images resized successfully', $this->tester->getDisplay()); + } } From 67c7234f207fae953a62769eaa56ee5f0b02a693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= <huenig@t-online.de> Date: Mon, 3 Aug 2020 22:26:20 +0200 Subject: [PATCH 0162/1013] disable DbIsolation for Integration Test: ImageResizeCommandTest --- .../MediaStorage/Console/Command/ImageResizeCommandTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php index 62dae6ba1c5e9..ac8aff07cb811 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Console/Command/ImageResizeCommandTest.php @@ -116,6 +116,7 @@ public function testExecuteWithZeroByteImage() * * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php * @magentoDataFixture Magento/MediaStorage/_files/product_with_missed_image.php + * @magentoDbIsolation disabled */ public function testDatabaseStorageMissingFile() { From d425596643b966a3f08229cf7dcdb6cd38c527bd Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Mon, 3 Aug 2020 16:21:48 -0500 Subject: [PATCH 0163/1013] MC-34939: Not able to export customers from backend --- .../VersionControl/AbstractCollectionTest.php | 8 ++++++- .../Db/VersionControl/SnapshotTest.php | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php index aa49a78ced7e3..0afe5a2c2d0d7 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php @@ -36,7 +36,7 @@ protected function setUp(): void $this->entitySnapshot = $this->createPartialMock( Snapshot::class, - ['registerSnapshot'] + ['registerSnapshot', 'clear'] ); $this->subject = $objectManager->getObject( @@ -82,4 +82,10 @@ public static function fetchItemDataProvider() [['attribute' => 'test']] ]; } + + public function testClearSnapshot() + { + $item = $this->getMagentoObject(); + $this->entitySnapshot->expects($this->once())->method('clear')->with($item); + } } diff --git a/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/VersionControl/SnapshotTest.php b/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/VersionControl/SnapshotTest.php index 9777e2df558bd..ed2dd09331f69 100644 --- a/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/VersionControl/SnapshotTest.php +++ b/lib/internal/Magento/Framework/Model/Test/Unit/ResourceModel/Db/VersionControl/SnapshotTest.php @@ -96,4 +96,27 @@ public function testIsModified() $this->entitySnapshot->registerSnapshot($this->model); $this->assertFalse($this->entitySnapshot->isModified($this->model)); } + + public function testClear() + { + $entityId = 1; + $data = [ + 'id' => $entityId, + 'name' => 'test', + 'description' => '', + 'custom_not_present_attribute' => '' + ]; + $fields = [ + 'id' => [], + 'name' => [], + 'description' => [] + ]; + $this->assertTrue($this->entitySnapshot->isModified($this->model)); + $this->model->setData($data); + $this->model->expects($this->any())->method('getId')->willReturn($entityId); + $this->entityMetadata->expects($this->any())->method('getFields')->with($this->model)->willReturn($fields); + $this->entitySnapshot->registerSnapshot($this->model); + $this->entitySnapshot->clear($this->model); + $this->assertTrue($this->entitySnapshot->isModified($this->model)); + } } From aad94802bd57bf05a6f46be48abc0375ec38918e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= <jonas@huenig.name> Date: Tue, 4 Aug 2020 03:38:50 +0200 Subject: [PATCH 0164/1013] disable Isolation of DatabaseTest as well disable Isolation of DatabaseTest as well --- .../Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php b/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php index 056ba4ae93cc6..c96e3213d7a06 100644 --- a/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaStorage/Helper/File/Storage/DatabaseTest.php @@ -52,7 +52,7 @@ protected function setUp(): void /** * test for \Magento\MediaStorage\Model\File\Storage\Database::deleteFolder() * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoDataFixture Magento/MediaStorage/_files/database_mode.php * @magentoConfigFixture current_store system/media_storage_configuration/media_storage 1 * @magentoConfigFixture current_store system/media_storage_configuration/media_database default_setup From 5e3ee96efdb20ea129b82e909c9fe88c9c58a5df Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Tue, 4 Aug 2020 07:34:05 -0500 Subject: [PATCH 0165/1013] MC-34939: Not able to export customers from backend --- .../Entity/Collection/VersionControl/AbstractCollectionTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php index 0afe5a2c2d0d7..bc3f81c7385d1 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Collection/VersionControl/AbstractCollectionTest.php @@ -87,5 +87,6 @@ public function testClearSnapshot() { $item = $this->getMagentoObject(); $this->entitySnapshot->expects($this->once())->method('clear')->with($item); + $this->subject->clear(); } } From 638e7f16178c81c31b448c7f8215fe54d039731d Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Tue, 4 Aug 2020 17:16:42 +0300 Subject: [PATCH 0166/1013] MC-35971: Not selected Parent page on CMS Page --- .../Magento/Store/Model/ScopeResolver.php | 147 +++++++++++++++ .../Test/Unit/Model/ScopeResolverTest.php | 178 ++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 app/code/Magento/Store/Model/ScopeResolver.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/ScopeResolverTest.php diff --git a/app/code/Magento/Store/Model/ScopeResolver.php b/app/code/Magento/Store/Model/ScopeResolver.php new file mode 100644 index 0000000000000..330ef29c8ac10 --- /dev/null +++ b/app/code/Magento/Store/Model/ScopeResolver.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ScopeTreeProviderInterface; + +/** + * Class used to check if some scope belongs to other scope + */ +class ScopeResolver +{ + /** + * @var ScopeTreeProviderInterface + */ + private $scopeTree; + + /** + * @param ScopeTreeProviderInterface $scopeTree + */ + public function __construct(ScopeTreeProviderInterface $scopeTree) + { + $this->scopeTree = $scopeTree; + } + + /** + * Check is some scope belongs to other scope + * + * @param string $baseScope + * @param int $baseScopeId + * @param string $requestedScope + * @param int $requestedScopeId + * @return bool + */ + public function isBelongsToScope( + string $baseScope, + int $baseScopeId, + string $requestedScope, + int $requestedScopeId + ) : bool { + /* All scopes belongs to All Store Views */ + if ($baseScope === ScopeConfigInterface::SCOPE_TYPE_DEFAULT) { + return true; + } + + $scopeNode = $this->getScopeNode($baseScope, $baseScopeId, [$this->scopeTree->get()]); + if (empty($scopeNode)) { + return false; + } + + return $this->isBelongsToScopeRecurse($requestedScope, $requestedScopeId, [$scopeNode]); + } + + /** + * Check is Belongs some scope to other scope (internal recurse) + * + * @param string $requestedScope + * @param int $requestedScopeId + * @param array $tree + * @return bool + */ + private function isBelongsToScopeRecurse( + string $requestedScope, + int $requestedScopeId, + array $tree + ) : bool { + foreach ($tree as $node) { + if ($this->isScopeEquals($node['scope'], $requestedScope) && (int)$node['scope_id'] === $requestedScopeId) { + return true; + } + if (!empty($node['scopes'])) { + $isBelongsToChild = $this->isBelongsToScopeRecurse( + $requestedScope, + $requestedScopeId, + $node['scopes'] + ); + if ($isBelongsToChild) { + return $isBelongsToChild; + } + } + } + + return false; + } + + /** + * Get tree by scope + * + * @param string $scope + * @param int $scopeId + * @param array $tree + * @return array + */ + private function getScopeNode(string $scope, int $scopeId, array $tree): array + { + foreach ($tree as $node) { + if ($this->isScopeEquals($node['scope'], $scope) && (int)$node['scope_id'] === $scopeId) { + return $node; + } + if (!empty($node['scopes'])) { + $found = $this->getScopeNode($scope, $scopeId, $node['scopes']); + if (!empty($found)) { + return $found; + } + } + } + + return []; + } + + /** + * Is scope equals with normalize names + * + * @param string $firstScope + * @param string $secondScope + * @return bool + */ + private function isScopeEquals(string $firstScope, string $secondScope): bool + { + return $this->normalizeScopeName($firstScope) === $this->normalizeScopeName($secondScope); + } + + /** + * Normalize scope name + * + * @param string $scope + * @return string + */ + private function normalizeScopeName(string $scope): string + { + switch ($scope) { + case ScopeInterface::SCOPE_STORES: + return ScopeInterface::SCOPE_STORE; + case ScopeInterface::SCOPE_WEBSITES: + return ScopeInterface::SCOPE_WEBSITE; + case ScopeInterface::SCOPE_GROUPS: + return ScopeInterface::SCOPE_GROUP; + default: + return $scope; + } + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/ScopeResolverTest.php b/app/code/Magento/Store/Test/Unit/Model/ScopeResolverTest.php new file mode 100644 index 0000000000000..d93c08eb3b27f --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/ScopeResolverTest.php @@ -0,0 +1,178 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ScopeTreeProviderInterface; +use Magento\Store\Model\ScopeInterface; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Store\Model\ScopeResolver; + +/** + * Test for ScopeResolver + */ +class ScopeResolverTest extends TestCase +{ + /** + * @var ScopeTreeProviderInterface|MockObject + */ + private $scopeTreeMock; + + /** + * @var ScopeResolver + */ + private $scopeResolver; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->scopeTreeMock = $this->getMockBuilder(ScopeTreeProviderInterface::class) + ->getMockForAbstractClass(); + $this->scopeResolver = new ScopeResolver($this->scopeTreeMock); + } + + /** + * Check is some scope belongs to other scope + * + * @param string $baseScope + * @param int $baseScopeId + * @param string $requestedScope + * @param int $requestedScopeId + * @param bool $isBelong + * @dataProvider testIsBelongsToScopeDataProvider + */ + public function testIsBelongsToScope( + string $baseScope, + int $baseScopeId, + string $requestedScope, + int $requestedScopeId, + bool $isBelong + ) { + $this->scopeTreeMock->expects($this->any()) + ->method('get') + ->willReturn( + $this->getTree() + ); + $this->assertEquals( + $isBelong, + $this->scopeResolver->isBelongsToScope($baseScope, $baseScopeId, $requestedScope, $requestedScopeId) + ); + } + + /** + * Data provider for testIsBelongsToScope + * + * @return array[] + */ + public function testIsBelongsToScopeDataProvider() + { + return [ + 'All scopes belongs to Default' => [ + 'baseScope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 'baseScopeId' => 0, + 'requestedScope' => ScopeInterface::SCOPE_WEBSITE, + 'requestedScopeId' => 1, + 'isBelong' => true + ], + 'Store group belongs to website' => [ + 'baseScope' => ScopeInterface::SCOPE_WEBSITE, + 'baseScopeId' => 1, + 'requestedScope' => ScopeInterface::SCOPE_GROUP, + 'requestedScopeId' => 1, + 'isBelong' => true + ], + 'Store belongs to store group' => [ + 'baseScope' => ScopeInterface::SCOPE_GROUP, + 'baseScopeId' => 1, + 'requestedScope' => ScopeInterface::SCOPE_STORE, + 'requestedScopeId' => 1, + 'isBelong' => true + ], + 'Store belongs to website' => [ + 'baseScope' => ScopeInterface::SCOPE_WEBSITE, + 'baseScopeId' => 1, + 'requestedScope' => ScopeInterface::SCOPE_STORE, + 'requestedScopeId' => 1, + 'isBelong' => true + ], + 'Store group not belongs to website' => [ + 'baseScope' => ScopeInterface::SCOPE_WEBSITE, + 'baseScopeId' => 1, + 'requestedScope' => ScopeInterface::SCOPE_GROUP, + 'requestedScopeId' => 2, + 'isBelong' => false + ], + 'Store not belongs to store group' => [ + 'baseScope' => ScopeInterface::SCOPE_GROUP, + 'baseScopeId' => 1, + 'requestedScope' => ScopeInterface::SCOPE_STORE, + 'requestedScopeId' => 2, + 'isBelong' => false + ], + 'Store not belongs to website' => [ + 'baseScope' => ScopeInterface::SCOPE_WEBSITE, + 'baseScopeId' => 1, + 'requestedScope' => ScopeInterface::SCOPE_STORE, + 'requestedScopeId' => 2, + 'isBelong' => false + ], + ]; + } + + /** + * Get scope tree with 2 websites, 2 groups and 2 stores + * + * @return array + */ + private function getTree() + { + return [ + 'scope' => ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + 'scope_id' => null, + 'scopes' => [ + [ + 'scope' => ScopeInterface::SCOPE_WEBSITE, + 'scope_id' => 1, + 'scopes' => [ + [ + 'scope' => ScopeInterface::SCOPE_GROUP, + 'scope_id' => 1, + 'scopes' => [ + [ + 'scope' => ScopeInterface::SCOPE_STORE, + 'scope_id' => 1, + 'scopes' => [], + ], + ], + ], + ], + ], + [ + 'scope' => ScopeInterface::SCOPE_WEBSITE, + 'scope_id' => 2, + 'scopes' => [ + [ + 'scope' => ScopeInterface::SCOPE_GROUP, + 'scope_id' => 2, + 'scopes' => [ + [ + 'scope' => ScopeInterface::SCOPE_STORE, + 'scope_id' => 2, + 'scopes' => [], + ], + ], + ], + ], + ], + ], + ]; + } +} From b1adaf89ca4a23dfac22464ff658d86286efd8e4 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 4 Aug 2020 15:27:11 -0500 Subject: [PATCH 0167/1013] MC-35013: SKU search in Advanced Search page doesn't work --- .../Magento/CatalogSearch/Model/Advanced.php | 5 ++ .../Product/FieldProvider/StaticField.php | 26 +++++++ .../Model/Adapter/Index/Builder.php | 39 ++++++++--- .../Controller/Advanced/ResultTest.php | 70 +++++++++++++++++++ .../product_for_search_with_hyphen_in_sku.php | 51 ++++++++++++++ ...for_search_with_hyphen_in_sku_rollback.php | 39 +++++++++++ 6 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku_rollback.php diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index 5143762a07e08..b498cb09e34fa 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -233,6 +233,11 @@ public function addFilters($values) ? date('Y-m-d\TH:i:s\Z', strtotime($value['to'])) : ''; } + + if ($attribute->getAttributeCode() === 'sku') { + $value = mb_strtolower($value); + } + $condition = $this->_getResource()->prepareCondition( $attribute, $value, 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..6eff57e0c90e3 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 @@ -102,6 +102,7 @@ public function __construct( * @param array $context * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function getFields(array $context = []): array { @@ -118,6 +119,9 @@ public function getFields(array $context = []): array $allAttributes[$fieldName] = [ 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), ]; + if ($this->isNeedToAddCustomAnalyzer($fieldName) && $this->getCustomAnalyzer($fieldName)) { + $allAttributes[$fieldName]['analyzer'] = $this->getCustomAnalyzer($fieldName); + } $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); if (null !== $index) { @@ -172,4 +176,26 @@ public function getFields(array $context = []): array return $allAttributes; } + + /** + * Check is the custom analyzer exists for the field + * + * @param string $fieldName + * @return bool + */ + private function isNeedToAddCustomAnalyzer(string $fieldName): bool + { + return $fieldName === 'sku'; + } + + /** + * Getter for the field custom analyzer if it's exists + * + * @param string $fieldName + * @return string|null + */ + private function getCustomAnalyzer(string $fieldName): ?string + { + return $fieldName === 'sku' ? 'sku' : null; + } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php b/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php index 773faf49f8fda..1cad781ad6d74 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Index/Builder.php @@ -8,6 +8,9 @@ use Magento\Framework\Locale\Resolver as LocaleResolver; use Magento\Elasticsearch\Model\Adapter\Index\Config\EsConfigInterface; +/** + * Index Builder + */ class Builder implements BuilderInterface { /** @@ -40,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function build() { @@ -59,6 +62,14 @@ public function build() array_keys($filter) ), 'char_filter' => array_keys($charFilter) + ], + 'sku' => [ + 'type' => 'custom', + 'tokenizer' => 'keyword', + 'filter' => array_merge( + ['lowercase', 'keyword_repeat'], + array_keys($filter) + ), ] ], 'tokenizer' => $tokenizer, @@ -71,7 +82,10 @@ public function build() } /** - * {@inheritdoc} + * Setter for storeId property + * + * @param int $storeId + * @return void */ public function setStoreId($storeId) { @@ -79,47 +93,52 @@ public function setStoreId($storeId) } /** + * Return tokenizer configuration + * * @return array */ protected function getTokenizer() { - $tokenizer = [ + return [ 'default_tokenizer' => [ - 'type' => 'standard', - ], + 'type' => 'standard' + ] ]; - return $tokenizer; } /** + * Return filter configuration + * * @return array */ protected function getFilter() { - $filter = [ + return [ 'default_stemmer' => $this->getStemmerConfig(), 'unique_stem' => [ 'type' => 'unique', 'only_on_same_position' => true ] ]; - return $filter; } /** + * Return char filter configuration + * * @return array */ protected function getCharFilter() { - $charFilter = [ + return [ 'default_char_filter' => [ 'type' => 'html_strip', ], ]; - return $charFilter; } /** + * Return stemmer configuration + * * @return array */ protected function getStemmerConfig() diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php index dcbaa4addd85e..d9b9a41f5fdbb 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php @@ -67,6 +67,76 @@ public function testExecute(array $searchParams): void $this->assertStringContainsString('Simple product name', $responseBody); } + /** + * Advanced search test by difference product attributes. + * + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 + * @magentoAppArea frontend + * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + * + * @return void + */ + public function testExecuteSkuWithHyphen(): void + { + $this->getRequest()->setQuery( + $this->_objectManager->create( + Parameters::class, + [ + 'values' => [ + 'name' => '', + 'sku' => '24-mb01', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => '', + 'to' => '', + ], + 'test_searchable_attribute' => '', + ] + ] + ) + ); + $this->dispatch('catalogsearch/advanced/result'); + $responseBody = $this->getResponse()->getBody(); + $this->assertContains('Simple product name', $responseBody); + } + + /** + * Advanced search test by difference product attributes. + * + * @magentoConfigFixture default/catalog/search/engine elasticsearch6 + * @magentoAppArea frontend + * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + * + * @return void + */ + public function testExecuteSkuWithHyphen(): void + { + $this->getRequest()->setQuery( + $this->_objectManager->create( + Parameters::class, + [ + 'values' => [ + 'name' => '', + 'sku' => '24-mb01', + 'description' => '', + 'short_description' => '', + 'price' => [ + 'from' => '', + 'to' => '', + ], + 'test_searchable_attribute' => '', + ] + ] + ) + ); + $this->dispatch('catalogsearch/advanced/result'); + $responseBody = $this->getResponse()->getBody(); + $this->assertContains('Simple product name', $responseBody); + } + /** * Data provider with strings for quick search. * diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php new file mode 100644 index 0000000000000..5503ff98078a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +require 'searchable_attribute.php'; + +use Magento\Catalog\Api\ProductRepositoryInterface; +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\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +$product = $productFactory->create(); +$product->isObjectNew(true); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple product name') + ->setSku('24-mb01') + ->setPrice(100) + ->setWeight(1) + ->setShortDescription('Product short description') + ->setTaxClassId(0) + ->setDescription('Product description') + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setTestSearchableAttribute($attribute->getSource()->getOptionId('Option 1')) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku_rollback.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku_rollback.php new file mode 100644 index 0000000000000..1e41f393894e7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku_rollback.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\Catalog\Model\Product; +use Magento\Eav\Setup\EavSetup; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var ObjectManager $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var EavSetupFactory $eavSetupFactory */ +$eavSetupFactory = $objectManager->create(EavSetupFactory::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('24-mb01'); +} catch (NoSuchEntityException $e) { + //Product already deleted. +} +/** @var EavSetup $eavSetup */ +$eavSetup = $eavSetupFactory->create(); +$eavSetup->removeAttribute(Product::ENTITY, 'test_searchable_attribute'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 700dc997c65a792b442d407db3ffea6859b34498 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 5 Aug 2020 10:52:03 -0500 Subject: [PATCH 0168/1013] MC-35013: SKU search in Advanced Search page doesn't work --- .../Controller/Advanced/ResultTest.php | 37 +------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php index d9b9a41f5fdbb..455668e83b518 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php @@ -70,42 +70,7 @@ public function testExecute(array $searchParams): void /** * Advanced search test by difference product attributes. * - * @magentoConfigFixture default/catalog/search/engine elasticsearch6 - * @magentoAppArea frontend - * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php - * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php - * - * @return void - */ - public function testExecuteSkuWithHyphen(): void - { - $this->getRequest()->setQuery( - $this->_objectManager->create( - Parameters::class, - [ - 'values' => [ - 'name' => '', - 'sku' => '24-mb01', - 'description' => '', - 'short_description' => '', - 'price' => [ - 'from' => '', - 'to' => '', - ], - 'test_searchable_attribute' => '', - ] - ] - ) - ); - $this->dispatch('catalogsearch/advanced/result'); - $responseBody = $this->getResponse()->getBody(); - $this->assertContains('Simple product name', $responseBody); - } - - /** - * Advanced search test by difference product attributes. - * - * @magentoConfigFixture default/catalog/search/engine elasticsearch6 + * @magentoConfigFixture default/catalog/search/engine elasticsearch7 * @magentoAppArea frontend * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php From 5e9739dd506877992fbbb2cc5c6dcaef6d1e149c Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 4 Aug 2020 15:44:19 -0500 Subject: [PATCH 0169/1013] MC-35655: Delete customer button is displayed for restricted admin user - Fix customer delete button is displayed for restricted admin --- .../Controller/Adminhtml/IndexTest.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php index 40c84d8b5db58..e70056be69ba7 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/IndexTest.php @@ -191,6 +191,32 @@ public function testResetPasswordActionSuccess() $this->assertRedirect($this->stringContains($this->baseControllerUrl . 'edit')); } + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testAclDeleteActionAllow() + { + $this->getRequest()->setParam('id', 1); + $this->dispatch('backend/customer/index/edit'); + $body = $this->getResponse()->getBody(); + $this->assertStringContainsString('Delete Customer', $body); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + */ + public function testAclDeleteActionDeny() + { + $resource= 'Magento_Customer::delete'; + $this->_objectManager->get(\Magento\Framework\Acl\Builder::class) + ->getAcl() + ->deny(null, $resource); + $this->getRequest()->setParam('id', 1); + $this->dispatch('backend/customer/index/edit'); + $body = $this->getResponse()->getBody(); + $this->assertStringNotContainsString('Delete Customer', $body); + } + /** * Prepare email mock to test emails. * From 2c7a66835c30863a273e8d05cb99f96184c783b8 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 5 Aug 2020 12:52:53 -0500 Subject: [PATCH 0170/1013] MC-35013: SKU search in Advanced Search page doesn't work --- .../_files/product_for_search_with_hyphen_in_sku.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php index 5503ff98078a3..2800af5115825 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php @@ -6,8 +6,6 @@ declare(strict_types=1); -require 'searchable_attribute.php'; - use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Type; @@ -15,6 +13,9 @@ use Magento\Catalog\Model\ProductFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/CatalogSearch/_files/searchable_attribute.php'); /** @var ObjectManager $objectManager */ $objectManager = Bootstrap::getObjectManager(); From 4ccc0380fb47c0324f351060742c8aaf8d04f66c Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 5 Aug 2020 12:54:41 -0500 Subject: [PATCH 0171/1013] MC-35013: SKU search in Advanced Search page doesn't work --- .../Magento/CatalogSearch/Controller/Advanced/ResultTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php index 455668e83b518..64444c4ecde21 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php @@ -99,7 +99,7 @@ public function testExecuteSkuWithHyphen(): void ); $this->dispatch('catalogsearch/advanced/result'); $responseBody = $this->getResponse()->getBody(); - $this->assertContains('Simple product name', $responseBody); + $this->assertStringContainsString('Simple product name', $responseBody); } /** From 6dc13d8ece81cd15bb4ebb09e22a785c2ff8ae7c Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Wed, 5 Aug 2020 17:36:19 -0500 Subject: [PATCH 0172/1013] MC-36418: checkout success page, there should be a registration form for the guest. --- .../Magento/Customer/view/frontend/web/js/customer-data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 8976d0dda4673..8282146959869 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 @@ -47,9 +47,9 @@ define([ if (new Date($.localStorage.get('mage-cache-timeout')) < new Date()) { storage.removeAll(); - date = new Date(Date.now() + parseInt(invalidateOptions.cookieLifeTime, 10) * 1000); - $.localStorage.set('mage-cache-timeout', date); } + date = new Date(Date.now() + parseInt(invalidateOptions.cookieLifeTime, 10) * 1000); + $.localStorage.set('mage-cache-timeout', date); }; /** From 0eeadc38950ac34d0e6e63cf107a94ff4ceafc70 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Thu, 6 Aug 2020 10:15:38 -0500 Subject: [PATCH 0173/1013] MC-35013: SKU search in Advanced Search page doesn't work --- .../_files/product_for_search_with_hyphen_in_sku.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php index 2800af5115825..9139fe69738b3 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php @@ -7,6 +7,7 @@ declare(strict_types=1); use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; @@ -19,6 +20,11 @@ /** @var ObjectManager $objectManager */ $objectManager = Bootstrap::getObjectManager(); + +/** @var ProductAttributeRepositoryInterface $productAttributeRepository */ +$productAttributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +$attribute = $productAttributeRepository->get('test_searchable_attribute'); + /** @var ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->get(ProductRepositoryInterface::class); /** @var ProductFactory $productFactory */ From 4d3ebd8289dd055f3d3196512a321ac15c04bd47 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 6 Aug 2020 16:38:10 -0500 Subject: [PATCH 0174/1013] MC-32659: Order Details by Order Number with additional different product types --- .../InvoiceItemTypeResolver.php | 29 ------------------- .../OrderItemTypeResolver.php | 29 ------------------- .../DownloadableGraphQl/etc/graphql/di.xml | 19 ++++++++---- 3 files changed, 13 insertions(+), 64 deletions(-) delete mode 100644 app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php delete mode 100644 app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php deleted file mode 100644 index 35120e4c0917d..0000000000000 --- a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/InvoiceItemTypeResolver.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; - -use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; - -/** - * Leaf for composite class to resolve invoice item type - */ -class InvoiceItemTypeResolver implements TypeResolverInterface -{ - /** - * @inheritDoc - */ - public function resolveType(array $data): string - { - if (isset($data['product_type'])) { - if ($data['product_type'] == 'downloadable') { - return 'DownloadableInvoiceItem'; - } - } - return ''; - } -} diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php b/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php deleted file mode 100644 index 2f835e790d0db..0000000000000 --- a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/OrderItemTypeResolver.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; - -use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; - -/** - * Leaf for composite class to resolve order item type - */ -class OrderItemTypeResolver implements TypeResolverInterface -{ - /** - * @inheritDoc - */ - public function resolveType(array $data): string - { - if (isset($data['product_type'])) { - if ($data['product_type'] == 'downloadable') { - return 'DownloadableOrderItem'; - } - } - return ''; - } -} diff --git a/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml b/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml index b54d88b0f1ae6..156466175beaa 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/DownloadableGraphQl/etc/graphql/di.xml @@ -39,17 +39,24 @@ </argument> </arguments> </type> - <type name="Magento\SalesGraphQl\Model\OrderItemInterfaceTypeResolverComposite"> + <type name="Magento\SalesGraphQl\Model\TypeResolver\OrderItem"> <arguments> - <argument name="orderItemTypeResolvers" xsi:type="array"> - <item name="downloadable_order_catalog_item_type_resolver" xsi:type="object">Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\OrderItemTypeResolver</item> + <argument name="productTypeMap" xsi:type="array"> + <item name="downloadable" xsi:type="string">DownloadableOrderItem</item> </argument> </arguments> </type> - <type name="Magento\SalesGraphQl\Model\InvoiceItemInterfaceTypeResolverComposite"> + <type name="Magento\SalesGraphQl\Model\TypeResolver\InvoiceItem"> <arguments> - <argument name="invoiceItemTypeResolvers" xsi:type="array"> - <item name="downloadable_invoice_catalog_item_type_resolver" xsi:type="object">Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\InvoiceItemTypeResolver</item> + <argument name="productTypeMap" xsi:type="array"> + <item name="downloadable" xsi:type="string">DownloadableInvoiceItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\CreditMemoItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="downloadable" xsi:type="string">DownloadableCreditMemoItem</item> </argument> </arguments> </type> From fe4b27b6a46810fb456bcfc6202e938eb3e288bc Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 7 Aug 2020 10:12:06 -0500 Subject: [PATCH 0175/1013] MC-32659: Order Details by Order Number with additional different product types --- .../{DownloadableOrderItem => Order/Item}/Links.php | 2 +- .../Magento/DownloadableGraphQl/etc/schema.graphqls | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) rename app/code/Magento/DownloadableGraphQl/Resolver/{DownloadableOrderItem => Order/Item}/Links.php (97%) diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Links.php b/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php similarity index 97% rename from app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Links.php rename to app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php index 97da31a6af912..7fd1f2e3079be 100644 --- a/app/code/Magento/DownloadableGraphQl/Resolver/DownloadableOrderItem/Links.php +++ b/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem; +namespace Magento\DownloadableGraphQl\Resolver\Order\Item; use Magento\Downloadable\Model\ResourceModel\Link\Collection; use Magento\Downloadable\Model\ResourceModel\Link\CollectionFactory; diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 9c9dd438746f0..451e135325720 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -66,7 +66,15 @@ type DownloadableProductSamples @doc(description: "DownloadableProductSamples de } type DownloadableOrderItem implements OrderItemInterface { - downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are ordered from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\DownloadableOrderItem\\Links") + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are ordered from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Order\\Item\\Links") +} + +type DownloadableInvoiceItem implements InvoiceItemInterface { + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are invoiced from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Order\\Item\\Links") +} + +type DownloadableCreditMemoItem implements CreditMemoItemInterface { + downloadable_links: [DownloadableItemsLinks] @doc(description: "A list of downloadable links that are refunded from the downloadable product") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Order\\Item\\Links") } type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { From 9c59b3bb0605078937addddff468811b0ffa0e9f Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Fri, 7 Aug 2020 10:16:51 -0500 Subject: [PATCH 0176/1013] MC-35013: SKU search in Advanced Search page doesn't work --- .../Magento/CatalogSearch/Controller/Advanced/ResultTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php index 64444c4ecde21..33fac8e670f3d 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Controller/Advanced/ResultTest.php @@ -70,7 +70,6 @@ public function testExecute(array $searchParams): void /** * Advanced search test by difference product attributes. * - * @magentoConfigFixture default/catalog/search/engine elasticsearch7 * @magentoAppArea frontend * @magentoDataFixture Magento/CatalogSearch/_files/product_for_search_with_hyphen_in_sku.php * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php From c4519d0c3557264b95507e0ac5bc3224bcebc588 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Fri, 7 Aug 2020 10:32:07 -0500 Subject: [PATCH 0177/1013] MC-32659: MyAccount :: Order Details :: Order Details by Order Number with additional different product types - added fixtures for downloadable product --- ...rieveOrdersWithDownloadableProductTest.php | 9 +- ...wnloadable_product_with_multiple_links.php | 88 +++++++++++++++++++ ...e_product_with_multiple_links_rollback.php | 39 ++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php create mode 100644 dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index 143e0bb1cddb6..f89559f2c4464 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -75,7 +75,14 @@ public function testGetCustomerOrdersDownloadableProduct() 'uid'=> base64_encode("downloadable/{$linkId}") ] ]; - $this->assertResponseFields($expectedDownloadableLinksData,$downloadableLinksFromResponse); + $this->assertResponseFields($expectedDownloadableLinksData, $downloadableLinksFromResponse); + } + + /** + * @magentoApiDataFixture Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php + */ + public function testGetCustomerOrdersDownloadableWithmultiplelinks() + { } /** diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php new file mode 100644 index 0000000000000..b80fa4fc93704 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php @@ -0,0 +1,88 @@ +<?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\ProductRepositoryInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Downloadable\Api\Data\LinkInterface; +use Magento\Downloadable\Model\Product\Type; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\AddressFactory; +use Magento\Sales\Model\Order\ItemFactory; +use Magento\Sales\Model\Order\PaymentFactory; +use Magento\Sales\Model\OrderFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; +$objectManager = Bootstrap::getObjectManager(); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->create(CustomerRegistry::class); +$customer = $customerRegistry->retrieve(1); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +/** @var AddressFactory $addressFactory */ +$addressFactory = $objectManager->get(AddressFactory::class); +$billingAddress = $addressFactory->create(['data' => $addressData]); +$billingAddress->setAddressType(Address::TYPE_BILLING); +/** @var ItemFactory $orderItemFactory */ +$orderItemFactory = $objectManager->get(ItemFactory::class); +/** @var PaymentFactory $orderPaymentFactory */ +$orderPaymentFactory = $objectManager->get(PaymentFactory::class); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +/** @var OrderFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderFactory::class); + +$payment = $orderPaymentFactory->create(); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + ['type' => 'free', 'fraudulent' => false] + ); +/** @var ProductInterface $product */ +$product = $productRepository->get('downloadable-product'); +/** @var LinkInterface $links */ +$links = $product->getExtensionAttributes()->getDownloadableProductLinks(); +$link = reset($links); + +$orderItem = $orderItemFactory->create(); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(1) + ->setBasePrice($product->getPrice()) + ->setProductOptions(['links' => [$link->getId()]]) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType(Type::TYPE_DOWNLOADABLE) + ->setName($product->getName()) + ->setSku($product->getSku()); + +$order = $orderFactory->create(); +$order->setIncrementId('100000002') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setCustomerId($customer->getId()) + ->setCustomerEmail($customer->getEmail()) + ->setBillingAddress($billingAddress) + ->setStoreId($storeManager->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php new file mode 100644 index 0000000000000..a15aa0cf57dbb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\Downloadable\Model\RemoveLinkPurchasedByOrderIncrementId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var RemoveLinkPurchasedByOrderIncrementId $removeLinkPurchasedByOrderIncrementId */ +$removeLinkPurchasedByOrderIncrementId = $objectManager->get(RemoveLinkPurchasedByOrderIncrementId::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +$orderIncrementIdToDelete = '100000002'; +$removeLinkPurchasedByOrderIncrementId->execute($orderIncrementIdToDelete); +/** @var OrderFactory $order */ +$order = $objectManager->get(OrderFactory::class)->create(); +$order->loadByIncrementId($orderIncrementIdToDelete); + +if ($order->getId()) { + $orderRepository->delete($order); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 536a728213fd4a36634dd958bbde243fe9154734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szubert?= <bartlomiejszubert@gmail.com> Date: Fri, 7 Aug 2020 18:56:25 +0200 Subject: [PATCH 0178/1013] Fix #24091 - reduce amount of js code needed to set options for configurable product on wishlist --- .../view/frontend/web/js/add-to-wishlist.js | 66 ++++++------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 1cdad4953b3c2..727a9751cc2f6 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -19,7 +19,6 @@ define([ qtyInfo: '#qty', actionElement: '[data-action="add-to-wishlist"]', productListItem: '.item.product-item', - productListPriceBox: '.price-box', isProductList: false }, @@ -68,16 +67,25 @@ define([ _updateWishlistData: function (event) { var dataToAdd = {}, isFileUploaded = false, - productId = null, + productItem = null, + handleObjSelector = null, self = this; if (event.handleObj.selector == this.options.qtyInfo) { //eslint-disable-line eqeqeq - this._updateAddToWishlistButton({}); + this._updateAddToWishlistButton({}, productItem); event.stopPropagation(); return; } - $(event.handleObj.selector).each(function (index, element) { + + if (this.options.isProductList) { + productItem = $(event.target).closest(this.options.productListItem); + handleObjSelector = productItem.find(event.handleObj.selector); + } else { + handleObjSelector = $(event.handleObj.selector); + } + + handleObjSelector.each(function (index, element) { if ($(element).is('input[type=text]') || $(element).is('input[type=email]') || $(element).is('input[type=number]') || @@ -87,19 +95,7 @@ define([ $(element).is('textarea') || $('#' + element.id + ' option:selected').length ) { - if (!($(element).data('selector') || $(element).attr('name'))) { - return; - } - - if (self.options.isProductList) { - productId = self.retrieveListProductId(this); - - dataToAdd[productId] = $.extend( - {}, - dataToAdd[productId] ? dataToAdd[productId] : {}, - self._getElementData(element) - ); - } else { + if ($(element).data('selector') || $(element).attr('name')) { dataToAdd = $.extend({}, dataToAdd, self._getElementData(element)); } @@ -114,26 +110,21 @@ define([ if (isFileUploaded) { this.bindFormSubmit(); } - this._updateAddToWishlistButton(dataToAdd); + this._updateAddToWishlistButton(dataToAdd, productItem); event.stopPropagation(); }, /** * @param {Object} dataToAdd + * @param {Object} productItem * @private */ - _updateAddToWishlistButton: function (dataToAdd) { - var productId = null, - self = this; + _updateAddToWishlistButton: function (dataToAdd, productItem) { + var self = this, + buttons = productItem ? productItem.find(this.options.actionElement) : $(this.options.actionElement); - $('[data-action="add-to-wishlist"]').each(function (index, element) { - var params = $(element).data('post'), - dataToAddObj = dataToAdd; - - if (self.options.isProductList) { - productId = self.retrieveListProductId(element); - dataToAddObj = typeof dataToAdd[productId] !== 'undefined' ? dataToAdd[productId] : {}; - } + buttons.each(function (index, element) { + var params = $(element).data('post'); if (!params) { params = { @@ -141,7 +132,7 @@ define([ }; } - params.data = $.extend({}, params.data, dataToAddObj, { + params.data = $.extend({}, params.data, dataToAdd, { 'qty': $(self.options.qtyInfo).val() }); $(element).data('post', params); @@ -264,21 +255,6 @@ define([ return; } - }, - - /** - * Retrieve product id from element on products list - * - * @param {jQuery.Object} element - * @private - */ - retrieveListProductId: function (element) { - return parseInt( - $(element).closest(this.options.productListItem) - .find(this.options.productListPriceBox) - .data('product-id'), - 10 - ); } }); From 4bc3b49b55435e96b922e60d28d0b229222f5c78 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Fri, 7 Aug 2020 14:10:16 -0500 Subject: [PATCH 0179/1013] MC-36064: Protect payment related web APIs by CAPTCHA --- .../Model/CompositeUserContext.php | 6 +- .../Magento/Captcha/Model/DefaultModel.php | 21 +- .../Observer/CaptchaStringResolver.php | 13 +- .../Captcha/Test/Unit/Model/DefaultTest.php | 30 ++- app/code/Magento/Captcha/composer.json | 1 + app/code/Magento/Captcha/i18n/en_US.csv | 1 + ...ntProcessingRateLimitExceededException.php | 19 ++ .../PaymentProcessingRateLimiterInterface.php | 25 +++ .../CaptchaPaymentProcessingRateLimiter.php | 123 ++++++++++++ .../GuestPaymentInformationManagement.php | 15 +- .../Model/PaymentCaptchaConfigProvider.php | 88 +++++++++ .../Model/PaymentInformationManagement.php | 19 +- .../GuestPaymentInformationManagementTest.php | 117 ++++++++--- .../PaymentInformationManagementTest.php | 124 ++++++++---- app/code/Magento/Checkout/composer.json | 3 +- app/code/Magento/Checkout/etc/config.xml | 4 + app/code/Magento/Checkout/etc/di.xml | 2 + app/code/Magento/Checkout/etc/frontend/di.xml | 8 + .../frontend/layout/checkout_index_index.xml | 6 + .../set-payment-information-extended.js | 17 +- .../web/js/model/payment/place-order-hooks.js | 13 ++ .../web/js/model/payment/set-payment-hooks.js | 13 ++ .../view/frontend/web/js/model/place-order.js | 16 +- .../web/js/view/checkout/placeOrderCaptcha.js | 38 ++++ .../web/js/view/checkout/setPaymentCaptcha.js | 38 ++++ .../view/frontend/web/template/payment.html | 4 + .../Multishipping/Block/Checkout/Overview.php | 16 ++ .../Controller/Checkout/OverviewPost.php | 32 +-- app/code/Magento/Multishipping/composer.json | 3 +- .../templates/checkout/overview.phtml | 1 + .../Model/Cart/SetPaymentMethodOnCart.php | 22 ++- .../Model/Cart/SetPaymentMethodOnCartTest.php | 61 ++++++ ...aptchaPaymentProcessingRateLimiterTest.php | 185 ++++++++++++++++++ .../set-payment-information-extended.test.js | 3 +- lib/web/mage/storage.js | 28 ++- 35 files changed, 1000 insertions(+), 115 deletions(-) create mode 100644 app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php create mode 100644 app/code/Magento/Checkout/Api/PaymentProcessingRateLimiterInterface.php create mode 100644 app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php create mode 100644 app/code/Magento/Checkout/Model/PaymentCaptchaConfigProvider.php create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/model/payment/place-order-hooks.js create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/model/payment/set-payment-hooks.js create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/view/checkout/placeOrderCaptcha.js create mode 100644 app/code/Magento/Checkout/view/frontend/web/js/view/checkout/setPaymentCaptcha.js create mode 100644 app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiterTest.php diff --git a/app/code/Magento/Authorization/Model/CompositeUserContext.php b/app/code/Magento/Authorization/Model/CompositeUserContext.php index 7678b7e639e13..9a6be4d96ef1c 100644 --- a/app/code/Magento/Authorization/Model/CompositeUserContext.php +++ b/app/code/Magento/Authorization/Model/CompositeUserContext.php @@ -56,7 +56,7 @@ protected function add(UserContextInterface $userContext) } /** - * {@inheritdoc} + * @inheritDoc */ public function getUserId() { @@ -64,7 +64,7 @@ public function getUserId() } /** - * {@inheritdoc} + * @inheritDoc */ public function getUserType() { @@ -78,7 +78,7 @@ public function getUserType() */ protected function getUserContext() { - if ($this->chosenUserContext === null) { + if (!$this->chosenUserContext) { /** @var UserContextInterface $userContext */ foreach ($this->userContexts as $userContext) { if ($userContext->getUserType() && $userContext->getUserId() !== null) { diff --git a/app/code/Magento/Captcha/Model/DefaultModel.php b/app/code/Magento/Captcha/Model/DefaultModel.php index 0bb46b53c42d3..cf6950ffe8205 100644 --- a/app/code/Magento/Captcha/Model/DefaultModel.php +++ b/app/code/Magento/Captcha/Model/DefaultModel.php @@ -7,7 +7,9 @@ namespace Magento\Captcha\Model; +use Magento\Authorization\Model\UserContextInterface; use Magento\Captcha\Helper\Data; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Math\Random; /** @@ -93,12 +95,18 @@ class DefaultModel extends \Laminas\Captcha\Image implements \Magento\Captcha\Mo */ private $randomMath; + /** + * @var UserContextInterface + */ + private $userContext; + /** * @param \Magento\Framework\Session\SessionManagerInterface $session * @param \Magento\Captcha\Helper\Data $captchaData * @param ResourceModel\LogFactory $resLogFactory * @param string $formId * @param Random $randomMath + * @param UserContextInterface|null $userContext * @throws \Laminas\Captcha\Exception\ExtensionNotLoadedException */ public function __construct( @@ -106,14 +114,16 @@ public function __construct( \Magento\Captcha\Helper\Data $captchaData, \Magento\Captcha\Model\ResourceModel\LogFactory $resLogFactory, $formId, - Random $randomMath = null + Random $randomMath = null, + ?UserContextInterface $userContext = null ) { parent::__construct(); $this->session = $session; $this->captchaData = $captchaData; $this->resLogFactory = $resLogFactory; $this->formId = $formId; - $this->randomMath = $randomMath ?? \Magento\Framework\App\ObjectManager::getInstance()->get(Random::class); + $this->randomMath = $randomMath ?? ObjectManager::getInstance()->get(Random::class); + $this->userContext = $userContext ?? ObjectManager::getInstance()->get(UserContextInterface::class); } /** @@ -152,6 +162,7 @@ public function isRequired($login = null) $this->formId, $this->getTargetForms() ) + || $this->userContext->getUserType() === UserContextInterface::USER_TYPE_INTEGRATION ) { return false; } @@ -241,7 +252,7 @@ private function isOverLimitLoginAttempts($login) */ private function isUserAuth() { - return $this->session->isLoggedIn(); + return $this->session->isLoggedIn() || $this->userContext->getUserId(); } /** @@ -427,7 +438,7 @@ public function getWordLen() $to = self::DEFAULT_WORD_LENGTH_TO; } - return \Magento\Framework\Math\Random::getRandomNumber($from, $to); + return Random::getRandomNumber($from, $to); } /** @@ -549,7 +560,7 @@ private function clearWord() */ protected function randomSize() { - return \Magento\Framework\Math\Random::getRandomNumber(280, 300) / 100; + return Random::getRandomNumber(280, 300) / 100; } /** diff --git a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php index d83abc7a6c7d1..059d395f6cf73 100644 --- a/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php +++ b/app/code/Magento/Captcha/Observer/CaptchaStringResolver.php @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Captcha\Observer; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Captcha\Helper\Data as CaptchaHelper; /** * Extract given captcha word. @@ -22,12 +26,13 @@ class CaptchaStringResolver */ public function resolve(RequestInterface $request, $formId) { - $captchaParams = $request->getPost(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE); + $value = ''; + $captchaParams = $request->getPost(CaptchaHelper::INPUT_NAME_FIELD_VALUE); if (!empty($captchaParams) && !empty($captchaParams[$formId])) { $value = $captchaParams[$formId]; - } else { - //For Web APIs - $value = $request->getHeader('X-Captcha'); + } elseif ($headerValue = $request->getHeader('X-Captcha')) { + //CAPTCHA was provided via header for this XHR/web API request. + $value = $headerValue; } return $value; diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php index a20ff898c222e..59399cde99ea8 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php @@ -7,6 +7,7 @@ namespace Magento\Captcha\Test\Unit\Model; +use Magento\Authorization\Model\UserContextInterface; use Magento\Captcha\Block\Captcha\DefaultCaptcha; use Magento\Captcha\Helper\Data; use Magento\Captcha\Model\DefaultModel; @@ -93,10 +94,15 @@ class DefaultTest extends TestCase protected $session; /** - * @var MockObject + * @var MockObject|LogFactory */ protected $_resLogFactory; + /** + * @var UserContextInterface|MockObject + */ + private $userContextMock; + /** * Sets up the fixture, for example, opens a network connection. * This method is called before a test is executed. @@ -139,11 +145,18 @@ protected function setUp(): void $this->_getResourceModelStub() ); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('random-string'); + + $this->userContextMock = $this->getMockForAbstractClass(UserContextInterface::class); + $this->_object = new DefaultModel( $this->session, $this->_getHelperStub(), $this->_resLogFactory, - 'user_create' + 'user_create', + $randomMock, + $this->userContextMock ); } @@ -163,6 +176,19 @@ public function testIsRequired() $this->assertTrue($this->_object->isRequired()); } + /** + * Validate that CAPTCHA is disabled for integrations. + * + * @return void + */ + public function testIsRequiredForIntegration(): void + { + $this->userContextMock->method('getUserType')->willReturn(UserContextInterface::USER_TYPE_INTEGRATION); + $this->userContextMock->method('getUserId')->willReturn(1); + + $this->assertFalse($this->_object->isRequired()); + } + /** * @covers \Magento\Captcha\Model\DefaultModel::isCaseSensitive */ diff --git a/app/code/Magento/Captcha/composer.json b/app/code/Magento/Captcha/composer.json index a6ee83d3f0924..3c3aa58c3fe2f 100644 --- a/app/code/Magento/Captcha/composer.json +++ b/app/code/Magento/Captcha/composer.json @@ -11,6 +11,7 @@ "magento/module-checkout": "*", "magento/module-customer": "*", "magento/module-store": "*", + "magento/module-authorization": "*", "laminas/laminas-captcha": "^2.7.1", "laminas/laminas-db": "^2.8.2", "laminas/laminas-session": "^2.7.3" diff --git a/app/code/Magento/Captcha/i18n/en_US.csv b/app/code/Magento/Captcha/i18n/en_US.csv index 480107df8adfe..ac6a7cf9d57e7 100644 --- a/app/code/Magento/Captcha/i18n/en_US.csv +++ b/app/code/Magento/Captcha/i18n/en_US.csv @@ -9,6 +9,7 @@ Always,Always "Reload captcha","Reload captcha" "Please type the letters and numbers below","Please type the letters and numbers below" "Attention: Captcha is case sensitive.","Attention: Captcha is case sensitive." +"Please provide CAPTCHA code and try again","Please provide CAPTCHA code and try again" CAPTCHA,CAPTCHA "Enable CAPTCHA in Admin","Enable CAPTCHA in Admin" Font,Font diff --git a/app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php b/app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php new file mode 100644 index 0000000000000..e398bf400391b --- /dev/null +++ b/app/code/Magento/Checkout/Api/Exception/PaymentProcessingRateLimitExceededException.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Api\Exception; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Thrown when too many payment processing requests have been initiated by a user. + */ +class PaymentProcessingRateLimitExceededException extends LocalizedException +{ + +} diff --git a/app/code/Magento/Checkout/Api/PaymentProcessingRateLimiterInterface.php b/app/code/Magento/Checkout/Api/PaymentProcessingRateLimiterInterface.php new file mode 100644 index 0000000000000..d81b79fc8e201 --- /dev/null +++ b/app/code/Magento/Checkout/Api/PaymentProcessingRateLimiterInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Api; + +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; + +/** + * Limits number of times a user can initiate payment processing. + */ +interface PaymentProcessingRateLimiterInterface +{ + /** + * Limit an attempt to initiate a new payment processing. + * + * @return void + * @throws PaymentProcessingRateLimitExceededException + */ + public function limit(): void; +} diff --git a/app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php b/app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php new file mode 100644 index 0000000000000..6f71423acbcaa --- /dev/null +++ b/app/code/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiter.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Model; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Captcha\Model\DefaultModel as Captcha; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Captcha\Observer\CaptchaStringResolver as CaptchaResolver; +use Magento\Framework\App\RequestInterface; + +/** + * Utilize CAPTCHA as a rate-limiting mechanism. + */ +class CaptchaPaymentProcessingRateLimiter implements PaymentProcessingRateLimiterInterface +{ + public const CAPTCHA_FORM = 'payment_processing_request'; + + /** + * @var UserContextInterface + */ + private $userContext; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @var RequestInterface + */ + private $request; + + /** + * @var CaptchaResolver + */ + private $captchaResolver; + + /** + * CaptchaPaymentProcessingRateLimiter constructor. + * + * @param UserContextInterface $userContext + * @param CustomerRepositoryInterface $customerRepo + * @param CaptchaHelper $captchaHelper + * @param RequestInterface $request + * @param CaptchaResolver $captchaResolver + */ + public function __construct( + UserContextInterface $userContext, + CustomerRepositoryInterface $customerRepo, + CaptchaHelper $captchaHelper, + RequestInterface $request, + CaptchaResolver $captchaResolver + ) { + $this->userContext = $userContext; + $this->customerRepo = $customerRepo; + $this->captchaHelper = $captchaHelper; + $this->request = $request; + $this->captchaResolver = $captchaResolver; + } + + /** + * @inheritDoc + */ + public function limit(): void + { + if ($this->userContext->getUserType() !== UserContextInterface::USER_TYPE_GUEST + && $this->userContext->getUserType() !== UserContextInterface::USER_TYPE_CUSTOMER + && $this->userContext->getUserType() !== null + ) { + return; + } + + $login = $this->retrieveLogin(); + /** @var Captcha $captcha */ + $captcha = $this->captchaHelper->getCaptcha(self::CAPTCHA_FORM); + /** @var PaymentProcessingRateLimitExceededException|null $exception */ + $exception = null; + if ($captcha->isRequired($login)) { + $value = $this->captchaResolver->resolve($this->request, self::CAPTCHA_FORM); + if ($value && !$captcha->isCorrect($value)) { + $exception = new PaymentProcessingRateLimitExceededException(__('Incorrect CAPTCHA')); + } elseif (!$value) { + $exception = new PaymentProcessingRateLimitExceededException( + __('Please provide CAPTCHA code and try again') + ); + } + } + + $captcha->logAttempt($login); + if ($exception) { + throw $exception; + } + } + + /** + * Retrieve current user login. + * + * @return string|null + */ + private function retrieveLogin(): ?string + { + $login = null; + if ($this->userContext->getUserId()) { + $login = $this->customerRepo->getById($this->userContext->getUserId())->getEmail(); + } + + return $login; + } +} diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 8b8d2602fbfc7..2b2824213df79 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -7,8 +7,8 @@ namespace Magento\Checkout\Model; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; use Magento\Framework\App\ObjectManager; -use Magento\Framework\App\ResourceConnection; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Quote\Model\Quote; @@ -56,6 +56,11 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa */ private $logger; + /** + * @var PaymentProcessingRateLimiterInterface + */ + private $paymentsRateLimiter; + /** * @param \Magento\Quote\Api\GuestBillingAddressManagementInterface $billingAddressManagement * @param \Magento\Quote\Api\GuestPaymentMethodManagementInterface $paymentMethodManagement @@ -63,6 +68,7 @@ class GuestPaymentInformationManagement implements \Magento\Checkout\Api\GuestPa * @param \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement * @param \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory * @param CartRepositoryInterface $cartRepository + * @param PaymentProcessingRateLimiterInterface|null $paymentsRateLimiter * @codeCoverageIgnore */ public function __construct( @@ -71,7 +77,8 @@ public function __construct( \Magento\Quote\Api\GuestCartManagementInterface $cartManagement, \Magento\Checkout\Api\PaymentInformationManagementInterface $paymentInformationManagement, \Magento\Quote\Model\QuoteIdMaskFactory $quoteIdMaskFactory, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + ?PaymentProcessingRateLimiterInterface $paymentsRateLimiter = null ) { $this->billingAddressManagement = $billingAddressManagement; $this->paymentMethodManagement = $paymentMethodManagement; @@ -79,6 +86,8 @@ public function __construct( $this->paymentInformationManagement = $paymentInformationManagement; $this->quoteIdMaskFactory = $quoteIdMaskFactory; $this->cartRepository = $cartRepository; + $this->paymentsRateLimiter = $paymentsRateLimiter + ?? ObjectManager::getInstance()->get(PaymentProcessingRateLimiterInterface::class); } /** @@ -121,6 +130,8 @@ public function savePaymentInformation( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { + $this->paymentsRateLimiter->limit(); + $quoteIdMask = $this->quoteIdMaskFactory->create()->load($cartId, 'masked_id'); /** @var Quote $quote */ $quote = $this->cartRepository->getActive($quoteIdMask->getQuoteId()); diff --git a/app/code/Magento/Checkout/Model/PaymentCaptchaConfigProvider.php b/app/code/Magento/Checkout/Model/PaymentCaptchaConfigProvider.php new file mode 100644 index 0000000000000..268e765571205 --- /dev/null +++ b/app/code/Magento/Checkout/Model/PaymentCaptchaConfigProvider.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Model; + +use Magento\Captcha\Helper\Data as Helper; +use Magento\Captcha\Model\DefaultModel; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Provides frontend with payments CAPTCHA configuration. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class PaymentCaptchaConfigProvider implements ConfigProviderInterface +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Helper + */ + private $captchaData; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @param StoreManagerInterface $storeManager + * @param Helper $captchaData + * @param CustomerSession $customerSession + */ + public function __construct( + StoreManagerInterface $storeManager, + Helper $captchaData, + CustomerSession $customerSession + ) { + $this->storeManager = $storeManager; + $this->captchaData = $captchaData; + $this->customerSession = $customerSession; + } + + /** + * @inheritDoc + */ + public function getConfig() + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + /** @var DefaultModel $captchaModel */ + $captchaModel = $this->captchaData->getCaptcha(CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM); + $login = null; + if ($this->customerSession->isLoggedIn()) { + $login = $this->customerSession->getCustomerData()->getEmail(); + } + $required = $captchaModel->isRequired($login); + if ($required) { + $captchaModel->generate(); + $imageSrc = $captchaModel->getImgSrc(); + } else { + $imageSrc = ''; + } + + return [ + 'captcha' => [ + CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM => [ + 'isCaseSensitive' => (bool)$captchaModel->isCaseSensitive(), + 'imageHeight' => $captchaModel->getHeight(), + 'imageSrc' => $imageSrc, + 'refreshUrl' => $store->getUrl('captcha/refresh', ['_secure' => $store->isCurrentlySecure()]), + 'isRequired' => $required, + 'timestamp' => time() + ] + ] + ]; + } +} diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 2f68aba5ec6ae..a6e448ecdb87e 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -6,6 +6,8 @@ namespace Magento\Checkout\Model; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotSaveException; /** @@ -51,12 +53,18 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor */ private $cartRepository; + /** + * @var PaymentProcessingRateLimiterInterface + */ + private $paymentRateLimiter; + /** * @param \Magento\Quote\Api\BillingAddressManagementInterface $billingAddressManagement * @param \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement * @param \Magento\Quote\Api\CartManagementInterface $cartManagement * @param PaymentDetailsFactory $paymentDetailsFactory * @param \Magento\Quote\Api\CartTotalRepositoryInterface $cartTotalsRepository + * @param PaymentProcessingRateLimiterInterface|null $paymentRateLimiter * @codeCoverageIgnore */ public function __construct( @@ -64,13 +72,16 @@ public function __construct( \Magento\Quote\Api\PaymentMethodManagementInterface $paymentMethodManagement, \Magento\Quote\Api\CartManagementInterface $cartManagement, \Magento\Checkout\Model\PaymentDetailsFactory $paymentDetailsFactory, - \Magento\Quote\Api\CartTotalRepositoryInterface $cartTotalsRepository + \Magento\Quote\Api\CartTotalRepositoryInterface $cartTotalsRepository, + ?PaymentProcessingRateLimiterInterface $paymentRateLimiter = null ) { $this->billingAddressManagement = $billingAddressManagement; $this->paymentMethodManagement = $paymentMethodManagement; $this->cartManagement = $cartManagement; $this->paymentDetailsFactory = $paymentDetailsFactory; $this->cartTotalsRepository = $cartTotalsRepository; + $this->paymentRateLimiter = $paymentRateLimiter + ?? ObjectManager::getInstance()->get(PaymentProcessingRateLimiterInterface::class); } /** @@ -110,6 +121,8 @@ public function savePaymentInformation( \Magento\Quote\Api\Data\PaymentInterface $paymentMethod, \Magento\Quote\Api\Data\AddressInterface $billingAddress = null ) { + $this->paymentRateLimiter->limit(); + if ($billingAddress) { /** @var \Magento\Quote\Api\CartRepositoryInterface $quoteRepository */ $quoteRepository = $this->getCartRepository(); @@ -157,7 +170,7 @@ public function getPaymentInformation($cartId) private function getLogger() { if (!$this->logger) { - $this->logger = \Magento\Framework\App\ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); + $this->logger = ObjectManager::getInstance()->get(\Psr\Log\LoggerInterface::class); } return $this->logger; } @@ -171,7 +184,7 @@ private function getLogger() private function getCartRepository() { if (!$this->cartRepository) { - $this->cartRepository = \Magento\Framework\App\ObjectManager::getInstance() + $this->cartRepository = ObjectManager::getInstance() ->get(\Magento\Quote\Api\CartRepositoryInterface::class); } return $this->cartRepository; diff --git a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php index f3edcfe8986f0..4a89443f02f6d 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/GuestPaymentInformationManagementTest.php @@ -7,6 +7,8 @@ namespace Magento\Checkout\Test\Unit\Model; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; use Magento\Checkout\Model\GuestPaymentInformationManagement; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\LocalizedException; @@ -67,6 +69,11 @@ class GuestPaymentInformationManagementTest extends TestCase */ private $loggerMock; + /** + * @var PaymentProcessingRateLimiterInterface|MockObject + */ + private $limiterMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -84,6 +91,7 @@ protected function setUp(): void ['create'] ); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->limiterMock = $this->getMockForAbstractClass(PaymentProcessingRateLimiterInterface::class); $this->model = $objectManager->getObject( GuestPaymentInformationManagement::class, [ @@ -91,7 +99,8 @@ protected function setUp(): void 'paymentMethodManagement' => $this->paymentMethodManagementMock, 'cartManagement' => $this->cartManagementMock, 'cartRepository' => $this->cartRepositoryMock, - 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock + 'quoteIdMaskFactory' => $this->quoteIdMaskFactoryMock, + 'paymentsRateLimiter' => $this->limiterMock ] ); $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); @@ -99,22 +108,21 @@ protected function setUp(): void public function testSavePaymentInformationAndPlaceOrder() { - $cartId = 100; $orderId = 200; - $email = 'email@magento.com'; - $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); - $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); - $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); - - $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); - - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $this->cartManagementMock->expects($this->once())->method('placeOrder')->with($cartId)->willReturn($orderId); + $this->assertEquals($orderId, $this->placeOrder($orderId)); + } - $this->assertEquals( - $orderId, - $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock) - ); + /** + * Validate that "testSavePaymentInformationAndPlaceOrderLimited" calls are limited. + * + * @return void + */ + public function testSavePaymentInformationAndPlaceOrderLimited(): void + { + $this->expectException(PaymentProcessingRateLimitExceededException::class); + $this->limiterMock->method('limit') + ->willThrowException(new PaymentProcessingRateLimitExceededException(__('Error'))); + $this->placeOrder(); } public function testSavePaymentInformationAndPlaceOrderException() @@ -141,16 +149,21 @@ public function testSavePaymentInformationAndPlaceOrderException() public function testSavePaymentInformation() { - $cartId = 100; - $email = 'email@magento.com'; - $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); - $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); - $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); - $billingAddressMock->expects($this->once())->method('setEmail')->with($email)->willReturnSelf(); + $this->assertTrue($this->savePayment()); + } - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + /** + * Validate that this method is rate-limited. + * + * @return void + */ + public function testSavePaymentInformationLimited(): void + { + $this->expectException(PaymentProcessingRateLimitExceededException::class); + $this->limiterMock->method('limit') + ->willThrowException(new PaymentProcessingRateLimitExceededException(__('Error'))); - $this->assertTrue($this->model->savePaymentInformation($cartId, $email, $paymentMock, $billingAddressMock)); + $this->savePayment(); } public function testSavePaymentInformationWithoutBillingAddress() @@ -246,31 +259,75 @@ private function getMockForAssignBillingAddress( $this->cartRepositoryMock->method('getActive') ->with($cartId) ->willReturn($quote); - $quote->expects($this->once()) + $quote->expects($this->any()) ->method('getBillingAddress') ->willReturn($quoteBillingAddress); - $quote->expects($this->once()) + $quote->expects($this->any()) ->method('getShippingAddress') ->willReturn($quoteShippingAddress); - $quoteBillingAddress->expects($this->once()) + $quoteBillingAddress->expects($this->any()) ->method('getId') ->willReturn($billingAddressId); - $quote->expects($this->once()) + $quote->expects($this->any()) ->method('removeAddress') ->with($billingAddressId); - $quote->expects($this->once()) + $quote->expects($this->any()) ->method('setBillingAddress') ->with($billingAddressMock); $quoteShippingAddress->expects($this->any()) ->method('getShippingRateByCode') ->willReturn($shippingRate); - $quote->expects($this->once()) + $quote->expects($this->any()) ->method('setDataChanges') ->willReturnSelf(); $quoteShippingAddress->method('getShippingMethod') ->willReturn('flatrate_flatrate'); - $quoteShippingAddress->expects($this->once()) + $quoteShippingAddress->expects($this->any()) ->method('setLimitCarrier') ->with('flatrate'); } + + /** + * Place order. + * + * @param int $orderId + * @return mixed Method call result. + */ + private function placeOrder(?int $orderId = 200) + { + $cartId = 100; + $email = 'email@magento.com'; + $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); + $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); + + $billingAddressMock->expects($this->any())->method('setEmail')->with($email)->willReturnSelf(); + + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); + $this->cartManagementMock->expects($this->any()) + ->method('placeOrder') + ->with($cartId) + ->willReturn($orderId); + + return $this->model->savePaymentInformationAndPlaceOrder($cartId, $email, $paymentMock, $billingAddressMock); + } + + /** + * Save payment information. + * + * @return mixed Call result. + */ + private function savePayment() + { + $cartId = 100; + $email = 'email@magento.com'; + $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); + $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); + $billingAddressMock->expects($this->any())->method('setEmail')->with($email)->willReturnSelf(); + + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); + + return $this->model->savePaymentInformation($cartId, $email, $paymentMock, $billingAddressMock); + } } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php index 75445f23aa887..294857765007e 100644 --- a/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Model/PaymentInformationManagementTest.php @@ -7,6 +7,8 @@ namespace Magento\Checkout\Test\Unit\Model; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; use Magento\Checkout\Model\PaymentInformationManagement; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Phrase; @@ -59,6 +61,11 @@ class PaymentInformationManagementTest extends TestCase */ private $cartRepositoryMock; + /** + * @var PaymentProcessingRateLimiterInterface|MockObject + */ + private $rateLimiterMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -73,12 +80,14 @@ protected function setUp(): void $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); $this->cartRepositoryMock = $this->getMockBuilder(CartRepositoryInterface::class) ->getMock(); + $this->rateLimiterMock = $this->getMockForAbstractClass(PaymentProcessingRateLimiterInterface::class); $this->model = $objectManager->getObject( PaymentInformationManagement::class, [ 'billingAddressManagement' => $this->billingAddressManagementMock, 'paymentMethodManagement' => $this->paymentMethodManagementMock, - 'cartManagement' => $this->cartManagementMock + 'cartManagement' => $this->cartManagementMock, + 'paymentRateLimiter' => $this->rateLimiterMock ] ); $objectManager->setBackwardCompatibleProperty($this->model, 'logger', $this->loggerMock); @@ -87,21 +96,27 @@ protected function setUp(): void public function testSavePaymentInformationAndPlaceOrder() { - $cartId = 100; $orderId = 200; - $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); - $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); - - $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $this->cartManagementMock->expects($this->once())->method('placeOrder')->with($cartId)->willReturn($orderId); - $this->assertEquals( $orderId, - $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock) + $this->placeOrder($orderId) ); } + /** + * Valdiate that the method is rate-limited. + * + * @return void + */ + public function testSavePaymentInformationAndPlaceOrderLimited(): void + { + $this->rateLimiterMock->method('limit') + ->willThrowException(new PaymentProcessingRateLimitExceededException(__('Error'))); + $this->expectException(PaymentProcessingRateLimitExceededException::class); + + $this->placeOrder(); + } + public function testSavePaymentInformationAndPlaceOrderException() { $this->expectException('Magento\Framework\Exception\CouldNotSaveException'); @@ -110,10 +125,10 @@ public function testSavePaymentInformationAndPlaceOrderException() $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); $exception = new \Exception('DB exception'); - $this->loggerMock->expects($this->once())->method('critical'); - $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); + $this->loggerMock->expects($this->any())->method('critical'); + $this->cartManagementMock->expects($this->any())->method('placeOrder')->willThrowException($exception); $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock); @@ -128,8 +143,8 @@ public function testSavePaymentInformationAndPlaceOrderIfBillingAddressNotExist( $orderId = 200; $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $this->cartManagementMock->expects($this->once())->method('placeOrder')->with($cartId)->willReturn($orderId); + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); + $this->cartManagementMock->expects($this->any())->method('placeOrder')->with($cartId)->willReturn($orderId); $this->assertEquals( $orderId, @@ -139,14 +154,21 @@ public function testSavePaymentInformationAndPlaceOrderIfBillingAddressNotExist( public function testSavePaymentInformation() { - $cartId = 100; - $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); - $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); + $this->assertTrue($this->savePayment()); + } - $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + /** + * Validate that the method is rate-limited. + * + * @return void + */ + public function testSavePaymentInformationLimited(): void + { + $this->rateLimiterMock->method('limit') + ->willThrowException(new PaymentProcessingRateLimitExceededException(__('Error'))); + $this->expectException(PaymentProcessingRateLimitExceededException::class); - $this->assertTrue($this->model->savePaymentInformation($cartId, $paymentMock, $billingAddressMock)); + $this->savePayment(); } public function testSavePaymentInformationWithoutBillingAddress() @@ -154,7 +176,7 @@ public function testSavePaymentInformationWithoutBillingAddress() $cartId = 100; $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); $this->assertTrue($this->model->savePaymentInformation($cartId, $paymentMock)); } @@ -169,10 +191,10 @@ public function testSavePaymentInformationAndPlaceOrderWithLocolizedException() $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); $phrase = new Phrase(__('DB exception')); $exception = new LocalizedException($phrase); - $this->cartManagementMock->expects($this->once())->method('placeOrder')->willThrowException($exception); + $this->cartManagementMock->expects($this->any())->method('placeOrder')->willThrowException($exception); $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock); } @@ -197,8 +219,8 @@ public function testSavePaymentInformationAndPlaceOrderWithNewBillingAddress(): $quoteBillingAddress->method('getId')->willReturn($quoteBillingAddressId); $this->cartRepositoryMock->method('getActive')->with($cartId)->willReturn($quoteMock); - $this->paymentMethodManagementMock->expects($this->once())->method('set')->with($cartId, $paymentMock); - $billingAddressMock->expects($this->once())->method('setCustomerId')->with($customerId); + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); + $billingAddressMock->expects($this->any())->method('setCustomerId')->with($customerId); $this->assertTrue($this->model->savePaymentInformation($cartId, $paymentMock, $billingAddressMock)); } @@ -220,14 +242,50 @@ private function getMockForAssignBillingAddress($cartId, $billingAddressMock) ->getMock(); $this->cartRepositoryMock->expects($this->any())->method('getActive')->with($cartId)->willReturn($quoteMock); $quoteMock->method('getBillingAddress')->willReturn($quoteBillingAddress); - $quoteMock->expects($this->once())->method('getShippingAddress')->willReturn($quoteShippingAddress); - $quoteBillingAddress->expects($this->once())->method('getId')->willReturn($billingAddressId); - $quoteBillingAddress->expects($this->once())->method('getId')->willReturn($billingAddressId); - $quoteMock->expects($this->once())->method('removeAddress')->with($billingAddressId); - $quoteMock->expects($this->once())->method('setBillingAddress')->with($billingAddressMock); - $quoteMock->expects($this->once())->method('setDataChanges')->willReturnSelf(); + $quoteMock->expects($this->any())->method('getShippingAddress')->willReturn($quoteShippingAddress); + $quoteBillingAddress->expects($this->any())->method('getId')->willReturn($billingAddressId); + $quoteBillingAddress->expects($this->any())->method('getId')->willReturn($billingAddressId); + $quoteMock->expects($this->any())->method('removeAddress')->with($billingAddressId); + $quoteMock->expects($this->any())->method('setBillingAddress')->with($billingAddressMock); + $quoteMock->expects($this->any())->method('setDataChanges')->willReturnSelf(); $quoteShippingAddress->expects($this->any())->method('getShippingRateByCode')->willReturn($shippingRate); $quoteShippingAddress->expects($this->any())->method('getShippingMethod')->willReturn('flatrate_flatrate'); - $quoteShippingAddress->expects($this->once())->method('setLimitCarrier')->with('flatrate')->willReturnSelf(); + $quoteShippingAddress->expects($this->any())->method('setLimitCarrier')->with('flatrate')->willReturnSelf(); + } + + /** + * Save payment information. + * + * @return mixed + */ + private function savePayment() + { + $cartId = 100; + $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); + $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); + + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); + + return $this->model->savePaymentInformation($cartId, $paymentMock, $billingAddressMock); + } + + /** + * Call `place order`. + * + * @param int|null $orderId + * @return mixed + */ + private function placeOrder(?int $orderId = 200) + { + $cartId = 100; + $paymentMock = $this->getMockForAbstractClass(PaymentInterface::class); + $billingAddressMock = $this->getMockForAbstractClass(AddressInterface::class); + + $this->getMockForAssignBillingAddress($cartId, $billingAddressMock); + $this->paymentMethodManagementMock->expects($this->any())->method('set')->with($cartId, $paymentMock); + $this->cartManagementMock->expects($this->any())->method('placeOrder')->with($cartId)->willReturn($orderId); + + return $this->model->savePaymentInformationAndPlaceOrder($cartId, $paymentMock, $billingAddressMock); } } diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 2b4fce7dc011a..5f7b5425667e5 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -24,7 +24,8 @@ "magento/module-tax": "*", "magento/module-theme": "*", "magento/module-ui": "*", - "magento/module-captcha": "*" + "magento/module-captcha": "*", + "magento/module-authorization": "*" }, "suggest": { "magento/module-cookie": "*" diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index 4db5f5bdc01c9..eac0bd849da35 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -41,6 +41,9 @@ <sales_rule_coupon_request> <label>Applying coupon code</label> </sales_rule_coupon_request> + <payment_processing_request> + <label>Checkout/Placing Order</label> + </payment_processing_request> </areas> </frontend> </captcha> @@ -48,6 +51,7 @@ <captcha> <shown_to_logged_in_user> <sales_rule_coupon_request>1</sales_rule_coupon_request> + <payment_processing_request>1</payment_processing_request> </shown_to_logged_in_user> </captcha> </customer> diff --git a/app/code/Magento/Checkout/etc/di.xml b/app/code/Magento/Checkout/etc/di.xml index 4ebd594a28562..0c1d866dfc2fb 100644 --- a/app/code/Magento/Checkout/etc/di.xml +++ b/app/code/Magento/Checkout/etc/di.xml @@ -49,4 +49,6 @@ </argument> </arguments> </type> + <preference for="Magento\Checkout\Api\PaymentProcessingRateLimiterInterface" + type="Magento\Checkout\Model\CaptchaPaymentProcessingRateLimiter" /> </config> diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 8f35fe9f37abf..1d50ec14c0bba 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -49,6 +49,7 @@ <argument name="configProviders" xsi:type="array"> <item name="checkout_default_config_provider" xsi:type="object">Magento\Checkout\Model\DefaultConfigProvider</item> <item name="checkout_summary_config_provider" xsi:type="object">Magento\Checkout\Model\Cart\CheckoutSummaryConfigProvider</item> + <item name="checkout_payment_provider" xsi:type="object">Magento\Checkout\Model\PaymentCaptchaConfigProvider</item> </argument> </arguments> </type> @@ -99,4 +100,11 @@ <type name="Magento\Quote\Model\Quote"> <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> </type> + <type name="Magento\Captcha\CustomerData\Captcha"> + <arguments> + <argument name="formIds" xsi:type="array"> + <item name="payment_processing_request" xsi:type="string">payment_processing_request</item> + </argument> + </arguments> + </type> </config> 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 ab058110fe66f..f087c04d1dfe3 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 @@ -287,6 +287,12 @@ </item> </item> </item> + <item name="place-order-captcha" xsi:type="array"> + <item name="component" xsi:type="string">Magento_Checkout/js/view/checkout/placeOrderCaptcha</item> + <item name="displayArea" xsi:type="string">place-order-captcha</item> + <item name="formId" xsi:type="string">payment_processing_request</item> + <item name="configSource" xsi:type="string">checkoutConfig</item> + </item> <item name="beforeMethods" xsi:type="array"> <item name="component" xsi:type="string">uiComponent</item> <item name="displayArea" xsi:type="string">beforeMethods</item> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js index 9de8a93905c99..ae5b0914e83a6 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/action/set-payment-information-extended.js @@ -14,8 +14,9 @@ define([ 'Magento_Customer/js/model/customer', 'Magento_Checkout/js/action/get-totals', 'Magento_Checkout/js/model/full-screen-loader', - 'underscore' -], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader, _) { + 'underscore', + 'Magento_Checkout/js/model/payment/set-payment-hooks' +], function (quote, urlBuilder, storage, errorProcessor, customer, getTotalsAction, fullScreenLoader, _, hooks) { 'use strict'; /** @@ -37,7 +38,8 @@ define([ return function (messageContainer, paymentData, skipBilling) { var serviceUrl, - payload; + payload, + headers = {}; paymentData = filterTemplateData(paymentData); skipBilling = skipBilling || false; @@ -64,8 +66,12 @@ define([ fullScreenLoader.startLoader(); + _.each(hooks.requestModifiers, function (modifier) { + modifier(headers, payload); + }); + return storage.post( - serviceUrl, JSON.stringify(payload) + serviceUrl, JSON.stringify(payload), true, 'application/json', headers ).fail( function (response) { errorProcessor.process(response, messageContainer); @@ -73,6 +79,9 @@ define([ ).always( function () { fullScreenLoader.stopLoader(); + _.each(hooks.afterRequestListeners, function (listener) { + listener(); + }); } ); }; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/place-order-hooks.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/place-order-hooks.js new file mode 100644 index 0000000000000..5cd31d85c9a29 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/place-order-hooks.js @@ -0,0 +1,13 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([], function () { + 'use strict'; + + return { + requestModifiers: [], + afterRequestListeners: [] + }; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/payment/set-payment-hooks.js b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/set-payment-hooks.js new file mode 100644 index 0000000000000..5cd31d85c9a29 --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/payment/set-payment-hooks.js @@ -0,0 +1,13 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([], function () { + 'use strict'; + + return { + requestModifiers: [], + afterRequestListeners: [] + }; +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js index c07878fcaea92..701c31944939b 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/place-order.js @@ -10,16 +10,23 @@ define( 'mage/storage', 'Magento_Checkout/js/model/error-processor', 'Magento_Checkout/js/model/full-screen-loader', - 'Magento_Customer/js/customer-data' + 'Magento_Customer/js/customer-data', + 'Magento_Checkout/js/model/payment/place-order-hooks', + 'underscore' ], - function (storage, errorProcessor, fullScreenLoader, customerData) { + function (storage, errorProcessor, fullScreenLoader, customerData, hooks, _) { 'use strict'; return function (serviceUrl, payload, messageContainer) { + var headers = {}; + fullScreenLoader.startLoader(); + _.each(hooks.requestModifiers, function (modifier) { + modifier(headers, payload); + }); return storage.post( - serviceUrl, JSON.stringify(payload) + serviceUrl, JSON.stringify(payload), true, 'application/json', headers ).fail( function (response) { errorProcessor.process(response, messageContainer); @@ -44,6 +51,9 @@ define( ).always( function () { fullScreenLoader.stopLoader(); + _.each(hooks.afterRequestListeners, function (listener) { + listener(); + }); } ); }; diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/placeOrderCaptcha.js b/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/placeOrderCaptcha.js new file mode 100644 index 0000000000000..d0e27ad8e0abb --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/placeOrderCaptcha.js @@ -0,0 +1,38 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'underscore', + 'Magento_Checkout/js/model/payment/place-order-hooks' +], +function (defaultCaptcha, captchaList, _, placeOrderHooks) { + 'use strict'; + + return defaultCaptcha.extend({ + /** @inheritdoc */ + initialize: function () { + var self = this, + currentCaptcha; + + this._super(); + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + placeOrderHooks.requestModifiers.push(function (headers) { + if (self.isRequired()) { + headers['X-Captcha'] = self.captchaValue()(); + } + }); + placeOrderHooks.afterRequestListeners.push(function () { + self.refresh(); + }); + } + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/setPaymentCaptcha.js b/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/setPaymentCaptcha.js new file mode 100644 index 0000000000000..93f3bb8b2a45c --- /dev/null +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/checkout/setPaymentCaptcha.js @@ -0,0 +1,38 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Captcha/js/view/checkout/defaultCaptcha', + 'Magento_Captcha/js/model/captchaList', + 'underscore', + 'Magento_Checkout/js/model/payment/set-payment-hooks' +], +function (defaultCaptcha, captchaList, _, setPaymentHooks) { + 'use strict'; + + return defaultCaptcha.extend({ + /** @inheritdoc */ + initialize: function () { + var self = this, + currentCaptcha; + + this._super(); + currentCaptcha = captchaList.getCaptchaByFormId(this.formId); + + if (currentCaptcha != null) { + currentCaptcha.setIsVisible(true); + this.setCurrentCaptcha(currentCaptcha); + setPaymentHooks.requestModifiers.push(function (headers) { + if (self.isRequired()) { + headers['X-Captcha'] = self.captchaValue()(); + } + }); + setPaymentHooks.afterRequestListeners.push(function () { + self.refresh(); + }); + } + } + }); +}); diff --git a/app/code/Magento/Checkout/view/frontend/web/template/payment.html b/app/code/Magento/Checkout/view/frontend/web/template/payment.html index a3e1a0f7aca90..1e3d3fed3876f 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/payment.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/payment.html @@ -21,6 +21,10 @@ <legend class="legend"> <span data-bind="i18n: 'Payment Information'"></span> </legend><br /> + <!-- ko foreach: getRegion('place-order-captcha') --> + <!-- ko template: getTemplate() --><!-- /ko --> + <!-- /ko --> + <br /> <!-- ko foreach: getRegion('beforeMethods') --> <!-- ko template: getTemplate() --><!-- /ko --> <!-- /ko --> diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 1ea2dc2618778..812ada81fac9b 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -6,6 +6,8 @@ namespace Magento\Multishipping\Block\Checkout; +use Magento\Captcha\Block\Captcha; +use Magento\Checkout\Model\CaptchaPaymentProcessingRateLimiter; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Quote\Model\Quote\Address; use Magento\Checkout\Helper\Data as CheckoutHelper; @@ -123,6 +125,20 @@ protected function _prepareLayout() $this->pageConfig->getTitle()->set( __('Review Order - %1', $this->pageConfig->getTitle()->getDefault()) ); + if (!$this->getChildBlock('captcha')) { + $this->addChild( + 'captcha', + Captcha::class, + [ + 'cacheable' => false, + 'after' => '-', + 'form_id' => CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM, + 'image_width' => 230, + 'image_height' => 230 + ] + ); + } + return parent::_prepareLayout(); } diff --git a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php index f05a7f43b8118..b3333d828a094 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/OverviewPost.php @@ -5,6 +5,9 @@ */ namespace Magento\Multishipping\Controller\Checkout; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Multishipping\Model\Checkout\Type\Multishipping\State; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; @@ -12,14 +15,15 @@ use Magento\Framework\Session\SessionManagerInterface; /** - * Class OverviewPost + * Placing orders. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class OverviewPost extends \Magento\Multishipping\Controller\Checkout +class OverviewPost extends \Magento\Multishipping\Controller\Checkout implements HttpPostActionInterface { /** * @var \Magento\Framework\Data\Form\FormKey\Validator + * @deprecated Form key validation is handled on the framework level. */ protected $formKeyValidator; @@ -38,6 +42,11 @@ class OverviewPost extends \Magento\Multishipping\Controller\Checkout */ private $session; + /** + * @var PaymentProcessingRateLimiterInterface + */ + private $paymentRateLimiter; + /** * @param \Magento\Framework\App\Action\Context $context * @param \Magento\Customer\Model\Session $customerSession @@ -47,6 +56,7 @@ class OverviewPost extends \Magento\Multishipping\Controller\Checkout * @param \Psr\Log\LoggerInterface $logger * @param \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator * @param SessionManagerInterface $session + * @param PaymentProcessingRateLimiterInterface|null $paymentRateLimiter */ public function __construct( \Magento\Framework\App\Action\Context $context, @@ -56,12 +66,15 @@ public function __construct( \Magento\Framework\Data\Form\FormKey\Validator $formKeyValidator, \Psr\Log\LoggerInterface $logger, \Magento\Checkout\Api\AgreementsValidatorInterface $agreementValidator, - SessionManagerInterface $session + SessionManagerInterface $session, + ?PaymentProcessingRateLimiterInterface $paymentRateLimiter = null ) { $this->formKeyValidator = $formKeyValidator; $this->logger = $logger; $this->agreementsValidator = $agreementValidator; $this->session = $session; + $this->paymentRateLimiter = $paymentRateLimiter + ?? ObjectManager::getInstance()->get(PaymentProcessingRateLimiterInterface::class); parent::__construct( $context, @@ -79,15 +92,12 @@ public function __construct( */ public function execute() { - if (!$this->formKeyValidator->validate($this->getRequest())) { - $this->_forward('backToAddresses'); - return; - } - if (!$this->_validateMinimumAmount()) { - return; - } - try { + $this->paymentRateLimiter->limit(); + if (!$this->_validateMinimumAmount()) { + return; + } + if (!$this->agreementsValidator->isValid(array_keys($this->getRequest()->getPost('agreement', [])))) { $this->messageManager->addError( __('Please agree to all Terms and Conditions before placing the order.') diff --git a/app/code/Magento/Multishipping/composer.json b/app/code/Magento/Multishipping/composer.json index 85f60985fe1b0..8834603562332 100644 --- a/app/code/Magento/Multishipping/composer.json +++ b/app/code/Magento/Multishipping/composer.json @@ -15,7 +15,8 @@ "magento/module-sales": "*", "magento/module-store": "*", "magento/module-tax": "*", - "magento/module-theme": "*" + "magento/module-theme": "*", + "magento/module-captcha": "*" }, "type": "magento2-module", "license": [ 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 3b72679bfc34e..35032b874374d 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml @@ -213,6 +213,7 @@ $checkoutHelper = $block->getData('checkoutHelper'); </div> <div class="actions-toolbar" id="review-buttons-container"> <div class="primary"> + <?= $block->getChildHtml('captcha') ?> <button type="submit" class="action primary submit" id="review-button"><span><?= $block->escapeHtml(__('Place Order')); ?></span> diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetPaymentMethodOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetPaymentMethodOnCart.php index 4deb794761efb..81a30216f0035 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetPaymentMethodOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetPaymentMethodOnCart.php @@ -7,6 +7,9 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; @@ -18,7 +21,7 @@ use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderPool; /** - * Set payment method on cart + * Saves related payment method info for a cart. */ class SetPaymentMethodOnCart { @@ -37,19 +40,28 @@ class SetPaymentMethodOnCart */ private $additionalDataProviderPool; + /** + * @var PaymentProcessingRateLimiterInterface + */ + private $paymentRateLimiter; + /** * @param PaymentMethodManagementInterface $paymentMethodManagement * @param PaymentInterfaceFactory $paymentFactory * @param AdditionalDataProviderPool $additionalDataProviderPool + * @param PaymentProcessingRateLimiterInterface|null $paymentRateLimiter */ public function __construct( PaymentMethodManagementInterface $paymentMethodManagement, PaymentInterfaceFactory $paymentFactory, - AdditionalDataProviderPool $additionalDataProviderPool + AdditionalDataProviderPool $additionalDataProviderPool, + ?PaymentProcessingRateLimiterInterface $paymentRateLimiter = null ) { $this->paymentMethodManagement = $paymentMethodManagement; $this->paymentFactory = $paymentFactory; $this->additionalDataProviderPool = $additionalDataProviderPool; + $this->paymentRateLimiter = $paymentRateLimiter + ?? ObjectManager::getInstance()->get(PaymentProcessingRateLimiterInterface::class); } /** @@ -62,6 +74,12 @@ public function __construct( */ public function execute(Quote $cart, array $paymentData): void { + try { + $this->paymentRateLimiter->limit(); + } catch (PaymentProcessingRateLimitExceededException $exception) { + throw new GraphQlInputException(__($exception->getMessage()), $exception); + } + if (!isset($paymentData['code']) || empty($paymentData['code'])) { throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); } diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php new file mode 100644 index 0000000000000..281e1233d8bbe --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/SetPaymentMethodOnCartTest.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Test\Unit\Model\Cart; + +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; +use Magento\Checkout\Api\PaymentProcessingRateLimiterInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class SetPaymentMethodOnCartTest extends TestCase +{ + /** + * @var SetPaymentMethodOnCart + */ + private $model; + + /** + * @var PaymentProcessingRateLimiterInterface|MockObject + */ + private $rateLimiterMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = new ObjectManager($this); + $this->rateLimiterMock = $this->getMockForAbstractClass(PaymentProcessingRateLimiterInterface::class); + $this->model = $objectManager->getObject( + SetPaymentMethodOnCart::class, + ['paymentRateLimiter' => $this->rateLimiterMock] + ); + } + + /** + * Verify that the method is rate-limited. + * + * @return void + */ + public function testLimited(): void + { + $this->rateLimiterMock->method('limit') + ->willThrowException(new PaymentProcessingRateLimitExceededException(__($message = 'Error'))); + $this->expectException(GraphQlInputException::class); + $this->expectExceptionMessage($message); + + $this->model->execute($this->createMock(Quote::class), []); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiterTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiterTest.php new file mode 100644 index 0000000000000..2a7b3c29223be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/CaptchaPaymentProcessingRateLimiterTest.php @@ -0,0 +1,185 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Checkout\Model; + +use Magento\Captcha\Model\DefaultModel; +use Magento\Checkout\Api\Exception\PaymentProcessingRateLimitExceededException; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Captcha\Helper\Data as CaptchaHelper; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\TestFramework\ObjectManager; +use Magento\Customer\Model\Session as CustomerSession; + +/** + * Test CAPTCHA-based rate limiter. + * + * @magentoDbIsolation enabled + * @magentoAppIsolation enabled + * @magentoAppArea frontend + */ +class CaptchaPaymentProcessingRateLimiterTest extends TestCase +{ + /** + * @var CaptchaPaymentProcessingRateLimiter + */ + private $model; + + /** + * @var CaptchaHelper + */ + private $captchaHelper; + + /** + * @var HttpRequest; + */ + private $request; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepo; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + /** @var ObjectManager $objectManager */ + $objectManager = Bootstrap::getObjectManager(); + $this->request = $objectManager->get(RequestInterface::class); + $this->request->getServer()->set('REMOTE_ADDR', '127.0.0.1'); + $objectManager->removeSharedInstance(RemoteAddress::class); + $this->captchaHelper = $objectManager->get(CaptchaHelper::class); + $this->customerSession = $objectManager->get(CustomerSession::class); + $this->customerRepo = $objectManager->get(CustomerRepositoryInterface::class); + $this->model = $objectManager->get(CaptchaPaymentProcessingRateLimiter::class); + } + + /** + * Verify that limits work for logged-in customers. + * + * @return void + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms payment_processing_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 2 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 10 + */ + public function testLoggedInLimits(): void + { + //Logging in + $customer = $this->customerRepo->get('customer@example.com'); + $this->customerSession->loginById($customer->getId()); + + $this->model->limit(); + $this->model->limit(); + try { + $this->model->limit(); + $limited = false; + } catch (PaymentProcessingRateLimitExceededException $exception) { + $limited = true; + } + $this->assertTrue($limited); + } + + /** + * Verify that limits work for guest. + * + * @return void + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms payment_processing_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 10 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 2 + */ + public function testGuestLimits(): void + { + $this->model->limit(); + $this->model->limit(); + try { + $this->model->limit(); + $limited = false; + } catch (PaymentProcessingRateLimitExceededException $exception) { + $limited = true; + } + $this->assertTrue($limited); + } + + /** + * Verify that CAPTCHA is validated. + * + * @return void + * + * @magentoConfigFixture default_store customer/captcha/enable 1 + * @magentoConfigFixture default_store customer/captcha/forms payment_processing_request + * @magentoConfigFixture default_store customer/captcha/failed_attempts_login 10 + * @magentoConfigFixture default_store customer/captcha/failed_attempts_ip 2 + */ + public function testCaptchaValidation(): void + { + $this->model->limit(); + $this->model->limit(); + try { + $this->model->limit(); + $limited = false; + } catch (PaymentProcessingRateLimitExceededException $exception) { + $limited = true; + } + //CAPTCHA is required + $this->assertTrue($limited); + + //Providing CAPTCHA value + /** @var DefaultModel $captcha */ + $captcha = $this->captchaHelper->getCaptcha(CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM); + $captcha->generate(); + $this->request->setPostValue( + 'captcha', + [CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM => $captcha->getWord()] + ); + $this->model->limit(); + //Providing CAPTCHA value in a header + /** @var DefaultModel $captcha */ + $captcha = $this->captchaHelper->getCaptcha(CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM); + $captcha->generate(); + $this->request->setPostValue( + 'captcha', + [CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM => ''] + ); + $this->request->getHeaders()->addHeaderLine('X-Captcha', $captcha->getWord()); + $this->model->limit(); + + //Providing invalid CAPTCHA value. + $this->request->setPostValue( + 'captcha', + [CaptchaPaymentProcessingRateLimiter::CAPTCHA_FORM => 'invalid'] + ); + $this->request->getHeaders()->removeHeader($this->request->getHeaders()->get('X-Captcha')); + try { + $this->model->limit(); + $limited = false; + } catch (PaymentProcessingRateLimitExceededException $exception) { + $limited = true; + } + //CAPTCHA was validated + $this->assertTrue($limited); + } +} diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/action/set-payment-information-extended.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/action/set-payment-information-extended.test.js index 66f7731415c05..3a31672848d5c 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/action/set-payment-information-extended.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/action/set-payment-information-extended.test.js @@ -78,7 +78,8 @@ define([ setPaymentInformation(messageContainer, paymentData, false); expect(mocks['Magento_Checkout/js/model/full-screen-loader'].startLoader).toHaveBeenCalled(); - expect(mocks['mage/storage'].post).toHaveBeenCalledWith(serviceUrl, JSON.stringify(payload)); + expect(mocks['mage/storage'].post) + .toHaveBeenCalledWith(serviceUrl, JSON.stringify(payload), true, 'application/json', {}); expect(mocks['Magento_Checkout/js/model/full-screen-loader'].stopLoader).toHaveBeenCalled(); }); }); diff --git a/lib/web/mage/storage.js b/lib/web/mage/storage.js index 1e136aa78477b..ba7cb6a8795cf 100644 --- a/lib/web/mage/storage.js +++ b/lib/web/mage/storage.js @@ -12,9 +12,11 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { * @param {String} url * @param {Boolean} global * @param {String} contentType + * @param {Object} headers * @returns {Deferred} */ - get: function (url, global, contentType) { + get: function (url, global, contentType, headers) { + headers = headers || {}; global = global === undefined ? true : global; contentType = contentType || 'application/json'; @@ -22,7 +24,8 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { url: urlBuilder.build(url), type: 'GET', global: global, - contentType: contentType + contentType: contentType, + headers: headers }); }, @@ -32,9 +35,11 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { * @param {String} data * @param {Boolean} global * @param {String} contentType + * @param {Object} headers * @returns {Deferred} */ - post: function (url, data, global, contentType) { + post: function (url, data, global, contentType, headers) { + headers = headers || {}; global = global === undefined ? true : global; contentType = contentType || 'application/json'; @@ -43,7 +48,8 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { type: 'POST', data: data, global: global, - contentType: contentType + contentType: contentType, + headers: headers }); }, @@ -53,11 +59,13 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { * @param {String} data * @param {Boolean} global * @param {String} contentType + * @param {Object} headers * @returns {Deferred} */ put: function (url, data, global, contentType, headers) { var ajaxSettings = {}; + headers = headers || {}; global = global === undefined ? true : global; contentType = contentType || 'application/json'; ajaxSettings.url = urlBuilder.build(url); @@ -65,10 +73,7 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { ajaxSettings.data = data; ajaxSettings.global = global; ajaxSettings.contentType = contentType; - - if (headers) { - ajaxSettings.headers = headers; - } + ajaxSettings.headers = headers; return $.ajax(ajaxSettings); }, @@ -78,9 +83,11 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { * @param {String} url * @param {Boolean} global * @param {String} contentType + * @param {Object} headers * @returns {Deferred} */ - delete: function (url, global, contentType) { + delete: function (url, global, contentType, headers) { + headers = headers || {}; global = global === undefined ? true : global; contentType = contentType || 'application/json'; @@ -88,7 +95,8 @@ define(['jquery', 'mage/url'], function ($, urlBuilder) { url: urlBuilder.build(url), type: 'DELETE', global: global, - contentType: contentType + contentType: contentType, + headers: headers }); } }; From 52f08f116e5f884305bef596c9c9573dd2ea9a98 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 7 Aug 2020 14:47:41 -0500 Subject: [PATCH 0180/1013] MC-32659: Order Details by Order Number with additional different product types --- .../Resolver/Order/Item/Links.php | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php b/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php index 7fd1f2e3079be..1bde9c3fbf3bb 100644 --- a/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php +++ b/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php @@ -12,8 +12,13 @@ use Magento\DownloadableGraphQl\Model\ConvertLinksToArray; 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\Model\Order\Item as OrderItem; use Magento\Store\Api\Data\StoreInterface; @@ -32,16 +37,34 @@ class Links implements ResolverInterface */ private $linkCollectionFactory; + /** + * Serializer + * + * @var Json + */ + private $serializer; + + /** + * @var ValueFactory + */ + private $valueFactory; + /** * @param ConvertLinksToArray $convertLinksToArray * @param CollectionFactory $linkCollectionFactory + * @param ValueFactory $valueFactory + * @param Json $serializer */ public function __construct( ConvertLinksToArray $convertLinksToArray, - CollectionFactory $linkCollectionFactory + CollectionFactory $linkCollectionFactory, + ValueFactory $valueFactory, + Json $serializer ) { $this->convertLinksToArray = $convertLinksToArray; $this->linkCollectionFactory = $linkCollectionFactory; + $this->valueFactory = $valueFactory; + $this->serializer = $serializer; } /** @@ -54,23 +77,54 @@ public function resolve( array $value = null, array $args = null ) { - if (!isset($value['model'])) { - throw new LocalizedException(__('"model" value should be specified')); - } /** @var StoreInterface $store */ $store = $context->getExtensionAttributes()->getStore(); - /** @var OrderItem $orderItem */ - $orderItem = $value['model']; + return $this->valueFactory->create(function () use ($value, $store) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if ($value['model'] instanceof OrderItemInterface) { + /** @var OrderItemInterface $item */ + $item = $value['model']; + return $this->formatLinksData($item, $value, $store); + } + if ($value['model'] instanceof InvoiceItemInterface || $value['model'] instanceof ShipmentItemInterface) { + /** @var InvoiceItemInterface|ShipmentItemInterface $item */ + $item = $value['model']; + // Have to pass down order and item to map to avoid re-fetching all data + return $this->formatLinksData($item->getOrderItem(), $value, $store); + } + return null; + }); + } - $orderLinks = $orderItem->getProductOptionByCode('links'); + /** + * Format values from order links item + * + * @param OrderItemInterface $item + * @param array $formattedItem + * @param StoreInterface $store + * @return array + */ + private function formatLinksData( + OrderItemInterface $item, + array $formattedItem, + StoreInterface $store + ): array { + $linksData = []; + if ($item->getProductType() === 'downloadable') { + $orderLinks = $item->getProductOptionByCode('links') ?? []; - /** @var Collection */ - $linksCollection = $this->linkCollectionFactory->create(); - $linksCollection->addTitleToResult($store->getStoreId()) - ->addPriceToResult($store->getWebsiteId()) - ->addFieldToFilter('main_table.link_id', ['in' => $orderLinks]); + /** @var Collection */ + $linksCollection = $this->linkCollectionFactory->create(); + $linksCollection->addTitleToResult($store->getId()) + ->addPriceToResult($store->getWebsiteId()) + ->addFieldToFilter('main_table.link_id', ['in' => $orderLinks]); - return $this->convertLinksToArray->execute($linksCollection->getItems()); + $linksData = $this->convertLinksToArray->execute($linksCollection->getItems()); + } + return $linksData; } } From 27d10e350a30adf0be515e1422bb5b154285781e Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 7 Aug 2020 17:01:28 -0500 Subject: [PATCH 0181/1013] MC-32659: Order Details by Order Number with additional different product types --- .../Resolver/Order/Item/Links.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php b/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php index 1bde9c3fbf3bb..a578e98e02727 100644 --- a/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php +++ b/app/code/Magento/DownloadableGraphQl/Resolver/Order/Item/Links.php @@ -16,10 +16,10 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Sales\Api\Data\CreditmemoItemInterface; use Magento\Sales\Api\Data\InvoiceItemInterface; use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Api\Data\ShipmentItemInterface; -use Magento\Sales\Model\Order\Item as OrderItem; use Magento\Store\Api\Data\StoreInterface; /** @@ -86,15 +86,12 @@ public function resolve( } if ($value['model'] instanceof OrderItemInterface) { - /** @var OrderItemInterface $item */ + return $this->formatLinksData($value['model'], $store); + } elseif ($value['model'] instanceof InvoiceItemInterface + || $value['model'] instanceof CreditmemoItemInterface + || $value['model'] instanceof ShipmentItemInterface) { $item = $value['model']; - return $this->formatLinksData($item, $value, $store); - } - if ($value['model'] instanceof InvoiceItemInterface || $value['model'] instanceof ShipmentItemInterface) { - /** @var InvoiceItemInterface|ShipmentItemInterface $item */ - $item = $value['model']; - // Have to pass down order and item to map to avoid re-fetching all data - return $this->formatLinksData($item->getOrderItem(), $value, $store); + return $this->formatLinksData($item->getOrderItem(), $store); } return null; }); @@ -104,13 +101,11 @@ public function resolve( * Format values from order links item * * @param OrderItemInterface $item - * @param array $formattedItem * @param StoreInterface $store * @return array */ private function formatLinksData( OrderItemInterface $item, - array $formattedItem, StoreInterface $store ): array { $linksData = []; From d2c368b4487334f221f9aaa5610edc5b0313bafa Mon Sep 17 00:00:00 2001 From: Konstantin Dubovik <konstantin.dubovik@guidance.com> Date: Sat, 1 Aug 2020 09:26:07 +0300 Subject: [PATCH 0182/1013] Guest print order form fixes - code sniffer fixes --- app/code/Magento/Sales/Helper/Guest.php | 4 +++- app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Sales/Helper/Guest.php b/app/code/Magento/Sales/Helper/Guest.php index 119a8aa810443..3b7e491086b17 100644 --- a/app/code/Magento/Sales/Helper/Guest.php +++ b/app/code/Magento/Sales/Helper/Guest.php @@ -15,6 +15,7 @@ /** * Sales module base helper * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class Guest extends \Magento\Framework\App\Helper\AbstractHelper { @@ -71,7 +72,7 @@ class Guest extends \Magento\Framework\App\Helper\AbstractHelper const COOKIE_NAME = 'guest-view'; /** - * Cookie path + * Cookie path value */ const COOKIE_PATH = '/'; @@ -225,6 +226,7 @@ private function setGuestViewCookie($cookieValue) */ private function loadFromCookie($fromCookie) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $cookieData = explode(':', base64_decode($fromCookie)); $protectCode = isset($cookieData[0]) ? $cookieData[0] : null; $incrementId = isset($cookieData[1]) ? $cookieData[1] : null; diff --git a/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php b/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php index 236da3de2c63d..07f740f7c1fd8 100644 --- a/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php +++ b/app/code/Magento/Sales/Test/Unit/Helper/GuestTest.php @@ -182,7 +182,8 @@ public function testLoadValidOrderNotEmptyPost($post) Address::class, ['getLastname', 'getEmail', 'getPostcode'] ); - $billingAddressMock->expects($this->once())->method('getLastname')->willReturn(trim($post['oar_billing_lastname'])); + $billingAddressMock->expects($this->once())->method('getLastname') + ->willReturn(trim($post['oar_billing_lastname'])); $billingAddressMock->expects($this->any())->method('getEmail')->willReturn(trim($post['oar_email'])); $billingAddressMock->expects($this->any())->method('getPostcode')->willReturn(trim($post['oar_zip'])); $this->salesOrderMock->expects($this->once())->method('getBillingAddress')->willReturn($billingAddressMock); From 9cb9d1a94a014175f427aa71cba0650be69ede3f Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Sat, 8 Aug 2020 20:44:18 -0500 Subject: [PATCH 0183/1013] MC-32659: Order Details by Order Number with additional different product types --- .../GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index f89559f2c4464..ad1e14dd2a938 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -136,7 +136,7 @@ private function getCustomerOrderQuery($orderNumber): array amount_including_tax{value} amount_excluding_tax{value} total_amount{value} - discounts{amount{value} label} + discounts{amount{value}} taxes {amount{value} title rate} } discounts {amount{value currency} label} From 1763d9c92433a923b59721f397528825ce964b76 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Mon, 10 Aug 2020 12:06:22 +0300 Subject: [PATCH 0184/1013] MC-35254: Customer group is automatically changed when editing customer on customer grid is assigned to the company --- .../Observer/AfterAddressSaveObserver.php | 1 + .../Adminhtml/Index/InlineEditTest.php | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php index 41311abee5da8..bbea0ce9dc052 100644 --- a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php +++ b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php @@ -127,6 +127,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) if (!$this->_customerAddress->isVatValidationEnabled($customer->getStore()) || $this->_coreRegistry->registry(self::VIV_PROCESSED_FLAG) || !$this->_canProcessAddress($customerAddress) + || $customerAddress->getShouldIgnoreValidation() ) { return; } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/InlineEditTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/InlineEditTest.php index 3edfe1eab8aa5..1b8965711d77d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/InlineEditTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/InlineEditTest.php @@ -7,12 +7,15 @@ namespace Magento\Customer\Controller\Adminhtml\Index; +use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Observer\AfterAddressSaveObserver; use Magento\Eav\Model\AttributeRepository; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; use Magento\Framework\Serialize\SerializerInterface; use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -23,6 +26,7 @@ * * @magentoAppArea adminhtml * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class InlineEditTest extends AbstractBackendController { @@ -41,6 +45,12 @@ class InlineEditTest extends AbstractBackendController /** @var AttributeRepository */ private $attributeRepository; + /** @var AddressRepositoryInterface */ + private $addressRepository; + + /** @var Registry */ + private $coreRegistry; + /** * @inheritdoc */ @@ -53,6 +63,8 @@ protected function setUp(): void $this->json = $this->objectManager->get(SerializerInterface::class); $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); $this->attributeRepository = $this->objectManager->get(AttributeRepository::class); + $this->addressRepository = $this->objectManager->get(AddressRepositoryInterface::class); + $this->coreRegistry = Bootstrap::getObjectManager()->get(Registry::class); } /** @@ -121,6 +133,53 @@ public function inlineEditParametersDataProvider(): array ]; } + /** + * Customer group should not change after saving customer via customer grid because of disabled address validation + * + * @magentoConfigFixture current_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture current_store customer/create_account/viv_invalid_group 2 + * @magentoDataFixture Magento/Customer/_files/customer_one_address.php + * + * @return void + */ + public function testInlineEditActionWithAddress(): void + { + $customer = $this->getCustomer(); + $params = [ + 'items' => [ + $customer->getId() => [] + ], + 'isAjax' => true, + ]; + $actual = $this->performInlineEditRequest($params); + $updatedCustomer = $this->customerRepository->get('customer_one_address@test.com'); + $this->assertEmpty($actual['messages']); + $this->assertFalse($actual['error']); + $this->assertEquals( + $customer->getGroupId(), + $updatedCustomer->getGroupId(), + 'Customer group was changed!' + ); + } + + /** + * Change customer address with setting country from EU and setting VAT number + * + * @return CustomerInterface + */ + private function getCustomer(): CustomerInterface + { + $customer = $this->customerRepository->get('customer_one_address@test.com'); + $address = $this->addressRepository->getById((int)$customer->getDefaultShipping()); + $address->setVatId(12345); + $address->setCountryId('DE'); + $address->setRegionId(0); + $this->addressRepository->save($address); + $this->coreRegistry->unregister(AfterAddressSaveObserver::VIV_PROCESSED_FLAG); + //return customer after address repository save + return $this->customerRepository->get('customer_one_address@test.com'); + } + /** * Perform inline edit request. * From e45c10ab8e62a3c69d6e233a6ded058a9c114b96 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Mon, 10 Aug 2020 12:25:58 +0300 Subject: [PATCH 0185/1013] MC-33150: Inconsistent address field labels on checkout and address book --- .../Customer/view/frontend/templates/address/edit.phtml | 7 +++++++ 1 file changed, 7 insertions(+) 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 7f09361e4d505..5f7016b6b0ac3 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -58,6 +58,13 @@ $viewModel = $block->getViewModel(); <div class="field street required"> <label for="street_1" class="label"><span><?= /* @noEscape */ $_street ?></span></label> <div class="control"> + <div class="field primary"> + <label for="street_1" class="label"> + <span> + <?= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('street') . ': Line 1' ?> + </span> + </label> + </div> <input type="text" name="street[]" value="<?= $escaper->escapeHtmlAttr($block->getStreetLine(1)) ?>" From 445bccb7d83a435ac97f54fecd85aa625372c071 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Mon, 10 Aug 2020 12:45:27 +0300 Subject: [PATCH 0186/1013] MC-35888: Confirmation email when creating shipment using REST is missing customer and shipment data --- .../Order/Shipment/Sender/EmailSender.php | 80 ++++++++++------- .../Order/Shipment/Sender/EmailSenderTest.php | 87 +++++++++++++++++-- 2 files changed, 130 insertions(+), 37 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php index fe68555d9f7c7..534bb127db067 100644 --- a/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php +++ b/app/code/Magento/Sales/Model/Order/Shipment/Sender/EmailSender.php @@ -5,9 +5,21 @@ */ namespace Magento\Sales\Model\Order\Shipment\Sender; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Payment\Helper\Data; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\ShipmentCommentCreationInterface; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Model\Order\Address\Renderer; +use Magento\Sales\Model\Order\Email\Container\ShipmentIdentity; +use Magento\Sales\Model\Order\Email\Container\Template; use Magento\Sales\Model\Order\Email\Sender; +use Magento\Sales\Model\Order\Email\SenderBuilderFactory; use Magento\Sales\Model\Order\Shipment\SenderInterface; use Magento\Framework\DataObject; +use Magento\Sales\Model\ResourceModel\Order\Shipment; +use Psr\Log\LoggerInterface; /** * Email notification sender for Shipment. @@ -17,46 +29,46 @@ class EmailSender extends Sender implements SenderInterface { /** - * @var \Magento\Payment\Helper\Data + * @var Data */ private $paymentHelper; /** - * @var \Magento\Sales\Model\ResourceModel\Order\Shipment + * @var Shipment */ private $shipmentResource; /** - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ private $globalConfig; /** - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ private $eventManager; /** - * @param \Magento\Sales\Model\Order\Email\Container\Template $templateContainer - * @param \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity $identityContainer - * @param \Magento\Sales\Model\Order\Email\SenderBuilderFactory $senderBuilderFactory - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer - * @param \Magento\Payment\Helper\Data $paymentHelper - * @param \Magento\Sales\Model\ResourceModel\Order\Shipment $shipmentResource - * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig - * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param Template $templateContainer + * @param ShipmentIdentity $identityContainer + * @param SenderBuilderFactory $senderBuilderFactory + * @param LoggerInterface $logger + * @param Renderer $addressRenderer + * @param Data $paymentHelper + * @param Shipment $shipmentResource + * @param ScopeConfigInterface $globalConfig + * @param ManagerInterface $eventManager */ public function __construct( - \Magento\Sales\Model\Order\Email\Container\Template $templateContainer, - \Magento\Sales\Model\Order\Email\Container\ShipmentIdentity $identityContainer, - \Magento\Sales\Model\Order\Email\SenderBuilderFactory $senderBuilderFactory, - \Psr\Log\LoggerInterface $logger, - \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, - \Magento\Payment\Helper\Data $paymentHelper, - \Magento\Sales\Model\ResourceModel\Order\Shipment $shipmentResource, - \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig, - \Magento\Framework\Event\ManagerInterface $eventManager + Template $templateContainer, + ShipmentIdentity $identityContainer, + SenderBuilderFactory $senderBuilderFactory, + LoggerInterface $logger, + Renderer $addressRenderer, + Data $paymentHelper, + Shipment $shipmentResource, + ScopeConfigInterface $globalConfig, + ManagerInterface $eventManager ) { parent::__construct( $templateContainer, @@ -83,18 +95,18 @@ public function __construct( * Otherwise, email will be sent later during running of * corresponding cron job. * - * @param \Magento\Sales\Api\Data\OrderInterface $order - * @param \Magento\Sales\Api\Data\ShipmentInterface $shipment - * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment + * @param OrderInterface $order + * @param ShipmentInterface $shipment + * @param ShipmentCommentCreationInterface|null $comment * @param bool $forceSyncMode * * @return bool * @throws \Exception */ public function send( - \Magento\Sales\Api\Data\OrderInterface $order, - \Magento\Sales\Api\Data\ShipmentInterface $shipment, - \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, + OrderInterface $order, + ShipmentInterface $shipment, + ShipmentCommentCreationInterface $comment = null, $forceSyncMode = false ) { $shipment->setSendEmail($this->identityContainer->isEnabled()); @@ -112,7 +124,13 @@ public function send( 'payment_html' => $this->getPaymentHtml($order), 'store' => $order->getStore(), 'formattedShippingAddress' => $this->getFormattedShippingAddress($order), - 'formattedBillingAddress' => $this->getFormattedBillingAddress($order) + 'formattedBillingAddress' => $this->getFormattedBillingAddress($order), + 'order_data' => [ + 'customer_name' => $order->getCustomerName(), + 'is_not_virtual' => $order->getIsNotVirtual(), + 'email_customer_note' => $order->getEmailCustomerNote(), + 'frontend_status_label' => $order->getFrontendStatusLabel() + ] ]; $transportObject = new DataObject($transport); @@ -147,12 +165,12 @@ public function send( /** * Returns payment info block as HTML. * - * @param \Magento\Sales\Api\Data\OrderInterface $order + * @param OrderInterface $order * * @return string * @throws \Exception */ - private function getPaymentHtml(\Magento\Sales\Api\Data\OrderInterface $order) + private function getPaymentHtml(OrderInterface $order) { return $this->paymentHelper->getInfoBlockHtml( $order->getPayment(), diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php index 81ed71ae7bb67..b170a72d691ea 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Shipment/Sender/EmailSenderTest.php @@ -259,20 +259,37 @@ protected function setUp(): void * @param bool $forceSyncMode * @param bool $isComment * @param bool $emailSendingResult + * @param array $orderData * * @dataProvider sendDataProvider * * @return void * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @throws \Exception */ - public function testSend($configValue, $forceSyncMode, $isComment, $emailSendingResult) + public function testSend($configValue, $forceSyncMode, $isComment, $emailSendingResult, $orderData) { $this->globalConfigMock->expects($this->once()) ->method('getValue') ->with('sales_email/general/async_sending') ->willReturn($configValue); + $this->orderMock->expects($this->any()) + ->method('getId') + ->willReturn($orderData['order_id']); + $this->orderMock->expects($this->any()) + ->method('getCustomerName') + ->willReturn($orderData['customer_name']); + $this->orderMock->expects($this->any()) + ->method('getIsNotVirtual') + ->willReturn($orderData['is_not_virtual']); + $this->orderMock->expects($this->any()) + ->method('getEmailCustomerNote') + ->willReturn($orderData['email_customer_note']); + $this->orderMock->expects($this->any()) + ->method('getFrontendStatusLabel') + ->willReturn($orderData['frontend_status_label']); if (!$isComment) { $this->commentMock = null; } @@ -296,6 +313,12 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending 'store' => $this->storeMock, 'formattedShippingAddress' => 'Formatted address', 'formattedBillingAddress' => 'Formatted address', + 'order_data' => [ + 'customer_name' => $orderData['customer_name'], + 'is_not_virtual' => $orderData['is_not_virtual'], + 'email_customer_note' => $orderData['email_customer_note'], + 'frontend_status_label' => $orderData['frontend_status_label'] + ] ]; $transport = new DataObject($transport); @@ -388,15 +411,67 @@ public function testSend($configValue, $forceSyncMode, $isComment, $emailSending /** * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function sendDataProvider() { return [ - 'Successful sync sending with comment' => [0, false, true, true], - 'Successful sync sending without comment' => [0, false, false, true], - 'Failed sync sending with comment' => [0, false, true, false], - 'Successful forced sync sending with comment' => [1, true, true, true], - 'Async sending' => [1, false, false, false], + 'Successful sync sending with comment' => [ + 0, false, true, true, + [ + 'order_id' => 1, + 'shipment_id' => 1, + 'customer_name' => 'test customer', + 'is_not_virtual' => true, + 'email_customer_note' => 1, + 'frontend_status_label' => 'email_sent' + ] + ], + 'Successful sync sending without comment' => [ + 0, false, false, true, + [ + 'order_id' => 2, + 'shipment_id' => 2, + 'customer_name' => 'test customer 1', + 'is_not_virtual' => true, + 'email_customer_note' => 1, + 'frontend_status_label' => 'email_sent' + ] + ], + 'Failed sync sending with comment' => [ + 0, false, true, false, + [ + 'order_id' => 3, + 'shipment_id' => 3, + 'customer_name' => 'test customer 2', + 'is_not_virtual' => true, + 'email_customer_note' => 1, + 'frontend_status_label' => 'send_email' + ] + ], + 'Successful forced sync sending with comment' => [ + 1, true, true, true, + [ + 'order_id' => 4, + 'shipment_id' => 4, + 'customer_name' => 'test customer 3', + 'is_not_virtual' => true, + 'email_customer_note' => 1, + 'frontend_status_label' => 'email_sent' + ] + ], + 'Async sending' => [ + 1, false, false, false, + [ + 'order_id' => 5, + 'shipment_id' => 5, + 'customer_name' => 'test customer 4', + 'is_not_virtual' => true, + 'email_customer_note' => 1, + 'frontend_status_label' => 'send_email' + ] + ], ]; } } From dd555b4ef32007ac92024fcbb25abcf7a8bf245e Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Mon, 10 Aug 2020 13:05:09 +0300 Subject: [PATCH 0187/1013] MC-35838: Image and image role discrepancies when creating and updating item via A REST api rquests --- .../Model/Product/Gallery/CreateHandler.php | 134 ++++++--- .../Model/ResourceModel/Product/Gallery.php | 14 +- .../Api/ProductRepositoryInterfaceTest.php | 263 +++++++++++++++--- .../Product/Gallery/UpdateHandlerTest.php | 15 + 4 files changed, 351 insertions(+), 75 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php index 225a3a4c44a9b..8bcd0e2521498 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/CreateHandler.php @@ -8,10 +8,12 @@ namespace Magento\Catalog\Model\Product\Gallery; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\Operation\ExtensionInterface; use Magento\MediaStorage\Model\File\Uploader as FileUploader; +use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; /** @@ -89,6 +91,15 @@ class CreateHandler implements ExtensionInterface */ private $storeManager; + /** + * @var string[] + */ + private $mediaAttributesWithLabels = [ + 'image', + 'small_image', + 'thumbnail' + ]; + /** * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository @@ -190,27 +201,8 @@ public function execute($product, $arguments = []) $value['duplicate'] = $duplicate; } - /* @var $mediaAttribute \Magento\Catalog\Api\Data\ProductAttributeInterface */ - foreach ($this->getMediaAttributeCodes() as $mediaAttrCode) { - $attrData = $product->getData($mediaAttrCode); - if (empty($attrData) && empty($clearImages) && empty($newImages) && empty($existImages)) { - continue; - } - $this->processMediaAttribute( - $product, - $mediaAttrCode, - $clearImages, - $newImages - ); - if (in_array($mediaAttrCode, ['image', 'small_image', 'thumbnail'])) { - $this->processMediaAttributeLabel( - $product, - $mediaAttrCode, - $clearImages, - $newImages, - $existImages - ); - } + if (!empty($value['images'])) { + $this->processMediaAttributes($product, $existImages, $newImages, $clearImages); } $product->setData($attrCode, $value); @@ -492,30 +484,39 @@ private function getMediaAttributeCodes() /** * Process media attribute * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param string $mediaAttrCode * @param array $clearImages * @param array $newImages */ private function processMediaAttribute( - \Magento\Catalog\Model\Product $product, - $mediaAttrCode, + Product $product, + string $mediaAttrCode, array $clearImages, array $newImages - ) { - $attrData = $product->getData($mediaAttrCode); - if (in_array($attrData, $clearImages)) { - $product->setData($mediaAttrCode, 'no_selection'); - } - - if (in_array($attrData, array_keys($newImages))) { - $product->setData($mediaAttrCode, $newImages[$attrData]['new_file']); - } - if (!empty($product->getData($mediaAttrCode))) { + ): void { + $storeId = $product->isObjectNew() ? Store::DEFAULT_STORE_ID : (int) $product->getStoreId(); + /*** + * Attributes values are saved as default value in single store mode + * @see \Magento\Catalog\Model\ResourceModel\AbstractResource::_saveAttributeValue + */ + if ($storeId === Store::DEFAULT_STORE_ID + || $this->storeManager->hasSingleStore() + || $this->getMediaAttributeStoreValue($product, $mediaAttrCode, $storeId) !== null + ) { + $value = $product->getData($mediaAttrCode); + $newValue = $value; + if (in_array($value, $clearImages)) { + $newValue = 'no_selection'; + } + if (in_array($value, array_keys($newImages))) { + $newValue = $newImages[$value]['new_file']; + } + $product->setData($mediaAttrCode, $newValue); $product->addAttributeUpdate( $mediaAttrCode, - $product->getData($mediaAttrCode), - $product->getStoreId() + $newValue, + $storeId ); } } @@ -523,19 +524,19 @@ private function processMediaAttribute( /** * Process media attribute label * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @param string $mediaAttrCode * @param array $clearImages * @param array $newImages * @param array $existImages */ private function processMediaAttributeLabel( - \Magento\Catalog\Model\Product $product, - $mediaAttrCode, + Product $product, + string $mediaAttrCode, array $clearImages, array $newImages, array $existImages - ) { + ): void { $resetLabel = false; $attrData = $product->getData($mediaAttrCode); if (in_array($attrData, $clearImages)) { @@ -607,4 +608,57 @@ private function canRemoveImage(ProductInterface $product, string $imageFile) :b return $canRemoveImage; } + + /** + * Get media attribute value for store view + * + * @param Product $product + * @param string $attributeCode + * @param int|null $storeId + * @return string|null + */ + private function getMediaAttributeStoreValue(Product $product, string $attributeCode, int $storeId = null): ?string + { + $gallery = $this->getImagesForAllStores($product); + $storeId = $storeId === null ? (int) $product->getStoreId() : $storeId; + foreach ($gallery as $image) { + if ($image['attribute_code'] === $attributeCode && ((int)$image['store_id']) === $storeId) { + return $image['filepath']; + } + } + return null; + } + + /** + * Update media attributes + * + * @param Product $product + * @param array $existImages + * @param array $newImages + * @param array $clearImages + */ + private function processMediaAttributes( + Product $product, + array $existImages, + array $newImages, + array $clearImages + ): void { + foreach ($this->getMediaAttributeCodes() as $mediaAttrCode) { + $this->processMediaAttribute( + $product, + $mediaAttrCode, + $clearImages, + $newImages + ); + if (in_array($mediaAttrCode, $this->mediaAttributesWithLabels)) { + $this->processMediaAttributeLabel( + $product, + $mediaAttrCode, + $clearImages, + $newImages, + $existImages + ); + } + } + } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php index a9741cd8e1ec7..a654608c1c2d9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Gallery.php @@ -6,6 +6,8 @@ namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\Store; /** @@ -31,22 +33,30 @@ class Gallery extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb * @since 101.0.0 */ protected $metadata; + /** + * @var Config|null + */ + private $mediaConfig; /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool * @param string $connectionName + * @param Config|null $mediaConfig + * @throws \Exception */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Framework\EntityManager\MetadataPool $metadataPool, - $connectionName = null + $connectionName = null, + ?Config $mediaConfig = null ) { $this->metadata = $metadataPool->getMetadata( \Magento\Catalog\Api\Data\ProductInterface::class ); parent::__construct($context, $connectionName); + $this->mediaConfig = $mediaConfig ?? ObjectManager::getInstance()->get(Config::class); } /** @@ -490,7 +500,7 @@ public function getProductImages($product, $storeIds) $storeIds )->where( 'attribute_code IN (?)', - ['small_image', 'thumbnail', 'image'] + $this->mediaConfig->getMediaAttributeCodes() ); return $this->getConnection()->fetchAll($select); diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 1fcfe79f39478..5e2b2f89ba97b 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -12,6 +12,7 @@ use Magento\Authorization\Model\Rules; use Magento\Authorization\Model\RulesFactory; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Downloadable\Api\DomainManagerInterface; use Magento\Downloadable\Model\Link; @@ -23,7 +24,9 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Integration\Api\AdminTokenServiceInterface; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreRepository; use Magento\Store\Model\Website; use Magento\Store\Model\WebsiteRepository; use Magento\TestFramework\Helper\Bootstrap; @@ -34,6 +37,7 @@ * * @magentoAppIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class ProductRepositoryInterfaceTest extends WebapiAbstract { @@ -77,6 +81,10 @@ class ProductRepositoryInterfaceTest extends WebapiAbstract * @var AdminTokenServiceInterface */ private $adminTokens; + /** + * @var array + */ + private $fixtureProducts = []; /** * @inheritDoc @@ -98,6 +106,7 @@ protected function setUp(): void */ protected function tearDown(): void { + $this->deleteFixtureProducts(); parent::tearDown(); $objectManager = Bootstrap::getObjectManager(); @@ -685,14 +694,15 @@ public function testProductOptions() */ public function testProductWithMediaGallery() { - $testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; - // @codingStandardsIgnoreLine - $encodedImage = base64_encode(file_get_contents($testImagePath)); + $encodedImage = $this->getTestImage(); //create a product with media gallery $filename1 = 'tiny1' . time() . '.jpg'; $filename2 = 'tiny2' . time() . '.jpeg'; $productData = $this->getSimpleProductData(); - $productData['media_gallery_entries'] = $this->getMediaGalleryData($filename1, $encodedImage, $filename2); + $productData['media_gallery_entries'] = [ + $this->getMediaGalleryData($filename1, $encodedImage, 1, 'tiny1', true), + $this->getMediaGalleryData($filename2, $encodedImage, 2, 'tiny2', false), + ]; $response = $this->saveProduct($productData); $this->assertArrayHasKey('media_gallery_entries', $response); $mediaGalleryEntries = $response['media_gallery_entries']; @@ -1595,38 +1605,33 @@ public function testUpdateProductCategoryLinksUnassign() /** * Get media gallery data * - * @param $filename1 - * @param $encodedImage - * @param $filename2 + * @param string $filename + * @param string $encodedImage + * @param int $position + * @param string $label + * @param bool $disabled + * @param array $types * @return array */ - private function getMediaGalleryData($filename1, $encodedImage, $filename2) - { + private function getMediaGalleryData( + string $filename, + string $encodedImage, + int $position, + string $label, + bool $disabled = false, + array $types = [] + ): array { return [ - [ - 'position' => 1, - 'media_type' => 'image', - 'disabled' => true, - 'label' => 'tiny1', - 'types' => [], - 'content' => [ - 'type' => 'image/jpeg', - 'name' => $filename1, - 'base64_encoded_data' => $encodedImage, - ] - ], - [ - 'position' => 2, - 'media_type' => 'image', - 'disabled' => false, - 'label' => 'tiny2', - 'types' => [], - 'content' => [ - 'type' => 'image/jpeg', - 'name' => $filename2, - 'base64_encoded_data' => $encodedImage, - ] - ], + 'position' => $position, + 'media_type' => 'image', + 'disabled' => $disabled, + 'label' => $label, + 'types' => $types, + 'content' => [ + 'type' => 'image/jpeg', + 'name' => $filename, + 'base64_encoded_data' => $encodedImage, + ] ]; } @@ -1901,4 +1906,196 @@ public function testSaveDesign(): void //We don't have permissions to do that. $this->assertEquals('Not allowed to edit the product\'s design attributes', $exceptionMessage); } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store.php + */ + public function testImageRolesWithMultipleStores() + { + $this->_markTestAsRestOnly( + 'Test skipped due to known issue with SOAP. NULL value is cast to corresponding attribute type.' + ); + $productData = $this->getSimpleProductData(); + $sku = $productData[ProductInterface::SKU]; + $defaultScope = Store::DEFAULT_STORE_ID; + $defaultWebsiteId = $this->loadWebsiteByCode('base')->getId(); + $defaultStoreId = $this->loadStoreByCode('default')->getId(); + $secondStoreId = $this->loadStoreByCode('fixture_second_store')->getId(); + $encodedImage = $this->getTestImage(); + $imageRoles = ['image', 'small_image', 'thumbnail']; + $img1 = uniqid('/t/e/test_image1_') . '.jpg'; + $img2 = uniqid('/t/e/test_image2_') . '.jpg'; + $productData['media_gallery_entries'] = [ + $this->getMediaGalleryData(basename($img1), $encodedImage, 1, 'front', false, ['image']), + $this->getMediaGalleryData(basename($img2), $encodedImage, 2, 'back', false, ['small_image', 'thumbnail']), + ]; + $productData[ProductInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids'] = [ + $defaultWebsiteId + ]; + $response = $this->saveProduct($productData, 'all'); + if (isset($response['id'])) { + $this->fixtureProducts[] = $sku; + } + $imageRolesPerStore = $this->getProductStoreImageRoles( + $sku, + [$defaultScope, $defaultStoreId, $secondStoreId], + $imageRoles + ); + $this->assertEquals($img1, $imageRolesPerStore[$defaultScope]['image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['small_image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); + /** + * Override image roles for default store + */ + $storeProductData = $response; + $storeProductData['media_gallery_entries'][0]['types'] = ['image', 'small_image', 'thumbnail']; + $storeProductData['media_gallery_entries'][1]['types'] = []; + $this->saveProduct($storeProductData, 'default'); + $imageRolesPerStore = $this->getProductStoreImageRoles( + $sku, + [$defaultScope, $defaultStoreId, $secondStoreId], + $imageRoles + ); + $this->assertEquals($img1, $imageRolesPerStore[$defaultScope]['image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['small_image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['thumbnail']); + $this->assertEquals($img1, $imageRolesPerStore[$defaultStoreId]['image']); + $this->assertEquals($img1, $imageRolesPerStore[$defaultStoreId]['small_image']); + $this->assertEquals($img1, $imageRolesPerStore[$defaultStoreId]['thumbnail']); + $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); + /** + * Inherit image roles from default scope + */ + $customAttributes = $this->convertCustomAttributesToAssociativeArray($response['custom_attributes']); + $customAttributes['image'] = null; + $customAttributes['small_image'] = null; + $customAttributes['thumbnail'] = null; + $customAttributes = $this->convertAssociativeArrayToCustomAttributes($customAttributes); + $storeProductData = $response; + $storeProductData['media_gallery_entries'] = null; + $storeProductData['custom_attributes'] = $customAttributes; + $this->saveProduct($storeProductData, 'default'); + $imageRolesPerStore = $this->getProductStoreImageRoles( + $sku, + [$defaultScope, $defaultStoreId, $secondStoreId], + $imageRoles + ); + $this->assertEquals($img1, $imageRolesPerStore[$defaultScope]['image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['small_image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['thumbnail']); + $this->assertArrayNotHasKey($defaultStoreId, $imageRolesPerStore); + $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); + } + + /** + * Test that updating product image with same image name will result in incremented image name + */ + public function testUpdateProductWithMediaGallery(): void + { + $productData = $this->getSimpleProductData(); + $sku = $productData[ProductInterface::SKU]; + $defaultScope = Store::DEFAULT_STORE_ID; + $defaultWebsiteId = $this->loadWebsiteByCode('base')->getId(); + $encodedImage = $this->getTestImage(); + $imageRoles = ['image', 'small_image', 'thumbnail']; + $img1 = uniqid('/t/e/test_image1_') . '.jpg'; + $img2 = uniqid('/t/e/test_image2_') . '.jpg'; + $productData['media_gallery_entries'] = [ + $this->getMediaGalleryData(basename($img1), $encodedImage, 1, 'front', false, ['image']), + $this->getMediaGalleryData(basename($img2), $encodedImage, 2, 'back', false, ['small_image', 'thumbnail']), + ]; + $productData[ProductInterface::EXTENSION_ATTRIBUTES_KEY]['website_ids'] = [ + $defaultWebsiteId + ]; + $response = $this->saveProduct($productData, 'all'); + if (isset($response['id'])) { + $this->fixtureProducts[] = $sku; + } + $imageRolesPerStore = $this->getProductStoreImageRoles($sku, [$defaultScope], $imageRoles); + $this->assertEquals($img1, $imageRolesPerStore[$defaultScope]['image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['small_image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['thumbnail']); + $this->saveProduct($productData, 'all'); + $imageRolesPerStore = $this->getProductStoreImageRoles($sku, [$defaultScope], $imageRoles); + $img1 = substr_replace($img1, '_1', -4, 0); + $img2 = substr_replace($img2, '_1', -4, 0); + $this->assertEquals($img1, $imageRolesPerStore[$defaultScope]['image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['small_image']); + $this->assertEquals($img2, $imageRolesPerStore[$defaultScope]['thumbnail']); + } + + /** + * @return string + */ + private function getTestImage(): string + { + $testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; + // @codingStandardsIgnoreLine + return base64_encode(file_get_contents($testImagePath)); + } + + /** + * @return void + */ + private function deleteFixtureProducts(): void + { + foreach ($this->fixtureProducts as $sku) { + $this->deleteProduct($sku); + } + $this->fixtureProducts = []; + } + + /** + * @param string $code + * @return StoreInterface + */ + private function loadStoreByCode(string $code): StoreInterface + { + try { + $store = Bootstrap::getObjectManager()->get(StoreRepository::class)->get($code); + } catch (NoSuchEntityException $e) { + $store = null; + $this->fail("Couldn`t load store: {$code}"); + } + return $store; + } + + /** + * @param string $sku + * @param int|null $storeId + * @return ProductInterface + */ + private function getProductModel(string $sku, int $storeId = null): ProductInterface + { + try { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $product = $productRepository->get($sku, false, $storeId, true); + } catch (NoSuchEntityException $e) { + $product = null; + $this->fail("Couldn`t load product: {$sku}"); + } + return $product; + } + + /** + * @param string $sku + * @param array $stores + * @param array $roles + * @return array + */ + private function getProductStoreImageRoles(string $sku, array $stores, array $roles = []): array + { + /** @var Gallery $galleryResource */ + $galleryResource = Bootstrap::getObjectManager()->get(Gallery::class); + $productModel = $this->getProductModel($sku); + $imageRolesPerStore = []; + foreach ($galleryResource->getProductImages($productModel, $stores) as $role) { + if (empty($roles) || in_array($role['attribute_code'], $roles)) { + $imageRolesPerStore[$role['store_id']][$role['attribute_code']] = $role['filepath']; + } + } + return $imageRolesPerStore; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index f9d235493297f..31cfb173b5b3d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -78,6 +78,10 @@ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase * @var int */ private $mediaAttributeId; + /** + * @var \Magento\Eav\Model\ResourceModel\UpdateHandler + */ + private $eavUpdateHandler; /** * @inheritdoc @@ -96,6 +100,8 @@ protected function setUp(): void $this->mediaDirectory = $this->objectManager->get(Filesystem::class) ->getDirectoryWrite(DirectoryList::MEDIA); $this->mediaDirectory->writeFile($this->fileName, 'Test'); + $this->updateHandler = $this->objectManager->create(UpdateHandler::class); + $this->eavUpdateHandler = $this->objectManager->create(\Magento\Eav\Model\ResourceModel\UpdateHandler::class); } /** @@ -185,6 +191,15 @@ public function testExecuteWithTwoImagesAndDifferentRolesOnStoreView(array $role $secondStoreId = (int)$this->storeRepository->get('fixture_second_store')->getId(); $imageRoles = ['image', 'small_image', 'thumbnail', 'swatch_image']; $product = $this->getProduct($secondStoreId); + $entityIdField = $product->getResource()->getLinkField(); + $entityData = []; + $entityData['store_id'] = $product->getStoreId(); + $entityData[$entityIdField] = $product->getData($entityIdField); + $entityData = array_merge($entityData, $roles); + $this->eavUpdateHandler->execute( + \Magento\Catalog\Api\Data\ProductInterface::class, + $entityData + ); $product->addData($roles); $this->updateHandler->execute($product); From 2c660c78d4989a8fe8cb771f1dfa6c7926484415 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Mon, 10 Aug 2020 14:48:00 +0300 Subject: [PATCH 0188/1013] MC-35838: Image and image role discrepancies when creating and updating item via A REST api rquests --- .../Magento/Catalog/Api/ProductRepositoryInterfaceTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php index 5e2b2f89ba97b..1b18949b0ac5b 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryInterfaceTest.php @@ -223,6 +223,7 @@ private function loadWebsiteByCode($websiteCode) try { $website = $websiteRepository->get($websiteCode); } catch (NoSuchEntityException $e) { + $website = null; $this->fail("Couldn`t load website: {$websiteCode}"); } From a1a4f802da33a96e7e1c078a1c206d60e42c0a3e Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Mon, 10 Aug 2020 08:25:19 -0500 Subject: [PATCH 0189/1013] MC-32659: Order Details by Order Number with additional different product types - Added test for invoices on downloadable product type --- ...rieveOrdersWithDownloadableProductTest.php | 53 +++++++++++++++++-- ...stomer_order_with_downloadable_product.php | 3 ++ ...rder_with_invoice_downloadable_product.php | 38 +++++++++++++ ..._invoice_downloadable_product_rollback.php | 9 ++++ 4 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php create mode 100644 dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product_rollback.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index ad1e14dd2a938..53fa9bd922965 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -16,7 +16,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Class RetrieveOrdersTest + * Class RetrieveOrdersTest for DownloadableProduct */ class RetrieveOrdersWithDownloadableProductTest extends GraphQlAbstract { @@ -44,6 +44,7 @@ protected function setUp():void /** * @magentoApiDataFixture Magento/Downloadable/_files/order_with_customer_and_downloadable_product.php + * @magentoApiDataFixture Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php */ public function testGetCustomerOrdersDownloadableProduct() { @@ -76,12 +77,36 @@ public function testGetCustomerOrdersDownloadableProduct() ] ]; $this->assertResponseFields($expectedDownloadableLinksData, $downloadableLinksFromResponse); + // invoices assertions + $customerOrderItemsInvoicesResponse = $response[0]['invoices'][0]; + $this->assertNotEmpty($customerOrderItemsInvoicesResponse); + $this->assertNotEmpty($customerOrderItemsInvoicesResponse['number']); + $customerOrderItemsInvoicesItemsResponse = $customerOrderItemsInvoicesResponse['items'][0]; + $this->assertEquals('Downloadable Product', $customerOrderItemsInvoicesItemsResponse['product_name']); + $this->assertEquals(10, $customerOrderItemsInvoicesItemsResponse['product_sale_price']['value']); + $this->assertEquals(1, $customerOrderItemsInvoicesItemsResponse['quantity_invoiced']); + $downloadableItemInTheInvoice = $customerOrderItemsInvoicesItemsResponse['downloadable_links']; + $this->assertNotEmpty($downloadableItemInTheInvoice); + + $downloadableProduct = $this->productRepository->get('downloadable-product'); + /** @var LinkInterface $downloadableProductLinks */ + $downloadableProductLinks = $downloadableProduct->getExtensionAttributes()->getDownloadableProductLinks(); + $linkId = $downloadableProductLinks[0]->getId(); + $expectedDownloadableLinksData = + [ + [ + 'title' =>'Downloadable Product Link', + 'sort_order' => 1, + 'uid'=> base64_encode("downloadable/{$linkId}") + ] + ]; + $this->assertResponseFields($expectedDownloadableLinksData, $downloadableItemInTheInvoice); } /** * @magentoApiDataFixture Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php */ - public function testGetCustomerOrdersDownloadableWithmultiplelinks() + public function testGetCustomerOrdersDownloadableWithMultipleLinks() { } @@ -124,7 +149,7 @@ private function getCustomerOrderQuery($orderNumber): array quantity_ordered } } - total { + total { base_grand_total{value currency} grand_total{value currency} subtotal {value currency } @@ -141,10 +166,30 @@ private function getCustomerOrderQuery($orderNumber): array } discounts {amount{value currency} label} } + invoices { + number + items { + product_name + product_sale_price{value currency} + quantity_invoiced + ... on DownloadableInvoiceItem { + downloadable_links + { + sort_order + title + uid + } + id + product_name + product_sale_price{value} + quantity_invoiced + } + } } } } - } + } +} QUERY; $currentEmail = 'customer@example.com'; $currentPassword = 'password'; diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_downloadable_product.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_downloadable_product.php index 74c52d4642a77..c9abb31dd547c 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_downloadable_product.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_downloadable_product.php @@ -43,8 +43,11 @@ $orderItem->setProductId(1) ->setProductType(\Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) + ->setName('Downloadable Product') ->setProductOptions(['links' => [$link->getId()]]) ->setBasePrice(100) + ->setPrice(10) + ->setSku('downloadable-product') ->setQtyOrdered(1); $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Sales\Model\Order::class); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php new file mode 100644 index 0000000000000..a31f1562c327e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\DB\Transaction; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\InvoiceManagementInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +$objectManager = Bootstrap::getObjectManager(); +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); + +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo'); +$order->setPayment($payment); +$order->save(); + +$orderService = ObjectManager::getInstance()->create( + InvoiceManagementInterface::class +); + +/** @var InvoiceManagementInterface $orderService */ +$orderService = $objectManager->create( + InvoiceManagementInterface::class +); +$invoice = $orderService->prepareInvoice($order); +$invoice->register(); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product_rollback.php new file mode 100644 index 0000000000000..aba4c3c972955 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product_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/default_rollback.php'); From e56ac979f59eb13fb66467152b30013533b56a16 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Mon, 10 Aug 2020 16:51:39 +0300 Subject: [PATCH 0190/1013] magento/magento2-login-as-customer#150: Opt-in/out - acl text fix. --- ...CustomerLoginFromCustomerPageManualChooseActionGroup.xml | 2 +- .../Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml | 2 +- .../AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml | 2 +- .../Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml | 2 +- .../Block/Adminhtml/ConfirmationPopup.php | 2 +- .../Controller/Adminhtml/Login/Login.php | 6 +++--- .../Magento/LoginAsCustomerAdminUi/etc/adminhtml/system.xml | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml index 8db34a05252ee..44ef227a34257 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml @@ -20,7 +20,7 @@ <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.title}}" userInput="Login as Customer: Select Store" 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"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml index 42d53113e20f4..5d447f665a275 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml @@ -11,7 +11,7 @@ <test name="AdminLoginAsCustomerAutoDetectionTest"> <annotations> <features value="Login as Customer"/> - <stories value="Select Store View based on 'Store View To Login In' setting"/> + <stories value="Select Store 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 diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml index 27aee2061f204..bd61b359f5bf6 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml @@ -11,7 +11,7 @@ <test name="AdminLoginAsCustomerManualChooseStoreCodeInUrlTest" extends="AdminLoginAsCustomerManualChooseTest"> <annotations> <features value="Login as Customer"/> - <stories value="Select Store View based on 'Store View To Login In' setting"/> + <stories value="Select Store 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 diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml index acae07d1cda11..b343e4c289658 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml @@ -11,7 +11,7 @@ <test name="AdminLoginAsCustomerManualChooseTest"> <annotations> <features value="Login as Customer"/> - <stories value="Select Store View based on 'Store View To Login In' setting"/> + <stories value="Select Store 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 diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php index 792fdcd073588..2498fa31c5e91 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php @@ -65,7 +65,7 @@ public function getJsLayout() $showStoreViewOptions = $this->config->isStoreManualChoiceEnabled(); $layout['components']['lac-confirmation-popup']['title'] = $showStoreViewOptions - ? __('Login as Customer: Select Store View') + ? __('Login as Customer: Select Store') : __('You are about to Login as Customer'); $layout['components']['lac-confirmation-popup']['content'] = __('Actions taken while in "Login as Customer" will affect actual customer data.'); diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 947ee68d000ad..e80c3700349df 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -94,7 +94,7 @@ class Login extends Action implements HttpGetActionInterface * @var ManageStoreCookie */ private $manageStoreCookie; - + /** * @var SetLoggedAsCustomerCustomerIdInterface */ @@ -133,7 +133,7 @@ public function __construct( DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, Url $url, ?Share $share = null, - ?ManageStoreCookie $manageStoreCookie = null + ?ManageStoreCookie $manageStoreCookie = null, ?SetLoggedAsCustomerCustomerIdInterface $setLoggedAsCustomerCustomerId = null, ?IsLoginAsCustomerEnabledForCustomerInterface $isLoginAsCustomerEnabled = null ) { @@ -191,7 +191,7 @@ public function execute(): ResultInterface if ($this->config->isStoreManualChoiceEnabled()) { $storeId = (int)$this->_request->getParam('store_id'); if (empty($storeId)) { - $this->messageManager->addNoticeMessage(__('Please select a Store View to login in.')); + $this->messageManager->addNoticeMessage(__('Please select a Store to login in.')); return $resultRedirect->setPath('customer/index/edit', ['id' => $customerId]); } } elseif ($this->share->isGlobalScope()) { diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/system.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/system.xml index 580f478f32780..fb8d990e5bab2 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/system.xml +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/system.xml @@ -23,7 +23,7 @@ <source_model>Magento\LoginAsCustomerAdminUi\Model\Config\Source\StoreViewLogin</source_model> <comment><![CDATA[ Use the "Manual Selection" option on a multi-website setup that has "Share Customer Accounts" enabled globally. - If set to "Manual Selection", the "Login as Customer" admin can select a store view after logging in. + If set to "Manual Selection", the "Login as Customer" admin can select a Store after logging in. ]]></comment> </field> </group> From 5d65a1594ce5d9b9186506724e8be7ece61fb65a Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Mon, 10 Aug 2020 10:18:51 -0500 Subject: [PATCH 0191/1013] MC-32659: Order Details by Order Number with additional different product types --- ...th_customer_and_downloadable_product_with_multiple_links.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php index b80fa4fc93704..a8cf0a509e0e0 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php @@ -54,7 +54,7 @@ ['type' => 'free', 'fraudulent' => false] ); /** @var ProductInterface $product */ -$product = $productRepository->get('downloadable-product'); +$product = $productRepository->get('downloadable-product-with-purchased-separately-links'); /** @var LinkInterface $links */ $links = $product->getExtensionAttributes()->getDownloadableProductLinks(); $link = reset($links); From 7ccfe2b60171bd68e2b3fbff78ea26ee2af18df7 Mon Sep 17 00:00:00 2001 From: Daniel Renaud <drenaud@magento.com> Date: Mon, 10 Aug 2020 12:01:57 -0500 Subject: [PATCH 0192/1013] MC-35987: Order related types not namespaced properly on schema --- .../Model/OrderItem/OptionsProcessor.php | 6 +++--- .../Magento/SalesGraphQl/etc/schema.graphqls | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php index 83b7e0cc46d96..2f386a3f05ec7 100644 --- a/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php @@ -49,12 +49,12 @@ private function processOptions(array $options): array if (isset($option['option_type'])) { if (in_array($option['option_type'], ['field', 'area', 'file', 'date', 'date_time', 'time'])) { $selectedOptions[] = [ - 'id' => $option['label'], + 'label' => $option['label'], 'value' => $option['print_value'] ?? $option['value'], ]; } elseif (in_array($option['option_type'], ['drop_down', 'radio', 'checkbox', 'multiple'])) { $enteredOptions[] = [ - 'id' => $option['label'], + 'label' => $option['label'], 'value' => $option['print_value'] ?? $option['value'], ]; } @@ -74,7 +74,7 @@ private function processAttributesInfo(array $attributesInfo): array $selectedOptions = []; foreach ($attributesInfo ?? [] as $option) { $selectedOptions[] = [ - 'id' => $option['label'], + 'label' => $option['label'], 'value' => $option['print_value'] ?? $option['value'], ]; } diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index 8b9d58e48d4b1..3544acd1564d0 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -48,12 +48,12 @@ type CustomerOrder @doc(description: "Contains details about each of the custome 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") + payment_methods: [OrderPaymentMethod] @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") + comments: [SalesCommentItem] @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") @@ -101,7 +101,7 @@ 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") + label: String! @doc(description: "The name of the option") value: String! @doc(description: "The value of the option") } @@ -127,7 +127,7 @@ type Invoice @doc(description: "Invoice details") { 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") + comments: [SalesCommentItem] @doc(description: "Comments on the invoice") } interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\InvoiceItem") { @@ -171,10 +171,10 @@ type OrderShipment @doc(description: "Order shipment details") { 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") + comments: [SalesCommentItem] @doc(description: "Comments added to the shipment") } -type CommentItem @doc(description: "Comment item details") { +type SalesCommentItem @doc(description: "Comment item details") { timestamp: String! @doc(description: "The timestamp of the comment") message: String! @doc(description: "The text of the message") } @@ -197,7 +197,7 @@ type ShipmentTracking @doc(description: "Order shipment tracking details") { 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") { +type OrderPaymentMethod @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") @@ -208,7 +208,7 @@ type CreditMemo @doc(description: "Credit memo details") { 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") + comments: [SalesCommentItem] @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") { From 112b62696431e35bb14953b15b35af311e64789b Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Mon, 10 Aug 2020 13:56:54 -0500 Subject: [PATCH 0193/1013] MC-32659: Order Details by Order Number with additional different product types --- ...ustomer_and_downloadable_product_with_multiple_links.php | 6 ++++-- ...nd_downloadable_product_with_multiple_links_rollback.php | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php index a8cf0a509e0e0..dd048b976afb0 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php @@ -21,8 +21,10 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture('Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php'); -Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance() + ->requireDataFixture('Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php'); +Resolver::getInstance() + ->requireDataFixture('Magento/Customer/_files/customer.php'); $addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; $objectManager = Bootstrap::getObjectManager(); diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php index a15aa0cf57dbb..668eb36aa4526 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php @@ -12,8 +12,10 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture('Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php'); -Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance() + ->requireDataFixture('Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php'); +Resolver::getInstance() + ->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); $objectManager = Bootstrap::getObjectManager(); /** @var Registry $registry */ From caadaa1c44c60bda6e8d2b9ea75ab2d77095975a Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Mon, 10 Aug 2020 14:18:53 -0500 Subject: [PATCH 0194/1013] MC-32659: Order Details by Order Number with additional different product types --- app/code/Magento/DownloadableGraphQl/composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/DownloadableGraphQl/composer.json b/app/code/Magento/DownloadableGraphQl/composer.json index 36f06b7ba8284..d03a5953506e5 100644 --- a/app/code/Magento/DownloadableGraphQl/composer.json +++ b/app/code/Magento/DownloadableGraphQl/composer.json @@ -10,11 +10,11 @@ "magento/module-quote": "*", "magento/module-sales": "*", "magento/module-quote-graph-ql": "*", - "magento/module-sales-graph-ql": "*", "magento/framework": "*" }, "suggest": { - "magento/module-catalog-graph-ql": "*" + "magento/module-catalog-graph-ql": "*", + "magento/module-sales-graph-ql": "*" }, "license": [ "OSL-3.0", From 36ade1b7688af04d53e1a8a58017232256a799ef Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Mon, 10 Aug 2020 14:53:48 -0500 Subject: [PATCH 0195/1013] MC-35013: SKU search in Advanced Search page doesn't work --- app/code/Magento/CatalogSearch/Model/Advanced.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index b498cb09e34fa..20b68c815526d 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -361,6 +361,8 @@ protected function addSearchCriteria($attribute, $value) */ protected function getPreparedSearchCriteria($attribute, $value) { + $from = null; + $to = null; if (is_array($value)) { if (isset($value['from']) && isset($value['to'])) { if (!empty($value['from']) || !empty($value['to'])) { From e04a84fe6060c6f90de69c4b01cf434adf57de0f Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Mon, 10 Aug 2020 17:25:47 -0500 Subject: [PATCH 0196/1013] MC-32659: Order Details by Order Number with additional different product types --- ..._downloadable_product_with_multiple_links_rollback.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php index 668eb36aa4526..4a28435ceb2d2 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links_rollback.php @@ -12,10 +12,10 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance() - ->requireDataFixture('Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php'); -Resolver::getInstance() - ->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links_rollback.php' +); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); $objectManager = Bootstrap::getObjectManager(); /** @var Registry $registry */ From 8703fd401a4bdebe874361cc0d2d0eb9b2f5bc7a Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Mon, 10 Aug 2020 19:58:37 -0500 Subject: [PATCH 0197/1013] MC-32659: Order Details by Order Number with additional different product types --- .../DownloadableGraphQl/etc/schema.graphqls | 4 ++-- ...rieveOrdersWithDownloadableProductTest.php | 19 +++++++++---------- ...rder_with_invoice_downloadable_product.php | 5 +---- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 451e135325720..cd3bd000248bc 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -53,7 +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. + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") } type DownloadableProductSamples @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") { @@ -80,5 +80,5 @@ type DownloadableCreditMemoItem implements CreditMemoItemInterface { type DownloadableItemsLinks @doc(description: "DownloadableProductLinks defines characteristics of a downloadable product") { title: String @doc(description: "The display name of the link") sort_order: Int @doc(description: "A number indicating the sort order") - uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") # A Base64 string that encodes option details. + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index 53fa9bd922965..57d3913dd2d11 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -16,7 +16,7 @@ use Magento\TestFramework\TestCase\GraphQlAbstract; /** - * Class RetrieveOrdersTest for DownloadableProduct + * Tests downloadable product fields in Orders, Invoices, CreditMemo and Shipments */ class RetrieveOrdersWithDownloadableProductTest extends GraphQlAbstract { @@ -34,7 +34,6 @@ class RetrieveOrdersWithDownloadableProductTest extends GraphQlAbstract protected function setUp():void { - parent::setUp(); $objectManager = Bootstrap::getObjectManager(); $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); @@ -49,8 +48,8 @@ protected function setUp():void public function testGetCustomerOrdersDownloadableProduct() { $orderNumber = '100000001'; - $response = $this->getCustomerOrderQuery($orderNumber); - $customerOrderItemsInResponse = $response[0]['items']; + $customerOrders = $this->getCustomersOrderQuery($orderNumber); + $customerOrderItemsInResponse = $customerOrders[0]['items']; $this->assertNotEmpty($customerOrderItemsInResponse); $downloadableItemInTheOrder = $customerOrderItemsInResponse[0]; @@ -78,15 +77,15 @@ public function testGetCustomerOrdersDownloadableProduct() ]; $this->assertResponseFields($expectedDownloadableLinksData, $downloadableLinksFromResponse); // invoices assertions - $customerOrderItemsInvoicesResponse = $response[0]['invoices'][0]; + $customerOrderItemsInvoicesResponse = $customerOrders[0]['invoices'][0]; $this->assertNotEmpty($customerOrderItemsInvoicesResponse); $this->assertNotEmpty($customerOrderItemsInvoicesResponse['number']); $customerOrderItemsInvoicesItemsResponse = $customerOrderItemsInvoicesResponse['items'][0]; $this->assertEquals('Downloadable Product', $customerOrderItemsInvoicesItemsResponse['product_name']); $this->assertEquals(10, $customerOrderItemsInvoicesItemsResponse['product_sale_price']['value']); $this->assertEquals(1, $customerOrderItemsInvoicesItemsResponse['quantity_invoiced']); - $downloadableItemInTheInvoice = $customerOrderItemsInvoicesItemsResponse['downloadable_links']; - $this->assertNotEmpty($downloadableItemInTheInvoice); + $downloadableItemLinks = $customerOrderItemsInvoicesItemsResponse['downloadable_links']; + $this->assertNotEmpty($downloadableItemLinks); $downloadableProduct = $this->productRepository->get('downloadable-product'); /** @var LinkInterface $downloadableProductLinks */ @@ -100,7 +99,7 @@ public function testGetCustomerOrdersDownloadableProduct() 'uid'=> base64_encode("downloadable/{$linkId}") ] ]; - $this->assertResponseFields($expectedDownloadableLinksData, $downloadableItemInTheInvoice); + $this->assertResponseFields($expectedDownloadableLinksData, $downloadableItemLinks); } /** @@ -111,12 +110,12 @@ public function testGetCustomerOrdersDownloadableWithMultipleLinks() } /** - * Get customer order query + * Get customer order query with invoices * * @param string $orderNumber * @return array */ - private function getCustomerOrderQuery($orderNumber): array + private function getCustomersOrderQuery($orderNumber): array { $query = <<<QUERY diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php index a31f1562c327e..5dea9a6c40754 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/customer_order_with_invoice_downloadable_product.php @@ -12,6 +12,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; +/** @var ObjectManager $objectManager */ $objectManager = Bootstrap::getObjectManager(); /** @var \Magento\Sales\Model\Order $order */ $order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); @@ -21,10 +22,6 @@ $order->setPayment($payment); $order->save(); -$orderService = ObjectManager::getInstance()->create( - InvoiceManagementInterface::class -); - /** @var InvoiceManagementInterface $orderService */ $orderService = $objectManager->create( InvoiceManagementInterface::class From d52f7245cd247c350b9e09e79c7a6286f9f685c3 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Mon, 10 Aug 2020 21:37:07 -0500 Subject: [PATCH 0198/1013] MC-32659: MyAccount :: Order Details :: Order Details by Order Number with additional different product types - added creditmemo for downloadable product --- .../CustomerPlaceOrderWithDownloadable.php | 417 ++++++++++++++++++ ...rieveOrdersWithDownloadableProductTest.php | 248 ++++++++++- 2 files changed, 663 insertions(+), 2 deletions(-) create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php new file mode 100644 index 0000000000000..7003983ae6f1e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php @@ -0,0 +1,417 @@ +<?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\Downloadable\Api\Data\LinkInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\TestCase\GraphQl\Client; + +class CustomerPlaceOrderWithDownloadable +{ + /** + * @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 placeOrderWithDownloadableProduct(array $customerLogin, array $productData): array + { + $this->customerLogin = $customerLogin; + $this->createCustomerCart(); + $this->addDownloadableProduct($productData); + $this->setBillingAddress(); + // $shippingMethod = $this->setShippingAddress(); + // $paymentMethod = $this->setShippingMethod($shippingMethod); + $paymentMethodCode ='checkmo'; + $this->setPaymentMethod($paymentMethodCode); + 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); + } + + private function addDownloadableProduct(array $productData) + { + $productSku = $productData['sku']; + $qty = $productData['quantity'] ?? 1; + /** @var Product $downloadableProduct */ + $downloadableProduct = $this->productRepository->get($productSku); + /** @var LinkInterface $downloadableProductLinks */ + $downloadableProductLinks = $downloadableProduct->getExtensionAttributes()->getDownloadableProductLinks(); + $linkId = $downloadableProductLinks[0]->getId(); + + $addProduct = <<<QUERY +mutation { + addDownloadableProductsToCart( + input: { + cart_id: "{$this->getCartId()}", + cart_items: [ + { + data: { + quantity: {$qty}, + sku: "{$productSku}" + }, + downloadable_product_links: [ + { + link_id: {$linkId} + } + ] + } + ] + } + ) { + cart { + items { + quantity + ... on DownloadableCartItem { + links { + title + link_type + price + } + } + } + } + } +} +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 string $paymentMethodCode + * @return array + */ + private function setPaymentMethod(string $paymentMethodCode): array + { + $setPaymentMethod = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$this->getCartId()}" + payment_method: { + code: "{$paymentMethodCode}" + } + } + ) { + 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/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index 53fa9bd922965..33589582b0ed7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -11,7 +11,14 @@ use Magento\Downloadable\Api\Data\LinkInterface; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\GraphQl\Sales\Fixtures\CustomerPlaceOrderWithDownloadable; +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; @@ -32,6 +39,15 @@ class RetrieveOrdersWithDownloadableProductTest extends GraphQlAbstract /** @var ProductRepositoryInterface */ private $productRepository; + /** @var CreditmemoService */ + private $creditMemoService; + + /** @var Order */ + private $order; + + /** @var CreditmemoFactory */ + private $creditMemoFactory; + protected function setUp():void { parent::setUp(); @@ -40,6 +56,15 @@ protected function setUp():void $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->order = $objectManager->create(Order::class); + $this->creditMemoService = $objectManager->get(CreditmemoService::class); + $this->creditMemoFactory = $objectManager->get(CreditmemoFactory::class); + } + + protected function tearDown(): void + { + $this->cleanUpCreditMemos(); + $this->deleteOrder(); } /** @@ -104,12 +129,129 @@ public function testGetCustomerOrdersDownloadableProduct() } /** - * @magentoApiDataFixture Magento/Downloadable/_files/order_with_customer_and_downloadable_product_with_multiple_links.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_purchased_separately_links.php */ - public function testGetCustomerOrdersDownloadableWithMultipleLinks() + public function testGetCustomerOrdersAndCreditMemoDownloadable() { + //Place order with downloadable product + $qty = 1; + $downloadableSku = 'downloadable-product-with-purchased-separately-links'; + /** @var CustomerPlaceOrderWithDownloadable $downloadableProductOrderFixture */ + $downloadableProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrderWithDownloadable::class); + $orderResponse = $downloadableProductOrderFixture->placeOrderWithDownloadableProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => $downloadableSku, 'quantity' => $qty] + ); + $orderNumber = $orderResponse['placeOrder']['order']['order_number']; + //End place order with downloadable product + + // prepare invoice + $this->prepareInvoice($orderNumber, 1); + $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(12); + $creditMemo->setBaseSubTotal(12); + $creditMemo->setBaseGrandTotal(12); + $creditMemo->setGrandTotal(12); + $creditMemo->setAdjustment(-2.00); + $creditMemo->addComment("Test comment for downloadable refund", false, true); + $creditMemo->save(); + /** @var \Magento\Sales\Model\Order\Creditmemo\Item $creditMemoItems */ + // $creditMemoItems = $creditMemo->getItemByOrderId($order->getId()); +// $creditMemoItems->setCreditmemo($creditMemo); +// $creditMemoItems->setOrderItemId($orderItem->getId()); + // $creditMemoItems->setQty(1); + // $creditMemoItems->save(); + + $this->creditMemoService->refund($creditMemo, true); + $response = $this->getCustomerOrderWithCreditMemoQuery(); + $expectedCreditMemoData = [ + [ + 'comments' => [ + ['message' => 'Test comment for downloadable refund'] + ], + + 'total' => [ + 'subtotal' => [ + 'value' => 12 + ], + 'grand_total' => [ + 'value' => 12, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 12, + 'currency' => 'USD' + ], + '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' => [] + + ], + 'adjustment' => [ + 'value' => 2 + ] + ] + ] + ]; + $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(); + } + + /** * Get customer order query * @@ -206,4 +348,106 @@ private function getCustomerOrderQuery($orderNumber): array $customerOrderItemsInResponse = $response['customer']['orders']['items']; return $customerOrderItemsInResponse; } + + /** + * Get CustomerOrder with credit memo details + * + * @return array + */ + private function getCustomerOrderWithCreditMemoQuery(): array + { + $query = + <<<QUERY +query { + customer { + orders { + items { + credit_memos { + comments { + message + } + + 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} + + } + adjustment { + value + } + } + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response; + } + + /** + * @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); + } } From 1c5bcc0af7eb981600b970a470d1fe4cbee05c7f Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Mon, 10 Aug 2020 21:40:36 -0500 Subject: [PATCH 0199/1013] MC-32659: MyAccount :: Order Details :: Order Details by Order Number with additional different product types - added creditmemo for downloadable product --- .../GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index 33589582b0ed7..a605d06e934bd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -180,7 +180,7 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() 'comments' => [ ['message' => 'Test comment for downloadable refund'] ], - + 'total' => [ 'subtotal' => [ 'value' => 12 From b5045c4abcbd70d82b712ba293c83ce171dace54 Mon Sep 17 00:00:00 2001 From: Peep van Puijenbroek <4386577+Swahjak@users.noreply.github.com> Date: Tue, 11 Aug 2020 09:09:00 +0200 Subject: [PATCH 0200/1013] Fix customer group update confirm Was broken with commit https://github.com/magento/magento2/commit/faa5f9a572ac1ea44750936bd9dc51c20513269d#diff-47459785dc7008bc9145a9609c9c7f6c and never fixed. --- .../adminhtml/web/order/create/scripts.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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 bbdd6f8fe8437..fb347a0f2dee1 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 @@ -1493,14 +1493,20 @@ define([ } if (action === 'change') { - var confirmText = message.replace(/%s/, customerGroupOption.text); + var self = this, + confirmText = message.replace(/%s/, customerGroupOption.text); confirmText = confirmText.replace(/%s/, currentCustomerGroupTitle); - if (confirm(confirmText)) { - $$('#' + groupIdHtmlId + ' option').each(function (o) { - o.selected = o.readAttribute('value') == groupId; - }); - this.accountGroupChange(); - } + confirm({ + content: confirmText, + actions: { + confirm: function() { + $$('#' + groupIdHtmlId + ' option').each(function (o) { + o.selected = o.readAttribute('value') == groupId; + }); + self.accountGroupChange(); + } + } + }) } else if (action === 'inform') { alert({ content: message + '\n' + groupMessage From 61a92c2775cfbaed9e8e3993f039aa885f6b4fe9 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Tue, 11 Aug 2020 11:05:20 +0300 Subject: [PATCH 0201/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - add disabled Store Groups. --- .../Block/Adminhtml/ConfirmationPopup.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php index 2498fa31c5e91..538ef3111fe43 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php @@ -10,7 +10,8 @@ use Magento\Backend\Block\Template; use Magento\Framework\Serialize\Serializer\Json; use Magento\LoginAsCustomerApi\Api\ConfigInterface; -use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\ConfirmationPopup\Options as StoreOptions; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\ConfirmationPopup\Options; +use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; /** * Login confirmation pop-up @@ -35,24 +36,32 @@ class ConfirmationPopup extends Template */ private $json; + /** + * @var Options + */ + private $options; + /** * @param Template\Context $context * @param StoreOptions $storeOptions * @param ConfigInterface $config * @param Json $json * @param array $data + * @param Options|null $options */ public function __construct( Template\Context $context, StoreOptions $storeOptions, ConfigInterface $config, Json $json, - array $data = [] + array $data = [], + ?Options $options = null ) { parent::__construct($context, $data); $this->storeOptions = $storeOptions; $this->config = $config; $this->json = $json; + $this->options = $options; } /** @@ -72,7 +81,7 @@ public function getJsLayout() $layout['components']['lac-confirmation-popup']['showStoreViewOptions'] = $showStoreViewOptions; $layout['components']['lac-confirmation-popup']['storeViewOptions'] = $showStoreViewOptions - ? $this->storeOptions->toOptionArray() + ? $this->options->toOptionArray() : []; return $this->json->serialize($layout); From 513c9cb9626c630c3398cccf0a5fbbfc1d2706f1 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 11 Aug 2020 11:28:45 +0300 Subject: [PATCH 0202/1013] MC-33150: Inconsistent address field labels on checkout and address book --- .../Customer/view/frontend/templates/address/edit.phtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5f7016b6b0ac3..a4a500b7d1b37 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -61,7 +61,7 @@ $viewModel = $block->getViewModel(); <div class="field primary"> <label for="street_1" class="label"> <span> - <?= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('street') . ': Line 1' ?> + <?= $escaper->escapeHtml(__('Street Address: Line %1', 1)) ?> </span> </label> </div> @@ -75,7 +75,7 @@ $viewModel = $block->getViewModel(); <?php for ($_i = 1, $_n = $viewModel->addressGetStreetLines(); $_i < $_n; $_i++): ?> <div class="field additional"> <label class="label" for="street_<?= /* @noEscape */ $_i + 1 ?>"> - <span><?= $escaper->escapeHtml(__('Street Address %1', $_i + 1)) ?></span> + <span><?= $escaper->escapeHtml(__('Street Address: Line %1', $_i + 1)) ?></span> </label> <div class="control"> <input type="text" name="street[]" From 7328f32d249845bde1507a61f2a2a663c162d044 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Tue, 11 Aug 2020 17:36:27 +0300 Subject: [PATCH 0203/1013] MC-35254: Customer group is automatically changed when editing customer on customer grid is assigned to the company --- app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php index bbea0ce9dc052..7dbaaf5cf181c 100644 --- a/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php +++ b/app/code/Magento/Customer/Observer/AfterAddressSaveObserver.php @@ -25,6 +25,7 @@ /** * Customer Observer Model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AfterAddressSaveObserver implements ObserverInterface { From cdf0d2810672430393204e9a7ab9e6be448bcbcf Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Tue, 11 Aug 2020 11:03:22 -0500 Subject: [PATCH 0204/1013] MC-32659: MyAccount :: Order Details :: Order Details by Order Number with additional different product types - CR comments --- ...trieveOrdersWithDownloadableProductTest.php | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index fd199ab28acde..847a2067da989 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -48,9 +48,10 @@ class RetrieveOrdersWithDownloadableProductTest extends GraphQlAbstract /** @var CreditmemoFactory */ private $creditMemoFactory; + private $creditmemoItemFactory; + protected function setUp():void { - parent::setUp(); $objectManager = Bootstrap::getObjectManager(); $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); @@ -59,6 +60,7 @@ protected function setUp():void $this->order = $objectManager->create(Order::class); $this->creditMemoService = $objectManager->get(CreditmemoService::class); $this->creditMemoFactory = $objectManager->get(CreditmemoFactory::class); + $this->creditmemoItemFactory = $objectManager->create(\Magento\Sales\Model\Order\Creditmemo\ItemFactory::class); } protected function tearDown(): void @@ -158,7 +160,6 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() $creditMemo = $this->creditMemoFactory->createByOrder($order, $order->getData()); $creditMemo->setOrder($order); $creditMemo->setState(1); - $creditMemo->setSubtotal(12); $creditMemo->setBaseSubTotal(12); $creditMemo->setBaseGrandTotal(12); @@ -166,13 +167,6 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() $creditMemo->setAdjustment(-2.00); $creditMemo->addComment("Test comment for downloadable refund", false, true); $creditMemo->save(); - /** @var \Magento\Sales\Model\Order\Creditmemo\Item $creditMemoItems */ - // $creditMemoItems = $creditMemo->getItemByOrderId($order->getId()); -// $creditMemoItems->setCreditmemo($creditMemo); -// $creditMemoItems->setOrderItemId($orderItem->getId()); - // $creditMemoItems->setQty(1); - // $creditMemoItems->save(); - $this->creditMemoService->refund($creditMemo, true); $response = $this->getCustomerOrderWithCreditMemoQuery(); $expectedCreditMemoData = [ @@ -223,8 +217,6 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() $creditMemos = $firstOrderItem['credit_memos']; $this->assertResponseFields($creditMemos, $expectedCreditMemoData); - - } /** @@ -363,9 +355,7 @@ private function getCustomerOrderWithCreditMemoQuery(): array orders { items { credit_memos { - comments { - message - } + comments { message} total { subtotal { From b6dc9737949c941e651a501d4e31f9dad52dd69a Mon Sep 17 00:00:00 2001 From: Serhii Voloshkov <serhii.voloshkov@transoftgroup.com> Date: Tue, 11 Aug 2020 19:50:46 +0300 Subject: [PATCH 0205/1013] =?UTF-8?q?MC-35313:=20=E2=80=9CAdd=20selections?= =?UTF-8?q?=20to=20my=20cart=E2=80=9D=20button=20does=20not=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminCustomerShoppingCartProductItemSection.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection/AdminCustomerShoppingCartProductItemSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection/AdminCustomerShoppingCartProductItemSection.xml index 40fc23b6c72c1..eedfe47b7775c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection/AdminCustomerShoppingCartProductItemSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerShoppingCartSection/AdminCustomerShoppingCartProductItemSection.xml @@ -14,5 +14,6 @@ <element name="firstProductCheckbox" type="checkbox" selector="//*[@id='source_products_table']/tbody/tr[1]//*[@name='source_products']"/> <element name="addSelectionsToMyCartButton" type="button" selector="//*[@id='products_search']/div[1]//*[text()='Add selections to my cart']"/> <element name="addedProductName" type="text" selector="//*[@id='order-items_grid']//*[text()='{{var}}']" parameterized="true"/> + <element name="addedProductQty" type="input" selector="//*[@id='order-items_grid']//*[text()='{{var}}']//..//..//*[@class='col-qty']//input" parameterized="true"/> </section> </sections> From 26193cbc9c393683eded0574bd11a6bded596d46 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Tue, 11 Aug 2020 11:58:04 -0500 Subject: [PATCH 0206/1013] MC-32659: Order Details by Order Number with additional different product types --- ...rieveOrdersWithDownloadableProductTest.php | 87 +++++++++++-------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index 847a2067da989..c1b913732e97a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -17,10 +17,14 @@ 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\ResourceModel\Order\Creditmemo\Collection as CreditmemoCollection; use Magento\Sales\Model\Service\CreditmemoService; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Sales\Model\Order\Creditmemo\ItemFactory; +use Magento\Framework\Registry; +use Magento\Framework\DB\Transaction; +use Magento\Sales\Api\InvoiceManagementInterface; /** * Tests downloadable product fields in Orders, Invoices, CreditMemo and Shipments @@ -48,8 +52,30 @@ class RetrieveOrdersWithDownloadableProductTest extends GraphQlAbstract /** @var CreditmemoFactory */ private $creditMemoFactory; + /** @var ItemFactory */ private $creditmemoItemFactory; + /** @var CustomerPlaceOrderWithDownloadable */ + private $customerPlaceOrderWithDownloadable; + + /** @var InvoiceManagementInterface */ + private $invoiceManagement; + + /** @var OrderCollection */ + private $orderCollection; + + /** @var CreditmemoRepositoryInterface */ + private $creditmemoRepository; + + /** @var CreditmemoCollection */ + private $creditmemoCollection; + + /** @var Registry */ + private $registry; + + /** @var Transaction */ + private $transaction; + protected function setUp():void { $objectManager = Bootstrap::getObjectManager(); @@ -60,7 +86,14 @@ protected function setUp():void $this->order = $objectManager->create(Order::class); $this->creditMemoService = $objectManager->get(CreditmemoService::class); $this->creditMemoFactory = $objectManager->get(CreditmemoFactory::class); - $this->creditmemoItemFactory = $objectManager->create(\Magento\Sales\Model\Order\Creditmemo\ItemFactory::class); + $this->creditmemoItemFactory = $objectManager->create(ItemFactory::class); + $this->customerPlaceOrderWithDownloadable = $objectManager->create(CustomerPlaceOrderWithDownloadable::class); + $this->invoiceManagement = $objectManager->create(InvoiceManagementInterface::class); + $this->orderCollection = $objectManager->create(OrderCollection::class); + $this->creditmemoRepository = $objectManager->get(CreditmemoRepositoryInterface::class); + $this->creditmemoCollection = $objectManager->create(CreditmemoCollection::class); + $this->registry = $objectManager->get(Registry::class); + $this->transaction = $objectManager->create(Transaction::class); } protected function tearDown(): void @@ -139,9 +172,7 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() //Place order with downloadable product $qty = 1; $downloadableSku = 'downloadable-product-with-purchased-separately-links'; - /** @var CustomerPlaceOrderWithDownloadable $downloadableProductOrderFixture */ - $downloadableProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrderWithDownloadable::class); - $orderResponse = $downloadableProductOrderFixture->placeOrderWithDownloadableProduct( + $orderResponse = $this->customerPlaceOrderWithDownloadable->placeOrderWithDownloadableProduct( ['email' => 'customer@example.com', 'password' => 'password'], ['sku' => $downloadableSku, 'quantity' => $qty] ); @@ -227,20 +258,14 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() */ 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); + /** @var Order $order */ + $order = $this->order->loadByIncrementId($orderNumber); $orderItem = current($order->getItems()); - $orderService = Bootstrap::getObjectManager()->create( - \Magento\Sales\Api\InvoiceManagementInterface::class - ); - $invoice = $orderService->prepareInvoice($order, [$orderItem->getId() => $qty]); + $invoice = $this->invoiceManagement->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(); + $this->transaction->addObject($invoice)->addObject($order)->save(); } @@ -409,18 +434,14 @@ private function getCustomerOrderWithCreditMemoQuery(): array */ 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->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + + foreach ($this->orderCollection as $order) { $this->orderRepository->delete($order); } - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', false); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); } /** @@ -428,16 +449,12 @@ private function deleteOrder(): 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); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + foreach ($this->creditmemoCollection as $creditmemo) { + $this->creditmemoRepository->delete($creditmemo); } - $registry->unregister('isSecureArea'); - $registry->register('isSecureArea', false); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); } } From a40101e1fea767b0e14e38c1655b735a54459519 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Tue, 11 Aug 2020 13:40:45 -0500 Subject: [PATCH 0207/1013] MC-32659: MyAccount :: Order Details :: Order Details by Order Number with additional different product types - remove commented code --- .../CustomerPlaceOrderWithDownloadable.php | 135 +----------------- 1 file changed, 1 insertion(+), 134 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php index 7003983ae6f1e..7d061abf17925 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php @@ -73,8 +73,6 @@ public function placeOrderWithDownloadableProduct(array $customerLogin, array $p $this->createCustomerCart(); $this->addDownloadableProduct($productData); $this->setBillingAddress(); - // $shippingMethod = $this->setShippingAddress(); - // $paymentMethod = $this->setShippingMethod($shippingMethod); $paymentMethodCode ='checkmo'; $this->setPaymentMethod($paymentMethodCode); return $this->doPlaceOrder(); @@ -140,63 +138,12 @@ private function createCustomerCart(): string } /** - * Add a bundle product to the cart + * Add downloadable product with link 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); - } - private function addDownloadableProduct(array $productData) { $productSku = $productData['sku']; @@ -283,86 +230,6 @@ private function setBillingAddress(): array 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 * From c9f29b85f45f003543128a4ea780068293430d14 Mon Sep 17 00:00:00 2001 From: Peep van Puijenbroek <4386577+Swahjak@users.noreply.github.com> Date: Tue, 11 Aug 2020 20:51:46 +0200 Subject: [PATCH 0208/1013] Use bind instead of self --- .../Sales/view/adminhtml/web/order/create/scripts.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 fb347a0f2dee1..63f33f6ae5975 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 @@ -1493,8 +1493,7 @@ define([ } if (action === 'change') { - var self = this, - confirmText = message.replace(/%s/, customerGroupOption.text); + var confirmText = message.replace(/%s/, customerGroupOption.text); confirmText = confirmText.replace(/%s/, currentCustomerGroupTitle); confirm({ content: confirmText, @@ -1503,8 +1502,8 @@ define([ $$('#' + groupIdHtmlId + ' option').each(function (o) { o.selected = o.readAttribute('value') == groupId; }); - self.accountGroupChange(); - } + this.accountGroupChange(); + }.bind(this) } }) } else if (action === 'inform') { From b1ed64f56c7909eeb203a406ef7623a7ff18b9da Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Tue, 11 Aug 2020 13:52:51 -0500 Subject: [PATCH 0209/1013] MC-20639: MyAccount :: Order Details :: Refund (creditMemo) Details by Order Number - CR comments --- .../Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php | 2 +- .../GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php index 7d061abf17925..4355662abe9ff 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrderWithDownloadable.php @@ -61,7 +61,7 @@ public function __construct( } /** - * Place order for a bundled product + * Place order for a downloadable product * * @param array $customerLogin * @param array $productData diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index c1b913732e97a..dac77143444a9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -268,7 +268,6 @@ private function prepareInvoice(string $orderNumber, int $qty = null) $this->transaction->addObject($invoice)->addObject($order)->save(); } - /** * Get customer order query with invoices * From 269aac1a129ff07c74e685e13a8fe2018491d1e6 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Tue, 11 Aug 2020 14:31:32 -0500 Subject: [PATCH 0210/1013] MC-32659: MyAccount :: Order Details :: Order Details by Order Number with additional different product types - added downloadable creditmemo item fragment coverage --- ...rieveOrdersWithDownloadableProductTest.php | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index dac77143444a9..96f5fd393a1bd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -182,11 +182,6 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() // prepare invoice $this->prepareInvoice($orderNumber, 1); $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); @@ -200,11 +195,29 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() $creditMemo->save(); $this->creditMemoService->refund($creditMemo, true); $response = $this->getCustomerOrderWithCreditMemoQuery(); + $downloadableProduct = $this->productRepository->get('downloadable-product-with-purchased-separately-links'); + /** @var LinkInterface $downloadableProductLinks */ + $downloadableProductLinks = $downloadableProduct->getExtensionAttributes()->getDownloadableProductLinks(); + $linkId = $downloadableProductLinks[0]->getId(); $expectedCreditMemoData = [ [ 'comments' => [ ['message' => 'Test comment for downloadable refund'] ], + 'items' => [ + [ + 'product_name'=> 'Downloadable Product (Links can be purchased separately)', + 'product_sku' => 'downloadable-product-with-purchased-separately-links', + 'product_sale_price' => ['value' => 12], + 'discounts' => [], + 'quantity_refunded' => 1, + 'downloadable_links' => [ + [ + 'uid'=> base64_encode("downloadable/{$linkId}"), + 'title' => 'Downloadable Product Link 1'] + ] + ] + ], 'total' => [ 'subtotal' => [ @@ -380,7 +393,23 @@ private function getCustomerOrderWithCreditMemoQuery(): array items { credit_memos { comments { message} - + items { + product_name + product_sku + product_sale_price {value } + discounts { amount{value currency} label } + quantity_refunded + ... on DownloadableCreditMemoItem + { + product_name + discounts{amount{value}} + downloadable_links{ + uid + title + } + quantity_refunded + } + } total { subtotal { value From 3d69b132c603dbb0d04f3e8911b0b2a1416aed0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szubert?= <bartlomiejszubert@gmail.com> Date: Tue, 11 Aug 2020 21:51:16 +0200 Subject: [PATCH 0211/1013] Update app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml Co-authored-by: Gabriel da Gama <gabriel@gabrielgama.com.br> --- .../view/frontend/layout/catalogsearch_result_index.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml index 1f597a9ce1e3a..b36a7cc2347af 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml @@ -15,11 +15,9 @@ </referenceBlock> </referenceContainer> <referenceBlock name="wishlist_page_head_components"> - <block - class="Magento\Wishlist\Block\AddToWishlist" + <block class="Magento\Wishlist\Block\AddToWishlist" name="catalogsearch.wishlist_addto" - template="Magento_Wishlist::addto.phtml" - > + template="Magento_Wishlist::addto.phtml"> <arguments> <argument name="is_product_list" xsi:type="boolean">true</argument> <argument name="product_list_block" xsi:type="string">search_result_list</argument> From 75a5f2523643e89aa9226d92551021924c75e44f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szubert?= <bartlomiejszubert@gmail.com> Date: Tue, 11 Aug 2020 21:51:28 +0200 Subject: [PATCH 0212/1013] Update app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml Co-authored-by: Gabriel da Gama <gabriel@gabrielgama.com.br> --- .../Wishlist/view/frontend/layout/catalog_category_view.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml index 8b784cfd31783..c305b7c489d59 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml @@ -21,11 +21,9 @@ template="Magento_Wishlist::catalog/product/list/addto/wishlist.phtml"/> </referenceBlock> <referenceContainer name="category.product.list.additional"> - <block - class="Magento\Wishlist\Block\AddToWishlist" + <block class="Magento\Wishlist\Block\AddToWishlist" name="category.product.list.additional.wishlist_addto" - template="Magento_Wishlist::addto.phtml" - > + template="Magento_Wishlist::addto.phtml"> <arguments> <argument name="is_product_list" xsi:type="boolean">true</argument> </arguments> From 3577ab16fe9293ef897210a5a37f81badc09129b Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Tue, 11 Aug 2020 18:26:51 -0500 Subject: [PATCH 0213/1013] MC-32659: Order Details by Order Number with additional different product types --- ...rieveOrdersWithDownloadableProductTest.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index 96f5fd393a1bd..f812b8d94ae1a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -28,6 +28,7 @@ /** * Tests downloadable product fields in Orders, Invoices, CreditMemo and Shipments + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RetrieveOrdersWithDownloadableProductTest extends GraphQlAbstract { @@ -206,19 +207,18 @@ public function testGetCustomerOrdersAndCreditMemoDownloadable() ], 'items' => [ [ - 'product_name'=> 'Downloadable Product (Links can be purchased separately)', - 'product_sku' => 'downloadable-product-with-purchased-separately-links', - 'product_sale_price' => ['value' => 12], - 'discounts' => [], - 'quantity_refunded' => 1, - 'downloadable_links' => [ - [ - 'uid'=> base64_encode("downloadable/{$linkId}"), - 'title' => 'Downloadable Product Link 1'] - ] - ] + 'product_name'=> 'Downloadable Product (Links can be purchased separately)', + 'product_sku' => 'downloadable-product-with-purchased-separately-links', + 'product_sale_price' => ['value' => 12], + 'discounts' => [], + 'quantity_refunded' => 1, + 'downloadable_links' => [ + [ + 'uid'=> base64_encode("downloadable/{$linkId}"), + 'title' => 'Downloadable Product Link 1'] + ] + ] ], - 'total' => [ 'subtotal' => [ 'value' => 12 From 9d8667e9dc1e95f418b0a41627b86f6c7b5d26d8 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Wed, 12 Aug 2020 11:35:37 +0300 Subject: [PATCH 0214/1013] MC-35893: Wrong 'Time of day to send data' field rendering --- lib/internal/Magento/Framework/Data/Form/Element/Time.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Time.php b/lib/internal/Magento/Framework/Data/Form/Element/Time.php index 53d72d704483c..713c3460262c0 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Time.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Time.php @@ -115,7 +115,7 @@ public function getElementHtml() [], <<<style .select80wide { - width: 80px; + width: 80px !important; } style , From cdd8560ceef9304c59492d24391e225dd04c5476 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Wed, 12 Aug 2020 11:38:44 +0300 Subject: [PATCH 0215/1013] =?UTF-8?q?MC-35345:=20=E2=80=9CExport=20Tax=20R?= =?UTF-8?q?ates=E2=80=9D=20button=20does=20not=20work=20properly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adminhtml/templates/importExport.phtml | 2 +- .../Block/Adminhtml/Rate/ImportExportTest.php | 62 ++++++++++++++----- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml index 79d833771768d..35b2ce454d3c1 100644 --- a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml +++ b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml @@ -74,7 +74,7 @@ script; <div class="fieldset admin__field"> <span class="admin__field-label"><span><?= $block->escapeHtml(__('Export Tax Rates')) ?></span></span> <div class="admin__field-control"> - <?= $block->getButtonHtml(__('Export Tax Rates'), "this.form.submit()") ?> + <?= $block->getButtonHtml(__('Export Tax Rates'), "export_form.submit()") ?> </div> </div> <?php if ($block->getUseContainer()):?> diff --git a/dev/tests/integration/testsuite/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportTest.php b/dev/tests/integration/testsuite/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportTest.php index 314d69aa0f0b7..955f15856953e 100644 --- a/dev/tests/integration/testsuite/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportTest.php +++ b/dev/tests/integration/testsuite/Magento/TaxImportExport/Block/Adminhtml/Rate/ImportExportTest.php @@ -3,40 +3,74 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\TaxImportExport\Block\Adminhtml\Rate; -class ImportExportTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests for Tax Rate Import/Export form. + * + * @magentoAppArea adminhtml + */ +class ImportExportTest extends TestCase { /** - * @var \Magento\TaxImportExport\Block\Adminhtml\Rate\ImportExport + * @var ObjectManagerInterface */ - protected $_block = null; + private $objectManager; + /** + * @var ImportExport + */ + protected $block = null; + + /** + * @inheritdoc + */ protected function setUp(): void { - \Magento\TestFramework\Helper\Bootstrap::getInstance() - ->loadArea(\Magento\Backend\App\Area\FrontNameResolver::AREA_CODE); - $this->_block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock(\Magento\TaxImportExport\Block\Adminhtml\Rate\ImportExport::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(ImportExport::class); } + /** + * @inheritdoc + */ protected function tearDown(): void { - $this->_block = null; + $this->block = null; } - public function testCreateBlock() + /** + * @return void + */ + public function testCreateBlock(): void { - $this->assertInstanceOf(\Magento\TaxImportExport\Block\Adminhtml\Rate\ImportExport::class, $this->_block); + $this->assertInstanceOf(ImportExport::class, $this->block); } - public function testFormExists() + /** + * @return void + */ + public function testFormExists(): void { - $html = $this->_block->toHtml(); - + $html = $this->block->toHtml(); $this->assertStringContainsString('<form id="import-form"', $html); + $this->assertStringContainsString('<form id="export_form"', $html); + } + /** + * @return void + */ + public function testExportFormButtonOnClick(): void + { + $html = $this->block->toHtml(); $this->assertStringContainsString('<form id="export_form"', $html); + $this->assertStringContainsString('export_form.submit();', $html); } } From 6a170bba98446d23f9857bca270b2d39f0d2576c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 12 Aug 2020 22:42:51 +0300 Subject: [PATCH 0216/1013] MC-35458: Config fields changes --- .../Block/System/Config/Form/Fieldset.php | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) 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 7fa0fbfa44b0a..0e918c23857ac 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php @@ -96,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; } } @@ -168,11 +170,13 @@ protected function _getFrontendClass($element) */ protected function _getHeaderTitleHtml($element) { + $styleTag = $this->addVisibilityTag($element); return '<a id="' . $element->getHtmlId() . '-head" href="#' . $element->getHtmlId() . '-link">' . $element->getLegend() . '</a>' . + $styleTag . /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( 'onclick', 'event.preventDefault();' . @@ -270,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; + } } From dfd46a1436dc5a42c6d24655a3db26b5c96e7017 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 12 Aug 2020 22:45:38 +0300 Subject: [PATCH 0217/1013] MC-34254: If disable module PageBuilder then in the product page, page white --- .../Magento/Cms/Controller/PageTest.php | 68 ++++++++++++++----- .../Magento/Cms/Fixtures/page_list.php | 18 ++++- .../Cms/Fixtures/page_list_rollback.php | 13 +++- .../Magento/Framework/View/Page/Builder.php | 23 ++++++- .../View/Test/Unit/Page/BuilderTest.php | 13 +++- 5 files changed, 109 insertions(+), 26 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php index d58aa4e049b78..4d9178f1a0659 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php @@ -4,29 +4,36 @@ * 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 { - Bootstrap::getObjectManager()->configure([ + parent::setUp(); + $this->_objectManager->configure([ 'preferences' => [ - \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => - \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + CustomLayoutManagerInterface::class => CustomLayoutManager::class, ] ]); - parent::setUp(); + $this->pageRetriever = $this->_objectManager->get(GetPageByIdentifierInterface::class); } public function testViewAction() @@ -51,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()); } @@ -90,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); } @@ -111,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/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/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); } From f62f0204f0cb5a0985b6bcf3b121dc90dd764932 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 12 Aug 2020 22:49:14 +0300 Subject: [PATCH 0218/1013] MC-36615: [MFTF] AdminProductGridUrlFilterApplierTest fails because of bad design --- .../Test/AdminProductGridUrlFilterApplierTest.xml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index 0565f2d08cc1f..fea4436446da2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -18,19 +18,24 @@ <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="simpleProductWithShortNameAndSku" stepKey="createSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> </before> + <after> - <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> <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"/> + + <amOnPage url="{{AdminProductIndexPage.url}}?filters[name]=$createSimpleProduct.name$" stepKey="navigateToProductGridWithFilters"/> <waitForPageLoad stepKey="waitForProductGrid"/> - <see selector="{{AdminProductGridSection.productGridNameProduct($$createSimpleProduct.name$$)}}" userInput="$$createSimpleProduct.name$$" stepKey="seeProduct"/> + <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"/> + <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="Name: $createSimpleProduct.name$" stepKey="seeProductNameFilter"/> </test> </tests> From 50c3d197238bc651eed006179ac697a6f7f036e3 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Wed, 12 Aug 2020 18:18:43 -0500 Subject: [PATCH 0219/1013] MC-32659: Order Details by Order Number with additional different product types --- .../Sales/RetrieveOrdersWithDownloadableProductTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php index f812b8d94ae1a..2f007fb922e57 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithDownloadableProductTest.php @@ -296,7 +296,7 @@ private function getCustomersOrderQuery($orderNumber): array orders(filter:{number:{eq:"{$orderNumber}"}}) { total_count items { - id + id number order_date status @@ -314,7 +314,7 @@ private function getCustomersOrderQuery($orderNumber): array sort_order uid } - entered_options{value id} + entered_options{value label} product_sku product_name quantity_ordered From dc79f565399e4a7c7b5313e35d443bd1144719c3 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Thu, 13 Aug 2020 02:32:37 -0500 Subject: [PATCH 0220/1013] MC-35804: Admin dashboard shows data from all stores - fixed - added functional test - some improvements to functional tests --- .../AdminReloadDashboardDataActionGroup.xml | 20 +++++++++++++++++++ .../Mftf/Section/AdminDashboardSection.xml | 2 ++ .../AdminLoginWithRestrictPermissionTest.xml | 5 ++--- ...ctedUserAddCategoryFromProductPageTest.xml | 6 ++---- ...ductQuantityAndAddToTheCartActionGroup.xml | 5 +++-- ...FillCustomerSignInPopupFormActionGroup.xml | 2 +- .../AdminDeleteRoleActionGroup.xml | 7 ++++--- 7 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminReloadDashboardDataActionGroup.xml diff --git a/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminReloadDashboardDataActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminReloadDashboardDataActionGroup.xml new file mode 100644 index 0000000000000..c4ea56a09a7eb --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AdminReloadDashboardDataActionGroup.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="AdminReloadDashboardDataActionGroup"> + <annotations> + <description>Go to Admin Dashboard Page, and reload Dashboard data.</description> + </annotations> + + <amOnPage url="{{AdminDashboardPage.url}}" stepKey="amOnDashboardPage"/> + <click selector="{{AdminDashboardSection.dashboardButtonReloadData}}" stepKey="reloadDashboardData"/> + <waitForPageLoad stepKey="waitForPageToReload"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml index e67025cfa68d5..cb5704413df22 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminDashboardSection.xml @@ -17,5 +17,7 @@ <element name="dashboardDiagramAmountsContentTab" type="block" selector="#diagram_tab_amounts_content"/> <element name="dashboardDiagramTotals" type="text" selector="#diagram_tab_amounts_content"/> <element name="dashboardTotals" type="text" selector="//*[@class='dashboard-totals-label' and contains(text(), '{{columnName}}')]/../*[@class='dashboard-totals-value']" parameterized="true"/> + <element name="productInBestsellers" type="text" selector="#productsOrderedGrid_table td.col-product.col-name"/> + <element name="dashboardButtonReloadData" type="button" selector=".action-primary[title='Reload Data'][type='submit']"/> </section> </sections> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml index b3797b0720400..bc0e883bf5089 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginWithRestrictPermissionTest.xml @@ -42,9 +42,8 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsSaleRoleUser"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Delete created data--> - <actionGroup ref="AdminUserOpenAdminRolesPageActionGroup" stepKey="navigateToUserRoleGrid"/> - <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> - <argument name="role" value="adminRole"/> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteUserRole"> + <argument name="roleName" value="{{adminRole.rolename}}"/> </actionGroup> <actionGroup ref="AdminOpenAdminUsersPageActionGroup" stepKey="goToAllUsersPage"/> <actionGroup ref="AdminDeleteNewUserActionGroup" stepKey="deleteUser"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml index 6ba300b9c5b57..795c2ac77acdd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRestrictedUserAddCategoryFromProductPageTest.xml @@ -36,10 +36,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Delete created data--> <comment userInput="Delete created data" stepKey="commentDeleteCreatedData"/> - <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> - <waitForPageLoad stepKey="waitForRolesGridLoad" /> - <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteUserRole"> - <argument name="role" value="adminRole"/> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteUserRole"> + <argument name="roleName" value="{{adminRole.rolename}}"/> </actionGroup> <amOnPage url="{{AdminUsersPage.url}}" stepKey="goToAllUsersPage"/> <waitForPageLoad stepKey="waitForUsersGridLoad" /> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontEnterProductQuantityAndAddToTheCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontEnterProductQuantityAndAddToTheCartActionGroup.xml index 293d1060a6c01..053038b896a68 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontEnterProductQuantityAndAddToTheCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontEnterProductQuantityAndAddToTheCartActionGroup.xml @@ -15,10 +15,11 @@ <arguments> <argument name="quantity" type="string"/> </arguments> - + <clearField selector="{{StorefrontBundleProductActionSection.quantityField}}" stepKey="clearTheQuantityField"/> <fillField selector="{{StorefrontBundleProductActionSection.quantityField}}" userInput="{{quantity}}" stepKey="fillTheProductQuantity"/> <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickOnAddToButton"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillCustomerSignInPopupFormActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillCustomerSignInPopupFormActionGroup.xml index 71c19b6d138d1..b0ff5f001e517 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillCustomerSignInPopupFormActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/FillCustomerSignInPopupFormActionGroup.xml @@ -15,7 +15,7 @@ <arguments> <argument name="customer" type="entity"/> </arguments> - + <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible"/> <fillField selector="{{StorefrontCustomerSignInPopupFormSection.email}}" userInput="{{customer.email}}" stepKey="fillCustomerEmail"/> <fillField selector="{{StorefrontCustomerSignInPopupFormSection.password}}" userInput="{{customer.password}}" stepKey="fillCustomerPassword"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml index 46ad2e228c6c1..af923e865b3f5 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml @@ -10,12 +10,13 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminDeleteRoleActionGroup"> <annotations> - <description>Deletes a User Role.</description> + <description>Deletes a User Role. Avoid using of this ActionGroup, it duplicates existing "AdminDeleteUserRoleActionGroup"</description> </annotations> <arguments> <argument name="role" defaultValue=""/> </arguments> - + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid" /> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> <click stepKey="clickResetFilterButtonBefore" selector="{{AdminRoleGridSection.resetButton}}"/> <waitForPageLoad stepKey="waitForRolesGridFilterResetBefore" time="10"/> <fillField stepKey="TypeRoleFilter" selector="{{AdminRoleGridSection.roleNameFilterTextField}}" userInput="{{role.name}}"/> @@ -33,7 +34,7 @@ <click stepKey="clickToConfirm" selector="{{AdminDeleteRoleSection.confirm}}"/> <waitForPageLoad stepKey="waitForPageLoad" time="10"/> <see stepKey="seeSuccessMessage" userInput="You deleted the role."/> - <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <waitForPageLoad stepKey="waitForRolesGridLoadAfterDelete" /> <waitForElementVisible stepKey="waitForResetFilterButtonAfter" selector="{{AdminRoleGridSection.resetButton}}" time="10"/> <click stepKey="clickResetFilterButtonAfter" selector="{{AdminRoleGridSection.resetButton}}"/> <waitForPageLoad stepKey="waitForRolesGridFilterResetAfter" time="10"/> From b3b80e68ab9b8d1bb8263f3e306bc97f3b9fa262 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Thu, 13 Aug 2020 10:19:31 -0500 Subject: [PATCH 0221/1013] MC-31652: File system directory write update - Updated write directory class - Added integration test coverage for change --- .../Filesystem/Directory/WriteTest.php | 32 ++++++++++++++-- .../Framework/Filesystem/Directory/Write.php | 37 ++++++++++++------- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php index fb367fd557416..c01a28a9ff9bf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php @@ -7,15 +7,17 @@ */ namespace Magento\Framework\Filesystem\Directory; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem\DriverPool; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * Class ReadTest * Test for Magento\Framework\Filesystem\Directory\Read class */ -class WriteTest extends \PHPUnit\Framework\TestCase +class WriteTest extends TestCase { /** * Test data to be cleaned @@ -231,6 +233,8 @@ public function renameTargetDirProvider() * @param int $permissions * @param string $name * @param string $newName + * @throws ValidatorException + * @throws FileSystemException */ public function testCopy($basePath, $permissions, $name, $newName) { @@ -298,6 +302,8 @@ public function testCopyOutside() * @param int $permission * @param string $name * @param string $newName + * @throws FileSystemException + * @throws ValidatorException */ public function testCopyTargetDir($firstDir, $secondDir, $permission, $name, $newName) { @@ -400,6 +406,8 @@ public function testChangePermissionsRecursivelyOutside() * @param int $permissions * @param string $path * @param int $time + * @throws FileSystemException + * @throws ValidatorException */ public function testTouch($basePath, $permissions, $path, $time) { @@ -485,6 +493,8 @@ public function testIsWritableOutside() * @param int $permissions * @param string $path * @param string $mode + * @throws FileSystemException + * @throws ValidatorException */ public function testOpenFile($basePath, $permissions, $path, $mode) { @@ -536,6 +546,8 @@ public function testOpenFileOutside() * @param string $path * @param string $content * @param string $extraContent + * @throws FileSystemException + * @throws ValidatorException */ public function testWriteFile($path, $content, $extraContent) { @@ -553,6 +565,8 @@ public function testWriteFile($path, $content, $extraContent) * @param string $path * @param string $content * @param string $extraContent + * @throws FileSystemException + * @throws ValidatorException */ public function testWriteFileAppend($path, $content, $extraContent) { @@ -595,6 +609,18 @@ public function testWriteFileOutside() $this->assertEquals(3, $exceptions); } + /** + * @throws ValidatorException + */ + public function testInvalidDeletePath() + { + $this->expectException(FileSystemException::class); + $directory = $this->getDirectoryInstance('newDir', 0777); + $invalidPath = 'invalidPath/../'; + $directory->create($invalidPath); + $directory->delete($invalidPath); + } + /** * Tear down */ @@ -620,8 +646,8 @@ private function getDirectoryInstance($path, $permissions) { $fullPath = __DIR__ . '/../_files/' . $path; $objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Framework\Filesystem\Directory\WriteFactory $directoryFactory */ - $directoryFactory = $objectManager->create(\Magento\Framework\Filesystem\Directory\WriteFactory::class); + /** @var WriteFactory $directoryFactory */ + $directoryFactory = $objectManager->create(WriteFactory::class); $directory = $directoryFactory->create($fullPath, DriverPool::FILE, $permissions); $this->testDirectories[] = $directory; return $directory; diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 484eed347be0f..1d60b7ce879bf 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -8,6 +8,8 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Phrase; /** * Write Interface implementation @@ -25,14 +27,14 @@ class Write extends Read implements WriteInterface * Constructor * * @param \Magento\Framework\Filesystem\File\WriteFactory $fileFactory - * @param \Magento\Framework\Filesystem\DriverInterface $driver + * @param DriverInterface $driver * @param string $path * @param int $createPermissions * @param PathValidatorInterface|null $pathValidator */ public function __construct( \Magento\Framework\Filesystem\File\WriteFactory $fileFactory, - \Magento\Framework\Filesystem\DriverInterface $driver, + DriverInterface $driver, $path, $createPermissions = null, ?PathValidatorInterface $pathValidator = null @@ -48,13 +50,13 @@ public function __construct( * * @param string $path * @return void - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException|ValidatorException */ protected function assertWritable($path) { if ($this->isWritable($path) === false) { $path = $this->getAbsolutePath($path); - throw new FileSystemException(new \Magento\Framework\Phrase('The path "%1" is not writable.', [$path])); + throw new FileSystemException(new Phrase('The path "%1" is not writable.', [$path])); } } @@ -63,7 +65,7 @@ protected function assertWritable($path) * * @param string $path * @return void - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException */ protected function assertIsFile($path) { @@ -71,7 +73,7 @@ protected function assertIsFile($path) clearstatcache(true, $absolutePath); if (!$this->driver->isFile($absolutePath)) { throw new FileSystemException( - new \Magento\Framework\Phrase('The "%1" file doesn\'t exist.', [$absolutePath]) + new Phrase('The "%1" file doesn\'t exist.', [$absolutePath]) ); } } @@ -149,7 +151,7 @@ public function copyFile($path, $destination, WriteInterface $targetDirectory = * @param string $destination * @param WriteInterface $targetDirectory [optional] * @return bool - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException * @throws ValidatorException */ public function createSymlink($path, $destination, WriteInterface $targetDirectory = null) @@ -178,10 +180,18 @@ public function delete($path = null) { $exceptionMessages = []; $this->validatePath($path); + if (!$this->isExist($path)) { return true; } + $absolutePath = $this->driver->getAbsolutePath($this->path, $path); + $basePath = $this->driver->getRealPathSafety($this->driver->getAbsolutePath($this->path, '')); + + if ($path !== null && $path !== '' && $this->driver->getRealPathSafety($absolutePath) === $basePath) { + throw new FileSystemException(new Phrase('The path "%1" is not writable.', [$path])); + } + if ($this->driver->isFile($absolutePath)) { $this->driver->deleteFile($absolutePath); } else { @@ -198,12 +208,13 @@ public function delete($path = null) if (!empty($exceptionMessages)) { throw new FileSystemException( - new \Magento\Framework\Phrase( + new Phrase( \implode(' ', $exceptionMessages) ) ); } } + return true; } @@ -231,7 +242,7 @@ private function deleteFilesRecursively(string $path) } if (!empty($exceptionMessages)) { throw new FileSystemException( - new \Magento\Framework\Phrase( + new Phrase( \implode(' ', $exceptionMessages) ) ); @@ -297,7 +308,7 @@ public function touch($path, $modificationTime = null) * * @param string|null $path * @return bool - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException * @throws ValidatorException */ public function isWritable($path = null) @@ -313,7 +324,7 @@ public function isWritable($path = null) * @param string $path * @param string $mode * @return \Magento\Framework\Filesystem\File\WriteInterface - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException * @throws ValidatorException */ public function openFile($path, $mode = 'w') @@ -334,7 +345,7 @@ public function openFile($path, $mode = 'w') * @param string $content * @param string|null $mode * @return int The number of bytes that were written. - * @throws FileSystemException + * @throws FileSystemException|ValidatorException */ public function writeFile($path, $content, $mode = 'w+') { @@ -344,7 +355,7 @@ public function writeFile($path, $content, $mode = 'w+') /** * Get driver * - * @return \Magento\Framework\Filesystem\DriverInterface + * @return DriverInterface */ public function getDriver() { From c82ced17686aba898342654021509f197f6268bd Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Thu, 13 Aug 2020 10:46:19 -0500 Subject: [PATCH 0222/1013] MC-31652: File system directory write update - Updated writeTest docblocks --- .../Filesystem/Directory/WriteTest.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php index c01a28a9ff9bf..ca8cca878d091 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filesystem/Directory/WriteTest.php @@ -43,6 +43,8 @@ public function testInstance() * @param string $basePath * @param int $permissions * @param string $path + * @throws FileSystemException + * @throws ValidatorException */ public function testCreate($basePath, $permissions, $path) { @@ -66,6 +68,11 @@ public function createProvider() ]; } + /** + * Test for create outside + * + * @throws FileSystemException + */ public function testCreateOutside() { $exceptions = 0; @@ -93,6 +100,8 @@ public function testCreateOutside() * * @dataProvider deleteProvider * @param string $path + * @throws FileSystemException + * @throws ValidatorException */ public function testDelete($path) { @@ -113,6 +122,11 @@ public function deleteProvider() return [['subdir'], ['subdir/subsubdir']]; } + /** + * Test for delete outside + * + * @throws FileSystemException + */ public function testDeleteOutside() { $exceptions = 0; @@ -143,6 +157,8 @@ public function testDeleteOutside() * @param int $permissions * @param string $name * @param string $newName + * @throws FileSystemException + * @throws ValidatorException */ public function testRename($basePath, $permissions, $name, $newName) { @@ -166,6 +182,11 @@ public function renameProvider() return [['newDir1', 0777, 'first_name.txt', 'second_name.txt']]; } + /** + * Test for rename outside + * + * @throws FileSystemException + */ public function testRenameOutside() { $exceptions = 0; @@ -200,6 +221,8 @@ public function testRenameOutside() * @param int $permission * @param string $name * @param string $newName + * @throws FileSystemException + * @throws ValidatorException */ public function testRenameTargetDir($firstDir, $secondDir, $permission, $name, $newName) { @@ -259,6 +282,11 @@ public function copyProvider() ]; } + /** + * Test for copy outside + * + * @throws FileSystemException|ValidatorException + */ public function testCopyOutside() { $exceptions = 0; @@ -333,6 +361,8 @@ public function copyTargetDirProvider() /** * Test for changePermissions method + * + * @throws FileSystemException|ValidatorException */ public function testChangePermissions() { @@ -341,6 +371,11 @@ public function testChangePermissions() $this->assertTrue($directory->changePermissions('test_directory', 0644)); } + /** + * Test for changePermissions outside + * + * @throws FileSystemException + */ public function testChangePermissionsOutside() { $exceptions = 0; @@ -365,6 +400,8 @@ public function testChangePermissionsOutside() /** * Test for changePermissionsRecursively method + * + * @throws FileSystemException|ValidatorException */ public function testChangePermissionsRecursively() { @@ -376,6 +413,11 @@ public function testChangePermissionsRecursively() $this->assertTrue($directory->changePermissionsRecursively('test_directory', 0777, 0644)); } + /** + * Test for changePermissionsRecursively outside + * + * @throws FileSystemException + */ public function testChangePermissionsRecursivelyOutside() { $exceptions = 0; @@ -430,6 +472,11 @@ public function touchProvider() ]; } + /** + * Test for touch outside + * + * @throws FileSystemException + */ public function testTouchOutside() { $exceptions = 0; @@ -454,6 +501,8 @@ public function testTouchOutside() /** * Test isWritable method + * + * @throws FileSystemException|ValidatorException */ public function testIsWritable() { @@ -463,6 +512,11 @@ public function testIsWritable() $this->assertTrue($directory->isWritable('bar')); } + /** + * Test isWritable method outside + * + * @throws FileSystemException + */ public function testIsWritableOutside() { $exceptions = 0; @@ -517,6 +571,11 @@ public function openFileProvider() ]; } + /** + * Test for openFile outside + * + * @throws FileSystemException + */ public function testOpenFileOutside() { $exceptions = 0; @@ -587,6 +646,11 @@ public function writeFileProvider() return [['file1', '123', '456'], ['folder1/file1', '123', '456']]; } + /** + * Test for writeFile outside + * + * @throws FileSystemException + */ public function testWriteFileOutside() { $exceptions = 0; @@ -610,6 +674,8 @@ public function testWriteFileOutside() } /** + * Test for invalidDeletePath + * * @throws ValidatorException */ public function testInvalidDeletePath() @@ -623,6 +689,8 @@ public function testInvalidDeletePath() /** * Tear down + * + * @throws ValidatorException|FileSystemException */ protected function tearDown(): void { From ee98385d832b4635d183946dc7f7cc916cda13b9 Mon Sep 17 00:00:00 2001 From: Anusha Vattam <avattam@adobe.com> Date: Thu, 13 Aug 2020 14:15:13 -0500 Subject: [PATCH 0223/1013] MC-36617: Performance degradation on Gift Wrapping cart item - Added new code changes to gift message options on ce --- .../Model/Resolver/Order/GiftMessage.php | 9 ++---- .../Quote/Guest/UpdateCartItemsTest.php | 28 ++++++++++++++----- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php index aae0e3709d87f..f7dd0ca9c62f5 100644 --- a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Order/GiftMessage.php @@ -58,17 +58,14 @@ public function resolve( if (!isset($value['id'])) { throw new GraphQlInputException(__('"id" value should be specified')); } - + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $orderId = (int)base64_decode($value['id']) ?: (int)$value['id']; try { - $orderGiftMessage = $this->orderRepository->get($value['id']); + $orderGiftMessage = $this->orderRepository->get($orderId); } catch (LocalizedException $e) { throw new GraphQlInputException(__('Can\'t load gift message for order')); } - if (!isset($orderGiftMessage)) { - return null; - } - return [ 'to' => $orderGiftMessage->getRecipient() ?? '', 'from' => $orderGiftMessage->getSender() ?? '', 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 0a22f3ca9721c..703e30314ef5f 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 @@ -284,7 +284,10 @@ private function getCartQuery(string $maskedQuoteId) */ public function testUpdateGiftMessageCartForItemNotAllow() { - $query = $this->getUpdateGiftMessageQuery(); + $messageTo = ""; + $messageFrom = ""; + $message = ""; + $query = $this->getUpdateGiftMessageQuery($messageTo, $messageFrom, $message); foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { self::assertNull($item['gift_message']); } @@ -297,16 +300,27 @@ public function testUpdateGiftMessageCartForItemNotAllow() */ public function testUpdateGiftMessageCartForItem() { - $query = $this->getUpdateGiftMessageQuery(); + $messageTo = "Alex"; + $messageFrom = "Mike"; + $message = "Best regards"; + $query = $this->getUpdateGiftMessageQuery($messageTo, $messageFrom, $message); 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']); + self::assertSame('Best regards', $item['gift_message']['message']); + } + $messageTo = ""; + $messageFrom = ""; + $message = ""; + $query = $this->getUpdateGiftMessageQuery($messageTo, $messageFrom, $message); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertSame(null, $item['gift_message']); } } - private function getUpdateGiftMessageQuery() + private function getUpdateGiftMessageQuery(string $messageTo, string $messageFrom, string $message) { $quote = $this->quoteFactory->create(); $this->quoteResource->load($quote, 'test_guest_order_with_gift_message', 'reserved_order_id'); @@ -323,9 +337,9 @@ private function getUpdateGiftMessageQuery() cart_item_id: $itemId quantity: 3 gift_message: { - to: "Alex" - from: "Mike" - message: "Best regards." + to: "$messageTo" + from: "$messageFrom" + message: "$message" } } ] From fdc7c9d94e368ca138c140bdf29065c77fd008d9 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Thu, 13 Aug 2020 17:38:50 -0500 Subject: [PATCH 0224/1013] MC-35804: Admin dashboard shows data from all stores - refactored fix --- .../User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml index af923e865b3f5..868f02337dedd 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml @@ -8,9 +8,9 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminDeleteRoleActionGroup"> + <actionGroup name="AdminDeleteRoleActionGroup" deprecated="ActionGroup duplicates existing 'AdminDeleteUserRoleActionGroup'"> <annotations> - <description>Deletes a User Role. Avoid using of this ActionGroup, it duplicates existing "AdminDeleteUserRoleActionGroup"</description> + <description>DEPRECATED. Deletes a User Role.</description> </annotations> <arguments> <argument name="role" defaultValue=""/> From 99209dad85ddf95d76d70bc7b03fdabc7de78f3c Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Thu, 13 Aug 2020 21:23:51 -0500 Subject: [PATCH 0225/1013] MC-35804: Admin dashboard shows data from all stores - static fix --- .../User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml index 868f02337dedd..1fd4b2fa4fe65 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml @@ -8,9 +8,9 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminDeleteRoleActionGroup" deprecated="ActionGroup duplicates existing 'AdminDeleteUserRoleActionGroup'"> + <actionGroup name="AdminDeleteRoleActionGroup"> <annotations> - <description>DEPRECATED. Deletes a User Role.</description> + <description>DEPRECATED. Deletes a User Role. ActionGroup duplicates existing 'AdminDeleteUserRoleActionGroup'</description> </annotations> <arguments> <argument name="role" defaultValue=""/> From c8f0f10950f294e7aa4f61c2c65c4a13614e3b7b Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 14 Aug 2020 11:57:07 +0300 Subject: [PATCH 0226/1013] MC-36717: SQLSTATE[42S02]: Base table or view not found while placing order --- app/code/Magento/Sales/Model/Order.php | 31 ++++++- .../Order/Address/Collection.php | 90 +------------------ .../Service/V1/OrderAddressUpdateTest.php | 4 +- .../Magento/Sales/Service/V1/OrderGetTest.php | 2 +- .../Order/Address/CollectionTest.php | 9 +- 5 files changed, 37 insertions(+), 99 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 0af42b0a99d09..e943d6a600ec8 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -7,6 +7,7 @@ use Magento\Config\Model\Config\Source\Nooptreq; use Magento\Directory\Model\Currency; +use Magento\Directory\Model\RegionFactory; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Config\ScopeConfigInterface; @@ -307,6 +308,16 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface */ private $scopeConfig; + /** + * @var RegionFactory + */ + private $regionFactory; + + /** + * @var array + */ + private $regionItems; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -340,6 +351,7 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface * @param OrderItemRepositoryInterface $itemRepository * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param ScopeConfigInterface $scopeConfig + * @param RegionFactory $regionFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -374,7 +386,8 @@ public function __construct( ProductOption $productOption = null, OrderItemRepositoryInterface $itemRepository = null, SearchCriteriaBuilder $searchCriteriaBuilder = null, - ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + RegionFactory $regionFactory = null ) { $this->_storeManager = $storeManager; $this->_orderConfig = $orderConfig; @@ -403,6 +416,8 @@ public function __construct( $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() ->get(SearchCriteriaBuilder::class); $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->regionFactory = $regionFactory ?: ObjectManager::getInstance()->get(RegionFactory::class); + $this->regionItems = []; parent::__construct( $context, @@ -1346,9 +1361,21 @@ public function getShippingMethod($asObject = false) */ public function getAddressesCollection() { + $region = $this->regionFactory->create(); $collection = $this->_addressCollectionFactory->create()->setOrderFilter($this); if ($this->getId()) { foreach ($collection as $address) { + if (isset($this->regionItems[$address->getCountryId()][$address->getRegion()])) { + if ($this->regionItems[$address->getCountryId()][$address->getRegion()]) { + $address->setRegion($this->regionItems[$address->getCountryId()][$address->getRegion()]); + } + } else { + $region->loadByName($address->getRegion(), $address->getCountryId()); + $this->regionItems[$address->getCountryId()][$address->getRegion()] = $region->getName(); + if ($region->getName()) { + $address->setRegion($region->getName()); + } + } $address->setOrder($this); } } @@ -1818,7 +1845,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/ResourceModel/Order/Address/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php index f2a28b613cfea..5cc170c8bfa4b 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php @@ -7,15 +7,8 @@ use Magento\Sales\Api\Data\OrderAddressSearchResultInterface; 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; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\ResourceModel\Order\Address as AddressResource; /** * Order addresses collection @@ -36,44 +29,6 @@ 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 * @@ -82,21 +37,11 @@ public function __construct( protected function _construct() { $this->_init( - \Magento\Sales\Model\Order\Address::class, - \Magento\Sales\Model\ResourceModel\Order\Address::class + Address::class, + AddressResource::class ); } - /** - * @inheritdoc - */ - protected function _initSelect() - { - parent::_initSelect(); - $this->joinRegions(); - return $this; - } - /** * Redeclare after load method for dispatch event * @@ -110,31 +55,4 @@ 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/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php index c5b06285f1fe1..ef47b8819f73b 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 @@ -29,7 +29,7 @@ public function testOrderAddressUpdate() $order = $objectManager->get(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); $address = [ - OrderAddress::REGION => 'California', + OrderAddress::REGION => 'CA', OrderAddress::POSTCODE => '11111', OrderAddress::LASTNAME => 'lastname', OrderAddress::STREET => ['street'], @@ -76,7 +76,7 @@ public function testOrderAddressUpdate() $billingAddress = $actualOrder->getBillingAddress(); $validate = [ - OrderAddress::REGION => 'California', + OrderAddress::REGION => 'CA', 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 e28cca72e8fb8..021698f874e55 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' => 'California' + 'region' => 'CA' ]; $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); 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 index 52284b3c9ddf9..6d3739d4bbf51 100644 --- 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 @@ -29,11 +29,6 @@ class CollectionTest extends TestCase */ private $localeResolverMock; - /** - * @var CollectionFactory - */ - private $addressCollectionFactory; - /** * @inheritdoc */ @@ -78,8 +73,6 @@ protected function setUp(): void ->setStoreId(Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId()) ->setPayment($payment); $order->save(); - - $this->addressCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); } /** @@ -93,7 +86,7 @@ public function testCollectionWithJpLocale(): void $order = Bootstrap::getObjectManager()->create(Order::class) ->loadByIncrementId('100000001'); - $collection = $this->addressCollectionFactory->create()->setOrderFilter($order); + $collection = $order->getAddressesCollection(); foreach ($collection as $address) { $this->assertEquals('アラバマ', $address->getData(OrderAddress::REGION)); } From 0471e4e9bd296d6b4872cb450b6e95795060e928 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Fri, 14 Aug 2020 15:15:57 +0300 Subject: [PATCH 0227/1013] add MFTF test --- ...atchToProductWithOutCreatedActionGroup.xml | 43 +++++++++ ...lSwatchOptionOnCategoryPageActionGroup.xml | 19 ++++ ...orefrontCategoryPageProductInfoSection.xml | 15 +++ ...oductToWishlistCategoryPageActionGroup.xml | 24 +++++ ...tionsConfigurableProductInWishlistTest.xml | 91 +++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchToProductWithOutCreatedActionGroup.xml create mode 100644 app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup.xml create mode 100644 app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategoryPageProductInfoSection.xml create mode 100644 app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToWishlistCategoryPageActionGroup.xml create mode 100644 app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchToProductWithOutCreatedActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchToProductWithOutCreatedActionGroup.xml new file mode 100644 index 0000000000000..604ef606e94e5 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddVisualSwatchToProductWithOutCreatedActionGroup.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AddVisualSwatchToProductWithOutCreatedActionGroup"> + <annotations> + <description>Does not create an attribute. Adds the provided Visual Swatch Attribute and Options (2) to a Product on the Admin Product creation/edit page. Clicks on Save. Validates that the Success Message is present. </description> + </annotations> + <arguments> + <argument name="attribute" defaultValue="visualSwatchAttribute"/> + </arguments> + + <seeInCurrentUrl url="{{ProductCatalogPage.url}}" stepKey="seeOnProductEditPage"/> + <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="openConfigurationSection"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="openConfigurationPanel"/> + <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="waitForSlideOut"/> + + <!--Find attribute in grid and select--> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminDataGridHeaderSection.attributeCodeFilterInput}}" userInput="{{attribute.default_label}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminDataGridTableSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep1"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.selectAllByAttribute(attribute.default_label)}}" stepKey="clickSelectAll"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickNextStep2"/> + + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="100" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextStep3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <seeElement selector="{{AdminMessagesSection.success}}" stepKey="seeSaveProductMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..5722210abf211 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup.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"> + <!--Click a swatch option on product page--> + <actionGroup name="StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup"> + <arguments> + <argument name="productId" type="string"/> + <argument name="visualSwatchOptionLabel" type="string" /> + </arguments> + <click selector="{{StorefrontCategoryPageProductInfoSection.visualSwatchOption(productId,visualSwatchOptionLabel)}}" stepKey="clickSwatchOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategoryPageProductInfoSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategoryPageProductInfoSection.xml new file mode 100644 index 0000000000000..5f321c7f17603 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontCategoryPageProductInfoSection.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="StorefrontCategoryPageProductInfoSection"> + <element name="visualSwatchOption" type="button" selector="#product-item-info_{{var1}} .swatch-option[data-option-label='{{var2}}']" parameterized="true"/> + <element name="productAddToWishlist" type="button" selector="#product-item-info_{{var1}} .action.towishlist" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToWishlistCategoryPageActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToWishlistCategoryPageActionGroup.xml new file mode 100644 index 0000000000000..baa4bfcab4ebc --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToWishlistCategoryPageActionGroup.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="StorefrontCustomerAddProductToWishlistCategoryPageActionGroup"> + <annotations> + <description>Adds the provided Product to the Wish List from the Storefront Category page. Validates that the Success Message is present and correct.</description> + </annotations> + <arguments> + <argument name="productVar"/> + </arguments> + + <click selector="{{StorefrontCategoryPageProductInfoSection.productAddToWishlist(productVar.id)}}" stepKey="addProductToWishlistClickAddToWishlist"/> + <waitForElement selector="{{StorefrontCustomerWishlistSection.successMsg}}" time="30" stepKey="addProductToWishlistWaitForSuccessMessage"/> + <see selector="{{StorefrontCustomerWishlistSection.successMsg}}" userInput="{{productVar.name}} has been added to your Wish List. Click here to continue shopping." stepKey="addProductToWishlistSeeProductNameAddedToWishlist"/> + <seeCurrentUrlMatches regex="~/wishlist_id/\d+/$~" stepKey="seeCurrentUrlMatches"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml new file mode 100644 index 0000000000000..638c8f4986a77 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontCheckOptionsConfigurableProductInWishlistTest.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckOptionsConfigurableProductInWishlistTest"> + <annotations> + <stories value="Wishlist"/> + <title value="Move first Configurable Product with selected optional from Category Page to Wishlist."/> + <description value="Move first Configurable Product with selected optional from Category Page to Wishlist. On Page will be present minimum two Configurable Product"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14211"/> + <group value="wishlist"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createFirstConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiConfigurableProduct" stepKey="createSecondConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer" stepKey="customer"/> + <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> + <after> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteFirstProducts"> + <argument name="sku" value="$$createFirstConfigProduct.sku$$"/> + </actionGroup> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteSecondProducts"> + <argument name="sku" value="$$createSecondConfigProduct.sku$$"/> + </actionGroup> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute" > + <argument name="productAttributeLabel" value="{{visualSwatchAttribute.default_label}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + </after> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToFirstConfigProductPage"> + <argument name="productId" value="$$createFirstConfigProduct.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForFirstProductPageLoad"/> + <actionGroup ref="AddVisualSwatchToProductWithStorefrontConfigActionGroup" stepKey="addSwatchToFirstProduct"> + <argument name="attribute" value="visualSwatchAttribute"/> + <argument name="option1" value="visualSwatchOption1"/> + <argument name="option2" value="visualSwatchOption2"/> + </actionGroup> + + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToSecondConfigProductPage"> + <argument name="productId" value="$$createSecondConfigProduct.id$$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForSecondProductPageLoad"/> + <actionGroup ref="AddVisualSwatchToProductWithOutCreatedActionGroup" stepKey="addSwatchToSecondProduct"> + <argument name="attribute" value="visualSwatchAttribute"/> + </actionGroup> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$$customer$$"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontSelectVisualSwatchOptionOnCategoryPageActionGroup" stepKey="selectVisualSwatch"> + <argument name="productId" value="$$createFirstConfigProduct.id$$" /> + <argument name="visualSwatchOptionLabel" value="{{visualSwatchOption1.default_label}}" /> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistCategoryPageActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$$createFirstConfigProduct$$"/> + </actionGroup> + + <seeElement selector="{{StorefrontCustomerWishlistProductSection.productSeeDetailsByName($$createFirstConfigProduct.name$$)}}" stepKey="seeDetails"/> + </test> +</tests> From 2daec13511986f51576b5991639857081ae683d9 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 14 Aug 2020 15:18:41 +0300 Subject: [PATCH 0228/1013] MC-36717: SQLSTATE[42S02]: Base table or view not found while placing order --- app/code/Magento/Sales/Model/Order.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index e943d6a600ec8..f899bab8d9fc6 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -2079,7 +2079,7 @@ public function getStoreGroupName() { $storeId = $this->getStoreId(); if ($storeId === null) { - return $this->getStoreName(1); + return $this->getStoreName(); } return $this->getStore()->getGroup()->getName(); } From 59b4c536405d3617d585b6837839d0c7c1d5930e Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Fri, 14 Aug 2020 07:32:09 -0500 Subject: [PATCH 0229/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/etc/di.xml | 4 ++++ app/etc/di.xml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index c18aadd3f6a80..0d0839365cc6a 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -284,6 +284,7 @@ <item name="img" xsi:type="string">img</item> <item name="hr" xsi:type="string">hr</item> <item name="figure" xsi:type="string">figure</item> + <item name="button" xsi:type="string">button</item> </argument> <argument name="allowedAttributes" xsi:type="array"> <item name="class" xsi:type="string">class</item> @@ -302,6 +303,9 @@ <item name="img" xsi:type="array"> <item name="src" xsi:type="string">src</item> </item> + <item name="button" xsi:type="array"> + <item name="type" xsi:type="string">type</item> + </item> </argument> <argument name="attributeValidators" xsi:type="array"> <item name="style" xsi:type="object">Magento\Framework\Validator\HTML\StyleAttributeValidator</item> diff --git a/app/etc/di.xml b/app/etc/di.xml index f3dac922b5a2d..1a33ac6597a3f 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -1858,6 +1858,7 @@ <item name="img" xsi:type="string">img</item> <item name="hr" xsi:type="string">hr</item> <item name="figure" xsi:type="string">figure</item> + <item name="button" xsi:type="string">button</item> </argument> <argument name="allowedAttributes" xsi:type="array"> <item name="class" xsi:type="string">class</item> @@ -1876,6 +1877,9 @@ <item name="img" xsi:type="array"> <item name="src" xsi:type="string">src</item> </item> + <item name="button" xsi:type="array"> + <item name="type" xsi:type="string">type</item> + </item> </argument> <argument name="attributeValidators" xsi:type="array"> <item name="style" xsi:type="object">Magento\Framework\Validator\HTML\StyleAttributeValidator</item> From 3b941a60b546f0a9397a5b20738cf3d49df83aea Mon Sep 17 00:00:00 2001 From: Stanislav Idolov <sidolov@magento.com> Date: Fri, 14 Aug 2020 12:16:16 -0500 Subject: [PATCH 0230/1013] MC-36748: strpos() expects parameter 1 to be string, bool given --- lib/internal/Magento/Framework/App/ResourceConnection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/App/ResourceConnection.php b/lib/internal/Magento/Framework/App/ResourceConnection.php index 00dc88dcd7b17..3ba50fb396a4c 100644 --- a/lib/internal/Magento/Framework/App/ResourceConnection.php +++ b/lib/internal/Magento/Framework/App/ResourceConnection.php @@ -178,7 +178,7 @@ public function getTableName($modelEntity, $connectionName = self::DEFAULT_CONNE list($modelEntity, $tableSuffix) = $modelEntity; } - $tableName = $modelEntity; + $tableName = (string)$modelEntity; $mappedTableName = $this->getMappedTableName($tableName); if ($mappedTableName) { From 45bbf367765c12e8e58bc59cea47bf85dc39d428 Mon Sep 17 00:00:00 2001 From: Stanislav Idolov <sidolov@magento.com> Date: Fri, 14 Aug 2020 14:51:31 -0500 Subject: [PATCH 0231/1013] MC-36748: strpos() expects parameter 1 to be string, bool given --- .../Test/Unit/App/ResourceConnectionTest.php | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/internal/Magento/Framework/Test/Unit/App/ResourceConnectionTest.php b/lib/internal/Magento/Framework/Test/Unit/App/ResourceConnectionTest.php index 1b12d68e683a3..67d5e303c6896 100644 --- a/lib/internal/Magento/Framework/Test/Unit/App/ResourceConnectionTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/App/ResourceConnectionTest.php @@ -84,7 +84,7 @@ public function testGetTablePrefixWithInjectedPrefix() public function testGetTablePrefix() { - $this->deploymentConfigMock->expects(self::once()) + $this->deploymentConfigMock->expects($this->once()) ->method('get') ->with(ConfigOptionsListConstants::CONFIG_PATH_DB_PREFIX) ->willReturn('pref_'); @@ -93,10 +93,10 @@ public function testGetTablePrefix() public function testGetConnectionByName() { - $this->deploymentConfigMock->expects(self::once())->method('get') + $this->deploymentConfigMock->expects($this->once())->method('get') ->with(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/default') ->willReturn(['config']); - $this->connectionFactoryMock->expects(self::once())->method('create') + $this->connectionFactoryMock->expects($this->once())->method('create') ->with(['config']) ->willReturn('connection'); @@ -112,15 +112,38 @@ public function testGetExistingConnectionByName() 'connections' => ['default_process_' . getmypid() => 'existing_connection'] ] ); - $this->deploymentConfigMock->expects(self::never())->method('get'); + $this->deploymentConfigMock->expects($this->never())->method('get'); self::assertEquals('existing_connection', $unit->getConnectionByName('default')); } public function testCloseConnection() { - $this->configMock->expects(self::once())->method('getConnectionName')->with('default'); + $this->configMock->expects($this->once())->method('getConnectionName')->with('default'); $this->unit->closeConnection('default'); } + + public function testGetTableNameWithBoolParam() + { + $this->deploymentConfigMock->expects($this->at(0)) + ->method('get') + ->with(ConfigOptionsListConstants::CONFIG_PATH_DB_PREFIX) + ->willReturn('pref_'); + $this->deploymentConfigMock->expects($this->at(1))->method('get') + ->with('db/connection/default') + ->willReturn(['config']); + $this->configMock->expects($this->atLeastOnce()) + ->method('getConnectionName') + ->with('default') + ->willReturn('default'); + + $connection = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)->getMock(); + $connection->expects($this->once())->method('getTableName')->with('pref_1'); + $this->connectionFactoryMock->expects($this->once())->method('create') + ->with(['config']) + ->willReturn($connection); + + $this->unit->getTableName(true); + } } From 93b25f1773ff1a774703fc2086ad0020fa9cec81 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Mon, 17 Aug 2020 10:25:37 +0300 Subject: [PATCH 0232/1013] MC-36049: Fixed Product Tax attribute is displayed on a product page after it was unassigned from the attribute set --- .../Catalog/Model/ResourceModel/Attribute.php | 47 ++----- .../Attribute/RemoveProductAttributeData.php | 70 ++++++++++ .../Attribute/RemoveProductWeeData.php | 63 +++++++++ app/code/Magento/Weee/etc/di.xml | 3 + .../Weee/Model/Attribute/Set/SaveTest.php | 122 ++++++++++++++++++ 5 files changed, 270 insertions(+), 35 deletions(-) create mode 100644 app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php create mode 100644 app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php create mode 100644 dev/tests/integration/testsuite/Magento/Weee/Model/Attribute/Set/SaveTest.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php index 8457e5d0eaa5c..778ca5b660060 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php @@ -5,8 +5,9 @@ */ namespace Magento\Catalog\Model\ResourceModel; -use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Attribute\LockValidatorInterface; +use Magento\Catalog\Model\ResourceModel\Attribute\RemoveProductAttributeData; +use Magento\Framework\App\ObjectManager; /** * Catalog attribute resource model @@ -28,9 +29,9 @@ class Attribute extends \Magento\Eav\Model\ResourceModel\Entity\Attribute protected $attrLockValidator; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var RemoveProductAttributeData|null */ - protected $metadataPool; + private $removeProductAttributeData; /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context @@ -38,7 +39,8 @@ class Attribute extends \Magento\Eav\Model\ResourceModel\Entity\Attribute * @param \Magento\Eav\Model\ResourceModel\Entity\Type $eavEntityType * @param \Magento\Eav\Model\Config $eavConfig * @param LockValidatorInterface $lockValidator - * @param string $connectionName + * @param string|null $connectionName + * @param RemoveProductAttributeData|null $removeProductAttributeData */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -46,10 +48,14 @@ public function __construct( \Magento\Eav\Model\ResourceModel\Entity\Type $eavEntityType, \Magento\Eav\Model\Config $eavConfig, LockValidatorInterface $lockValidator, - $connectionName = null + $connectionName = null, + RemoveProductAttributeData $removeProductAttributeData = null ) { $this->attrLockValidator = $lockValidator; $this->_eavConfig = $eavConfig; + $this->removeProductAttributeData = $removeProductAttributeData ?? ObjectManager::getInstance() + ->get(RemoveProductAttributeData::class); + parent::__construct($context, $storeManager, $eavEntityType, $connectionName); } @@ -135,24 +141,7 @@ public function deleteEntity(\Magento\Framework\Model\AbstractModel $object) ); } - $backendTable = $attribute->getBackend()->getTable(); - if ($backendTable) { - $linkField = $this->getMetadataPool() - ->getMetadata(ProductInterface::class) - ->getLinkField(); - - $backendLinkField = $attribute->getBackend()->getEntityIdField(); - - $select = $this->getConnection()->select() - ->from(['b' => $backendTable]) - ->join( - ['e' => $attribute->getEntity()->getEntityTable()], - "b.$backendLinkField = e.$linkField" - )->where('b.attribute_id = ?', $attribute->getId()) - ->where('e.attribute_set_id = ?', $result['attribute_set_id']); - - $this->getConnection()->query($select->deleteFromSelect('b')); - } + $this->removeProductAttributeData->execute($object, (int)$result['attribute_set_id']); } $condition = ['entity_attribute_id = ?' => $object->getEntityAttributeId()]; @@ -160,16 +149,4 @@ public function deleteEntity(\Magento\Framework\Model\AbstractModel $object) return $this; } - - /** - * @return \Magento\Framework\EntityManager\MetadataPool - */ - private function getMetadataPool() - { - if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); - } - return $this->metadataPool; - } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php new file mode 100644 index 0000000000000..0db57fa66bdd6 --- /dev/null +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Attribute; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Model\AbstractModel; + +/** + * Class for deleting data from attribute additional table by attribute set id. + */ +class RemoveProductAttributeData +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param ResourceConnection $resourceConnection + * @param MetadataPool $metadataPool + */ + public function __construct( + ResourceConnection $resourceConnection, + MetadataPool $metadataPool + ) { + $this->resourceConnection = $resourceConnection; + $this->metadataPool = $metadataPool; + } + + /** + * Deletes data from attribute table by attribute set id. + * + * @param AbstractModel $object + * @param int $attributeSetId + * @return void + */ + public function execute(AbstractModel $object, int $attributeSetId): void + { + $backendTable = $object->getBackend()->getTable(); + if ($backendTable) { + $linkField = $this->metadataPool + ->getMetadata(ProductInterface::class) + ->getLinkField(); + + $backendLinkField = $object->getBackend()->getEntityIdField(); + + $select = $this->resourceConnection->getConnection()->select() + ->from(['b' => $backendTable]) + ->join( + ['e' => $object->getEntity()->getEntityTable()], + "b.$backendLinkField = e.$linkField" + )->where('b.attribute_id = ?', $object->getId()) + ->where('e.attribute_set_id = ?', $attributeSetId); + + $this->resourceConnection->getConnection()->query($select->deleteFromSelect('b')); + } + } +} diff --git a/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php new file mode 100644 index 0000000000000..4a113848fbb17 --- /dev/null +++ b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Weee\Plugin\Catalog\ResourceModel\Attribute; + +use Magento\Catalog\Model\ResourceModel\Attribute\RemoveProductAttributeData; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Model\AbstractModel; + +/** + * Plugin for deleting wee tax attributes data on unassigning weee attribute from attribute set. + */ +class RemoveProductWeeData +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Deletes wee tax attributes data on unassigning weee attribute from attribute set. + * + * @param RemoveProductAttributeData $subject + * @param \Closure $proceed + * @param AbstractModel $object + * @param int $attributeSetId + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundExecute( + RemoveProductAttributeData $subject, + \Closure $proceed, + AbstractModel $object, + int $attributeSetId + ) { + if ($object->getFrontendInput() == 'weee') { + $select =$this->resourceConnection->getConnection()->select() + ->from(['b' => $this->resourceConnection->getTableName('weee_tax')]) + ->join( + ['e' => $object->getEntity()->getEntityTable()], + 'b.entity_id = e.entity_id' + )->where('b.attribute_id = ?', $object->getAttributeId()) + ->where('e.attribute_set_id = ?', $attributeSetId); + + $this->resourceConnection->getConnection()->query($select->deleteFromSelect('b')); + } else { + $proceed($object, $attributeSetId); + } + } +} diff --git a/app/code/Magento/Weee/etc/di.xml b/app/code/Magento/Weee/etc/di.xml index 8b433163cad22..ccc849f4d8493 100644 --- a/app/code/Magento/Weee/etc/di.xml +++ b/app/code/Magento/Weee/etc/di.xml @@ -81,4 +81,7 @@ <type name="Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper"> <plugin name="weeeAttributeOptionsProcess" type="Magento\Weee\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper\ProcessTaxAttribute"/> </type> + <type name="Magento\Catalog\Model\ResourceModel\Attribute\RemoveProductAttributeData"> + <plugin name="removeWeeAttributesData" type="Magento\Weee\Plugin\Catalog\ResourceModel\Attribute\RemoveProductWeeData" /> + </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/Weee/Model/Attribute/Set/SaveTest.php b/dev/tests/integration/testsuite/Magento/Weee/Model/Attribute/Set/SaveTest.php new file mode 100644 index 0000000000000..628e681dfabd8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Weee/Model/Attribute/Set/SaveTest.php @@ -0,0 +1,122 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Weee\Model\Attribute\Set; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\SetRepository; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Type; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Eav\Model\ResourceModel\GetEntityIdByAttributeId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Weee\Model\ResourceModel\Tax as WeeTaxResource; +use PHPUnit\Framework\TestCase; + +/** + * Test checks that wee attributes data was deleted after unassigning wee attributes from attribute set. + * + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Weee/_files/product_with_fpt.php + */ +class SaveTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Type + */ + private $productEntityType; + + /** + * @var GetEntityIdByAttributeId + */ + private $getEntityIdByAttributeId; + + /** + * @var SetRepository + */ + private $setRepository; + + /** + * @var Config + */ + private $eavConfig; + + /** + * @var WeeTaxResource + */ + private $taxResource; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->productEntityType = $this->objectManager->get(Type::class) + ->loadByCode(ProductAttributeInterface::ENTITY_TYPE_CODE); + $this->getEntityIdByAttributeId = $this->objectManager->get(GetEntityIdByAttributeId::class); + $this->setRepository = $this->objectManager->get(SetRepository::class); + $this->eavConfig = $this->objectManager->get(Config::class); + $this->taxResource = $this->objectManager->get(WeeTaxResource::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + } + + /** + * @return void + */ + public function testSaveAttributeSet(): void + { + $fptAttribute = $this->eavConfig->getAttribute(ProductAttributeInterface::ENTITY_TYPE_CODE, 'fpt_for_all'); + $attributeSet = $this->setRepository->get($this->productEntityType->getDefaultAttributeSetId()); + $entityAttributeId = $this->getEntityIdByAttributeId->execute( + (int)$attributeSet->getAttributeSetId(), + (int)$fptAttribute->getAttributeId(), + (int)$attributeSet->getDefaultGroupId($attributeSet->getAttributeSetId()) + ); + $attributeSet->organizeData(['attribute_set_name' => 'Default', 'not_attributes' => [$entityAttributeId]]); + $this->setRepository->save($attributeSet); + $this->assertEmpty( + $this->getWeeTaxDataByAttributeAndProduct( + (int)$this->productRepository->get('simple-with-ftp')->getId(), + (int)$fptAttribute->getAttributeId() + ) + ); + } + + /** + * Loads data from wee_tax table. + * + * @param int $productId + * @param int $attributeId + * @return array + */ + private function getWeeTaxDataByAttributeAndProduct(int $productId, int $attributeId): array + { + $select = $this->taxResource->getConnection() + ->select() + ->from(['main' => $this->taxResource->getMainTable()]) + ->where('entity_id = ?', $productId) + ->where('attribute_id = ?', $attributeId); + + return $this->taxResource->getConnection()->fetchAll($select); + } +} From 5aa786c97ffbc59116b63fb96dbfae746f0bb27c Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Mon, 17 Aug 2020 11:31:55 +0300 Subject: [PATCH 0233/1013] MC-36049: Fixed Product Tax attribute is displayed on a product page after it was unassigned from the attribute set --- .../Model/ResourceModel/AttributeTest.php | 59 ++++--------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php index abe6949d87b90..6c8fffff88da5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php @@ -8,17 +8,15 @@ namespace Magento\Catalog\Test\Unit\Model\ResourceModel; -use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Attribute\LockValidatorInterface; use Magento\Catalog\Model\ResourceModel\Attribute; +use Magento\Catalog\Model\ResourceModel\Attribute\RemoveProductAttributeData; use Magento\Eav\Model\Config; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend; use Magento\Eav\Model\ResourceModel\Entity\Type; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface as Adapter; -use Magento\Framework\EntityManager\EntityMetadataInterface; -use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\ResourceConnections\DB\Select; @@ -72,9 +70,9 @@ class AttributeTest extends TestCase private $lockValidatorMock; /** - * @var EntityMetadataInterface|MockObject + * @var RemoveProductAttributeData|MockObject */ - private $entityMetaDataInterfaceMock; + private $removeProductAttributeDataMock; /** * @inheritDoc @@ -88,13 +86,7 @@ protected function setUp(): void $this->connectionMock = $this->getMockBuilder(Adapter::class) ->getMockForAbstractClass(); - $this->connectionMock->expects($this->once())->method('select')->willReturn($this->selectMock); - $this->connectionMock->expects($this->once())->method('query')->willReturn($this->selectMock); $this->connectionMock->expects($this->once())->method('delete')->willReturn($this->selectMock); - $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('join')->willReturnSelf(); - $this->selectMock->expects($this->any())->method('where')->willReturnSelf(); - $this->selectMock->expects($this->any())->method('deleteFromSelect')->willReturnSelf(); $this->resourceMock = $this->getMockBuilder(ResourceConnection::class) ->disableOriginalConstructor() @@ -117,26 +109,10 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['validate']) ->getMockForAbstractClass(); - $this->entityMetaDataInterfaceMock = $this->getMockBuilder(EntityMetadataInterface::class) + $this->removeProductAttributeDataMock = $this->getMockBuilder(RemoveProductAttributeData::class) + ->setMethods(['execute']) ->disableOriginalConstructor() - ->getMockForAbstractClass(); - } - - /** - * Sets object non-public property. - * - * @param mixed $object - * @param string $propertyName - * @param mixed $value - * - * @return void - */ - private function setObjectProperty($object, string $propertyName, $value) : void - { - $reflectionClass = new \ReflectionClass($object); - $reflectionProperty = $reflectionClass->getProperty($propertyName); - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($object, $value); + ->getMock(); } /** @@ -159,7 +135,7 @@ public function testDeleteEntity() : void $backendFieldName = 'value_id'; $attributeModel = $this->getMockBuilder(Attribute::class) - ->setMethods(['getEntityAttribute', 'getMetadataPool', 'getConnection', 'getTable']) + ->setMethods(['getEntityAttribute', 'getConnection', 'getTable']) ->setConstructorArgs([ $this->contextMock, $this->storeManagerMock, @@ -167,17 +143,12 @@ public function testDeleteEntity() : void $this->eavConfigMock, $this->lockValidatorMock, null, + $this->removeProductAttributeDataMock ])->getMock(); $attributeModel->expects($this->any()) ->method('getEntityAttribute') ->with($entityAttributeId) ->willReturn($result); - $metadataPoolMock = $this->getMockBuilder(MetadataPool::class) - ->disableOriginalConstructor() - ->setMethods(['getMetadata']) - ->getMock(); - - $this->setObjectProperty($attributeModel, 'metadataPool', $metadataPoolMock); $eavAttributeMock = $this->getMockBuilder(AbstractAttribute::class) ->disableOriginalConstructor() @@ -204,7 +175,7 @@ public function testDeleteEntity() : void $backendModelMock = $this->getMockBuilder(AbstractBackend::class) ->disableOriginalConstructor() - ->setMethods(['getBackend', 'getTable', 'getEntityIdField']) + ->setMethods(['getBackend', 'getTable']) ->getMock(); $abstractAttributeMock = $this->getMockBuilder(AbstractAttribute::class) @@ -216,16 +187,10 @@ public function testDeleteEntity() : void $eavAttributeMock->expects($this->any())->method('getEntity')->willReturn($abstractAttributeMock); $backendModelMock->expects($this->any())->method('getTable')->willReturn($backendTableName); - $backendModelMock->expects($this->once())->method('getEntityIdField')->willReturn($backendFieldName); - - $metadataPoolMock->expects($this->any()) - ->method('getMetadata') - ->with(ProductInterface::class) - ->willReturn($this->entityMetaDataInterfaceMock); - $this->entityMetaDataInterfaceMock->expects($this->any()) - ->method('getLinkField') - ->willReturn('row_id'); + $this->removeProductAttributeDataMock->expects($this->once()) + ->method('execute') + ->with($abstractModelMock, $result['attribute_set_id']); $attributeModel->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); $attributeModel->expects($this->any()) From d27b464d5f4463cd5e7dd090774f72e9e0ed33d5 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Mon, 17 Aug 2020 13:45:13 +0300 Subject: [PATCH 0234/1013] MC-36049: Fixed Product Tax attribute is displayed on a product page after it was unassigned from the attribute set --- app/code/Magento/Catalog/Model/ResourceModel/Attribute.php | 4 +--- .../ResourceModel/Attribute/RemoveProductAttributeData.php | 2 +- .../Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php | 5 ++--- .../Catalog/ResourceModel/Attribute/RemoveProductWeeData.php | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php index 778ca5b660060..203126cf1fd8c 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute.php @@ -11,8 +11,6 @@ /** * Catalog attribute resource model - * - * @author Magento Core Team <core@magentocommerce.com> */ class Attribute extends \Magento\Eav\Model\ResourceModel\Entity\Attribute { @@ -141,7 +139,7 @@ public function deleteEntity(\Magento\Framework\Model\AbstractModel $object) ); } - $this->removeProductAttributeData->execute($object, (int)$result['attribute_set_id']); + $this->removeProductAttributeData->removeData($object, (int)$result['attribute_set_id']); } $condition = ['entity_attribute_id = ?' => $object->getEntityAttributeId()]; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php index 0db57fa66bdd6..2782047902048 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Attribute/RemoveProductAttributeData.php @@ -46,7 +46,7 @@ public function __construct( * @param int $attributeSetId * @return void */ - public function execute(AbstractModel $object, int $attributeSetId): void + public function removeData(AbstractModel $object, int $attributeSetId): void { $backendTable = $object->getBackend()->getTable(); if ($backendTable) { diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php index 6c8fffff88da5..2cbee50b5f590 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/AttributeTest.php @@ -110,7 +110,7 @@ protected function setUp(): void ->setMethods(['validate']) ->getMockForAbstractClass(); $this->removeProductAttributeDataMock = $this->getMockBuilder(RemoveProductAttributeData::class) - ->setMethods(['execute']) + ->setMethods(['removeData']) ->disableOriginalConstructor() ->getMock(); } @@ -132,7 +132,6 @@ public function testDeleteEntity() : void ]; $backendTableName = 'weee_tax'; - $backendFieldName = 'value_id'; $attributeModel = $this->getMockBuilder(Attribute::class) ->setMethods(['getEntityAttribute', 'getConnection', 'getTable']) @@ -189,7 +188,7 @@ public function testDeleteEntity() : void $backendModelMock->expects($this->any())->method('getTable')->willReturn($backendTableName); $this->removeProductAttributeDataMock->expects($this->once()) - ->method('execute') + ->method('removeData') ->with($abstractModelMock, $result['attribute_set_id']); $attributeModel->expects($this->any())->method('getConnection')->willReturn($this->connectionMock); diff --git a/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php index 4a113848fbb17..b1b228296eb6b 100644 --- a/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php +++ b/app/code/Magento/Weee/Plugin/Catalog/ResourceModel/Attribute/RemoveProductWeeData.php @@ -40,7 +40,7 @@ public function __construct( * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundExecute( + public function aroundRemoveData( RemoveProductAttributeData $subject, \Closure $proceed, AbstractModel $object, From f36d5eb3fbbdf68e1a3a208086891c042a775062 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Mon, 17 Aug 2020 12:32:22 +0100 Subject: [PATCH 0235/1013] Added no store cache header to webapis requests --- .../Plugin/AppendNoStoreCacheHeader.php | 30 +++++++++++++++++++ .../Magento/PageCache/etc/webapi_rest/di.xml | 12 ++++++++ .../Magento/PageCache/etc/webapi_soap/di.xml | 12 ++++++++ 3 files changed, 54 insertions(+) create mode 100644 app/code/Magento/PageCache/Plugin/AppendNoStoreCacheHeader.php create mode 100644 app/code/Magento/PageCache/etc/webapi_rest/di.xml create mode 100644 app/code/Magento/PageCache/etc/webapi_soap/di.xml diff --git a/app/code/Magento/PageCache/Plugin/AppendNoStoreCacheHeader.php b/app/code/Magento/PageCache/Plugin/AppendNoStoreCacheHeader.php new file mode 100644 index 0000000000000..fc18855a51710 --- /dev/null +++ b/app/code/Magento/PageCache/Plugin/AppendNoStoreCacheHeader.php @@ -0,0 +1,30 @@ +<?php +/** + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PageCache\Plugin; + +use Magento\Framework\App\FrontControllerInterface; +use Magento\Framework\App\Response\HttpInterface; + +class AppendNoStoreCacheHeader +{ + /** + * Set cache-control header + * + * @param FrontControllerInterface $controller + * @param HttpInterface $response + * @return HttpInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterDispatch(FrontControllerInterface $controller, HttpInterface $response): HttpInterface + { + $response->setHeader('Cache-Control', 'no-store'); + return $response; + } +} diff --git a/app/code/Magento/PageCache/etc/webapi_rest/di.xml b/app/code/Magento/PageCache/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..04906a615a9df --- /dev/null +++ b/app/code/Magento/PageCache/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\App\FrontControllerInterface"> + <plugin name="append_no_store_cache_header" type="Magento\PageCache\Plugin\AppendNoStoreCacheHeader" /> + </type> +</config> diff --git a/app/code/Magento/PageCache/etc/webapi_soap/di.xml b/app/code/Magento/PageCache/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..04906a615a9df --- /dev/null +++ b/app/code/Magento/PageCache/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\App\FrontControllerInterface"> + <plugin name="append_no_store_cache_header" type="Magento\PageCache\Plugin\AppendNoStoreCacheHeader" /> + </type> +</config> From b2af5dbb0a4b8ac3ec1f427004eff8b4b55b2083 Mon Sep 17 00:00:00 2001 From: Daniel Renaud <drenaud@magento.com> Date: Mon, 17 Aug 2020 15:21:28 -0500 Subject: [PATCH 0236/1013] MC-36276: Internal Server error when adding a configurable product with customized option to cart --- .../Resolver/ConfigurableCartItemOptions.php | 4 + .../etc/schema.graphqls | 2 +- .../AddConfigurableProductToCartTest.php | 123 ++++++++++++++++++ ...nfigurable_with_custom_option_dropdown.php | 50 +++++++ ...e_with_custom_option_dropdown_rollback.php | 10 ++ 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown_rollback.php diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php index fed05208e0d55..6624a2624f1c3 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableCartItemOptions.php @@ -55,6 +55,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $result = []; foreach ($this->configurationHelper->getOptions($cartItem) as $option) { + if (isset($option['option_type'])) { + //Don't return customizable options in this resolver + continue; + } $result[] = [ 'id' => $option['option_id'], 'option_label' => $option['label'], diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 6e85653380acc..68ff3184dc0ee 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -58,7 +58,7 @@ input ConfigurableProductCartItemInput { } type ConfigurableCartItem implements CartItemInterface { - customizable_options: [SelectedCustomizableOption]! + customizable_options: [SelectedCustomizableOption] @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\CustomizableOptions") configurable_options: [SelectedConfigurableOption!]! @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableCartItemOptions") } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php index 519f5fef13fdc..5bc543f9f122a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartTest.php @@ -328,6 +328,94 @@ public function testOutOfStockVariationToCart() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddConfigurableProductToCartWithCustomOption() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $sku = 'configurable'; + $variantSku = 'simple_10'; + $productOptions = $this->getAvailableProductCustomOption($sku); + $optionId = $productOptions[0]['option_id']; + $optionValueId = $productOptions[0]['value'][1]['option_type_id']; + + $mutation = <<<QUERY +mutation { + addConfigurableProductsToCart(input: { + cart_id: "{$maskedQuoteId}", + cart_items: [ + { + parent_sku: "{$sku}", + variant_sku: "{$variantSku}", + data: { + sku: "{$variantSku}", + quantity: 1 + }, + customizable_options: [ + {id: {$optionId}, value_string: "{$optionValueId}"}] + } + ] + }) { + cart { + items { + id + quantity + product { + sku + name + } + ... on ConfigurableCartItem { + configurable_options { + option_label + value_label + } + customizable_options { + id + label + values{ + label + value + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($mutation); + $this->assertArrayNotHasKey('errors', $response); + $this->assertCount(1, $response['addConfigurableProductsToCart']['cart']['items']); + $item = $response['addConfigurableProductsToCart']['cart']['items'][0]; + $this->assertEquals($sku, $item['product']['sku']); + $expectedOptions = [ + 'configurable_options' => [ + [ + 'option_label' => 'Test Configurable', + 'value_label' => 'Option 1' + ] + ], + 'customizable_options' => [ + [ + 'id' => $optionId, + 'label' => 'Dropdown Options', + 'values' => [ + [ + 'label' => 'Option 2', + 'value' => $optionValueId + ] + ] + ] + ] + ]; + + $this->assertResponseFields($item['configurable_options'], $expectedOptions['configurable_options']); + $this->assertResponseFields($item['customizable_options'], $expectedOptions['customizable_options']); + } + /** * @param string $maskedQuoteId * @param string $parentSku @@ -406,4 +494,39 @@ private function getFetchProductQuery(string $term): string } QUERY; } + + /** + * Get product customizable dropdown options + * + * @param string $productSku + * @return array + */ + private function getAvailableProductCustomOption(string $productSku): array + { + $query = <<<QUERY +{ + products(filter: {sku: {eq: "${productSku}"}}) { + items { + name + ... on CustomizableProductInterface { + options { + option_id + title + ... on CustomizableDropDownOption { + value { + option_type_id + title + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['products']['items'], "No result for product with sku: '{$productSku}'"); + return $response['products']['items'][0]['options']; + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown.php new file mode 100644 index 0000000000000..cc440eb4c474f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown.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\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/product_configurable.php'); + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductCustomOptionInterfaceFactory $optionRepository */ +$optionRepository = $objectManager->get(ProductCustomOptionInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); +$product = $productRepository->get('configurable'); +$dropdownOption = [ + 'previous_group' => 'select', + 'title' => 'Dropdown Options', + 'type' => 'drop_down', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => '10.00', + 'price_type' => 'fixed', + 'sku' => 'opt1', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => '20.00', + 'price_type' => 'fixed', + 'sku' => 'opt2', + ], + ] +]; + +$createdOption = $optionRepository->create(['data' => $dropdownOption]); +$createdOption->setProductSku($product->getSku()); +$product->setOptions([$createdOption]); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown_rollback.php new file mode 100644 index 0000000000000..26e15b60dc695 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_custom_option_dropdown_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/ConfigurableProduct/_files/product_configurable_rollback.php'); From 89173faabd93790a9c4d0468366cd732da211ce3 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 18 Aug 2020 17:24:47 +0300 Subject: [PATCH 0237/1013] MC-36810: Unexpected behavior in the login page during loding --- .../view/frontend/templates/form/login.phtml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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 a1d1a0260672a..73e9ec35d51c8 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml @@ -50,14 +50,13 @@ </fieldset> </form> </div> -</div> - -<script type="text/x-magento-init"> - { - "*": { - "Magento_Customer/js/block-submit-on-send": { - "formId": "login-form" + <script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "login-form" + } } } - } -</script> + </script> +</div> From 2eaee52aeeb1bdb61d653e2a73bb91f2f8e7d1d5 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 18 Aug 2020 11:17:44 -0500 Subject: [PATCH 0238/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/Model/PageRepository.php | 25 +++++++++++-------- app/code/Magento/Cms/etc/di.xml | 2 ++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index f5c64b26c42ec..301c0efa740bd 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -17,6 +17,7 @@ use Magento\Framework\EntityManager\HydratorInterface; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Store\Model\StoreManagerInterface; @@ -162,24 +163,28 @@ private function validateLayoutUpdate(Data\PageInterface $page): void */ public function save(\Magento\Cms\Api\Data\PageInterface $page) { - if ($page->getStoreId() === null) { - $storeId = $this->storeManager->getStore()->getId(); - $page->setStoreId($storeId); - } - $pageId = $page->getId(); - if ($pageId && !($page instanceof Page && $page->getOrigData())) { - $page = $this->hydrator->hydrate($this->getById($pageId), $this->hydrator->extract($page)); - } - try { + $pageId = $page->getId(); + if ($pageId && !($page instanceof Page && $page->getOrigData())) { + $page = $this->hydrator->hydrate($this->getById($pageId), $this->hydrator->extract($page)); + } + if ($page->getStoreId() === null) { + $storeId = $this->storeManager->getStore()->getId(); + $page->setStoreId($storeId); + } $this->validateLayoutUpdate($page); $this->resource->save($page); $this->identityMap->add($page); - } catch (\Exception $exception) { + } catch (LocalizedException $exception) { throw new CouldNotSaveException( __('Could not save the page: %1', $exception->getMessage()), $exception ); + } catch (\Throwable $exception) { + throw new CouldNotSaveException( + __('Could not save the page: %1', __('Something went wrong while saving the page.')), + $exception + ); } return $page; } diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 0d0839365cc6a..2c265f881acf8 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -285,6 +285,8 @@ <item name="hr" xsi:type="string">hr</item> <item name="figure" xsi:type="string">figure</item> <item name="button" xsi:type="string">button</item> + <item name="i" xsi:type="string">i</item> + <item name="u" xsi:type="string">u</item> </argument> <argument name="allowedAttributes" xsi:type="array"> <item name="class" xsi:type="string">class</item> From 14db4adc885b0b8a2dd7254b357b452ad66c116d Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 18 Aug 2020 11:26:17 -0500 Subject: [PATCH 0239/1013] MC-36835: [Cloud] Adding new disabled products to Magento flushes categories cache --- app/code/Magento/Catalog/Model/Product.php | 31 +++++++++++++--------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 7c463267e5a58..33803f587e5c4 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductLinkRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Filesystem\DirectoryList; @@ -202,7 +203,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * Catalog product status * - * @var \Magento\Catalog\Model\Product\Attribute\Source\Status + * @var Status */ protected $_catalogProductStatus; @@ -408,7 +409,7 @@ public function __construct( \Magento\CatalogInventory\Api\Data\StockItemInterfaceFactory $stockItemFactory, \Magento\Catalog\Model\Product\OptionFactory $catalogProductOptionFactory, \Magento\Catalog\Model\Product\Visibility $catalogProductVisibility, - \Magento\Catalog\Model\Product\Attribute\Source\Status $catalogProductStatus, + Status $catalogProductStatus, \Magento\Catalog\Model\Product\Media\Config $catalogProductMediaConfig, Product\Type $catalogProductType, \Magento\Framework\Module\Manager $moduleManager, @@ -668,7 +669,7 @@ public function getTypeId() public function getStatus() { $status = $this->_getData(self::STATUS); - return $status !== null ? $status : \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED; + return $status !== null ? $status : Status::STATUS_ENABLED; } /** @@ -1103,7 +1104,7 @@ public function afterDeleteCommit() protected function _afterLoad() { if (!$this->hasData(self::STATUS)) { - $this->setData(self::STATUS, \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); + $this->setData(self::STATUS, Status::STATUS_ENABLED); } parent::_afterLoad(); return $this; @@ -1780,7 +1781,7 @@ public function isSaleable() */ public function isInStock() { - return $this->getStatus() == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED; + return $this->getStatus() == Status::STATUS_ENABLED; } /** @@ -2341,7 +2342,7 @@ public function getProductEntitiesInfo($columns = null) */ public function isDisabled() { - return $this->getStatus() == \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED; + return $this->getStatus() == Status::STATUS_DISABLED; } /** @@ -2365,17 +2366,21 @@ public function getImage() public function getIdentities() { $identities = [self::CACHE_TAG . '_' . $this->getId()]; - if ($this->getIsChangedCategories()) { - foreach ($this->getAffectedCategoryIds() as $categoryId) { - $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + + if (!$this->isObjectNew() || $this->getStatus() == Status::STATUS_ENABLED) { + if ($this->getIsChangedCategories()) { + foreach ($this->getAffectedCategoryIds() as $categoryId) { + $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + } } - } - if (($this->getOrigData('status') != $this->getData('status')) || $this->isStockStatusChanged()) { - foreach ($this->getCategoryIds() as $categoryId) { - $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + if (($this->getOrigData('status') != $this->getData('status')) || $this->isStockStatusChanged()) { + foreach ($this->getCategoryIds() as $categoryId) { + $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + } } } + if ($this->_appState->getAreaCode() == \Magento\Framework\App\Area::AREA_FRONTEND) { $identities[] = self::CACHE_TAG; } From 424a1314ebdd52d53c0ca7f5ca3391344d03dbad Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 18 Aug 2020 15:15:48 -0500 Subject: [PATCH 0240/1013] MC-36835: [Cloud] Adding new disabled products to Magento flushes categories cache --- app/code/Magento/Catalog/Model/Product.php | 4 +- .../Catalog/Test/Unit/Model/ProductTest.php | 96 ++++++++++++------- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 33803f587e5c4..04ad0c0fef1ce 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -1034,7 +1034,7 @@ public function priceReindexCallback() */ public function eavReindexCallback() { - if ($this->isObjectNew() || $this->isDataChanged($this)) { + if ($this->isObjectNew() || $this->isDataChanged()) { $this->_productEavIndexerProcessor->reindexRow($this->getEntityId()); } } @@ -1180,7 +1180,7 @@ public function getTierPrice($qty = null) /** * Get formatted by currency product price * - * @return array|double + * @return array|double * @since 102.0.6 */ public function getFormattedPrice() diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index 48a081aaeda54..c97c4f6acb7fa 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -313,10 +313,7 @@ protected function setUp(): void $contextMock = $this->createPartialMock( Context::class, - ['getEventDispatcher', 'getCacheManager', 'getAppState', 'getActionValidator'], - [], - '', - false + ['getEventDispatcher', 'getCacheManager', 'getAppState', 'getActionValidator'] ); $contextMock->expects($this->any())->method('getAppState')->willReturn($this->appStateMock); $contextMock->expects($this->any()) @@ -541,7 +538,7 @@ public function testGetStoreSingleSiteModelIds( /** * @return array */ - public function getSingleStoreIds() + public function getSingleStoreIds(): array { return [ [ @@ -619,7 +616,7 @@ public function testGetCategoryCollectionCollectionNull($initCategoryCollection, $result = $product->getCategoryCollection(); - $productIdCachedActual = $this->getPropertyValue($product, '_productIdCached', $productIdCached); + $productIdCachedActual = $this->getPropertyValue($product, '_productIdCached'); $this->assertEquals($getIdResult, $productIdCachedActual); $this->assertEquals($initCategoryCollection, $result); } @@ -627,7 +624,7 @@ public function testGetCategoryCollectionCollectionNull($initCategoryCollection, /** * @return array */ - public function getCategoryCollectionCollectionNullDataProvider() + public function getCategoryCollectionCollectionNullDataProvider(): array { return [ [ @@ -742,7 +739,7 @@ public function testReindex($productChanged, $isScheduled, $productFlatCount, $c /** * @return array */ - public function getProductReindexProvider() + public function getProductReindexProvider(): array { return [ 'set 1' => [true, false, 1, 1], @@ -774,12 +771,18 @@ public function testPriceReindexCallback() /** * @dataProvider getIdentitiesProvider * @param array $expected - * @param array $origData + * @param array|null $origData * @param array $data * @param bool $isDeleted - */ - public function testGetIdentities($expected, $origData, $data, $isDeleted = false) - { + * @param bool $isNew + */ + public function testGetIdentities( + array $expected, + ?array $origData, + array $data, + bool $isDeleted = false, + bool $isNew = false + ) { $this->model->setIdFieldName('id'); if (is_array($origData)) { foreach ($origData as $key => $value) { @@ -790,13 +793,14 @@ public function testGetIdentities($expected, $origData, $data, $isDeleted = fals $this->model->setData($key, $value); } $this->model->isDeleted($isDeleted); + $this->model->isObjectNew($isNew); $this->assertEquals($expected, $this->model->getIdentities()); } /** * @return array */ - public function getIdentitiesProvider() + public function getIdentitiesProvider(): array { $extensionAttributesMock = $this->getMockBuilder(ExtensionAttributesInterface::class) ->disableOriginalConstructor() @@ -814,60 +818,61 @@ public function getIdentitiesProvider() ['id' => 1, 'name' => 'value', 'category_ids' => [1]], ], 'new product' => $this->getNewProductProviderData(), + 'new disabled product' => $this->getNewDisabledProductProviderData(), 'status and category change' => [ [0 => 'cat_p_1', 1 => 'cat_c_p_1', 2 => 'cat_c_p_2'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 2], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_DISABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [2], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, 'affected_category_ids' => [1, 2], 'is_changed_categories' => true ], ], 'status change only' => [ [0 => 'cat_p_1', 1 => 'cat_c_p_7'], - ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => 1], - ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => 2], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_ENABLED], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_DISABLED], ], 'status changed, category unassigned' => $this->getStatusAndCategoryChangesData(), 'no status changes' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], ], 'no stock status changes' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, 'stock_data' => ['is_in_stock' => true], ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], ], 'no stock status data 1' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], ], 'no stock status data 2' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, 'stock_data' => ['is_in_stock' => true], ], ], @@ -878,16 +883,16 @@ public function getIdentitiesProvider() /** * @return array */ - private function getStatusAndCategoryChangesData() + private function getStatusAndCategoryChangesData(): array { return [ [0 => 'cat_p_1', 1 => 'cat_c_p_5'], - ['id' => 1, 'name' => 'value', 'category_ids' => [5], 'status' => 2], + ['id' => 1, 'name' => 'value', 'category_ids' => [5], 'status' => Status::STATUS_DISABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, 'is_changed_categories' => true, 'affected_category_ids' => [5] ], @@ -897,7 +902,7 @@ private function getStatusAndCategoryChangesData() /** * @return array */ - private function getNewProductProviderData() + private function getNewProductProviderData(): array { return [ ['cat_p_1', 'cat_c_p_1'], @@ -908,7 +913,30 @@ private function getNewProductProviderData() 'category_ids' => [1], 'affected_category_ids' => [1], 'is_changed_categories' => true - ] + ], + false, + true, + ]; + } + + /** + * @return array + */ + private function getNewDisabledProductProviderData(): array + { + return [ + ['cat_p_1'], + null, + [ + 'id' => 1, + 'name' => 'value', + 'category_ids' => [1], + 'status' => Status::STATUS_DISABLED, + 'affected_category_ids' => [1], + 'is_changed_categories' => true + ], + false, + true, ]; } @@ -916,16 +944,16 @@ private function getNewProductProviderData() * @param MockObject $extensionAttributesMock * @return array */ - private function getStatusStockProviderData($extensionAttributesMock) + private function getStatusStockProviderData($extensionAttributesMock): array { return [ [0 => 'cat_p_1', 1 => 'cat_c_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => 1], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => 1, + 'status' => Status::STATUS_ENABLED, 'stock_data' => ['is_in_stock' => false], ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], @@ -1440,7 +1468,7 @@ public function testGetCustomAttributes() /** * @return array */ - public function priceDataProvider() + public function priceDataProvider(): array { return [ 'receive empty array' => [[]], From 98ec18d674c8e6a3151ab99af4c980439ca7d6ff Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Tue, 18 Aug 2020 22:09:09 -0500 Subject: [PATCH 0241/1013] MC-36837: events.xml is added for quote submit succes to trigger sales --- .../Magento/GraphQl/Quote/Guest/PlaceOrderTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php index 00b960d66cfc6..f1aa4f02a6922 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -11,6 +11,7 @@ use Magento\Framework\Registry; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\OrderFactory; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -40,6 +41,11 @@ class PlaceOrderTest extends GraphQlAbstract */ private $registry; + /** + * @var OrderFactory + */ + private $orderFactory; + /** * @inheritdoc */ @@ -49,6 +55,7 @@ protected function setUp(): void $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->orderFactory = $objectManager->get(OrderFactory::class); $this->registry = Bootstrap::getObjectManager()->get(Registry::class); } @@ -80,6 +87,10 @@ public function testPlaceOrder() self::assertArrayHasKey('placeOrder', $response); self::assertArrayHasKey('order_number', $response['placeOrder']['order']); self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); + $orderIncrementId = $response['placeOrder']['order']['order_number']; + $order = $this->orderFactory->create(); + $order->loadByIncrementId($orderIncrementId); + $this->assertNotEmpty($order->getEmailSent()); } /** From e8d8886a6083bf2ec78373c1d658835e9a94cea6 Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Wed, 12 Aug 2020 21:17:09 +0800 Subject: [PATCH 0242/1013] magento/adobe-stock-integration#1742: Cover SynchronizeFilesInterface with integration - Integration test coverage and add test image with metadata --- .../Model/SynchronizeFilesTest.php | 151 ++++++++++++++++++ .../Integration/_files/magento_metadata.jpg | Bin 0 -> 34986 bytes 2 files changed, 151 insertions(+) create mode 100644 app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_metadata.jpg diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php new file mode 100644 index 0000000000000..6c4338c0935dc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php @@ -0,0 +1,151 @@ +<?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\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for SynchronizeFiles. + */ +class SynchronizeFilesTest extends TestCase +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPath; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->synchronizeFiles = Bootstrap::getObjectManager()->get(SynchronizeFilesInterface::class); + $this->getAssetsByPath = Bootstrap::getObjectManager()->get(GetAssetsByPathsInterface::class); + $this->getAssetKeywords = Bootstrap::getObjectManager()->get(GetAssetsKeywordsInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * Test for SynchronizeFiles::execute + * + * @dataProvider filesProvider + * @param null|string $file + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @throws FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testExecute( + ?string $file, + ?string $title, + ?string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../_files/' . $file); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($file); + $this->driver->copy( + $path, + $modifiableFilePath + ); + + $this->synchronizeFiles->execute([$file]); + + $loadedAssets = $this->getAssetsByPath->execute([$file])[0]; + $loadedKeywords = $this->getKeywords($loadedAssets) ?: null; + + $this->assertEquals($title, $loadedAssets->getTitle()); + $this->assertEquals($description, $loadedAssets->getDescription()); + $this->assertEquals($keywords, $loadedKeywords); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + '/magento.jpg', + 'magento', + null, + null + ], + [ + '/magento_metadata.jpg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ] + ]; + } + + /** + * 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 + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_metadata.jpg b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_metadata.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6dc8cd69e41c1ed0fcdbd38370c3b5b90d6c3ded GIT binary patch literal 34986 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 literal 0 HcmV?d00001 From 21819fe47d7ff332ab884ab8868e18e2365252f1 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Sun, 9 Aug 2020 17:47:50 +0100 Subject: [PATCH 0243/1013] magento/adobe-stock-integration#1726: Simplified AssetDetailsProvider implementation --- .../etc/adminhtml/di.xml | 2 +- .../MediaGalleryCmsUi/etc/adminhtml/di.xml | 2 +- .../Model/AssetDetailsProvider/CreatedAt.php | 59 --------- .../Model/AssetDetailsProvider/Height.php | 33 ----- .../Model/AssetDetailsProvider/Size.php | 49 -------- .../Model/AssetDetailsProvider/Type.php | 59 --------- .../Model/AssetDetailsProvider/UpdatedAt.php | 59 --------- .../Model/AssetDetailsProvider/UsedIn.php | 113 ----------------- .../Model/AssetDetailsProvider/Width.php | 33 ----- .../Model/AssetDetailsProviderInterface.php | 24 ---- .../Model/AssetDetailsProviderPool.php | 46 ------- .../MediaGalleryUi/Model/GetAssetDetails.php | 102 +++++++++++++++ .../Model/GetAssetUsageDetails.php | 119 ++++++++++++++++++ .../Model/GetDetailsByAssetId.php | 12 +- app/code/Magento/MediaGalleryUi/etc/di.xml | 20 --- 15 files changed, 229 insertions(+), 503 deletions(-) delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php delete mode 100644 app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php create mode 100644 app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml index 500ac10f4745a..ae01c29928b4a 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml @@ -24,7 +24,7 @@ <argument name="entityType" xsi:type="string">catalog_category</argument> </arguments> </virtualType> - <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <type name="Magento\MediaGalleryUi\Model\GetAssetUsageDetails"> <arguments> <argument name="contentTypes" xsi:type="array"> <item name="catalog_category" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml index b06ad0fff1df6..65ed3b7197f83 100644 --- a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml @@ -24,7 +24,7 @@ <argument name="entityType" xsi:type="string">cms_block</argument> </arguments> </virtualType> - <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <type name="Magento\MediaGalleryUi\Model\GetAssetUsageDetails"> <arguments> <argument name="contentTypes" xsi:type="array"> <item name="cms_block" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php deleted file mode 100644 index 7c3eccfea521f..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php +++ /dev/null @@ -1,59 +0,0 @@ -<?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 deleted file mode 100644 index b2b0f389f6b9a..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php +++ /dev/null @@ -1,33 +0,0 @@ -<?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 deleted file mode 100644 index 55841cc5abd3f..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php +++ /dev/null @@ -1,49 +0,0 @@ -<?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 deleted file mode 100644 index 5b47616398ef7..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php +++ /dev/null @@ -1,59 +0,0 @@ -<?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 deleted file mode 100644 index 2f50bd9a72208..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php +++ /dev/null @@ -1,59 +0,0 @@ -<?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 deleted file mode 100644 index ca3883d5c937c..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php +++ /dev/null @@ -1,113 +0,0 @@ -<?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 deleted file mode 100644 index 64e9cf8ad1a8f..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php +++ /dev/null @@ -1,33 +0,0 @@ -<?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 deleted file mode 100644 index 92375adfdd4f2..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php +++ /dev/null @@ -1,24 +0,0 @@ -<?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 deleted file mode 100644 index 207f35bb99d6a..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php +++ /dev/null @@ -1,46 +0,0 @@ -<?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/GetAssetDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php new file mode 100644 index 0000000000000..469d62646292e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.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\Model; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +class GetAssetDetails +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @var GetAssetUsageDetails + */ + private $getAssetUsageDetails; + + /** + * @param GetAssetUsageDetails $getAssetUsageDetails + * @param TimezoneInterface $dateTime + */ + public function __construct( + GetAssetUsageDetails $getAssetUsageDetails, + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + $this->getAssetUsageDetails = $getAssetUsageDetails; + } + + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array + { + $details = [ + [ + 'title' => __('Type'), + 'value' => __('Asset'), + ], + [ + 'title' => __('Created'), + 'value' => $this->formatDate($asset->getCreatedAt()) + ], + [ + 'title' => __('Modified'), + 'value' => $this->formatDate($asset->getUpdatedAt()) + ], + [ + 'title' => __('Width'), + 'value' => sprintf('%spx', $asset->getWidth()) + ], + [ + 'title' => __('Height'), + 'value' => sprintf('%spx', $asset->getHeight()) + ], + [ + 'title' => __('Size'), + 'value' => $this->formatSize($asset->getSize()) + ], + [ + 'title' => __('Used In'), + 'value' => $this->getAssetUsageDetails->execute($asset->getId()) + ] + ]; + return $details; + } + + /** + * Format image size + * + * @param int $imageSize + * @return string + */ + private function formatSize(int $imageSize): string + { + return $imageSize === 0 ? '' : sprintf('%sKb', $imageSize / 1000); + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php new file mode 100644 index 0000000000000..30b58667a6244 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.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\Model; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; + +/** + * Provide information on which content asset is used in + */ +class GetAssetUsageDetails +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContent; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @var array + */ + private $contentTypes; + + /** + * @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 int $id + * @return array + * @throws IntegrationException + */ + public function execute(int $id): array + { + $details = []; + + foreach ($this->getUsageByEntities($id) as $type => $entities) { + $details[] = [ + 'name' => $this->getName($type), + 'number' => count($entities), + 'link' => $this->getLinkUrl($type) + ]; + } + + return $details; + } + + /** + * Retrieve the type name from content types configuration + * + * @param string $type + * @return string + */ + private function getName(string $type): string + { + if (isset($this->contentTypes[$type]) && !empty($this->contentTypes[$type]['name'])) { + return $this->contentTypes[$type]['name']; + } + return $type; + } + + /** + * Retrieve the type link from content types configuration + * + * @param string $type + * @return string|null + */ + private function getLinkUrl(string $type): ?string + { + if (isset($this->contentTypes[$type]) && !empty($this->contentTypes[$type]['link'])) { + return $this->contentTypes[$type]['link']; + } + return null; + } + + /** + * Get used in counts per type + * + * @param int $assetId + * @return int[] + * @throws IntegrationException + */ + private function getUsageByEntities(int $assetId): array + { + $usage = []; + + foreach ($this->getContent->execute([$assetId]) as $contentIdentity) { + $id = $contentIdentity->getEntityId(); + $type = $contentIdentity->getEntityType(); + $usage[$type][$id] = isset($usage[$type][$id]) ? $usage[$type][$id]++ : 0; + } + + return $usage; + } +} + diff --git a/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php index b870082ea2aa1..f6972637b3610 100644 --- a/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php +++ b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php @@ -44,25 +44,25 @@ class GetDetailsByAssetId private $getAssetKeywords; /** - * @var AssetDetailsProviderPool + * @var GetAssetDetails */ - private $detailsProviderPool; + private $getAssetDetails; /** - * @param AssetDetailsProviderPool $detailsProviderPool + * @param GetAssetDetails $getAssetDetails * @param GetAssetsByIdsInterface $getAssetById * @param StoreManagerInterface $storeManager * @param SourceIconProvider $sourceIconProvider * @param GetAssetsKeywordsInterface $getAssetKeywords */ public function __construct( - AssetDetailsProviderPool $detailsProviderPool, + GetAssetDetails $getAssetDetails, GetAssetsByIdsInterface $getAssetById, StoreManagerInterface $storeManager, SourceIconProvider $sourceIconProvider, GetAssetsKeywordsInterface $getAssetKeywords ) { - $this->detailsProviderPool = $detailsProviderPool; + $this->getAssetDetails = $getAssetDetails; $this->getAssetsById = $getAssetById; $this->storeManager = $storeManager; $this->sourceIconProvider = $sourceIconProvider; @@ -89,7 +89,7 @@ public function execute(array $assetIds): array 'path' => $asset->getPath(), 'description' => $asset->getDescription(), 'id' => $asset->getId(), - 'details' => $this->detailsProviderPool->execute($asset), + 'details' => $this->getAssetDetails->execute($asset), 'size' => $asset->getSize(), 'tags' => $this->getKeywords($asset), 'source' => $asset->getSource() ? diff --git a/app/code/Magento/MediaGalleryUi/etc/di.xml b/app/code/Magento/MediaGalleryUi/etc/di.xml index 56ccf7c1aa727..a8c4e2a8d8963 100644 --- a/app/code/Magento/MediaGalleryUi/etc/di.xml +++ b/app/code/Magento/MediaGalleryUi/etc/di.xml @@ -33,27 +33,7 @@ <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> From 59f11118d1640c6c9586104a15a5338e24e03f56 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 10 Aug 2020 11:56:58 +0100 Subject: [PATCH 0244/1013] magento/magento2#29449: Corrected file size formatting --- app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php index 469d62646292e..853f256e10615 100644 --- a/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php @@ -81,12 +81,12 @@ public function execute(AssetInterface $asset): array /** * Format image size * - * @param int $imageSize + * @param int $size * @return string */ - private function formatSize(int $imageSize): string + private function formatSize(int $size): string { - return $imageSize === 0 ? '' : sprintf('%sKb', $imageSize / 1000); + return $size === 0 ? '' : sprintf('%.2f KB', $size / 1024); } /** From 751e8ae9a97c1b852bfa1607589a8cebeccd22e3 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 10 Aug 2020 14:52:27 +0100 Subject: [PATCH 0245/1013] magento/magento2#29449: Corrected link url retrieval --- app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php index 30b58667a6244..67a95f4404279 100644 --- a/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php @@ -91,7 +91,7 @@ private function getName(string $type): string private function getLinkUrl(string $type): ?string { if (isset($this->contentTypes[$type]) && !empty($this->contentTypes[$type]['link'])) { - return $this->contentTypes[$type]['link']; + return $this->url->getUrl($this->contentTypes[$type]['link']); } return null; } From d2690b3696dbe5a9515893bf2fa93a1f7f6a33a7 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 11 Aug 2020 12:51:29 +0100 Subject: [PATCH 0246/1013] magento/magento2#29449: Fixed tests --- app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php | 2 +- app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php index 853f256e10615..88bd5cf96e534 100644 --- a/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetDetails.php @@ -48,7 +48,7 @@ public function execute(AssetInterface $asset): array $details = [ [ 'title' => __('Type'), - 'value' => __('Asset'), + 'value' => __('Image'), ], [ 'title' => __('Created'), diff --git a/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php index 67a95f4404279..1dd8b736a9c90 100644 --- a/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php +++ b/app/code/Magento/MediaGalleryUi/Model/GetAssetUsageDetails.php @@ -116,4 +116,3 @@ private function getUsageByEntities(int $assetId): array return $usage; } } - From f0d92e725653b7ad24d31b650c1cf2233748251d Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 12 Aug 2020 15:23:58 +0300 Subject: [PATCH 0247/1013] Extract images from content only when new media gallery enabled magento/adobe-stock-integration#1750 --- .../Magento/MediaContentApi/Model/Config.php | 47 +++++++++++++++++++ .../MediaContentCatalog/Observer/Category.php | 13 +++++ .../Observer/CategoryDelete.php | 19 ++++++-- .../MediaContentCatalog/Observer/Product.php | 15 +++++- .../Observer/ProductDelete.php | 19 ++++++-- .../MediaContentCms/Observer/Block.php | 12 +++++ .../MediaContentCms/Observer/BlockDelete.php | 21 +++++++-- .../Magento/MediaContentCms/Observer/Page.php | 13 +++++ .../MediaContentCms/Observer/PageDelete.php | 23 +++++++-- 9 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 app/code/Magento/MediaContentApi/Model/Config.php diff --git a/app/code/Magento/MediaContentApi/Model/Config.php b/app/code/Magento/MediaContentApi/Model/Config.php new file mode 100644 index 0000000000000..dd3b30124cdd9 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/Config.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Class responsible to provide access to system configuration related to the Media Gallery + */ +class Config +{ + /** + * 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 new media gallery emabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/Category.php b/app/code/Magento/MediaContentCatalog/Observer/Category.php index 5c2deeab258df..53afae53a2aa7 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/Category.php +++ b/app/code/Magento/MediaContentCatalog/Observer/Category.php @@ -13,6 +13,7 @@ use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the catalog_category_save_after event and run processing relation between category content and media asset. @@ -29,6 +30,11 @@ class Category implements ObserverInterface */ private $updateContentAssetLinks; + /** + * @var Config + */ + private $config; + /** * @var array */ @@ -50,17 +56,20 @@ class Category implements ObserverInterface * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param GetEntityContentsInterface $getContent * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, GetEntityContentsInterface $getContent, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; $this->getContent = $getContent; $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->config = $config; $this->fields = $fields; } @@ -72,6 +81,10 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $model = $observer->getEvent()->getData('category'); if ($model instanceof CatalogCategory) { diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php index 1565d455cc43f..2a94a2d3963a3 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php +++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php @@ -15,6 +15,7 @@ use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the catalog_category_delete_after event and deletes relation between category content and media asset. @@ -25,7 +26,7 @@ class CategoryDelete implements ObserverInterface private const TYPE = 'entityType'; private const ENTITY_ID = 'entityId'; private const FIELD = 'field'; - + /** * @var ContentIdentityInterfaceFactory */ @@ -51,17 +52,23 @@ class CategoryDelete implements ObserverInterface */ private $getContent; + /** + * @var Config + */ + private $config; + /** * @var ExtractAssetsFromContentInterface */ private $extractAssetsFromContent; - + /** * @param ExtractAssetsFromContentInterface $extractAssetsFromContent * @param GetEntityContentsInterface $getContent * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config * @param array $fields */ public function __construct( @@ -70,6 +77,7 @@ public function __construct( DeleteContentAssetLinksInterface $deleteContentAssetLinks, ContentIdentityInterfaceFactory $contentIdentityFactory, ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, array $fields ) { $this->extractAssetsFromContent = $extractAssetsFromContent; @@ -77,6 +85,7 @@ public function __construct( $this->deleteContentAssetLinks = $deleteContentAssetLinks; $this->contentAssetLinkFactory = $contentAssetLinkFactory; $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; $this->fields = $fields; } @@ -88,9 +97,13 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $category = $observer->getEvent()->getData('category'); $contentAssetLinks = []; - + if ($category instanceof CatalogCategory) { foreach ($this->fields as $field) { $contentIdentity = $this->contentIdentityFactory->create( diff --git a/app/code/Magento/MediaContentCatalog/Observer/Product.php b/app/code/Magento/MediaContentCatalog/Observer/Product.php index 306bcc0b466c2..b7f52d95068fb 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/Product.php +++ b/app/code/Magento/MediaContentCatalog/Observer/Product.php @@ -13,6 +13,7 @@ use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the catalog_product_save_after event and run processing relation between product content and media asset @@ -34,6 +35,11 @@ class Product implements ObserverInterface */ private $fields; + /** + * @var Config + */ + private $config; + /** * @var ContentIdentityInterfaceFactory */ @@ -45,22 +51,25 @@ class Product implements ObserverInterface private $getContent; /** - * * Create links for product content + * Product observer constructor * * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param GetEntityContentsInterface $getContent * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, GetEntityContentsInterface $getContent, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; $this->getContent = $getContent; $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->config = $config; $this->fields = $fields; } @@ -72,6 +81,10 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $model = $observer->getEvent()->getData('product'); if ($model instanceof CatalogProduct) { foreach ($this->fields as $field) { diff --git a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php index 421bb5a33fa1d..38622178e2119 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php +++ b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php @@ -15,6 +15,7 @@ use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the catalog_product_delete_before event and deletes relation between category content and media asset. @@ -25,7 +26,7 @@ class ProductDelete implements ObserverInterface private const TYPE = 'entityType'; private const ENTITY_ID = 'entityId'; private const FIELD = 'field'; - + /** * @var ContentIdentityInterfaceFactory */ @@ -51,17 +52,23 @@ class ProductDelete implements ObserverInterface */ private $getContent; + /** + * @var Config + */ + private $config; + /** * @var ExtractAssetsFromContentInterface */ private $extractAssetsFromContent; - + /** * @param ExtractAssetsFromContentInterface $extractAssetsFromContent * @param GetEntityContentsInterface $getContent * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config * @param array $fields */ public function __construct( @@ -70,6 +77,7 @@ public function __construct( DeleteContentAssetLinksInterface $deleteContentAssetLinks, ContentIdentityInterfaceFactory $contentIdentityFactory, ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, array $fields ) { $this->extractAssetsFromContent = $extractAssetsFromContent; @@ -77,6 +85,7 @@ public function __construct( $this->deleteContentAssetLinks = $deleteContentAssetLinks; $this->contentAssetLinkFactory = $contentAssetLinkFactory; $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; $this->fields = $fields; } @@ -88,9 +97,13 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $product = $observer->getEvent()->getData('product'); $contentAssetLinks = []; - + if ($product instanceof CatalogProduct) { foreach ($this->fields as $field) { $contentIdentity = $this->contentIdentityFactory->create( diff --git a/app/code/Magento/MediaContentCms/Observer/Block.php b/app/code/Magento/MediaContentCms/Observer/Block.php index ccd1abb98bc60..fadf1b8918c90 100644 --- a/app/code/Magento/MediaContentCms/Observer/Block.php +++ b/app/code/Magento/MediaContentCms/Observer/Block.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\ObserverInterface; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Model\Config; /** * Observe cms_block_save_after event and run processing relation between cms block content and media asset @@ -28,6 +29,11 @@ class Block implements ObserverInterface */ private $updateContentAssetLinks; + /** + * @var Config + */ + private $config; + /** * @var array */ @@ -41,15 +47,18 @@ class Block implements ObserverInterface /** * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->config = $config; $this->fields = $fields; } @@ -60,6 +69,9 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } $model = $observer->getEvent()->getData('object'); if ($model instanceof CmsBlock) { diff --git a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php index 582f0a9ec6701..b9be5c54d79bd 100644 --- a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php +++ b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php @@ -15,6 +15,7 @@ use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the adminhtml_cmspage_on_delete event and deletes relation between page content and media asset. @@ -25,7 +26,12 @@ class BlockDelete implements ObserverInterface private const TYPE = 'entityType'; private const ENTITY_ID = 'entityId'; private const FIELD = 'field'; - + + /** + * @var Config + */ + private $config; + /** * @var ContentIdentityInterfaceFactory */ @@ -55,13 +61,14 @@ class BlockDelete implements ObserverInterface * @var ExtractAssetsFromContentInterface */ private $extractAssetsFromContent; - + /** * @param ExtractAssetsFromContentInterface $extractAssetsFromContent * @param GetEntityContentsInterface $getContent * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config * @param array $fields */ public function __construct( @@ -70,6 +77,7 @@ public function __construct( DeleteContentAssetLinksInterface $deleteContentAssetLinks, ContentIdentityInterfaceFactory $contentIdentityFactory, ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, array $fields ) { $this->extractAssetsFromContent = $extractAssetsFromContent; @@ -77,6 +85,7 @@ public function __construct( $this->deleteContentAssetLinks = $deleteContentAssetLinks; $this->contentAssetLinkFactory = $contentAssetLinkFactory; $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; $this->fields = $fields; } @@ -88,9 +97,13 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $block = $observer->getEvent()->getData('object'); $contentAssetLinks = []; - + if ($block instanceof CmsBlock) { foreach ($this->fields as $field) { $contentIdentity = $this->contentIdentityFactory->create( @@ -101,7 +114,7 @@ public function execute(Observer $observer): void ] ); $assets = $this->extractAssetsFromContent->execute((string) $block->getData($field)); - + foreach ($assets as $asset) { $contentAssetLinks[] = $this->contentAssetLinkFactory->create( [ diff --git a/app/code/Magento/MediaContentCms/Observer/Page.php b/app/code/Magento/MediaContentCms/Observer/Page.php index 4c0ed5c628d1c..ef83d6bb58718 100644 --- a/app/code/Magento/MediaContentCms/Observer/Page.php +++ b/app/code/Magento/MediaContentCms/Observer/Page.php @@ -12,6 +12,7 @@ use Magento\Framework\Event\ObserverInterface; use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Model\Config; /** * Observe cms_page_save_after event and run processing relation between cms page content and media asset. @@ -28,6 +29,11 @@ class Page implements ObserverInterface */ private $updateContentAssetLinks; + /** + * @var Config + */ + private $config; + /** * @var array */ @@ -41,15 +47,18 @@ class Page implements ObserverInterface /** * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param Config $config * @param array $fields */ public function __construct( ContentIdentityInterfaceFactory $contentIdentityFactory, UpdateContentAssetLinksInterface $updateContentAssetLinks, + Config $config, array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->config = $config; $this->fields = $fields; } @@ -60,6 +69,10 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $model = $observer->getEvent()->getData('object'); if ($model instanceof CmsPage) { diff --git a/app/code/Magento/MediaContentCms/Observer/PageDelete.php b/app/code/Magento/MediaContentCms/Observer/PageDelete.php index 96d2bf89873bd..e3e59f0991a75 100644 --- a/app/code/Magento/MediaContentCms/Observer/PageDelete.php +++ b/app/code/Magento/MediaContentCms/Observer/PageDelete.php @@ -15,6 +15,7 @@ use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; use Magento\MediaContentApi\Model\GetEntityContentsInterface; use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; +use Magento\MediaContentApi\Model\Config; /** * Observe the cms_page_delete_before event and deletes relation between page content and media asset. @@ -25,7 +26,7 @@ class PageDelete implements ObserverInterface private const TYPE = 'entityType'; private const ENTITY_ID = 'entityId'; private const FIELD = 'field'; - + /** * @var ContentIdentityInterfaceFactory */ @@ -55,13 +56,19 @@ class PageDelete implements ObserverInterface * @var ExtractAssetsFromContentInterface */ private $extractAssetsFromContent; - + + /** + * @var Config + */ + private $config; + /** * @param ExtractAssetsFromContentInterface $extractAssetsFromContent * @param GetEntityContentsInterface $getContent * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks * @param ContentIdentityInterfaceFactory $contentIdentityFactory * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param Config $config * @param arry $fields */ public function __construct( @@ -70,6 +77,7 @@ public function __construct( DeleteContentAssetLinksInterface $deleteContentAssetLinks, ContentIdentityInterfaceFactory $contentIdentityFactory, ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + Config $config, array $fields ) { $this->extractAssetsFromContent = $extractAssetsFromContent; @@ -77,6 +85,7 @@ public function __construct( $this->deleteContentAssetLinks = $deleteContentAssetLinks; $this->contentAssetLinkFactory = $contentAssetLinkFactory; $this->contentIdentityFactory = $contentIdentityFactory; + $this->config = $config; $this->fields = $fields; } @@ -88,9 +97,13 @@ public function __construct( */ public function execute(Observer $observer): void { + if (!$this->config->isEnabled()) { + return; + } + $page = $observer->getEvent()->getData('object'); $contentAssetLinks = []; - + if ($page instanceof CmsPage) { foreach ($this->fields as $field) { $contentIdentity = $this->contentIdentityFactory->create( @@ -100,9 +113,9 @@ public function execute(Observer $observer): void self::ENTITY_ID => (string) $page->getId(), ] ); - + $assets = $this->extractAssetsFromContent->execute((string) $page->getData($field)); - + foreach ($assets as $asset) { $contentAssetLinks[] = $this->contentAssetLinkFactory->create( [ From 49686821fde792716e55f6abdd0560677667b753 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 12 Aug 2020 15:31:59 +0300 Subject: [PATCH 0248/1013] Fix typo --- app/code/Magento/MediaContentApi/Model/Config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaContentApi/Model/Config.php b/app/code/Magento/MediaContentApi/Model/Config.php index dd3b30124cdd9..ab2b4e7ae0dbe 100644 --- a/app/code/Magento/MediaContentApi/Model/Config.php +++ b/app/code/Magento/MediaContentApi/Model/Config.php @@ -36,7 +36,7 @@ public function __construct(ScopeConfigInterface $scopeConfig) } /** - * Check if new media gallery emabled + * Check if new media gallery enabled * * @return bool */ From 03d87037778d7356f762c45a106d8ef1043aa918 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 12 Aug 2020 17:08:42 +0300 Subject: [PATCH 0249/1013] Fix static tests cover changes with integration tests --- .../Observer/CategoryDelete.php | 2 +- .../MediaContentCms/Observer/Block.php | 2 +- .../GetAssetIdsByContentFieldTest.php | 19 ++++++++++++++++++- .../_files/product_with_asset.php | 4 ---- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php index 2a94a2d3963a3..8722f6568310c 100644 --- a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php +++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php @@ -82,10 +82,10 @@ public function __construct( ) { $this->extractAssetsFromContent = $extractAssetsFromContent; $this->getContent = $getContent; + $this->config = $config; $this->deleteContentAssetLinks = $deleteContentAssetLinks; $this->contentAssetLinkFactory = $contentAssetLinkFactory; $this->contentIdentityFactory = $contentIdentityFactory; - $this->config = $config; $this->fields = $fields; } diff --git a/app/code/Magento/MediaContentCms/Observer/Block.php b/app/code/Magento/MediaContentCms/Observer/Block.php index fadf1b8918c90..48db872f4056f 100644 --- a/app/code/Magento/MediaContentCms/Observer/Block.php +++ b/app/code/Magento/MediaContentCms/Observer/Block.php @@ -57,8 +57,8 @@ public function __construct( array $fields ) { $this->contentIdentityFactory = $contentIdentityFactory; - $this->updateContentAssetLinks = $updateContentAssetLinks; $this->config = $config; + $this->updateContentAssetLinks = $updateContentAssetLinks; $this->fields = $fields; } diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php index 2194200181729..1b08c7b55769a 100644 --- a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php @@ -43,6 +43,7 @@ protected function setUp(): void * Test for getting asset id by category fields * * @dataProvider dataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php * @@ -63,9 +64,9 @@ public function testCategoryFields(string $field, string $value, array $expected * Test for getting asset id by product fields * * @dataProvider dataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php - * * @param string $field * @param string $value * @param array $expectedAssetIds @@ -79,6 +80,22 @@ public function testProductFields(string $field, string $value, array $expectedA ); } + /** + * Test for getting asset when media gallery disabled + * + * @magentoConfigFixture system/media_gallery/enabled 0 + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @throws InvalidArgumentException + */ + public function testProductFieldsWithDisabledMediaGallery(): void + { + $this->assertEquals( + [], + $this->getAssetIdsByContentField->execute(self::STATUS_FIELD, self::STATUS_ENABLED) + ); + } + /** * Data provider for tests * diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php index 2a1177661572a..2d54b5fa862ea 100644 --- a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/_files/product_with_asset.php @@ -4,8 +4,6 @@ * See COPYING.txt for license details. */ -use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; -use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; @@ -14,8 +12,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; -Bootstrap::getInstance()->reinitialize(); - /** @var ObjectManager $objectManager */ $objectManager = Bootstrap::getObjectManager(); From dc6c24968e94226631a455760af6f5c16443d07d Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 12 Aug 2020 19:29:40 +0300 Subject: [PATCH 0250/1013] Correct integration tests --- .../Model/ResourceModel/GetAssetIdsByContentFieldTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php b/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php index bd6a08a7ab189..c7fb0a38340c1 100644 --- a/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php @@ -44,6 +44,7 @@ protected function setUp(): void * Test for getting asset id by block field * * @dataProvider blockDataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php * @@ -64,6 +65,7 @@ public function testBlockFields(string $field, string $value, array $expectedAss * Test for getting asset id by page field * * @dataProvider pageDataProvider + * @magentoConfigFixture system/media_gallery/enabled 1 * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php * From 3226d069cdf0779b9e2a1a8d8041a8223b42aa55 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 12 Aug 2020 12:51:35 +0300 Subject: [PATCH 0251/1013] fix Used in entities magento/adobe-stock-integration#1749 --- .../deleteImageWithDetailConfirmation.js | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) 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 index 51d124ca319e6..2bff49a256c96 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -52,13 +52,15 @@ define([ */ getRecordRelatedContentMessage: function (images) { var usedInMessage = $t('The selected assets are used in the content of the following entities: '), - usedIn = []; + 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 + ')'); + usedIn[entityTypeData.name] = entityTypeData.name in usedIn ? + usedIn[entityTypeData.name] + entityTypeData.number : + entityTypeData.number; }); } }); @@ -68,7 +70,26 @@ define([ return ''; } - return usedInMessage + usedIn.join(', ') + '.'; + return usedInMessage + this.usedInObjectToString(usedIn); + }, + + /** + * Fromats usedIn object to string + * + * @param {Object} usedIn + * @return {String} + */ + usedInObjectToString: function (usedIn) { + var message = '', + count = 0; + + $.each(usedIn, function (entityName, number) { + count++; + message += entityName + '(' + number + ')'; + message += count != Object.keys(usedIn).length ? ', ' : '.'; + }); + + return message; } }; }); From 624ebcdcc399b856fe673628c18cc7ea2a3edc99 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 12 Aug 2020 14:26:59 +0300 Subject: [PATCH 0252/1013] Fix static tests --- .../web/js/action/deleteImageWithDetailConfirmation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 2bff49a256c96..f7ea1b8459904 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -86,7 +86,7 @@ define([ $.each(usedIn, function (entityName, number) { count++; message += entityName + '(' + number + ')'; - message += count != Object.keys(usedIn).length ? ', ' : '.'; + message += count !== Object.keys(usedIn).length ? ', ' : '.'; }); return message; From 0cd3df6de5a20edf1c17512c0a5ecd7f5f392718 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 13 Aug 2020 12:35:28 +0300 Subject: [PATCH 0253/1013] Code review suggestion --- .../web/js/action/deleteImageWithDetailConfirmation.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 index f7ea1b8459904..ed40674df20f0 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -80,16 +80,13 @@ define([ * @return {String} */ usedInObjectToString: function (usedIn) { - var message = '', - count = 0; + var entities = []; $.each(usedIn, function (entityName, number) { - count++; - message += entityName + '(' + number + ')'; - message += count !== Object.keys(usedIn).length ? ', ' : '.'; + entities.push(entityName + '(' + number + ')'); }); - return message; + return entities.join(', ') + '.'; } }; }); From b56ac4f2c75e997c38ad776b40bbfc0fd1e20696 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 11 Aug 2020 18:14:55 +0100 Subject: [PATCH 0254/1013] magento/adobe-stock-integration#1746: Corrected placeholders --- .../view/adminhtml/web/template/image/image-edit.html | 2 +- .../base/web/templates/grid/filters/elements/ui-select.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index e8448e1a64aef..80d1e29fd683f 100644 --- 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 @@ -54,7 +54,7 @@ <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" + <input type="text" id="keyword" data-ui-id="keyword" name="keyword" placeholder="New Tag" class="admin__control-text minimum-length-0 maximum-length-128" ko-value="newKeyword" data-validate="{'validate-image-keyword': true, 'validate-length': true}"/> </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 3e5108b53ccd5..5036b7121c626 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,7 +141,7 @@ </div> <div ifnot="options().length" class="admin__action-multiselect-empty-area"> - <ul data-bind="html: getEmptyOptionsUnsanitizedHtml"/> + <ul data-bind="html: getEmptyOptionsUnsanitizedHtml()"/> </div> <!-- /ko --> <ul class="admin__action-multiselect-menu-inner _root" From e1540f0f7cc463ba20094fad89147adda69c2497 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 11 Aug 2020 15:58:32 +0300 Subject: [PATCH 0255/1013] Fix new iTxt segment creation for PNG image XMP segment --- .../Model/Png/Segment/WriteXmp.php | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php index 292a52322d621..7cda517ed1b76 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php @@ -80,9 +80,11 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI } if (empty($pngXmpSegments)) { + $segments[] = $this->createPngXmpSegment($metadata); + return $this->fileFactory->create([ 'path' => $file->getPath(), - 'segments' => $this->insertPngXmpSegment($segments, $this->createPngXmpSegment($metadata)) + 'segments' => $segments ]); } @@ -96,18 +98,6 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI ]); } - /** - * 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 * From aabcbfc5f262006aec7f547ab3a1ce15732c7fe7 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 11 Aug 2020 16:26:20 +0300 Subject: [PATCH 0256/1013] write PNG metadata chunks before IEND chunk --- .../Model/Png/Segment/WriteIptc.php | 20 ++++++++++++++++--- .../Model/Png/Segment/WriteXmp.php | 20 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php index d40dbc13d2962..becbd8861affa 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php @@ -71,11 +71,9 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI } if (empty($pngIptcSegments)) { - $segments[] = $this->createPngIptcSegment($metadata); - return $this->fileFactory->create([ 'path' => $file->getPath(), - 'segments' => $segments + 'segments' => $this->insertPngIptcSegment($segments, $this->createPngIptcSegment($metadata)) ]); } @@ -89,6 +87,22 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI ]); } + /** + * Insert IPTC segment to image png segments before IEND chunk + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertPngIptcSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge( + array_slice($segments, 0, count($segments) - 1), + [$xmpSegment], + array_slice($segments, count($segments) - 1) + ); + } + /** * Create new zTXt segment with metadata * diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php index 7cda517ed1b76..84fea00f99ea9 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php @@ -80,11 +80,9 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI } if (empty($pngXmpSegments)) { - $segments[] = $this->createPngXmpSegment($metadata); - return $this->fileFactory->create([ 'path' => $file->getPath(), - 'segments' => $segments + 'segments' => $this->insertPngXmpSegment($segments, $this->createPngXmpSegment($metadata)) ]); } @@ -98,6 +96,22 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI ]); } + /** + * Insert XMP segment to image png segments before IEND chunk + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertPngXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge( + array_slice($segments, 0, count($segments) - 1), + [$xmpSegment], + array_slice($segments, count($segments) - 1) + ); + } + /** * Write new png segment metadata * From f44976fd85d1c1b0a4beeff2fd9a41f3f1677a85 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 11 Aug 2020 18:47:06 +0300 Subject: [PATCH 0257/1013] Correct variable names --- .../MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php index becbd8861affa..fd4b2e7252a1f 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php @@ -91,14 +91,14 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI * Insert IPTC segment to image png segments before IEND chunk * * @param SegmentInterface[] $segments - * @param SegmentInterface $xmpSegment + * @param SegmentInterface $iptcSegment * @return SegmentInterface[] */ - private function insertPngIptcSegment(array $segments, SegmentInterface $xmpSegment): array + private function insertPngIptcSegment(array $segments, SegmentInterface $iptcSegment): array { return array_merge( array_slice($segments, 0, count($segments) - 1), - [$xmpSegment], + [$iptcSegment], array_slice($segments, count($segments) - 1) ); } From c986c812627280dd7bc66a08d67ccc74dac02a73 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 13 Aug 2020 11:25:57 +0300 Subject: [PATCH 0258/1013] Code review improvements --- .../MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php | 6 ++++-- .../MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php index fd4b2e7252a1f..9025ba9363fde 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php @@ -96,10 +96,12 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI */ private function insertPngIptcSegment(array $segments, SegmentInterface $iptcSegment): array { + $iendSegmentIndex = count($segments) - 1; + return array_merge( - array_slice($segments, 0, count($segments) - 1), + array_slice($segments, 0, $iendSegmentIndex), [$iptcSegment], - array_slice($segments, count($segments) - 1) + array_slice($segments, $iendSegmentIndex) ); } diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php index 84fea00f99ea9..f03482ecf6054 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php @@ -105,10 +105,11 @@ public function execute(FileInterface $file, MetadataInterface $metadata): FileI */ private function insertPngXmpSegment(array $segments, SegmentInterface $xmpSegment): array { + $iendSegmentIndex = count($segments) - 1; return array_merge( - array_slice($segments, 0, count($segments) - 1), + array_slice($segments, 0, $iendSegmentIndex), [$xmpSegment], - array_slice($segments, count($segments) - 1) + array_slice($segments, $iendSegmentIndex) ); } From 31dc97c3458eb01b0f31dc1e195752607e37ec26 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Wed, 5 Aug 2020 18:39:19 +0800 Subject: [PATCH 0259/1013] magento/magento2#1684: Login failed error contains HTML tags - added a function to format messages to render as HTML --- .../view/adminhtml/web/js/grid/messages.js | 18 +++++++++++++++--- .../adminhtml/web/template/grid/messages.html | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) 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 index 7116784f41a0d..6e68b8e679e17 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js @@ -4,15 +4,17 @@ */ define([ - 'uiElement' -], function (Element) { + 'uiElement', + 'escaper' +], function (Element, escaper) { 'use strict'; return Element.extend({ defaults: { template: 'Magento_MediaGalleryUi/grid/messages', messageDelay: 5, - messages: [] + messages: [], + allowedTags: ['div', 'span', 'b', 'strong', 'i', 'em', 'u', 'a'] }, /** @@ -72,6 +74,16 @@ define([ clearTimeout(timerId); this.clear(); }.bind(this), Number(delay) * 1000); + }, + + /** + * 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/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html index 1ec084e223e98..ca758b721e8fc 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html @@ -8,7 +8,7 @@ <div class="messages" outereach="messages"> <div attr="class: 'message message-'+code"> <div data-ui-id="messages-message-error"> - <span text="message"></span> + <span data-bind="html: $parent.prepareMessageForHtml(message)"></span> </div> </div> </div> From 162e583474521e296d9b7df2ccf658b95da0277d Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Thu, 6 Aug 2020 18:22:07 +0800 Subject: [PATCH 0260/1013] magento/magento2#1684: Login failed error contains HTML tags - fix static tests and added jasmine tests --- .../view/adminhtml/web/js/grid/messages.js | 2 +- .../adminhtml/web/template/grid/messages.html | 2 +- .../adminhtml/js/grid/messages.test.js | 56 +++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js 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 index 6e68b8e679e17..8ed802d53825a 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js @@ -82,7 +82,7 @@ define([ * @param {String} message * @return {String} */ - prepareMessageForHtml: function (message) { + prepareMessageUnsanitizedHtml: function (message) { return escaper.escapeHtml(message, this.allowedTags); } }); 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 index ca758b721e8fc..eb526c68de23a 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html @@ -8,7 +8,7 @@ <div class="messages" outereach="messages"> <div attr="class: 'message message-'+code"> <div data-ui-id="messages-message-error"> - <span data-bind="html: $parent.prepareMessageForHtml(message)"></span> + <span data-bind="html: $parent.prepareMessageUnsanitizedHtml(message)"></span> </div> </div> </div> diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js new file mode 100644 index 0000000000000..7de3e61695f37 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js @@ -0,0 +1,56 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_MediaGalleryUi/js/grid/messages', +], function (Messages) { + 'use strict'; + + describe('Magento_MediaGalleryUi/grid/messages', function () { + var message, + messageText, + errorType, + successType; + + beforeEach(function () { + message = Messages; + messageText = 'test message'; + errorType = 'error'; + successType = 'success'; + }); + + describe('message handling', function () { + it('add error message, get error message', function () { + message.add(errorType, messageText); + expect(message.get()).toEqual([messageText]); + }); + + it('add success message, get success message', function () { + message.add(successType, messageText); + expect(message.get()).toEqual([messageText]); + }); + + it('scheduled cleaning messages', function () { + message.add(errorType, messageText); + message.scheduleCleanup(); + expect(message.get()).toEqual([]); + }); + }); + + describe('prepareMessageUnsanitizedHtml', function () { + var messageData, + expectedData; + + beforeEach(function () { + messageData = 'Login failed. Please check if the <a href="%1">Secret Key</a> is set correctly and try again.'; + expectedData = 'Login failed. Please check if the <a href="%1">Secret Key</a> is set correctly and try again.'; + }); + + it('prepare message to be rendered as HTML', function () { + expect(message.prepareMessageUnsanitizedHtml(messageData)).toEqual(expectedData) + }); + }); + }); +}); From 72e2cfe62a6d174d71c77afe37ef8c4ff4f57177 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Thu, 6 Aug 2020 19:23:02 +0800 Subject: [PATCH 0261/1013] magento/magento2#1684: Login failed error contains HTML tags - fix static tests --- .../MediaGalleryUi/adminhtml/js/grid/messages.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js index 7de3e61695f37..2f1e56a737e71 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js @@ -4,11 +4,11 @@ */ define([ - 'Magento_MediaGalleryUi/js/grid/messages', + 'Magento_MediaGalleryUi/js/grid/messages' ], function (Messages) { 'use strict'; - describe('Magento_MediaGalleryUi/grid/messages', function () { + describe('Magento_MediaGalleryUi/js/grid/messages', function () { var message, messageText, errorType, @@ -49,7 +49,7 @@ define([ }); it('prepare message to be rendered as HTML', function () { - expect(message.prepareMessageUnsanitizedHtml(messageData)).toEqual(expectedData) + expect(message.prepareMessageUnsanitizedHtml(messageData)).toEqual(expectedData) }); }); }); From 02da4f06667c1389bcb680960757f618a53b1912 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Sun, 9 Aug 2020 19:29:16 +0100 Subject: [PATCH 0262/1013] magento/magento2#29398: Fixed jasmine test --- .../adminhtml/js/grid/messages.test.js | 76 ++++++++++++------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js index 2f1e56a737e71..a906c28fdbd56 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js @@ -4,53 +4,75 @@ */ define([ - 'Magento_MediaGalleryUi/js/grid/messages' -], function (Messages) { + 'Magento_MediaGalleryUi/js/grid/messages', + 'escaper' +], function (Messages, Escaper) { 'use strict'; describe('Magento_MediaGalleryUi/js/grid/messages', function () { - var message, + var messagesInstance, + escaperInstance, messageText, errorType, successType; beforeEach(function () { - message = Messages; + escaperInstance = Escaper; + messagesInstance = Messages({ + escaper: escaperInstance + }); messageText = 'test message'; errorType = 'error'; successType = 'success'; }); - describe('message handling', function () { - it('add error message, get error message', function () { - message.add(errorType, messageText); - expect(message.get()).toEqual([messageText]); - }); + it('add error message, get error message', function () { + messagesInstance.add(errorType, messageText); + expect(messagesInstance.get()).toEqual([{ + code: errorType, + message: messageText + }]); + }); - it('add success message, get success message', function () { - message.add(successType, messageText); - expect(message.get()).toEqual([messageText]); - }); + it('add success message, get success message', function () { + messagesInstance.add(successType, messageText); + expect(messagesInstance.get()).toEqual([{ + code: successType, + message: messageText + }]); + }); - it('scheduled cleaning messages', function () { - message.add(errorType, messageText); - message.scheduleCleanup(); - expect(message.get()).toEqual([]); - }); + it('handles multiple messages', function () { + messagesInstance.add(successType, messageText); + messagesInstance.add(errorType, messageText); + expect(messagesInstance.get()).toEqual([ + { + code: successType, + message: messageText + }, + { + code: errorType, + message: messageText + } + ]); }); - describe('prepareMessageUnsanitizedHtml', function () { - var messageData, - expectedData; + it('cleans messages', function () { + messagesInstance.add(errorType, messageText); + messagesInstance.clear(); - beforeEach(function () { - messageData = 'Login failed. Please check if the <a href="%1">Secret Key</a> is set correctly and try again.'; - expectedData = 'Login failed. Please check if the <a href="%1">Secret Key</a> is set correctly and try again.'; - }); + expect(messagesInstance.get()).toEqual([]); + }); - it('prepare message to be rendered as HTML', function () { - expect(message.prepareMessageUnsanitizedHtml(messageData)).toEqual(expectedData) + it('prepare message to be rendered as HTML', function () { + var escapedMessage = 'escaped message'; + + // eslint-disable-next-line max-nested-callbacks + spyOn(escaperInstance, 'escapeHtml').and.callFake(function () { + return escapedMessage; }); + + expect(messagesInstance.prepareMessageUnsanitizedHtml(messageText)).toEqual(escapedMessage); }); }); }); From 856f5c15e625531e21068c4bc582542f2e68e4d6 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Sun, 9 Aug 2020 19:35:18 +0100 Subject: [PATCH 0263/1013] magento/magento2#29398: Corrected binding --- .../view/adminhtml/web/template/grid/messages.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index eb526c68de23a..3279856895d77 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html @@ -8,7 +8,7 @@ <div class="messages" outereach="messages"> <div attr="class: 'message message-'+code"> <div data-ui-id="messages-message-error"> - <span data-bind="html: $parent.prepareMessageUnsanitizedHtml(message)"></span> + <span html="$parent.prepareMessageUnsanitizedHtml(message)"></span> </div> </div> </div> From 411187957e097aabd546dde7811fc4dbb8f0ce97 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 11 Aug 2020 12:47:09 +0100 Subject: [PATCH 0264/1013] magento/magento2#29398: Corrected objects comparison in jasmine test --- .../adminhtml/js/grid/messages.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js index a906c28fdbd56..39444f8859465 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/MediaGalleryUi/adminhtml/js/grid/messages.test.js @@ -28,24 +28,24 @@ define([ it('add error message, get error message', function () { messagesInstance.add(errorType, messageText); - expect(messagesInstance.get()).toEqual([{ + expect(JSON.stringify(messagesInstance.get())).toEqual(JSON.stringify([{ code: errorType, message: messageText - }]); + }])); }); it('add success message, get success message', function () { messagesInstance.add(successType, messageText); - expect(messagesInstance.get()).toEqual([{ + expect(JSON.stringify(messagesInstance.get())).toEqual(JSON.stringify([{ code: successType, message: messageText - }]); + }])); }); it('handles multiple messages', function () { messagesInstance.add(successType, messageText); messagesInstance.add(errorType, messageText); - expect(messagesInstance.get()).toEqual([ + expect(JSON.stringify(messagesInstance.get())).toEqual(JSON.stringify([ { code: successType, message: messageText @@ -54,14 +54,14 @@ define([ code: errorType, message: messageText } - ]); + ])); }); it('cleans messages', function () { messagesInstance.add(errorType, messageText); messagesInstance.clear(); - expect(messagesInstance.get()).toEqual([]); + expect(JSON.stringify(messagesInstance.get())).toEqual(JSON.stringify([])); }); it('prepare message to be rendered as HTML', function () { From 7882f6abcfd8b15245df906c96e16c524734f696 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 12 Aug 2020 19:24:25 +0300 Subject: [PATCH 0265/1013] Exif Reader IMplementation --- .../Model/Jpeg/Segment/ReadExif.php | 74 ++++++++++++++++++ .../Test/_files/exif-image.jpeg | Bin 0 -> 22905 bytes .../Magento/MediaGalleryMetadata/etc/di.xml | 1 + 3 files changed, 75 insertions(+) create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php new file mode 100644 index 0000000000000..0c142403affd1 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php @@ -0,0 +1,74 @@ +<?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\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 EXIF Reader + */ +class ReadExif implements ReadMetadataInterface +{ + private const EXIF_SEGMENT_NAME = 'APP1'; + private const EXIF_SEGMENT_START = "Exif\x00"; + private const EXIF_DATA_START_POSITION = 0; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($file->getSegments() as $segment) { + if ($this->isExifSegment($segment)) { + } + } + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? $keywords : null + ]); + } + + /** + * Does segment contain Exif data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isExifSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::EXIF_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::EXIF_DATA_START_POSITION, 4), + self::EXIF_SEGMENT_START, + self::EXIF_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7c1dbc3a6a465fa7f9026e088f623387de657a4c GIT binary patch literal 22905 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 U|8Nkv2VWR5Bd@z1^lrTg0QL`_O#lD@ literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/etc/di.xml b/app/code/Magento/MediaGalleryMetadata/etc/di.xml index d2f1f90510488..d6b2899729fb7 100644 --- a/app/code/Magento/MediaGalleryMetadata/etc/di.xml +++ b/app/code/Magento/MediaGalleryMetadata/etc/di.xml @@ -121,6 +121,7 @@ <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> + <item name="exif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadExif</item> </argument> </arguments> </virtualType> From 732d9f57e33ab9d4c2c4fa5353faf0a5a1dbbd05 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 13 Aug 2020 14:49:16 +0300 Subject: [PATCH 0266/1013] add integration test for exif standart --- .../Model/Jpeg/Segment/ExifTest.php | 132 ++++++++++++++++++ .../Test/_files/exif-image.jpeg | Bin 22905 -> 19494 bytes 2 files changed, 132 insertions(+) create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php new file mode 100644 index 0000000000000..93c4ced52bc9f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php @@ -0,0 +1,132 @@ +<?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\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for EXIF reader + */ +class ExifTest 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/_files/exif-image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg index 7c1dbc3a6a465fa7f9026e088f623387de657a4c..666488a923b2cbe4e2b1150e27da140b2016b29c 100644 GIT binary patch delta 156 zcmeyliE-Hs#`^y^{y$`x<64oK#=zj~%b>-;z`()4#>mUS1Y`*VaTp^Xm@NflXE91K zumWjDAZ}rlhO>JZH5iz|;y_(Y!caCy6%c^TR`5+sPt7aIS18FW$w`HZrKA=oPuwcP R>j6{*QVXUxf97rT1^{$f8%F>D delta 3610 zcmeHK&2Q936n{>F_!1wDdV(A*BLpg}?RCOOtT+e~0x4QRBvPy19DBU$#%mjU!a95L z4K9do_kcJc6bUKf2t6V7(0_n4Y9$VE<AS(Vs#IzFX4dv@+rV<!+pab8Z)e{8-h032 zXN|WX34i?}q@CY8Z{hH_OUp|j2mmd-pz}_6c{y<%fJz0v0f0TQS2zG;h`z?r6b^EH z1n~o562|c^;03}F*u^oxGvR1~p9?258)Oi)g6HKu7t)Dr!~4fi7zIq>c!dL403vm% zN5ndzrfpKQJ7<&FirkR8f!|xf4H!6Q;CD^cR9!u*YnrH)^%+f{R^c)v;DQ6EVI2r; zLIVOwBd{T6{`R%T^T-6JHBsPZ>no0TBm0BaWPu;H?*h)jB+fr+|IFtn&Y_!jyN9=g zo;q7&kR?0T-Wtzv3h+1czEiwA&7D%qx~c-?_lqkF%geZb#fk{=<61yF@Am&uqcqeN z#gApv4%Ucl1$8AcL!~UM3d~(dLep9&R9qv~)b$PN<FCI-qH7z{cV{YUC0rmi_i8gD zE6wYxR&(8&wWSO5;#{I9^^lc9OzNH=>xm(8b{#RBRwR*|&~-!lzH(Ds2qGfR$TPB9 z!dl+ZB;<%}N58eWlriAH)R3}Z8;yqCn3jX6s+4DEXBAabG_8atrFhe)W>WIwV>!iQ z-y^^%&9z{I8WIN3jjSFPv^#2$8-}$spaH7(3HuQB<jY*Sd>3^cXJ>x4@-I`{Dh$Jo z$m7}BmO?yICq9jpaw?aZZuL-Kh=@taA`U}hl`pAhOPaQtKfPU}N?^Or=4XtGh;?Sv z<;22v2^nWW*nNUh346?O^c#_j0hz3+45`d?wk^G{)3VxK^k{}Wmx--=re8Irq+}Dv z-0-M0-(Q*l8@Pt;YgUmv5M2i56jm_LMH+@uy-TV5&mB(i?*C%XtNdN@1wQ*+(v)pN z7w9eCSo@J!d48A7YLV%DmO)?aSrT<(yJpq&JQ8i@Z>c^jo@<whCD`-87noFO_9n~Z zV9mi`(QEVKaDVA==?Aip{1239s0Jhbhbr$t@{#@nB^s*1NdKYAJCJ;&|3HcUW6(Rk z6CY1@4LqB7KETzR`B7DzmQ^@^X}L1NzBT|3O$k51_|fB6rts@)3@^TRoUa`|cj)}d X?fCY6|MjsGKfU>L`{|3VyVw2!O?J2y From 7acd2fd3fc63259340f5b1ed719ab0224ddb197a Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 13 Aug 2020 14:49:56 +0300 Subject: [PATCH 0267/1013] skip tests cases --- .../Integration/Model/ExtractMetadataTest.php | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php index 982ccbb20fe2c..a5ff5de0aeaef 100644 --- a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php @@ -63,50 +63,59 @@ public function filesProvider(): array { return [ [ - 'macos-photos.jpeg', + 'exif-image.jpeg', 'Title of the magento image', - 'Description of the magento image', + 'exif fromat title', [ - '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', + 'exif', 'awesome' ] ], + //[ + // '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' + // ] + ///], ]; } } From 8d4adce22a58926992ce4eb76c6a98efbc511f7a Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 17 Aug 2020 14:21:45 +0300 Subject: [PATCH 0268/1013] Add suport of reading PNG JPEG Exif metadata --- .../Model/Jpeg/Segment/ReadExif.php | 36 ++++++- .../Model/Png/Segment/ReadExif.php | 91 ++++++++++++++++ .../Integration/Model/ExtractMetadataTest.php | 101 +++++++++--------- .../Test/_files/exif-image.jpeg | Bin 19494 -> 19630 bytes .../Test/_files/exif_image.png | Bin 0 -> 47756 bytes .../Magento/MediaGalleryMetadata/etc/di.xml | 1 + 6 files changed, 177 insertions(+), 52 deletions(-) create mode 100644 app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php create mode 100644 app/code/Magento/MediaGalleryMetadata/Test/_files/exif_image.png diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php index 0c142403affd1..5e5deb0e119fc 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php @@ -41,14 +41,44 @@ public function __construct( */ public function execute(FileInterface $file): MetadataInterface { - $title = null; - $description = null; - $keywords = []; + if (!is_callable('exif_read_data')) { + throw new LocalizedException( + __('exif_read_data() must be enabled in php configuration') + ); + } foreach ($file->getSegments() as $segment) { if ($this->isExifSegment($segment)) { + return $this->getExifData($file->getPath()); } } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Parese exif data from segment + * + * @param string $filePath + */ + private function getExifData(string $filePath): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + $data = exif_read_data($filePath); + + if ($data) { + $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; + $description = isset($data['ImageDescription']) ? $data['ImageDescription'] : null; + $keywords = ''; + } + return $this->metadataFactory->create([ 'title' => $title, 'description' => $description, diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php new file mode 100644 index 0000000000000..cd1160ed92589 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -0,0 +1,91 @@ +<?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\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 EXIF Reader + */ +class ReadExif implements ReadMetadataInterface +{ + private const EXIF_SEGMENT_NAME = 'eXIf'; + + /** + * @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->isExifSegment($segment)) { + return $this->getExifData($segment); + } + } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Parese exif data from segment + * + * @param FileInterface $filePath + */ + private function getExifData(SegmentInterface $segment): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + $data = exif_read_data('data://image/jpeg;base64,' . base64_encode($segment->getData())); + + if ($data) { + $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; + $description = isset($data['ImageDescription']) ? $data['ImageDescription'] : null; + $keywords = ''; + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? $keywords : null + ]); + } + + /** + * Does segment contain Exif data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isExifSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::EXIF_SEGMENT_NAME; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php index a5ff5de0aeaef..ebe96183eb1f2 100644 --- a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php @@ -37,14 +37,14 @@ protected function setUp(): void * @param string $fileName * @param string $title * @param string $description - * @param array $keywords + * @param null|array $keywords * @throws LocalizedException */ public function testExecute( string $fileName, string $title, string $description, - array $keywords + ?array $keywords ): void { $path = realpath(__DIR__ . '/../../_files/' . $fileName); $metadata = $this->extractMetadata->execute($path); @@ -62,60 +62,63 @@ public function testExecute( public function filesProvider(): array { return [ + [ + 'exif_image.png', + 'Exif title png imge', + 'Exif description png imge', + null + ], [ 'exif-image.jpeg', + 'Exif Magento title', + 'Exif description metadata', + null + ], + [ + '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', - 'exif fromat title', + 'PNG format is awesome', [ - 'exif', + 'png', 'awesome' ] ], - //[ - // '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/_files/exif-image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif-image.jpeg index 666488a923b2cbe4e2b1150e27da140b2016b29c..cfe27433fd9fce9d4b0202f604c13a3848f90c35 100644 GIT binary patch delta 269 zcmZ2BgK^zV#`^y^{y$_?ajnQqV_@+0Wzb?^VBlcjVB}?B0<wgGIFFGJ%$5SOn;4}S zSb;Pn5YJ(hhO?J2YA`T^#euq*grRJps`g9<7O47TbLN5dfy`M0#0(%i6(*)z)~7O5 zFk~{MG3YQPGL$o<G88l9GvoqkAYh%qz`zcqSqzMfjV3Ze*o;if43pZxY!RSaI2ORv z7#bKXV1!uy|364O1A}9*v!^GJP6b+~lUSZwoS&Qe{{Vv^2g4txNz9B&3`~L>*Oz+( E02;e1cmMzZ delta 109 zcmZ2ClX2M$#<>4C{y$`x<64oK#=zj~%b>-;z`()4#>mUS1Y`*VaTp^Xm@NflXE91K lumWjDAZ}rlhO>JZH5iz|;y_(Y!caC))x<KV%^Hkt-T+V<4>te+ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exif_image.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/exif_image.png new file mode 100644 index 0000000000000000000000000000000000000000..4a6bf30c2d516ee21c515ccff1fbe812299cd0a8 GIT binary patch literal 47756 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 UKBqk>1m_4!a_W#Onb#rz3wPioP5=M^ literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGalleryMetadata/etc/di.xml b/app/code/Magento/MediaGalleryMetadata/etc/di.xml index d6b2899729fb7..4cd9a34e43a93 100644 --- a/app/code/Magento/MediaGalleryMetadata/etc/di.xml +++ b/app/code/Magento/MediaGalleryMetadata/etc/di.xml @@ -112,6 +112,7 @@ <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> + <item name="exif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadExif</item> </argument> </arguments> </virtualType> From 982d015bfbc5b62131fd69216222216aeada043e Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 17 Aug 2020 14:31:35 +0300 Subject: [PATCH 0269/1013] Add validation --- .../MediaGalleryMetadata/Model/Png/Segment/ReadExif.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php index cd1160ed92589..94e5f4a9ef0e9 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -39,6 +39,12 @@ public function __construct( */ public function execute(FileInterface $file): MetadataInterface { + if (!is_callable('exif_read_data')) { + throw new LocalizedException( + __('exif_read_data() must be enabled in php configuration') + ); + } + foreach ($file->getSegments() as $segment) { if ($this->isExifSegment($segment)) { return $this->getExifData($segment); From 36ea7b5423282545e656268c7501c723cf402c73 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 17 Aug 2020 14:34:06 +0300 Subject: [PATCH 0270/1013] Remove obsolete tests --- .../Model/Jpeg/Segment/ExifTest.php | 132 ------------------ 1 file changed, 132 deletions(-) delete mode 100644 app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php deleted file mode 100644 index 93c4ced52bc9f..0000000000000 --- a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/ExifTest.php +++ /dev/null @@ -1,132 +0,0 @@ -<?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\ReadFile; -use Magento\MediaGalleryMetadata\Model\MetadataFactory; - -/** - * Test for EXIF reader - */ -class ExifTest 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' - ] - ] - ]; - } -} From 78dc5b9251d542cf5129ae1ee19d4a0938199546 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 17 Aug 2020 14:36:07 +0300 Subject: [PATCH 0271/1013] Update docBlock --- .../Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php index 94e5f4a9ef0e9..4c13d9a97255f 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -61,7 +61,7 @@ public function execute(FileInterface $file): MetadataInterface /** * Parese exif data from segment * - * @param FileInterface $filePath + * @param SegmentInterface $segment */ private function getExifData(SegmentInterface $segment): MetadataInterface { From 9acc3fb935ebbf4888eb850dc8bfa0ee3d9c0882 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 17 Aug 2020 15:49:37 +0300 Subject: [PATCH 0272/1013] Fix static tests --- .../Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php | 1 + .../Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php index 5e5deb0e119fc..edacd8bb65cec 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php @@ -12,6 +12,7 @@ use Magento\MediaGalleryMetadataApi\Model\FileInterface; use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\Framework\Exception\LocalizedException; /** * Jpeg EXIF Reader diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php index 4c13d9a97255f..a0e70eef62a0b 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -12,6 +12,7 @@ use Magento\MediaGalleryMetadataApi\Model\FileInterface; use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\Framework\Exception\LocalizedException; /** * Jpeg EXIF Reader From eac8e9c2466faf2d0f0610e56d1a5af9e96ae6b1 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 17 Aug 2020 21:23:21 +0300 Subject: [PATCH 0273/1013] Improve metadata types && isExifSegment() verification for jpeg --- .../Model/GetIptcMetadata.php | 7 +++---- .../Model/Jpeg/Segment/ReadExif.php | 11 +++++------ .../Model/Jpeg/Segment/ReadIptc.php | 6 +++--- .../Model/Jpeg/Segment/ReadXmp.php | 6 +++--- .../Model/Png/Segment/ReadExif.php | 1 - .../Model/Png/Segment/ReadXmp.php | 6 +++--- .../Test/_files/empty_iptc.jpeg | Bin 19416 -> 19416 bytes 7 files changed, 17 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php index d7290f31ee34e..e100a7f852e42 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php @@ -9,7 +9,6 @@ use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; -use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; /** * Get metadata from IPTC block @@ -42,8 +41,8 @@ public function __construct( */ public function execute(string $data): MetadataInterface { - $title = ''; - $description = ''; + $title = null; + $description = null; $keywords = []; if (is_callable('iptcparse')) { @@ -65,7 +64,7 @@ public function execute(string $data): MetadataInterface return $this->metadataFactory->create([ 'title' => $title, 'description' => $description, - 'keywords' => $keywords + 'keywords' => !empty($keywords) ? $keywords : null ]); } } diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php index edacd8bb65cec..b6c32296f3f7d 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadExif.php @@ -70,20 +70,19 @@ private function getExifData(string $filePath): MetadataInterface { $title = null; $description = null; - $keywords = []; + $keywords = null; $data = exif_read_data($filePath); - if ($data) { + if (!empty($data)) { $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; $description = isset($data['ImageDescription']) ? $data['ImageDescription'] : null; - $keywords = ''; } return $this->metadataFactory->create([ 'title' => $title, 'description' => $description, - 'keywords' => !empty($keywords) ? $keywords : null + 'keywords' => $keywords ]); } @@ -97,9 +96,9 @@ private function isExifSegment(SegmentInterface $segment): bool { return $segment->getName() === self::EXIF_SEGMENT_NAME && strncmp( - substr($segment->getData(), self::EXIF_DATA_START_POSITION, 4), + substr($segment->getData(), self::EXIF_DATA_START_POSITION, 5), self::EXIF_SEGMENT_START, - self::EXIF_DATA_START_POSITION + 5 ) == 0; } } diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php index 94ccb400e5e0a..e56993528a041 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php @@ -56,9 +56,9 @@ public function execute(FileInterface $file): MetadataInterface } } return $this->metadataFactory->create([ - 'title' => '', - 'description' => '', - 'keywords' => [] + 'title' => null, + 'description' => null, + 'keywords' => null ]); } diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php index 81ff7200c3475..e68c86d35eb97 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php @@ -54,9 +54,9 @@ public function execute(FileInterface $file): MetadataInterface } } return $this->metadataFactory->create([ - 'title' => '', - 'description' => '', - 'keywords' => [] + 'title' => null, + 'description' => null, + 'keywords' => null ]); } diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php index a0e70eef62a0b..81ecf57efaf32 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -75,7 +75,6 @@ private function getExifData(SegmentInterface $segment): MetadataInterface if ($data) { $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; $description = isset($data['ImageDescription']) ? $data['ImageDescription'] : null; - $keywords = ''; } return $this->metadataFactory->create([ diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php index 83ba554f7bf5d..518697d421474 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php @@ -55,9 +55,9 @@ public function execute(FileInterface $file): MetadataInterface } } return $this->metadataFactory->create([ - 'title' => '', - 'description' => '', - 'keywords' => [] + 'title' => null, + 'description' => null, + 'keywords' => null ]); } diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg index 144a56dac2d3e748d47cf56e0fdde0bab2f49def..1a345c2d33fdddfb2a66fee2f40d822d8604d029 100644 GIT binary patch delta 29 XcmcaHo$<zW#tBja3}|4Z);ezhV_*eu delta 29 hcmcaHo$<zW#tBjatPBhcQa}s>d5O8H8@1MX0|0OW2QL5s From 16a98de93143b65fe856e416e8fa4785ed58a708 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 18 Aug 2020 10:27:15 +0300 Subject: [PATCH 0274/1013] Improve comparsion remove exif data from other images --- .../Model/Png/Segment/ReadExif.php | 3 +-- .../Test/_files/macos-preview.png | Bin 57535 -> 57155 bytes 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php index 81ecf57efaf32..98c763d131b22 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -70,7 +70,6 @@ private function getExifData(SegmentInterface $segment): MetadataInterface $description = null; $keywords = []; - $data = exif_read_data('data://image/jpeg;base64,' . base64_encode($segment->getData())); if ($data) { $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; @@ -92,6 +91,6 @@ private function getExifData(SegmentInterface $segment): MetadataInterface */ private function isExifSegment(SegmentInterface $segment): bool { - return $segment->getName() === self::EXIF_SEGMENT_NAME; + return strcmp($segment->getName(), self::EXIF_SEGMENT_NAME) === 0; } } diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png index 966520f0d01124eec138a9e29298dbe8e2fea5ab..95eb45f69b3eafcedbadbb204baabb01b3bec775 100644 GIT binary patch delta 13 VcmdmgkooXF<_QLy*Dwm-2LLUY1{weW delta 400 zcmX@Sk9q$=<_QM%j0LF?o@u_m3|b5f3>*yXjC>4CK$ap9Cou{!Fav2uAY@>aVqgWc z85mj^rQz%zMh&PMpe{xuuwD_Mx+(3M3@lLfD}XeEOKNd)QD#9&W`3SRewso_Myf(? zVtVRie`d~lkUYZ#AO_k4p^XfT46F>ytc;8lj0~+zjI4|e7#O%FFvHAeRGYv8XIo7W z1hd(J2KY@7g0sUWwJ`w27=(c0ag!#3#aYaOCQX`zU@u@~sE2!U0Tay8Op_Kc!`KXe zfQB$&a}qW&Z39Cq1E6bRv}3TdrzcQ<aB@*<YF=?heu<04`Po1L&H|6fVg?2=)s0r1 F_W}GlJTU+O From 01422493bd1cfbbb758161da53ab29981965028a Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 18 Aug 2020 10:32:06 +0300 Subject: [PATCH 0275/1013] rever removed line by php-cs-fixer --- .../Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php index 98c763d131b22..09aeaf526443a 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadExif.php @@ -70,6 +70,7 @@ private function getExifData(SegmentInterface $segment): MetadataInterface $description = null; $keywords = []; + $data = exif_read_data('data://image/jpeg;base64,' . base64_encode($segment->getData())); if ($data) { $title = isset($data['DocumentName']) ? $data['DocumentName'] : null; From ca205f8d3d2b26c58c72210489ab63423bcf6f1f Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 12 Aug 2020 21:49:45 +0800 Subject: [PATCH 0276/1013] magento/adobe-stock-integration#1748: Some category grid columns are empty - fixed issue of empty columns and added column renderer for in menu and enabled columns --- .../Ui/Component/Listing/Columns/InMenu.php | 36 +++++++++++++++++++ .../Ui/Component/Listing/Columns/IsActive.php | 36 +++++++++++++++++++ .../media_gallery_category_listing.xml | 4 +-- 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/InMenu.php create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/IsActive.php diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/InMenu.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/InMenu.php new file mode 100644 index 0000000000000..fe4720b4a3e60 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/InMenu.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class InMenu column for Category grid + */ +class InMenu extends Column +{ + /** + * 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] == 1) { + $item[$fieldName] = 'Yes'; + } else { + $item[$fieldName] = 'No'; + } + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/IsActive.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/IsActive.php new file mode 100644 index 0000000000000..c6f20c937d5b3 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/IsActive.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class IsActive column for Category grid + */ +class IsActive extends Column +{ + /** + * 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] == 1) { + $item[$fieldName] = 'Yes'; + } else { + $item[$fieldName] = 'No'; + } + } + } + + return $dataSource; + } +} 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 index 9945643ccffef..e12d90b95303b 100644 --- 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 @@ -167,12 +167,12 @@ <label translate="true">Products</label> </settings> </column> - <column name="include_in_menu" component="Magento_Ui/js/grid/columns/select"> + <column name="include_in_menu" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\InMenu"> <settings> <label translate="true">In Menu</label> </settings> </column> - <column name="is_active" component="Magento_Ui/js/grid/columns/select" > + <column name="is_active" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\IsActive"> <settings> <label translate="true">Enabled</label> </settings> From fc7a3d7c63b1aa82d7c0b21393395c86a9f3a1df Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Thu, 13 Aug 2020 19:40:27 +0800 Subject: [PATCH 0277/1013] magento/adobe-stock-integration#1748: Some category grid columns are empty - added columns to section and added verification to action group --- .../AdminAssertCategoryGridPageDetailsActionGroup.xml | 2 ++ .../Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml index 0788bbd60291a..7875c62f9591d 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml @@ -16,5 +16,7 @@ <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"/> + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.inMenu('1', 'Yes')}}" stepKey="assertInMenuColumn"/> + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.enabled('1', 'Yes')}}" stepKey="assertEnabledColumn"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml index 5267a215c8edd..41bec6f6220a0 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -13,6 +13,8 @@ <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="inMenu" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'In Menu')]/preceding-sibling::th) +1 ]//*[text()='{{inMenuValue}}']" parameterized="true"/> + <element name="enabled" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Enabled')]/preceding-sibling::th) +1 ]//*[text()='{{enabledValue}}']" 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> From 9bd7256d556763ac69fbad4b75e70afe05b56589 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 12 Aug 2020 19:58:22 +0800 Subject: [PATCH 0278/1013] magento/adobe-stock-integration#1712: Remove DataObject usage from OpenDialogUrl provider - remove data object usage from OpenDialogUrl provider, added new plugin and modified the integration test --- .../Plugin/NewMediaGalleryOpenDialogUrl.php | 43 +++++++++++++++++++ ...ProviderTest.php => OpenDialogUrlTest.php} | 16 +++---- .../etc/adminhtml/di.xml | 4 +- .../Element/DataType/Media/OpenDialogUrl.php | 14 +----- 4 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php rename app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/{OpenDialogUrlProviderTest.php => OpenDialogUrlTest.php} (80%) diff --git a/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php new file mode 100644 index 0000000000000..f076aa43b6f96 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Plugin; + +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl; + +class NewMediaGalleryOpenDialogUrl +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @param ConfigInterface $config + */ + public function __construct(ConfigInterface $config) + { + $this->config = $config; + } + + /** + * @param OpenDialogUrl $subject + * @param string $result + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @return string + */ + public function afterGet(OpenDialogUrl $subject, string $result) + { + $writer = new \Zend\Log\Writer\Stream(BP . '/var/log/newmediagalleryplugin.log'); + $logger = new \Zend\Log\Logger(); + $logger->addWriter($writer); + $logger->debug(__METHOD__); + $logger->debug("PASSING HERE!"); + return $this->config->isEnabled() ? 'media_gallery/index/index' : $result; + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlTest.php similarity index 80% rename from app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php rename to app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlTest.php index 7a3316f293879..90f363d6d792b 100644 --- a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlTest.php @@ -9,16 +9,16 @@ 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 Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl; use PHPUnit\Framework\TestCase; /** * Provide tests cover getting correct url based on the config settings. * @magentoAppArea adminhtml */ -class OpenDialogUrlProviderTest extends TestCase +class OpenDialogUrlTest extends TestCase { /** * @var ObjectManagerInterface @@ -26,9 +26,9 @@ class OpenDialogUrlProviderTest extends TestCase private $objectManger; /** - * @var OpenDialogUrlProvider + * @var OpenDialogUrl */ - private $openDialogUrlProvider; + private $openDialogUrl; /** * @inheritdoc @@ -37,8 +37,8 @@ protected function setUp(): void { $this->objectManger = Bootstrap::getObjectManager(); $config = $this->objectManger->create(ConfigInterface::class); - $this->openDialogUrlProvider = $this->objectManger->create( - OpenDialogUrlProvider::class, + $this->openDialogUrl = $this->objectManger->create( + OpenDialogUrl::class, ['config' => $config] ); } @@ -49,7 +49,7 @@ protected function setUp(): void */ public function testWithEnhancedMediaGalleryDisabled(): void { - self::assertEquals('cms/wysiwyg_images/index', $this->openDialogUrlProvider->getUrl()); + self::assertEquals('cms/wysiwyg_images/index', $this->openDialogUrl->get()); } /** @@ -58,6 +58,6 @@ public function testWithEnhancedMediaGalleryDisabled(): void */ public function testWithEnhancedMediaGalleryEnabled(): void { - self::assertEquals('media_gallery/index/index', $this->openDialogUrlProvider->getUrl()); + self::assertEquals('media_gallery/index/index', $this->openDialogUrl->get()); } } diff --git a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml index 1559a6d7dfcd5..08e83ce6cad88 100644 --- a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml +++ b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml @@ -7,9 +7,7 @@ --> <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> + <plugin name="new_media_gallery_open_dialog_url" type="Magento\MediaGalleryIntegration\Plugin\NewMediaGalleryOpenDialogUrl" /> </type> <type name="Magento\Framework\File\Uploader"> <plugin name="save_asset_image" type="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"/> diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php index 27370cbfbd68c..cd116a6f1bff8 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php @@ -8,10 +8,8 @@ namespace Magento\Ui\Component\Form\Element\DataType\Media; -use Magento\Framework\DataObject; - /** - * Basic configuration for OdenDialogUrl + * Basic configuration for OpenDialogUrl */ class OpenDialogUrl { @@ -22,14 +20,6 @@ class OpenDialogUrl */ private $openDialogUrl; - /** - * @param DataObject $url - */ - public function __construct(DataObject $url = null) - { - $this->openDialogUrl = $url; - } - /** * Returns open dialog url for media browser * @@ -38,7 +28,7 @@ public function __construct(DataObject $url = null) public function get(): string { if ($this->openDialogUrl) { - return $this->openDialogUrl->getUrl(); + return $this->openDialogUrl; } return self::DEFAULT_OPEN_DIALOG_URL; } From 05cd07c6b6ca56fa224fb3731e12a239ad59c3b7 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 12 Aug 2020 20:02:43 +0800 Subject: [PATCH 0279/1013] magento/adobe-stock-integration#1712: Remove DataObject usage from OpenDialogUrl provider - remove logs --- .../Plugin/NewMediaGalleryOpenDialogUrl.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php index f076aa43b6f96..13068721634e8 100644 --- a/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php @@ -33,11 +33,6 @@ public function __construct(ConfigInterface $config) */ public function afterGet(OpenDialogUrl $subject, string $result) { - $writer = new \Zend\Log\Writer\Stream(BP . '/var/log/newmediagalleryplugin.log'); - $logger = new \Zend\Log\Logger(); - $logger->addWriter($writer); - $logger->debug(__METHOD__); - $logger->debug("PASSING HERE!"); return $this->config->isEnabled() ? 'media_gallery/index/index' : $result; } } From 538d0adac57a8df90466bd211674919f5db24239 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 12 Aug 2020 22:13:59 +0800 Subject: [PATCH 0280/1013] magento/adobe-stock-integration#1712: Remove DataObject usage from OpenDialogUrl provider - fixed static test fails and removed unused class --- .../Model/OpenDialogUrlProvider.php | 40 ------------------- .../Plugin/NewMediaGalleryOpenDialogUrl.php | 5 +++ .../MediaGalleryIntegration/composer.json | 3 +- 3 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php diff --git a/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php b/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php deleted file mode 100644 index 317b811df5692..0000000000000 --- a/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php +++ /dev/null @@ -1,40 +0,0 @@ -<?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/NewMediaGalleryOpenDialogUrl.php b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php index 13068721634e8..ed8108f012af0 100644 --- a/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/NewMediaGalleryOpenDialogUrl.php @@ -10,6 +10,9 @@ use Magento\MediaGalleryUiApi\Api\ConfigInterface; use Magento\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl; +/** + * Plugin to get open media gallery dialog URL for WYSIWYG and widgets + */ class NewMediaGalleryOpenDialogUrl { /** @@ -26,6 +29,8 @@ public function __construct(ConfigInterface $config) } /** + * Get Url based on media gallery configuration + * * @param OpenDialogUrl $subject * @param string $result * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/MediaGalleryIntegration/composer.json b/app/code/Magento/MediaGalleryIntegration/composer.json index c55d6e0b89733..a9709da81222e 100644 --- a/app/code/Magento/MediaGalleryIntegration/composer.json +++ b/app/code/Magento/MediaGalleryIntegration/composer.json @@ -6,7 +6,8 @@ "magento/framework": "*", "magento/module-media-gallery-ui-api": "*", "magento/module-media-gallery-api": "*", - "magento/module-media-gallery-synchronization-api": "*" + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-ui": "*" }, "require-dev": { "magento/module-cms": "*" From b8a7ef051917fcc636d193bc40ed2400c642e776 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Thu, 13 Aug 2020 21:29:48 +0800 Subject: [PATCH 0281/1013] magento/adobe-stock-integration#1712: Remove DataObject usage from OpenDialogUrl provider - apply requested changes --- .../Form/Element/DataType/Media/OpenDialogUrl.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php b/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php index cd116a6f1bff8..9961fc41fc70d 100644 --- a/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php +++ b/app/code/Magento/Ui/Component/Form/Element/DataType/Media/OpenDialogUrl.php @@ -20,6 +20,14 @@ class OpenDialogUrl */ private $openDialogUrl; + /** + * @param string $url + */ + public function __construct(string $url = null) + { + $this->openDialogUrl = $url ?? self::DEFAULT_OPEN_DIALOG_URL; + } + /** * Returns open dialog url for media browser * @@ -27,9 +35,6 @@ class OpenDialogUrl */ public function get(): string { - if ($this->openDialogUrl) { - return $this->openDialogUrl; - } - return self::DEFAULT_OPEN_DIALOG_URL; + return $this->openDialogUrl; } } From a4ce1c424e74697686bb3e9c5e0d93c93f733922 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Tue, 11 Aug 2020 01:59:52 +0800 Subject: [PATCH 0282/1013] magento/adobe-stock-integration#1727: Introduce internal class wrapping SplFileInfo - implement class wrapping SplFileInfo and modified the files to use the newly added classes --- .../Model/CreateAssetFromFile.php | 14 +- .../Model/Filesystem/FileInfo.php | 296 ++++++++++++++++++ .../Model/Filesystem/GetFileInfo.php | 63 ++++ .../Model/GetAssetFromPath.php | 11 +- .../Model/SynchronizeFiles.php | 14 +- 5 files changed, 374 insertions(+), 24 deletions(-) create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php create mode 100644 app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index 87d477507b680..6f1f05a750085 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -16,7 +16,7 @@ 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\Filesystem\GetFileInfo; use Magento\MediaGallerySynchronization\Model\GetContentHash; /** @@ -60,9 +60,9 @@ class CreateAssetFromFile private $extractMetadata; /** - * @var SplFileInfoFactory + * @var GetFileInfo */ - private $splFileInfoFactory; + private $getFileInfo; /** * @param Filesystem $filesystem @@ -71,7 +71,7 @@ class CreateAssetFromFile * @param AssetInterfaceFactory $assetFactory * @param GetContentHash $getContentHash * @param ExtractMetadataInterface $extractMetadata - * @param SplFileInfoFactory $splFileInfoFactory + * @param GetFileInfo $getFileInfo */ public function __construct( Filesystem $filesystem, @@ -80,7 +80,7 @@ public function __construct( AssetInterfaceFactory $assetFactory, GetContentHash $getContentHash, ExtractMetadataInterface $extractMetadata, - SplFileInfoFactory $splFileInfoFactory + GetFileInfo $getFileInfo ) { $this->filesystem = $filesystem; $this->driver = $driver; @@ -88,7 +88,7 @@ public function __construct( $this->assetFactory = $assetFactory; $this->getContentHash = $getContentHash; $this->extractMetadata = $extractMetadata; - $this->splFileInfoFactory = $splFileInfoFactory; + $this->getFileInfo = $getFileInfo; } /** @@ -101,7 +101,7 @@ public function __construct( public function execute(string $path): AssetInterface { $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); - $file = $this->splFileInfoFactory->create($absolutePath); + $file = $this->getFileInfo->execute($absolutePath); [$width, $height] = getimagesize($absolutePath); $metadata = $this->extractMetadata->execute($absolutePath); diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php new file mode 100644 index 0000000000000..20acefe4ab034 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php @@ -0,0 +1,296 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model\Filesystem; + +/** + * Class FileInfo + */ +class FileInfo extends \SplFileInfo +{ + /** + * @var string + */ + private $path; + + /** + * @var string + */ + private $filename; + + /** + * @var string + */ + private $extension; + + /** + * @var $basename + */ + private $basename; + + /** + * @var string + */ + private $pathname; + + /** + * @var int + */ + private $perms; + + /** + * @var int + */ + private $inode; + + /** + * @var int + */ + private $size; + + /** + * @var int + */ + private $owner; + + /** + * @var int + */ + private $group; + + /** + * @var int + */ + private $aTime; + + /** + * @var int + */ + private $mTime; + + /** + * @var int + */ + private $cTime; + + /** + * @var string + */ + private $type; + + /** + * @var false|string + */ + private $realPath; + + /** + * @var \SplFileInfo + */ + private $fileInfo; + + /** + * @var \SplFileInfo + */ + private $pathInfo; + + /** + * FileInfo constructor. + * @param string $file_name + * @param string $path + * @param string $filename + * @param string $extension + * @param string $basename + * @param string $pathname + * @param int $perms + * @param int $inode + * @param int $size + * @param int $owner + * @param int $group + * @param int $aTime + * @param int $mTime + * @param int $cTime + * @param string $type + * @param false|string $realPath + * @param \SplFileInfo $fileInfo + * @param \SplFileInfo $pathInfo + */ + public function __construct( + string $file_name, + string $path, + string $filename, + string $extension, + string $basename, + string $pathname, + int $perms, + int $inode, + int $size, + int $owner, + int $group, + int $aTime, + int $mTime, + int $cTime, + string $type, + $realPath, + \SplFileInfo $fileInfo, + \SplFileInfo $pathInfo + ) { + parent::__construct($file_name); + $this->path = $path; + $this->filename = $filename; + $this->extension = $extension; + $this->basename = $basename; + $this->pathname = $pathname; + $this->perms = $perms; + $this->inode = $inode; + $this->size = $size; + $this->owner = $owner; + $this->group = $group; + $this->aTime = $aTime; + $this->mTime = $mTime; + $this->cTime = $cTime; + $this->type = $type; + $this->realPath = $realPath; + $this->fileInfo = $fileInfo; + $this->pathInfo = $pathInfo; + } + + /** + * @inheritDoc + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @inheritDoc + */ + public function getFilename(): string + { + return $this->filename; + } + + /** + * @inheritDoc + */ + public function getExtension(): string + { + return $this->extension; + } + + /** + * @inheritDoc + */ + public function getBasename($suffix = null): string + { + return $this->basename; + } + + /** + * @inheritDoc + */ + public function getPathname(): string + { + return $this->pathname; + } + + /** + * @inheritDoc + */ + public function getPerms(): int + { + return $this->perms; + } + + /** + * @inheritDoc + */ + public function getInode(): int + { + return $this->inode; + } + + /** + * @inheritDoc + */ + public function getSize(): int + { + return $this->size; + } + + /** + * @inheritDoc + */ + public function getOwner(): int + { + return $this->owner; + } + + /** + * @inheritDoc + */ + public function getGroup(): int + { + return $this->group; + } + + /** + * @inheritDoc + */ + public function getATime(): int + { + return $this->aTime; + } + + /** + * @inheritDoc + */ + public function getMTime(): int + { + return $this->mTime; + } + + /** + * @inheritDoc + */ + public function getCTime(): int + { + return $this->cTime; + } + + /** + * @inheritDoc + */ + public function getType(): string + { + return $this->type; + } + + /** + * @inheritDoc + */ + public function getRealPath() + { + return $this->realPath; + } + + /** + * @inheritDoc + */ + public function getFileInfo($class_name = null): \SplFileInfo + { + return $this->fileInfo; + } + + /** + * @inheritDoc + */ + public function getPathInfo($class_name = null): \SplFileInfo + { + return $this->pathInfo; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php new file mode 100644 index 0000000000000..ef382732275bd --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model\Filesystem; + +use Magento\MediaGallerySynchronization\Model\Filesystem\FileInfoFactory; + +/** + * Get file information + */ +class GetFileInfo +{ + /** + * @var FileInfoFactory + */ + private $fileInfoFactory; + + /** + * GetFileInfo constructor. + * @param FileInfoFactory $fileInfoFactory + */ + public function __construct( + FileInfoFactory $fileInfoFactory + ) { + $this->fileInfoFactory = $fileInfoFactory; + } + + /** + * Get file information based on path provided. + * + * @param string $path + * @return FileInfo + */ + public function execute(string $path): FileInfo + { + $splFileInfo = new \SplFileInfo($path); + + return $this->fileInfoFactory->create([ + 'file_name' => $path, + 'path' => $splFileInfo->getPath(), + 'filename' => $splFileInfo->getFilename(), + 'extension' => $splFileInfo->getExtension(), + 'basename' => $splFileInfo->getBasename(), + 'pathname' => $splFileInfo->getPathname(), + 'perms' => $splFileInfo->getPerms(), + 'inode' => $splFileInfo->getInode(), + 'size' => $splFileInfo->getSize(), + 'owner' => $splFileInfo->getOwner(), + 'group' => $splFileInfo->getGroup(), + 'aTime' => $splFileInfo->getATime(), + 'mTime' => $splFileInfo->getMTime(), + 'cTime' => $splFileInfo->getCTime(), + 'type' => $splFileInfo->getType(), + 'realPath' => $splFileInfo->getRealPath(), + 'fileInfo' => $splFileInfo->getFileInfo(), + 'pathInfo' => $splFileInfo->getPathInfo() + ]); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php index 5e825d57c5ce7..533d814c9f1d0 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php @@ -12,7 +12,6 @@ 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 @@ -34,27 +33,19 @@ class GetAssetFromPath */ 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 + CreateAssetFromFile $createAssetFromFile ) { $this->assetFactory = $assetFactory; $this->getAssetsByPaths = $getMediaGalleryAssetByPath; $this->createAssetFromFile = $createAssetFromFile; - $this->splFileInfoFactory= $splFileInfoFactory; } /** diff --git a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php index 81e9629f703f3..eebb172e48202 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php @@ -16,7 +16,7 @@ use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; -use Magento\MediaGallerySynchronization\Model\Filesystem\SplFileInfoFactory; +use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; use Psr\Log\LoggerInterface; /** @@ -50,9 +50,9 @@ class SynchronizeFiles implements SynchronizeFilesInterface private $driver; /** - * @var SplFileInfoFactory + * @var GetFileInfo */ - private $splFileInfoFactory; + private $getFileInfo; /** * @var ImportFilesInterface @@ -69,7 +69,7 @@ class SynchronizeFiles implements SynchronizeFilesInterface * @param Filesystem $filesystem * @param DateTime $date * @param LoggerInterface $log - * @param SplFileInfoFactory $splFileInfoFactory + * @param GetFileInfo $getFileInfo * @param GetAssetsByPathsInterface $getAssetsByPaths * @param ImportFilesInterface $importFiles */ @@ -78,7 +78,7 @@ public function __construct( Filesystem $filesystem, DateTime $date, LoggerInterface $log, - SplFileInfoFactory $splFileInfoFactory, + GetFileInfo $getFileInfo, GetAssetsByPathsInterface $getAssetsByPaths, ImportFilesInterface $importFiles ) { @@ -86,7 +86,7 @@ public function __construct( $this->filesystem = $filesystem; $this->date = $date; $this->log = $log; - $this->splFileInfoFactory = $splFileInfoFactory; + $this->getFileInfo = $getFileInfo; $this->getAssetsByPaths = $getAssetsByPaths; $this->importFiles = $importFiles; } @@ -150,7 +150,7 @@ private function getFileModificationTime(string $path): string { return $this->date->gmtDate( self::DATE_FORMAT, - $this->splFileInfoFactory->create($this->getMediaDirectory()->getAbsolutePath($path))->getMTime() + $this->getFileInfo->execute($this->getMediaDirectory()->getAbsolutePath($path))->getMTime() ); } From 8728f337fd17cc920a19bf50c9a60e6c488ad55c Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 12 Aug 2020 01:37:46 +0800 Subject: [PATCH 0283/1013] magento/adobe-stock-integration#1727: Introduce internal class wrapping SplFileInfo - fix static and mftf fails, added integration tets --- .../Model/Filesystem/FileInfo.php | 55 +---------- .../Model/Filesystem/GetFileInfo.php | 6 +- .../Model/Filesystem/GetFileInfoTest.php | 94 +++++++++++++++++++ 3 files changed, 101 insertions(+), 54 deletions(-) create mode 100644 app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php index 20acefe4ab034..034ae7c0bff5a 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php @@ -8,7 +8,9 @@ namespace Magento\MediaGallerySynchronization\Model\Filesystem; /** - * Class FileInfo + * Internal class wrapping \SplFileInfo + * + * @SuppressWarnings(PHPMD.TooManyFields) */ class FileInfo extends \SplFileInfo { @@ -37,11 +39,6 @@ class FileInfo extends \SplFileInfo */ private $pathname; - /** - * @var int - */ - private $perms; - /** * @var int */ @@ -87,16 +84,6 @@ class FileInfo extends \SplFileInfo */ private $realPath; - /** - * @var \SplFileInfo - */ - private $fileInfo; - - /** - * @var \SplFileInfo - */ - private $pathInfo; - /** * FileInfo constructor. * @param string $file_name @@ -105,7 +92,6 @@ class FileInfo extends \SplFileInfo * @param string $extension * @param string $basename * @param string $pathname - * @param int $perms * @param int $inode * @param int $size * @param int $owner @@ -115,8 +101,7 @@ class FileInfo extends \SplFileInfo * @param int $cTime * @param string $type * @param false|string $realPath - * @param \SplFileInfo $fileInfo - * @param \SplFileInfo $pathInfo + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( string $file_name, @@ -125,7 +110,6 @@ public function __construct( string $extension, string $basename, string $pathname, - int $perms, int $inode, int $size, int $owner, @@ -134,9 +118,7 @@ public function __construct( int $mTime, int $cTime, string $type, - $realPath, - \SplFileInfo $fileInfo, - \SplFileInfo $pathInfo + $realPath ) { parent::__construct($file_name); $this->path = $path; @@ -144,7 +126,6 @@ public function __construct( $this->extension = $extension; $this->basename = $basename; $this->pathname = $pathname; - $this->perms = $perms; $this->inode = $inode; $this->size = $size; $this->owner = $owner; @@ -154,8 +135,6 @@ public function __construct( $this->cTime = $cTime; $this->type = $type; $this->realPath = $realPath; - $this->fileInfo = $fileInfo; - $this->pathInfo = $pathInfo; } /** @@ -198,14 +177,6 @@ public function getPathname(): string return $this->pathname; } - /** - * @inheritDoc - */ - public function getPerms(): int - { - return $this->perms; - } - /** * @inheritDoc */ @@ -277,20 +248,4 @@ public function getRealPath() { return $this->realPath; } - - /** - * @inheritDoc - */ - public function getFileInfo($class_name = null): \SplFileInfo - { - return $this->fileInfo; - } - - /** - * @inheritDoc - */ - public function getPathInfo($class_name = null): \SplFileInfo - { - return $this->pathInfo; - } } diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php index ef382732275bd..e2ffa39d0aba9 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php @@ -44,7 +44,7 @@ public function execute(string $path): FileInfo 'path' => $splFileInfo->getPath(), 'filename' => $splFileInfo->getFilename(), 'extension' => $splFileInfo->getExtension(), - 'basename' => $splFileInfo->getBasename(), + 'basename' => $splFileInfo->getBasename('.' . $splFileInfo->getExtension()), 'pathname' => $splFileInfo->getPathname(), 'perms' => $splFileInfo->getPerms(), 'inode' => $splFileInfo->getInode(), @@ -55,9 +55,7 @@ public function execute(string $path): FileInfo 'mTime' => $splFileInfo->getMTime(), 'cTime' => $splFileInfo->getCTime(), 'type' => $splFileInfo->getType(), - 'realPath' => $splFileInfo->getRealPath(), - 'fileInfo' => $splFileInfo->getFileInfo(), - 'pathInfo' => $splFileInfo->getPathInfo() + 'realPath' => $splFileInfo->getRealPath() ]); } } diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php new file mode 100644 index 0000000000000..c88a6fe7d39d7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Test\Integration\Model\Filesystem; + +use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Integration test for GetFileInfo + */ +class GetFileInfoTest extends TestCase +{ + /** + * @var GetFileInfo + */ + private $getFileInfo; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->getFileInfo = Bootstrap::getObjectManager()->get(GetFileInfo::class); + } + + /** + * @dataProvider filesProvider + * @param string $file + */ + public function testExecute( + string $file + ): void { + + $path = $this->getImageFilePath($file); + + $fileInfo = $this->getFileInfo->execute($path); + $this->assertNotEmpty($fileInfo->getPath()); + $this->assertNotEmpty($fileInfo->getFilename()); + $this->assertNotEmpty($fileInfo->getExtension()); + $this->assertNotEmpty($fileInfo->getBasename()); + $this->assertNotEmpty($fileInfo->getPathname()); + $this->assertNotEmpty($fileInfo->getPerms()); + $this->assertNotEmpty($fileInfo->getInode()); + $this->assertNotEmpty($fileInfo->getSize()); + $this->assertNotEmpty($fileInfo->getOwner()); + $this->assertNotEmpty($fileInfo->getGroup()); + $this->assertNotEmpty($fileInfo->getATime()); + $this->assertNotEmpty($fileInfo->getMTime()); + $this->assertNotEmpty($fileInfo->getCTime()); + $this->assertNotEmpty($fileInfo->getType()); + $this->assertNotEmpty($fileInfo->getRealPath()); + + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'magento.jpg', + 'magento_2.jpg' + ] + ]; + } + + /** + * Return image file path + * + * @param string $filename + * @return string + */ + private function getImageFilePath(string $filename): string + { + return dirname(__DIR__, 2) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + $filename + ] + ); + } +} From 0cb60017323eca1a107e67c632d0c94228cad3ad Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 12 Aug 2020 03:35:25 +0800 Subject: [PATCH 0284/1013] magento/adobe-stock-integration#1727: Introduce internal class wrapping SplFileInfo - fix static test --- .../Test/Integration/Model/Filesystem/GetFileInfoTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php index c88a6fe7d39d7..4031de4226105 100644 --- a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php @@ -55,7 +55,6 @@ public function testExecute( $this->assertNotEmpty($fileInfo->getCTime()); $this->assertNotEmpty($fileInfo->getType()); $this->assertNotEmpty($fileInfo->getRealPath()); - } /** From c0b574c86c08116f871516ed8ca8df88a39077bf Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Thu, 13 Aug 2020 18:47:51 +0800 Subject: [PATCH 0285/1013] magento/adobe-stock-integration#1727: Introduce internal class wrapping SplFileInfo - apply requested changes --- .../Model/CreateAssetFromFile.php | 2 +- .../Model/Filesystem/FileInfo.php | 155 +++--------------- .../Model/Filesystem/GetFileInfo.php | 11 +- .../Model/Filesystem/GetFileInfoTest.php | 23 +-- 4 files changed, 36 insertions(+), 155 deletions(-) diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index 6f1f05a750085..b257c3da55c84 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -110,7 +110,7 @@ public function execute(string $path): AssetInterface [ 'id' => null, 'path' => $path, - 'title' => $metadata->getTitle() ?: $file->getBasename('.' . $file->getExtension()), + 'title' => $metadata->getTitle() ?: $file->getBasename(), 'description' => $metadata->getDescription(), 'createdAt' => $this->date->date($file->getCTime())->format(self::DATE_FORMAT), 'updatedAt' => $this->date->date($file->getMTime())->format(self::DATE_FORMAT), diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php index 034ae7c0bff5a..5e523fd0e905a 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/FileInfo.php @@ -8,11 +8,9 @@ namespace Magento\MediaGallerySynchronization\Model\Filesystem; /** - * Internal class wrapping \SplFileInfo - * - * @SuppressWarnings(PHPMD.TooManyFields) + * Class for getting image file information. */ -class FileInfo extends \SplFileInfo +class FileInfo { /** * @var string @@ -34,36 +32,11 @@ class FileInfo extends \SplFileInfo */ private $basename; - /** - * @var string - */ - private $pathname; - - /** - * @var int - */ - private $inode; - /** * @var int */ private $size; - /** - * @var int - */ - private $owner; - - /** - * @var int - */ - private $group; - - /** - * @var int - */ - private $aTime; - /** * @var int */ @@ -74,71 +47,39 @@ class FileInfo extends \SplFileInfo */ private $cTime; - /** - * @var string - */ - private $type; - - /** - * @var false|string - */ - private $realPath; - /** * FileInfo constructor. - * @param string $file_name + * * @param string $path * @param string $filename * @param string $extension * @param string $basename - * @param string $pathname - * @param int $inode * @param int $size - * @param int $owner - * @param int $group - * @param int $aTime * @param int $mTime * @param int $cTime - * @param string $type - * @param false|string $realPath - * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - string $file_name, string $path, string $filename, string $extension, string $basename, - string $pathname, - int $inode, int $size, - int $owner, - int $group, - int $aTime, int $mTime, - int $cTime, - string $type, - $realPath + int $cTime ) { - parent::__construct($file_name); $this->path = $path; $this->filename = $filename; $this->extension = $extension; $this->basename = $basename; - $this->pathname = $pathname; - $this->inode = $inode; $this->size = $size; - $this->owner = $owner; - $this->group = $group; - $this->aTime = $aTime; $this->mTime = $mTime; $this->cTime = $cTime; - $this->type = $type; - $this->realPath = $realPath; } /** - * @inheritDoc + * Get path without filename. + * + * @return string */ public function getPath(): string { @@ -146,7 +87,9 @@ public function getPath(): string } /** - * @inheritDoc + * Get filename. + * + * @return string */ public function getFilename(): string { @@ -154,7 +97,9 @@ public function getFilename(): string } /** - * @inheritDoc + * Get file extension. + * + * @return string */ public function getExtension(): string { @@ -162,31 +107,19 @@ public function getExtension(): string } /** - * @inheritDoc + * Get file basename. + * + * @return string */ - public function getBasename($suffix = null): string + public function getBasename(): string { return $this->basename; } /** - * @inheritDoc - */ - public function getPathname(): string - { - return $this->pathname; - } - - /** - * @inheritDoc - */ - public function getInode(): int - { - return $this->inode; - } - - /** - * @inheritDoc + * Get file size. + * + * @return int */ public function getSize(): int { @@ -194,31 +127,9 @@ public function getSize(): int } /** - * @inheritDoc - */ - public function getOwner(): int - { - return $this->owner; - } - - /** - * @inheritDoc - */ - public function getGroup(): int - { - return $this->group; - } - - /** - * @inheritDoc - */ - public function getATime(): int - { - return $this->aTime; - } - - /** - * @inheritDoc + * Get last modified time. + * + * @return int */ public function getMTime(): int { @@ -226,26 +137,12 @@ public function getMTime(): int } /** - * @inheritDoc + * Get inode change time. + * + * @return int */ public function getCTime(): int { return $this->cTime; } - - /** - * @inheritDoc - */ - public function getType(): string - { - return $this->type; - } - - /** - * @inheritDoc - */ - public function getRealPath() - { - return $this->realPath; - } } diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php index e2ffa39d0aba9..8f9080767d6e3 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/GetFileInfo.php @@ -40,22 +40,13 @@ public function execute(string $path): FileInfo $splFileInfo = new \SplFileInfo($path); return $this->fileInfoFactory->create([ - 'file_name' => $path, 'path' => $splFileInfo->getPath(), 'filename' => $splFileInfo->getFilename(), 'extension' => $splFileInfo->getExtension(), 'basename' => $splFileInfo->getBasename('.' . $splFileInfo->getExtension()), - 'pathname' => $splFileInfo->getPathname(), - 'perms' => $splFileInfo->getPerms(), - 'inode' => $splFileInfo->getInode(), 'size' => $splFileInfo->getSize(), - 'owner' => $splFileInfo->getOwner(), - 'group' => $splFileInfo->getGroup(), - 'aTime' => $splFileInfo->getATime(), 'mTime' => $splFileInfo->getMTime(), - 'cTime' => $splFileInfo->getCTime(), - 'type' => $splFileInfo->getType(), - 'realPath' => $splFileInfo->getRealPath() + 'cTime' => $splFileInfo->getCTime() ]); } } diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php index 4031de4226105..cc9b70e623d9f 100644 --- a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php @@ -40,21 +40,14 @@ public function testExecute( $path = $this->getImageFilePath($file); $fileInfo = $this->getFileInfo->execute($path); - $this->assertNotEmpty($fileInfo->getPath()); - $this->assertNotEmpty($fileInfo->getFilename()); - $this->assertNotEmpty($fileInfo->getExtension()); - $this->assertNotEmpty($fileInfo->getBasename()); - $this->assertNotEmpty($fileInfo->getPathname()); - $this->assertNotEmpty($fileInfo->getPerms()); - $this->assertNotEmpty($fileInfo->getInode()); - $this->assertNotEmpty($fileInfo->getSize()); - $this->assertNotEmpty($fileInfo->getOwner()); - $this->assertNotEmpty($fileInfo->getGroup()); - $this->assertNotEmpty($fileInfo->getATime()); - $this->assertNotEmpty($fileInfo->getMTime()); - $this->assertNotEmpty($fileInfo->getCTime()); - $this->assertNotEmpty($fileInfo->getType()); - $this->assertNotEmpty($fileInfo->getRealPath()); + $expectedResult = new \SplFileInfo($path); + $this->assertEquals($expectedResult->getPath(), $fileInfo->getPath()); + $this->assertEquals($expectedResult->getFilename(), $fileInfo->getFilename()); + $this->assertEquals($expectedResult->getExtension(), $fileInfo->getExtension()); + $this->assertEquals($expectedResult->getBasename('.' . $expectedResult->getExtension()), $fileInfo->getBasename()); + $this->assertEquals($expectedResult->getSize(), $fileInfo->getSize()); + $this->assertEquals($expectedResult->getMTime(), $fileInfo->getMTime()); + $this->assertEquals($expectedResult->getCTime(), $fileInfo->getCTime()); } /** From 55a591e8e2574ff29fa3b08eb9926bca45c37c87 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Fri, 14 Aug 2020 10:16:54 +0100 Subject: [PATCH 0286/1013] Fixed static test --- .../Test/Integration/Model/Filesystem/GetFileInfoTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php index cc9b70e623d9f..6b1e8a676d02b 100644 --- a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/Filesystem/GetFileInfoTest.php @@ -44,7 +44,10 @@ public function testExecute( $this->assertEquals($expectedResult->getPath(), $fileInfo->getPath()); $this->assertEquals($expectedResult->getFilename(), $fileInfo->getFilename()); $this->assertEquals($expectedResult->getExtension(), $fileInfo->getExtension()); - $this->assertEquals($expectedResult->getBasename('.' . $expectedResult->getExtension()), $fileInfo->getBasename()); + $this->assertEquals( + $expectedResult->getBasename('.' . $expectedResult->getExtension()), + $fileInfo->getBasename() + ); $this->assertEquals($expectedResult->getSize(), $fileInfo->getSize()); $this->assertEquals($expectedResult->getMTime(), $fileInfo->getMTime()); $this->assertEquals($expectedResult->getCTime(), $fileInfo->getCTime()); From 56e97248794de37b954c7ed03216e76e79490af3 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 10 Aug 2020 12:58:32 +0300 Subject: [PATCH 0287/1013] Fix date timestamp typo --- app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php index ff82b990d2a01..7712bc088f518 100644 --- a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php @@ -86,8 +86,8 @@ public function execute(int $id, MetadataInterface $data): void 'description' => $data->getDescription() ?? $asset->getDescription(), 'source' => $asset->getSource(), 'hash' => $asset->getHash(), - 'created_at' => $asset->getCreatedAt(), - 'updated_at' => $asset->getUpdatedAt() + 'createdAt' => $asset->getCreatedAt(), + 'updatedAt' => $asset->getUpdatedAt() ] ); From 21ac52096c950d1c70c453291431762df358de34 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 10 Aug 2020 13:04:51 +0300 Subject: [PATCH 0288/1013] Apply correct sort order --- .../Magento/MediaGalleryUi/Model/UpdateAsset.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php index 7712bc088f518..f81c0449306da 100644 --- a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php @@ -76,18 +76,18 @@ public function execute(int $id, MetadataInterface $data): void $updatedAsset = $this->assetFactory->create( [ + 'id' => $asset->getId(), 'path' => $asset->getPath(), - 'contentType' => $asset->getContentType(), + 'title' => $data->getTitle() ?? $asset->getTitle(), + 'description' => $data->getDescription() ?? $asset->getDescription(), + 'createdAt' => $asset->getCreatedAt(), + 'updatedAt' => $asset->getUpdatedAt(), '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(), - 'createdAt' => $asset->getCreatedAt(), - 'updatedAt' => $asset->getUpdatedAt() + 'contentType' => $asset->getContentType(), + 'source' => $asset->getSource() ] ); From 04d0c921318184391acbd93acf19c97bbca2bb40 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 10 Aug 2020 13:38:02 +0300 Subject: [PATCH 0289/1013] represent the create/updated time of database record --- .../Model/CreateAssetFromFile.php | 16 ---------------- .../Magento/MediaGalleryUi/Model/UpdateAsset.php | 2 -- 2 files changed, 18 deletions(-) diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index b257c3da55c84..80b334733ed43 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -12,7 +12,6 @@ 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; @@ -24,11 +23,6 @@ */ class CreateAssetFromFile { - /** - * Date format - */ - private const DATE_FORMAT = 'Y-m-d H:i:s'; - /** * @var Filesystem */ @@ -39,11 +33,6 @@ class CreateAssetFromFile */ private $driver; - /** - * @var TimezoneInterface; - */ - private $date; - /** * @var AssetInterfaceFactory */ @@ -67,7 +56,6 @@ class CreateAssetFromFile /** * @param Filesystem $filesystem * @param File $driver - * @param TimezoneInterface $date * @param AssetInterfaceFactory $assetFactory * @param GetContentHash $getContentHash * @param ExtractMetadataInterface $extractMetadata @@ -76,7 +64,6 @@ class CreateAssetFromFile public function __construct( Filesystem $filesystem, File $driver, - TimezoneInterface $date, AssetInterfaceFactory $assetFactory, GetContentHash $getContentHash, ExtractMetadataInterface $extractMetadata, @@ -84,7 +71,6 @@ public function __construct( ) { $this->filesystem = $filesystem; $this->driver = $driver; - $this->date = $date; $this->assetFactory = $assetFactory; $this->getContentHash = $getContentHash; $this->extractMetadata = $extractMetadata; @@ -112,8 +98,6 @@ public function execute(string $path): AssetInterface 'path' => $path, 'title' => $metadata->getTitle() ?: $file->getBasename(), '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), diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php index f81c0449306da..85522c6b07e00 100644 --- a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php @@ -80,8 +80,6 @@ public function execute(int $id, MetadataInterface $data): void 'path' => $asset->getPath(), 'title' => $data->getTitle() ?? $asset->getTitle(), 'description' => $data->getDescription() ?? $asset->getDescription(), - 'createdAt' => $asset->getCreatedAt(), - 'updatedAt' => $asset->getUpdatedAt(), 'width' => $asset->getWidth(), 'height' => $asset->getHeight(), 'size' => $asset->getSize(), From 0a12e374f2ed0fe06e131ec36f20a9d1b652cb2a Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 10 Aug 2020 15:15:44 +0300 Subject: [PATCH 0290/1013] Cover changes with mftf tests --- ...tedAtNotEqualsUpdatedAtTimeActionGroup.xml | 24 +++++++++++++++++++ ...UploadedImageDateTimeEqualsActionGroup.xml | 24 +++++++++++++++++++ ...EnhancedMediaGalleryViewDetailsSection.xml | 2 ++ ...daloneMediaGalleryEditImageDetailsTest.xml | 5 ++++ 4 files changed, 55 insertions(+) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml new file mode 100644 index 0000000000000..248c6de468193 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.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="AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup"> + <annotations> + <description>Assert that created_at updated_at time NOT equals</description> + </annotations> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.createdAtDate}}" stepKey="grabCreatedTime"/> + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.updatedAtDate}}" stepKey="grabModifietTime"/> + <assertNotEquals stepKey="verifyContentType"> + <actualResult type="variable">grabCreatedTime</actualResult> + <expectedResult type="variable">grabModifietTime</expectedResult> + </assertNotEquals> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.xml new file mode 100644 index 0000000000000..f26931f08586f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.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="AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup"> + <annotations> + <description>Assert that created_at updated_at time are the same for newly uploaded image </description> + </annotations> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.createdAtDate}}" stepKey="grabCreatedTime"/> + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.updatedAtDate}}" stepKey="grabModifietTime"/> + <assertEquals stepKey="verifyContentType"> + <actualResult type="variable">grabCreatedTime</actualResult> + <expectedResult type="variable">grabModifietTime</expectedResult> + </assertEquals> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml index 048739ed3f81d..e63429677fbae 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml @@ -18,6 +18,8 @@ <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="createdAtDate" type="button" selector="//div[@class='attribute']/span[contains(text(), 'Created')]/following-sibling::div"/> + <element name="updatedAtDate" type="button" selector="//div[@class='attribute']/span[contains(text(), 'Modified')]/following-sibling::div"/> <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}}')]"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml index ede3a452e4ca5..c5247dc21ec63 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -29,6 +29,10 @@ <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> <argument name="image" value="ImageUpload"/> </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="clickViewDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> + <wait time="10" stepKey="waitForUpdateTimeToBeGreater"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> <argument name="image" value="UpdatedImageDetails"/> @@ -40,6 +44,7 @@ <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> <argument name="image" value="UpdatedImageDetails"/> </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup" stepKey="assertUpdatedAtTimeChanged" /> <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> <argument name="description" value="UpdatedImageDetails.description"/> </actionGroup> From aaa8d64d48cda9b94553e8cb9c392cd41baed02a Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 11 Aug 2020 10:23:50 +0300 Subject: [PATCH 0291/1013] CodeReview suggestions --- ...erifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml | 2 +- ...dMediaGalleryUploadedImageDateTimeEqualsActionGroup.xml} | 2 +- .../AdminStandaloneMediaGalleryEditImageDetailsTest.xml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/{AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.xml => AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup.xml} (94%) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml index 248c6de468193..9460d0b339ca4 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.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="AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup"> + <actionGroup name="AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup"> <annotations> <description>Assert that created_at updated_at time NOT equals</description> </annotations> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup.xml similarity index 94% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup.xml index f26931f08586f..076885ddaf8b6 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup.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="AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup"> + <actionGroup name="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup"> <annotations> <description>Assert that created_at updated_at time are the same for newly uploaded image </description> </annotations> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml index c5247dc21ec63..3fd1eacbf3504 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -30,8 +30,8 @@ <argument name="image" value="ImageUpload"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="clickViewDetails"/> - <actionGroup ref="AdminEnhancedMediaGalleryVerifyUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> - <wait time="10" stepKey="waitForUpdateTimeToBeGreater"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> + <wait time="20" stepKey="waitForUpdateTimeToBeGreater"/> <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> @@ -44,7 +44,7 @@ <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> <argument name="image" value="UpdatedImageDetails"/> </actionGroup> - <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup" stepKey="assertUpdatedAtTimeChanged" /> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup" stepKey="assertUpdatedAtTimeChanged" /> <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> <argument name="description" value="UpdatedImageDetails.description"/> </actionGroup> From 573276a9645dde42175c4565c0aebd4ab315a6cf Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 11 Aug 2020 10:25:30 +0300 Subject: [PATCH 0292/1013] Rename file --- ...diaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/{AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml => AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml} (100%) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml similarity index 100% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup.xml From 4f99a9a2ff67416fb82478995a633ebec51e3760 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 11 Aug 2020 10:34:12 +0300 Subject: [PATCH 0293/1013] Fix flaky bahavior --- .../Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml index 3fd1eacbf3504..58c6f32b8d72f 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -31,7 +31,7 @@ </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="clickViewDetails"/> <actionGroup ref="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> - <wait time="20" stepKey="waitForUpdateTimeToBeGreater"/> + <wait time="100" stepKey="waitForUpdateTimeToBeGreater"/> <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> From 08b55935a6c5cb51d4ad7aae83cb3ac703c62cde Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Fri, 7 Aug 2020 16:26:30 +0800 Subject: [PATCH 0294/1013] magento/adobe-stock-integration#1711: Use product model instead of data object for catalog image helper init --- .../Ui/Component/Listing/Columns/Thumbnail.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php index efb2ad2f8dae5..7a2662b59d198 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php @@ -6,7 +6,7 @@ namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; use Magento\Catalog\Helper\Image; -use Magento\Framework\DataObject; +use Magento\Catalog\Model\ProductFactory; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\Store; @@ -29,11 +29,17 @@ class Thumbnail extends Column */ private $imageHelper; + /** + * @var ProductFactory + */ + private $productFactory; + /** * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param StoreManagerInterface $storeManager * @param Image $image + * @param ProductFactory $productFactory * @param array $components * @param array $data */ @@ -42,12 +48,14 @@ public function __construct( UiComponentFactory $uiComponentFactory, StoreManagerInterface $storeManager, Image $image, + ProductFactory $productFactory, array $components = [], array $data = [] ) { parent::__construct($context, $uiComponentFactory, $components, $data); $this->imageHelper = $image; $this->storeManager = $storeManager; + $this->productFactory = $productFactory; } /** @@ -64,7 +72,7 @@ public function prepareDataSource(array $dataSource) if (isset($item[$fieldName])) { $item[$fieldName . '_src'] = $this->getUrl($item[$fieldName]); } else { - $category = new DataObject($item); + $category = $this->productFactory->create($item); $imageHelper = $this->imageHelper->init($category, 'product_listing_thumbnail'); $item[$fieldName . '_src'] = $imageHelper->getUrl(); } From 62b29d37e26ccf431ab7d4abb6009011c32deedc Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Fri, 7 Aug 2020 18:36:39 +0800 Subject: [PATCH 0295/1013] magento/adobe-stock-integration#1711: Use product model instead of data object for catalog image helper init - Apply PR suggestion --- .../Ui/Component/Listing/Columns/Thumbnail.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php index 7a2662b59d198..4cacaea0d99bf 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php @@ -72,7 +72,7 @@ public function prepareDataSource(array $dataSource) if (isset($item[$fieldName])) { $item[$fieldName . '_src'] = $this->getUrl($item[$fieldName]); } else { - $category = $this->productFactory->create($item); + $category = $this->productFactory->create(['data' => $item]); $imageHelper = $this->imageHelper->init($category, 'product_listing_thumbnail'); $item[$fieldName . '_src'] = $imageHelper->getUrl(); } From 4d5be024b12f21bc1a58132250e2b648505889c3 Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Fri, 7 Aug 2020 23:33:10 +0800 Subject: [PATCH 0296/1013] magento/adobe-stock-integration#1711: Use product model instead of data object for catalog image helper init - Revised approach based from pr suggestion --- .../Component/Listing/Columns/Thumbnail.php | 72 +++++++++++++----- .../etc/adminhtml/di.xml | 7 ++ .../web/images/category/placeholder/image.jpg | Bin 0 -> 820 bytes 3 files changed, 59 insertions(+), 20 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/images/category/placeholder/image.jpg diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php index 4cacaea0d99bf..dada8ee7acc19 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php @@ -5,8 +5,9 @@ */ namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; -use Magento\Catalog\Helper\Image; -use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\Category\Image; +use Magento\Catalog\Model\CategoryRepository; +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; @@ -27,19 +28,32 @@ class Thumbnail extends Column /** * @var Image */ - private $imageHelper; + private $categoryImage; /** - * @var ProductFactory + * @var CategoryRepository */ - private $productFactory; + private $categoryRepository; /** + * @var AssetRepository + */ + private $assetRepository; + + /** + * @var string[] + */ + private $defaultPlaceholder; + + /** + * Thumbnail constructor. * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param StoreManagerInterface $storeManager - * @param Image $image - * @param ProductFactory $productFactory + * @param Image $categoryImage + * @param CategoryRepository $categoryRepository + * @param AssetRepository $assetRepository + * @param array $defaultPlaceholder * @param array $components * @param array $data */ @@ -47,15 +61,19 @@ public function __construct( ContextInterface $context, UiComponentFactory $uiComponentFactory, StoreManagerInterface $storeManager, - Image $image, - ProductFactory $productFactory, + Image $categoryImage, + CategoryRepository $categoryRepository, + AssetRepository $assetRepository, + array $defaultPlaceholder = [], array $components = [], array $data = [] ) { parent::__construct($context, $uiComponentFactory, $components, $data); - $this->imageHelper = $image; $this->storeManager = $storeManager; - $this->productFactory = $productFactory; + $this->categoryImage = $categoryImage; + $this->categoryRepository = $categoryRepository; + $this->assetRepository = $assetRepository; + $this->defaultPlaceholder = $defaultPlaceholder; } /** @@ -63,20 +81,34 @@ public function __construct( * * @param array $dataSource * @return array + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\NoSuchEntityException */ 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 = $this->productFactory->create(['data' => $item]); - $imageHelper = $this->imageHelper->init($category, 'product_listing_thumbnail'); - $item[$fieldName . '_src'] = $imageHelper->getUrl(); + if (!isset($dataSource['data']['items'])) { + return $dataSource; + } + + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName . '_src'] = $this->getUrl($item[$fieldName]); + continue; + } + + if (isset($item['entity_id'])) { + $src = $this->categoryImage->getUrl( + $this->categoryRepository->get($item['entity_id']) + ); + + if (!empty($src)) { + $item[$fieldName . '_src'] = $src; + continue; } } + + $item[$fieldName . '_src'] = $this->assetRepository->getUrl($this->defaultPlaceholder['image']); } return $dataSource; diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml index ae01c29928b4a..2aaf5a56cf837 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml @@ -38,4 +38,11 @@ </argument> </arguments> </type> + <type name="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Thumbnail"> + <arguments> + <argument name="defaultPlaceholder" xsi:type="array"> + <item name="image" xsi:type="string">Magento_MediaGalleryCatalogUi::images/category/placeholder/image.jpg</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/images/category/placeholder/image.jpg b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/images/category/placeholder/image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0d5ef7e1bd4127242b442957d4f543d7e9e0320e GIT binary patch literal 820 zcmex=<NpH&0WUXCHwH!~28I+MWcYuZp@o5wc))}J%y0nJ&cw(Flm%kA1hI_&2N(o7 z7>pQ<m>C5bm;@P_1sVSzVJKr@0Gh?ffCN|=S(%vGxC9uO7+6>s*?5^*A(BA<FtZ4< z3Mm>2D;WhQ78Z*r8=IIqIu(^PHgDXVyy;NW#7PG~Frt_R(kX}`^8XeC5715~L1sY) zdxr0g3=X=%;Yw4BeG99LQrGqzdlMh<+Dw2u=+Ts~DvEn%w%iO$l#E`i{lYl<N?mxv zrUnJI=gU_=y~xP!>vxEG{;H!l`?)%OPo6%iU8eH#(dF3ujaFYBlKorG&bgCyJt3kg z^}fmV_wq5#tLsj%Wo@W5X3Q!+SYYB@towGmr<<)+2g}m1Yl-g<u5z2Hle4DMq2$yI z$1{7Q`cCjQ&07@fv(YS2)l+#2heGc`S>;uam`m97&*YcxJH|Zkz=200rfsQ_l~IRY z7hT_YC+-5vsh^)$KlRwp7qV;V^oe>FGt-{#IkxxKx7!)KH<pIHikekE>Dx#DQme%b zkKKY7-eWF}InQ~h=b0<dsUEh6(Os*&Ca>7haGL$I{i^>A$C$1$i$62pbw8Q${DYcb z^ULDzJ`nWx;_tJcbHKc>va0Uoe}<|9M)z7|Z`GAH+;9Fl|7!hX#%xBG7=bdS`}dgk zbu}M&c3#`SW@V!h6G!Q(sE2ze?|a15Xscmo@voTSp5@b*znA}g&G2WXZeYvlNlUb? zG<dFW*mCE{?CcE<Ws_!Rs&r}{{i+kupm#R=25-Z*2|Nb`SXgW0WR+G=vGI9(<ciGW zU)^uZUL=SwKeW>=H|KDmacaz&u*0r8XVY@oZkHX1ZV+Kupuuzim;|aCq&PSm6ja{T aU%Rit(V)P=#L#g1Pv!paUmfcI-vj^#YZ+<) literal 0 HcmV?d00001 From febb063333060bcf41b3c572f6d764142b81e3fc Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Tue, 11 Aug 2020 10:15:06 +0800 Subject: [PATCH 0297/1013] magento/adobe-stock-integration#1711: Use product model instead of data object for catalog image helper init - Update MFTF for image placeholder verification, update branch --- .../AdminAssertCategoryGridPageDetailsActionGroup.xml | 1 + .../Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml index 7875c62f9591d..884fa47152932 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml @@ -12,6 +12,7 @@ <description>Assert category grid page basic columns values for default category</description> </annotations> + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.image('1','image')}}" stepKey="assertImageColumn"/> <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.path('1')}}" stepKey="assertPathColumn"/> <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.name('1', 'Default Category')}}" stepKey="assertNameColumn"/> <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.displayMode('1', 'PRODUCTS')}}" stepKey="assertDisplayModeColumn"/> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml index 41bec6f6220a0..db260d85b62ce 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminMediaGalleryCatalogUiCategoryGridSection"> + <element name="image" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Image')]/preceding-sibling::th) +1]//img[contains(@src, '{{imageName}}')]" parameterized="true"/> <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"/> From d51bc97028544ed8ff4242081e892f584543ce60 Mon Sep 17 00:00:00 2001 From: Shankar Konar <konar.shankar2013@gmail.com> Date: Fri, 14 Aug 2020 13:58:35 +0530 Subject: [PATCH 0298/1013] Reversed Store configuration --- .../Model/Config/MediaGallery/Yesno.php | 21 +++++++++++++++++++ .../MediaGalleryUi/etc/adminhtml/system.xml | 6 +++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Model/Config/MediaGallery/Yesno.php diff --git a/app/code/Magento/MediaGalleryUi/Model/Config/MediaGallery/Yesno.php b/app/code/Magento/MediaGalleryUi/Model/Config/MediaGallery/Yesno.php new file mode 100644 index 0000000000000..40cf7630d9911 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Config/MediaGallery/Yesno.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Config\MediaGallery; + +class Yesno implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * Options getter + * + * @return array + */ + public function toOptionArray() :array + { + return [['value' => 0, 'label' => __('Yes')], ['value' => 1, 'label' => __('No')]]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml index 77544b42e899a..17aa08b5363ca 100644 --- a/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml @@ -9,10 +9,10 @@ <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> + <label>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> + <label>Enable Old Media Gallery</label> + <source_model>Magento\MediaGalleryUi\Model\Config\MediaGallery\Yesno</source_model> <config_path>system/media_gallery/enabled</config_path> </field> </group> From 44b283d4caac160965cc5fa550976c15a744b916 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Mon, 17 Aug 2020 16:18:15 +0300 Subject: [PATCH 0299/1013] Fix ui-select options placeholders for url-filter-applier && Clean cached options for ui-select component after closing slide form --- ...lleryAssetFilterPlaceHolderActionGroup.xml | 20 +++ ...diaGalleryCatalogUiCategoryGridSection.xml | 1 + ...alleryCatalogUiUsedInProductFilterTest.xml | 10 +- ...alogUiVerifyUsedInLinkCategoryGridTest.xml | 3 + .../Listing/Filters/UsedInProducts.php | 68 +++------ .../Listing/Filters/UsedInBlocks.php | 66 +++----- .../Component/Listing/Filters/UsedInPages.php | 67 +++----- .../Controller/Adminhtml/Asset/Search.php | 2 +- ...diaGalleryFilterPlaceholderActionGroup.xml | 20 +++ ...dminEnhancedMediaGalleryFiltersSection.xml | 1 + .../Ui/Component/Listing/Filters/Asset.php | 143 ++++++++++++++++-- .../adminhtml/web/js/image/image-actions.js | 1 + .../grid/filters/elements/ui-select.html | 8 +- 13 files changed, 266 insertions(+), 144 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceholderActionGroup.xml diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup.xml new file mode 100644 index 0000000000000..c9c9a25d8a2a3 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup.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="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup"> + <annotations> + <description>Assert asset filter placeholder value</description> + </annotations> + <arguments> + <argument name="filterPlaceholder" type="string"/> + </arguments> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.activeFilterPlaceholder(filterPlaceholder)}}" stepKey="assertFilterPLaceHolder" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml index db260d85b62ce..ffd3c14c297c3 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -9,6 +9,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminMediaGalleryCatalogUiCategoryGridSection"> + <element name="activeFilterPlaceholder" type="text" selector="//div[@class='admin__current-filters-list-wrap']//li//span[contains(text(), '{{filterPlaceholder}}')]" parameterized="true"/> <element name="image" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Image')]/preceding-sibling::th) +1]//img[contains(@src, '{{imageName}}')]" parameterized="true"/> <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"/> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml index d68fd4cb7cca8..74633fbb73542 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml @@ -62,12 +62,20 @@ <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> <argument name="title" value="ImageMetadata.title"/> </actionGroup> + + <wait time="10" stepKey="waitForBookmarkToSaveView"/> + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForGridReloaded"/> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceholderActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="$$product.name$$"/> + </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/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml index e761ef5cd08ba..7e0fa6c477c45 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -54,6 +54,9 @@ <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInCategories"> <argument name="entityName" value="Categories"/> </actionGroup> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> <argument name="categoryName" value="$$category.name$$"/> </actionGroup> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php index 254ebd047c954..d86617e12b8f8 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php @@ -80,54 +80,36 @@ 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()]; - } - } + $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( + 'current', + $this->context->getNameSpace() + ); + if ($bookmark === null) { + parent::prepare(); + return; } - foreach ($productIds as $id) { - $product = $this->productRepository->getById($id); - $options[] = [ - 'value' => $id, - 'label' => $product->getName(), - 'is_active' => $product->getStatus(), - 'path' => $product->getSku(), - 'optgroup' => false + $applied = $bookmark->getConfig()['current']['filters']['applied']; - ]; + if (isset($applied[$this->getName()])) { + $productIds = $applied[$this->getName()]; } - $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(); - + foreach ($productIds as $id) { + try { + $product = $this->productRepository->getById($id); + $options[] = [ + 'value' => $id, + 'label' => $product->getName(), + 'is_active' => $product->getStatus(), + 'path' => $product->getSku(), + 'optgroup' => false + ]; + } catch (\Exception $e) { + continue; + } + } + $this->optionsProvider = $options; parent::prepare(); } } diff --git a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php index 09fea24c8a2a9..66f8caa71d70a 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php +++ b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php @@ -80,52 +80,36 @@ 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 - ]; + $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( + 'current', + $this->context->getNameSpace() + ); + if ($bookmark === null) { + parent::prepare(); + return; } - $this->wrappedComponent = $this->uiComponentFactory->create( - $this->getName(), - parent::COMPONENT, - [ - 'context' => $this->getContext(), - 'options' => $options - ] - ); + $applied = $bookmark->getConfig()['current']['filters']['applied']; - $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') - ) - ); + if (isset($applied[$this->getName()])) { + $blockIds = $applied[$this->getName()]; + } - $this->applyFilter(); + foreach ($blockIds as $id) { + try { + $block = $this->blockRepository->getById($id); + $options[] = [ + 'value' => $id, + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } catch (\Exception $e) { + continue; + } + } + $this->optionsProvider = $options; 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 index 235a77cdcb8c5..78ab1b63d32d1 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php +++ b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php @@ -80,52 +80,35 @@ 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) + $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( + 'current', + $this->context->getNameSpace() ); - $this->setData('js_config', $pagesFilterjsConfig); + if ($bookmark === null) { + parent::prepare(); + return; + } - $this->setData( - 'config', - array_replace_recursive( - (array)$this->wrappedComponent->getData('config'), - (array)$this->getData('config') - ) - ); + $applied = $bookmark->getConfig()['current']['filters']['applied']; - $this->applyFilter(); + if (isset($applied[$this->getName()])) { + $pageIds = $applied[$this->getName()]; + } + foreach ($pageIds as $id) { + try { + $page = $this->pageRepository->getById($id); + $options[] = [ + 'value' => $id, + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } catch (\Exception $e) { + continue; + } + } + $this->optionsProvider = $options; parent::prepare(); } } diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php index df13250eacb5f..9b6c08edbc86d 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php @@ -139,7 +139,7 @@ public function execute() $responseContent['options'][] = [ 'value' => (string) $asset->getId(), 'label' => $asset->getTitle(), - 'path' => $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()) + 'src' => $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()) ]; $responseContent['total'] = count($responseContent['options']); } diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceholderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceholderActionGroup.xml new file mode 100644 index 0000000000000..db400ff151ae3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceholderActionGroup.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="AdminAssertMediaGalleryFilterPlaceholderActionGroup"> + <annotations> + <description>Assert asset filter placeholder value</description> + </annotations> + <arguments> + <argument name="filterPlaceholder" type="string"/> + </arguments> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.activeFilterPlaceholder(filterPlaceholder)}}" stepKey="assertFilterPLaceHolder" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml index 32b109f1e0483..da9f773d0f75e 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml @@ -25,5 +25,6 @@ <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']"/> + <element name="activeFilterValue" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']//li//span[contains(text(), '{{filterPlaceholder}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php index 273cf9e37554b..e8dc232584adb 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -15,9 +15,13 @@ use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Select; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Ui\Api\BookmarkManagementInterface; /** - * Asset filter + * Asset filter */ class Asset extends Select { @@ -27,14 +31,41 @@ class Asset extends Select private $getContentIdentities; /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var Images + */ + private $images; + + /** + * @var Storage + */ + private $storage; + + /** + * @var BookmarkManagementInterface + */ + private $bookmarkManagement; + + /** + * Constructor + * * @param ContextInterface $context * @param UiComponentFactory $uiComponentFactory * @param FilterBuilder $filterBuilder * @param FilterModifier $filterModifier * @param OptionSourceInterface $optionsProvider * @param GetContentByAssetIdsInterface $getContentIdentities + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param BookmarkManagementInterface $bookmarkManagement + * @param Images $images + * @param Storage $storage * @param array $components * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( ContextInterface $context, @@ -43,6 +74,10 @@ public function __construct( FilterModifier $filterModifier, OptionSourceInterface $optionsProvider = null, GetContentByAssetIdsInterface $getContentIdentities, + GetAssetsByIdsInterface $getAssetsByIds, + BookmarkManagementInterface $bookmarkManagement, + Images $images, + Storage $storage, array $components = [], array $data = [] ) { @@ -58,6 +93,89 @@ public function __construct( $data ); $this->getContentIdentities = $getContentIdentities; + $this->getAssetsByIds = $getAssetsByIds; + $this->bookmarkManagement = $bookmarkManagement; + $this->images = $images; + $this->storage = $storage; + } + + /** + * Prepare component configuration + * + * @return void + */ + public function prepare() + { + $options = []; + $assetIds = $this->getAssetIds(); + + if (empty($assetIds)) { + parent::prepare(); + return; + } + + $assets = $this->getAssetsByIds->execute($assetIds); + + foreach ($assets as $asset) { + $assetPath = $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()); + $options[] = [ + 'value' => (string) $asset->getId(), + 'label' => $asset->getTitle(), + 'src' => $assetPath + ]; + } + + $this->optionsProvider = $options; + parent::prepare(); + } + + /** + * Get asset ids from filterData or from bookmarks + */ + private function getAssetIds(): array + { + $assetIds = []; + + if (isset($this->filterData[$this->getName()])) { + $assetIds = $this->filterData[$this->getName()]; + + if (!is_array($assetIds)) { + $assetIds = $this->stringToArray($assetIds); + } + + return $assetIds; + } + + $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( + 'current', + $this->context->getNameSpace() + ); + + if ($bookmark === null) { + return $assetIds; + } + + $applied = $bookmark->getConfig()['current']['filters']['applied']; + + if (isset($applied[$this->getName()])) { + $assetIds = $applied[$this->getName()]; + } + + if (!is_array($assetIds)) { + $assetIds = $this->stringToArray($assetIds); + } + + return $assetIds; + } + + /** + * Converts string array from url-applier to array + * + * @param string $string + */ + private function stringToArray(string $string): array + { + return explode(',', str_replace(['[', ']'], '', $string)); } /** @@ -67,17 +185,20 @@ public function __construct( */ 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); + if (!isset($this->filterData[$this->getName()])) { + return; } + + $assetIds = $this->filterData[$this->getName()]; + if (!is_array($assetIds)) { + $assetIds = $this->stringToArray($assetIds); + } + + $filter = $this->filterBuilder->setConditionType('in') + ->setField($this->_data['config']['identityColumn']) + ->setValue($this->getEntityIdsByAsset($assetIds)) + ->create(); + $this->getContext()->getDataProvider()->addFilter($filter); } /** 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 index c7ca95bed863c..dfd4420c701bb 100644 --- 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 @@ -51,6 +51,7 @@ define([ return; } + this.mediaGalleryEditDetails().keywordsSelect().cacheOptions.plain = []; modalElement.modal('closeModal'); }, 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 index cce859f331d9a..a0d21672eafdb 100644 --- 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 @@ -77,8 +77,7 @@ </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)}}"> + 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 }" @@ -108,9 +107,8 @@ </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 }"/> + <img class="admin__action-multiselect-item-path" + attr="{ src: option.src }"/> </label> </div> <if args="$data.optgroup"> From a01b8ced36db6bcf1498c60dddc0a8c4c4846356 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Wed, 5 Aug 2020 19:16:31 +0800 Subject: [PATCH 0300/1013] magento/magento2#1703: The deleted tags are not removed from Tags drop-down until the web page is refreshed - replaced outdated keywords after saving new details --- .../view/adminhtml/web/js/image/image-actions.js | 4 +++- .../view/adminhtml/web/js/image/image-edit.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) 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 index dfd4420c701bb..3ae4fc049db4d 100644 --- 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 @@ -87,7 +87,8 @@ define([ form = modalElement.find('#image-edit-details-form'), imageId = this.imageModel().getSelected().id, keywords = this.mediaGalleryEditDetails().selectedKeywords(), - imageDetails = this.mediaGalleryImageDetails(); + imageDetails = this.mediaGalleryImageDetails(), + imageEditDetails = this.mediaGalleryEditDetails(); if (form.validation('isValid')) { saveDetails( @@ -99,6 +100,7 @@ define([ this.closeModal(); this.imageModel().reloadGrid(); imageDetails.removeCached(imageId); + imageEditDetails.removeCached(imageId, keywords); if (imageDetails.isActive()) { imageDetails.showImageDetailsById(imageId); 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 index c31bc848bdc70..e142fb12aa96a 100644 --- 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 @@ -223,6 +223,16 @@ define([ } return true; + }, + + /** + * Remove cached image details in edit form + * + * @param {String} id + * @param {String} tags + */ + removeCached: function (id, tags) { + this.images[id].tags = tags; } }); }); From 3b479e833e5585a3e3ea0a69e7f91d7ad156580a Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Mon, 10 Aug 2020 22:03:50 +0800 Subject: [PATCH 0301/1013] magento/magento2#1703: The deleted tags are not removed from Tags drop-down until the web page is refreshed - covered MFTF test --- ...ancedMediaGalleryVerifyUpdatedTagsTest.xml | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml new file mode 100644 index 0000000000000..3672f582b0877 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.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="AdminEnhancedMediaGalleryVerifyUpdatedTagsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1703"/> + <title value="User checks if the deleted tags are removed from Edit page, Tags field"/> + <stories value="User checks if the deleted tags are removed from Edit page, Tags field"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/5064888"/> + <description value="User checks if changes made on the tags are updated from Edit page, Tags field"/> + <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="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="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> + <argument name="keywords" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyMetadataKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + </test> +</tests> From 2ef490f0f8d0d93a6c980d173453daf605d64719 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Tue, 11 Aug 2020 21:19:45 +0800 Subject: [PATCH 0302/1013] magento/magento2#1703: The deleted tags are not removed from Tags drop-down until the web page is refreshed - covered MFTF test --- .../AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml index 3672f582b0877..db59369638da9 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml @@ -5,7 +5,6 @@ * 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="AdminEnhancedMediaGalleryVerifyUpdatedTagsTest"> <annotations> @@ -26,8 +25,9 @@ <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> </after> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> - <argument name="image" value="ImageUpload"/> + <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"/> @@ -35,6 +35,12 @@ <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="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> <argument name="keywords" value="UpdatedImageDetails.keyword"/> </actionGroup> From a8a3f56d4bf94611418690257c1afdc15dbbaa3c Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Wed, 12 Aug 2020 01:06:45 +0800 Subject: [PATCH 0303/1013] magento/magento2#1703: The deleted tags are not removed from Tags drop-down until the web page is refreshed - covered MFTF test --- .../Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml | 6 ------ .../view/adminhtml/web/js/image/image-actions.js | 2 +- .../view/adminhtml/web/js/image/image-edit.js | 5 ++--- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml index db59369638da9..4abb819bed215 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml @@ -35,12 +35,6 @@ <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="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> <argument name="keywords" value="UpdatedImageDetails.keyword"/> </actionGroup> 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 index 3ae4fc049db4d..ea4de9e1feefa 100644 --- 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 @@ -100,7 +100,7 @@ define([ this.closeModal(); this.imageModel().reloadGrid(); imageDetails.removeCached(imageId); - imageEditDetails.removeCached(imageId, keywords); + imageEditDetails.removeCached(imageId); if (imageDetails.isActive()) { imageDetails.showImageDetailsById(imageId); 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 index e142fb12aa96a..e1404a16d7125 100644 --- 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 @@ -229,10 +229,9 @@ define([ * Remove cached image details in edit form * * @param {String} id - * @param {String} tags */ - removeCached: function (id, tags) { - this.images[id].tags = tags; + removeCached: function (id) { + delete this.images[id]; } }); }); From 465ed0b645b22d14643feb2ca4432458aa0387b2 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Wed, 12 Aug 2020 18:10:46 +0800 Subject: [PATCH 0304/1013] magento/magento2#1703: The deleted tags are not removed from Tags drop-down until the web page is refreshed - added verify removal of keyword MFTF test --- ...yVerifyRemovedImageKeywordsActionGroup.xml | 25 +++++++++++++++++++ ...lleryEditAssetRemoveKeywordActionGroup.xml | 21 ++++++++++++++++ ...EnhancedMediaGalleryEditDetailsSection.xml | 1 + ...ancedMediaGalleryVerifyUpdatedTagsTest.xml | 11 ++++++-- 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.xml new file mode 100644 index 0000000000000..b94fbf369ef01 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.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="AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup"> + <annotations> + <description>Verifies removed image keywords on the View Details panel</description> + </annotations> + <arguments> + <argument name="keywords"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.keywords}}" stepKey="grabKeywords"/> + <assertStringNotContainsString stepKey="verifyKeywords"> + <actualResult type="variable">grabKeywords</actualResult> + <expectedResult type="string">{{keywords}}</expectedResult> + </assertStringNotContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml new file mode 100644 index 0000000000000..7bba4fd637189 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.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="AdminMediaGalleryEditAssetRemoveKeywordActionGroup"> + <annotations> + <description>Remove Keywords on the Edit Details panel</description> + </annotations> + <arguments> + <argument name="selectedKeywords"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.removeSelectedKeyword(selectedKeywords)}}" stepKey="removeKeyword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml index b8e2f698ccfe8..7456db0b4d988 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -14,6 +14,7 @@ <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="removeSelectedKeyword" type="button" selector="//span[contains(text(), '{{selectedKeywords}}')]/following-sibling::button[@data-action='remove-selected-item']" parameterized="true"/> <element name="cancel" type="button" selector="#image-details-action-cancel"/> <element name="save" type="button" selector="#image-details-action-save"/> </section> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml index 4abb819bed215..9571d49a88130 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml @@ -38,8 +38,15 @@ <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> <argument name="keywords" value="UpdatedImageDetails.keyword"/> </actionGroup> - <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyMetadataKeywords"> - <argument name="keywords" value="ImageMetadata.keywords"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="updateImageDetails"/> + <actionGroup ref="AdminMediaGalleryEditAssetRemoveKeywordActionGroup" stepKey="removeKeywords"> + <argument name="selectedKeywords" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveUpdatedImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup" stepKey="verifyRemovedKeywords"> + <argument name="keywords" value="UpdatedImageDetails.keyword"/> </actionGroup> </test> </tests> From adb86c6ec0a0186e556f284dc1d350b2e3cce547 Mon Sep 17 00:00:00 2001 From: yolouiese <honeymay@abovethefray.io> Date: Fri, 14 Aug 2020 20:06:50 +0800 Subject: [PATCH 0305/1013] magento/magento2#1703: The deleted tags are not removed from Tags drop-down until the web page is refreshed - added requested changes --- .../AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml | 4 ++-- ...edMediaGalleryAssetDetailsKeywordsAbsentActionGroup.xml} | 6 +++--- .../Section/AdminEnhancedMediaGalleryEditDetailsSection.xml | 2 +- .../Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/{AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.xml => AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup.xml} (73%) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml index 7bba4fd637189..b2ce726b3bd6c 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetRemoveKeywordActionGroup.xml @@ -13,9 +13,9 @@ <description>Remove Keywords on the Edit Details panel</description> </annotations> <arguments> - <argument name="selectedKeywords"/> + <argument name="keyword" type="string"/> </arguments> - <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.removeSelectedKeyword(selectedKeywords)}}" stepKey="removeKeyword"/> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.removeSelectedKeyword(keyword)}}" stepKey="removeKeyword"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup.xml similarity index 73% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup.xml index b94fbf369ef01..be9c7e939103d 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup.xml @@ -8,12 +8,12 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup"> + <actionGroup name="AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup"> <annotations> - <description>Verifies removed image keywords on the View Details panel</description> + <description>Verifies that the passed comma-separated list of keywords are not present on the View Details panel</description> </annotations> <arguments> - <argument name="keywords"/> + <argument name="keywords" type="string"/> </arguments> <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.keywords}}" stepKey="grabKeywords"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml index 7456db0b4d988..b0bed4563003e 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -14,7 +14,7 @@ <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="removeSelectedKeyword" type="button" selector="//span[contains(text(), '{{selectedKeywords}}')]/following-sibling::button[@data-action='remove-selected-item']" parameterized="true"/> + <element name="removeSelectedKeyword" type="button" selector="//span[contains(text(), '{{keyword}}')]/following-sibling::button[@data-action='remove-selected-item']" parameterized="true"/> <element name="cancel" type="button" selector="#image-details-action-cancel"/> <element name="save" type="button" selector="#image-details-action-save"/> </section> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml index 9571d49a88130..f47d6d9202c05 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUpdatedTagsTest.xml @@ -40,13 +40,13 @@ </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="updateImageDetails"/> <actionGroup ref="AdminMediaGalleryEditAssetRemoveKeywordActionGroup" stepKey="removeKeywords"> - <argument name="selectedKeywords" value="UpdatedImageDetails.keyword"/> + <argument name="keyword" value="{{UpdatedImageDetails.keyword}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveUpdatedImage"> <argument name="image" value="UpdatedImageDetails"/> </actionGroup> - <actionGroup ref="AdminEnhancedMediaGalleryVerifyRemovedImageKeywordsActionGroup" stepKey="verifyRemovedKeywords"> - <argument name="keywords" value="UpdatedImageDetails.keyword"/> + <actionGroup ref="AssetAdminEnhancedMediaGalleryAssetDetailsKeywordsAbsentActionGroup" stepKey="verifyRemovedKeywords"> + <argument name="keywords" value="{{UpdatedImageDetails.keyword}}"/> </actionGroup> </test> </tests> From 268382f762b82a5da256accc3d50bafdc70dddbc Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Wed, 19 Aug 2020 16:15:22 +0300 Subject: [PATCH 0306/1013] magento/magento2#29226: [B2B] Add possibility to use "Login as Customer" functionality for Company Admin. --- .../Block/Adminhtml/ConfirmationPopup.php | 5 +++-- .../view/adminhtml/web/js/not-allowed-popup.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php index 538ef3111fe43..646a0df296101 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php @@ -8,9 +8,10 @@ namespace Magento\LoginAsCustomerAdminUi\Block\Adminhtml; use Magento\Backend\Block\Template; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\Serializer\Json; -use Magento\LoginAsCustomerApi\Api\ConfigInterface; use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\ConfirmationPopup\Options; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; /** @@ -61,7 +62,7 @@ public function __construct( $this->storeOptions = $storeOptions; $this->config = $config; $this->json = $json; - $this->options = $options; + $this->options = $options ?? ObjectManager::getInstance()->get(Options::class); } /** 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 index 59d8dd4a7ed49..2e54a249c5be6 100644 --- 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 @@ -35,7 +35,7 @@ define([ modalClass: 'confirm lac-confirm', buttons: [ { - text: $t('Cancel'), + text: $t('Close'), class: 'action-secondary action-dismiss', /** From c129d31741819d1cac2919e2ec38031e802e35d7 Mon Sep 17 00:00:00 2001 From: Soumya Unnikrishnan <sunnikri@adobe.com> Date: Wed, 19 Aug 2020 15:17:55 -0500 Subject: [PATCH 0307/1013] MQE-2271: Release 3.1.0 Delivery Composer + staticRuleset update --- composer.json | 2 +- composer.lock | 636 +++++++++++++++++++++--- dev/tests/acceptance/staticRuleset.json | 3 +- 3 files changed, 571 insertions(+), 70 deletions(-) diff --git a/composer.json b/composer.json index 25be12b5bb72f..1f91e7b8594a1 100644 --- a/composer.json +++ b/composer.json @@ -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", + "magento/magento2-functional-testing-framework": "^3.1", "pdepend/pdepend": "~2.7.1", "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "^2.8.0", diff --git a/composer.lock b/composer.lock index c2eed9d87cc00..5b7e6c3da431a 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": "0b51badfd1978bb34febd90226af9e27", + "content-hash": "b5562151b3be7e921e3ebc8080da557f", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -206,6 +206,16 @@ "ssl", "tls" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], "time": "2020-04-08T08:27:21+00:00" }, { @@ -1346,12 +1356,6 @@ "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" }, { @@ -3319,12 +3323,6 @@ "laminas", "zf" ], - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], "time": "2020-05-20T16:45:56+00:00" }, { @@ -3564,16 +3562,6 @@ "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" }, { @@ -4366,16 +4354,6 @@ "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" }, { @@ -7337,6 +7315,555 @@ ], "time": "2020-06-27T23:57:46+00:00" }, + { + "name": "hoa/consistency", + "version": "1.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Consistency.git", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", + "shasum": "" + }, + "require": { + "hoa/exception": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "hoa/stream": "~1.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Consistency\\": "." + }, + "files": [ + "Prelude.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Consistency library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autoloader", + "callable", + "consistency", + "entity", + "flex", + "keyword", + "library" + ], + "time": "2017-05-02T12:18:12+00:00" + }, + { + "name": "hoa/console", + "version": "3.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Console.git", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Console/zipball/e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/file": "~1.0", + "hoa/protocol": "~1.0", + "hoa/stream": "~1.0", + "hoa/ustring": "~4.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-pcntl": "To enable hoa://Event/Console/Window:resize.", + "hoa/dispatcher": "To use the console kit.", + "hoa/router": "To use the console kit." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Console\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Console library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autocompletion", + "chrome", + "cli", + "console", + "cursor", + "getoption", + "library", + "option", + "parser", + "processus", + "readline", + "terminfo", + "tput", + "window" + ], + "time": "2017-05-02T12:26:19+00:00" + }, + { + "name": "hoa/event", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Event.git", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Event\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Event library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "event", + "library", + "listener", + "observer" + ], + "time": "2017-01-13T15:30:50+00:00" + }, + { + "name": "hoa/exception", + "version": "1.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Exception.git", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Exception\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Exception library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "exception", + "library" + ], + "time": "2017-01-16T07:53:27+00:00" + }, + { + "name": "hoa/file", + "version": "1.17.07.11", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/File.git", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/stream": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\File\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\File library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Socket", + "directory", + "file", + "finder", + "library", + "link", + "temporary" + ], + "time": "2017-07-11T07:42:15+00:00" + }, + { + "name": "hoa/iterator", + "version": "2.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Iterator.git", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Iterator\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Iterator library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "iterator", + "library" + ], + "time": "2017-01-10T10:34:47+00:00" + }, + { + "name": "hoa/protocol", + "version": "1.17.01.14", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Protocol.git", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Protocol\\": "." + }, + "files": [ + "Wrapper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Protocol library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "protocol", + "resource", + "stream", + "wrapper" + ], + "time": "2017-01-14T12:26:10+00:00" + }, + { + "name": "hoa/stream", + "version": "1.17.02.21", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Stream.git", + "reference": "3293cfffca2de10525df51436adf88a559151d82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", + "reference": "3293cfffca2de10525df51436adf88a559151d82", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/protocol": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Stream\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Stream library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Context", + "bucket", + "composite", + "filter", + "in", + "library", + "out", + "protocol", + "stream", + "wrapper" + ], + "time": "2017-02-21T16:01:06+00:00" + }, + { + "name": "hoa/ustring", + "version": "4.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Ustring.git", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", + "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Ustring\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Ustring library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "search", + "string", + "unicode" + ], + "time": "2017-01-16T07:08:25+00:00" + }, { "name": "jms/metadata", "version": "1.7.0", @@ -7709,16 +8236,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699" + "reference": "8a106ea029f222f4354854636861273c7577bee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8d98efa7434a30ab9e82ef128c430ef8e3a50699", - "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", + "reference": "8a106ea029f222f4354854636861273c7577bee9", "shasum": "" }, "require": { @@ -7736,6 +8263,7 @@ "ext-intl": "*", "ext-json": "*", "ext-openssl": "*", + "hoa/console": "~3.0", "monolog/monolog": "^1.17", "mustache/mustache": "~2.5", "php": "^7.3", @@ -7795,7 +8323,7 @@ "magento", "testing" ], - "time": "2020-07-09T21:26:19+00:00" + "time": "2020-08-19T19:57:27+00:00" }, { "name": "mikey179/vfsstream", @@ -8817,20 +9345,6 @@ "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" }, { @@ -9120,12 +9634,6 @@ "keywords": [ "timer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-04-20T06:00:37+00:00" }, { @@ -9181,6 +9689,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-06-27T06:36:25+00:00" }, { @@ -9269,16 +9778,6 @@ "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" }, { @@ -9786,6 +10285,7 @@ ], "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.", "homepage": "https://github.com/sebastianbergmann/finder-facade", + "abandoned": true, "time": "2020-02-08T06:07:58+00:00" }, { diff --git a/dev/tests/acceptance/staticRuleset.json b/dev/tests/acceptance/staticRuleset.json index 74fe3469e353b..82cc9dfe74152 100644 --- a/dev/tests/acceptance/staticRuleset.json +++ b/dev/tests/acceptance/staticRuleset.json @@ -2,6 +2,7 @@ "tests": [ "actionGroupArguments", "deprecatedEntityUsage", - "annotations" + "annotations", + "pauseActionUsage" ] } From e9c14e2da95817a92372b927543507513aca38c1 Mon Sep 17 00:00:00 2001 From: Vadim Malesh <51680850+engcom-Charlie@users.noreply.github.com> Date: Wed, 19 Aug 2020 17:24:52 +0300 Subject: [PATCH 0308/1013] Revert "Add validation phone field on checkout page" --- .../Block/Checkout/LayoutProcessor.php | 3 -- ...rontOnePageCheckoutPhoneValidationTest.xml | 44 ------------------- .../frontend/layout/checkout_index_index.xml | 3 -- .../Customer/Test/Mftf/Data/AddressData.xml | 8 ++-- 4 files changed, 4 insertions(+), 54 deletions(-) delete mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutPhoneValidationTest.xml diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php index 4c84ea5ae764f..16450ec6ff2c2 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php @@ -351,9 +351,6 @@ private function getBillingAddressComponent($paymentCode, $elements) ], ], 'telephone' => [ - 'validation' => [ - 'validate-phoneStrict' => 0, - ], 'config' => [ 'tooltip' => [ 'description' => __('For delivery questions.'), diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutPhoneValidationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutPhoneValidationTest.xml deleted file mode 100644 index b001128fee906..0000000000000 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutPhoneValidationTest.xml +++ /dev/null @@ -1,44 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="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/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index f087c04d1dfe3..289200b83dc2b 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 @@ -223,9 +223,6 @@ </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/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index 695fc138e592b..e31be78185aaf 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">123-456-7890</data> + <data key="telephone">1234568910</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-444-4444</data> + <data key="telephone">444-44-444-44</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">888-777-7890</data> + <data key="telephone">0123456789-02134567</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">123-456-7899</data> + <data key="telephone">44 20 7123 1234</data> <array key="street"> <item>1 London Bridge Street</item> </array> From 8d427e919f2a7ed66a52b4f9721fc9874f445010 Mon Sep 17 00:00:00 2001 From: Daniel Renaud <drenaud@magento.com> Date: Wed, 19 Aug 2020 16:07:13 -0500 Subject: [PATCH 0309/1013] MC-34841: [GraphQL] Free Payment is returned on Address change (No Tax state to Tax enabled state ) --- app/code/Magento/Payment/Model/Method/Free.php | 2 +- app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Payment/Model/Method/Free.php b/app/code/Magento/Payment/Model/Method/Free.php index a02fa4b18d5e1..1df2f942c885a 100644 --- a/app/code/Magento/Payment/Model/Method/Free.php +++ b/app/code/Magento/Payment/Model/Method/Free.php @@ -103,7 +103,7 @@ public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null) return parent::isAvailable( $quote ) && null !== $quote && $this->priceCurrency->round( - $quote->getGrandTotal() + $quote->collectTotals()->getGrandTotal() ) == 0; } diff --git a/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php b/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php index 9fc69a87c5303..4e643f313de18 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php @@ -99,6 +99,9 @@ public function testIsAvailable($grandTotal, $isActive, $notEmptyQuote, $result) $quote = null; if ($notEmptyQuote) { $quote = $this->createMock(Quote::class); + $quote->expects($this->once()) + ->method('collectTotals') + ->willReturn($quote); $quote->expects($this->any()) ->method('__call') ->with('getGrandTotal') From b532d4e46dd6eaa21ffdc94e67e27afd404b4ec7 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Thu, 20 Aug 2020 12:49:35 +0300 Subject: [PATCH 0310/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - MFTF tests fix. --- ...romCustomerPageManualChooseActionGroup.xml | 6 ++--- ...oginAsCustomerConfirmationModalSection.xml | 2 +- ...CustomerManualChooseStoreCodeInUrlTest.xml | 4 +-- .../AdminLoginAsCustomerManualChooseTest.xml | 25 +++++++++++++------ ...sCustomerSeeSpecialPriceOnCategoryTest.xml | 3 --- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml index 44ef227a34257..dc953061ca433 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml @@ -14,15 +14,15 @@ </annotations> <arguments> <argument name="customerId" type="string"/> - <argument name="storeViewName" type="string" defaultValue="default"/> + <argument name="storeName" 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" 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"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <selectOption selector="{{AdminLoginAsCustomerConfirmationModalSection.store}}" userInput="{{storeName}}" stepKey="selectStore"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> <switchToNextTab stepKey="switchToNewTab"/> <waitForPageLoad stepKey="waitForPageLoad"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml index f400ba02a5392..96a9ed6a77f90 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml @@ -9,6 +9,6 @@ <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']"/> + <element name="store" type="select" selector="//select[@id='lac-confirmation-popup-store-id']"/> </section> </sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml index bd61b359f5bf6..4f888e33ac801 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml @@ -29,8 +29,8 @@ 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 ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeCustomStoreViewCodeInUrl" after="assertCustomStoreView"> + <argument name="storeCode" value="{{customStoreEN.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 index 4853d4d69382f..5f706a814eb71 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml @@ -18,9 +18,6 @@ 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"/> @@ -30,11 +27,23 @@ stepKey="enableLoginAsCustomerManualChoose"/> <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> </before> <after> - <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"> - <argument name="customStore" value="customStore"/> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" @@ -49,7 +58,7 @@ <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup" stepKey="loginAsCustomerFromCustomerPage"> <argument name="customerId" value="$$createCustomer.id$$"/> - <argument name="storeViewName" value="{{customStore.name}}"/> + <argument name="storeName" value="{{customStoreGroup.name}}"/> </actionGroup> <!-- Assert Customer logged on on custom store view --> @@ -58,7 +67,7 @@ <argument name="customerEmail" value="$$createCustomer.email$$"/> </actionGroup> <actionGroup ref="StorefrontAssertCustomerOnStoreViewActionGroup" stepKey="assertCustomStoreView"> - <argument name="storeViewName" value="{{customStore.name}}"/> + <argument name="storeViewName" value="{{customStoreEN.name}}"/> </actionGroup> <!-- Log out Customer and close tab --> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml index 3e70da8f8158d..c0d7d26816fa2 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml @@ -16,9 +16,6 @@ <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" From 520e8abf8a31b05cfd57da7c08539161466b7fad Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Thu, 20 Aug 2020 16:55:02 +0300 Subject: [PATCH 0311/1013] magento/magento2-login-as-customer#58: If multiple stores exist under a specific website, user is logged into the default website for that store - pop-up options update. --- .../Component/ConfirmationPopup/Options.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index c11337bbc5fe8..4ec2bed41c799 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -124,9 +124,10 @@ private function generateCurrentOptions(int $customerId): array foreach ($websiteCollection as $website) { $groups = $this->fillStoreGroupOptions($website, $customer); if (!empty($groups)) { + $code = $website->getCode(); $name = $this->sanitizeName($website->getName()); - $options[$name]['label'] = $name; - $options[$name]['value'] = array_values($groups); + $options[$code]['label'] = $name; + $options[$code]['value'] = $groups; } } } @@ -154,11 +155,12 @@ private function fillStoreGroupOptions(Website $website, CustomerInterface $cust if ($group->getWebsiteId() == $websiteId) { $storeViewIds = $group->getStoreIds(); if (!empty($storeViewIds)) { + $code = $group->getCode(); $name = $this->sanitizeName($group->getName()); - $groups[$name]['label'] = str_repeat(' ', 4) . $name; - $groups[$name]['value'] = array_values($storeViewIds)[0]; - $groups[$name]['disabled'] = !$isGlobalScope && $customerWebsiteId !== $websiteId; - $groups[$name]['selected'] = in_array($customerStoreId, $storeViewIds) ? true : false; + $groups[$code]['label'] = str_repeat(' ', 4) . $name; + $groups[$code]['value'] = array_values($storeViewIds)[0]; + $groups[$code]['disabled'] = !$isGlobalScope && $customerWebsiteId !== $websiteId; + $groups[$code]['selected'] = in_array($customerStoreId, $storeViewIds) ? true : false; } } } From 568385f9e35ce4bc4b2580e67eb69eba83ccdfdf Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Thu, 20 Aug 2020 10:28:30 +0300 Subject: [PATCH 0312/1013] improve test --- .../Customer/Controller/AccountTest.php | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php index c2e55029cab13..5527a39ce0507 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/AccountTest.php @@ -22,15 +22,17 @@ use Magento\Store\Model\StoreManager; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\Mail\Template\TransportBuilderMock; use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractController; use Magento\Theme\Controller\Result\MessagePlugin; use PHPUnit\Framework\Constraint\StringContains; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AccountTest extends \Magento\TestFramework\TestCase\AbstractController +class AccountTest extends AbstractController { /** * @var TransportBuilderMock @@ -54,9 +56,8 @@ protected function setUp(): void */ protected function login($customerId) { - /** @var \Magento\Customer\Model\Session $session */ - $session = Bootstrap::getObjectManager() - ->get(\Magento\Customer\Model\Session::class); + /** @var Session $session */ + $session = Bootstrap::getObjectManager()->get(Session::class); $session->loginById($customerId); } @@ -148,8 +149,8 @@ public function testCreatepasswordActionWithSession() $customer->setData('confirmation', 'confirmation'); $customer->save(); - /** @var \Magento\Customer\Model\Session $customer */ - $session = Bootstrap::getObjectManager()->get(\Magento\Customer\Model\Session::class); + /** @var Session $customer */ + $session = Bootstrap::getObjectManager()->get(Session::class); $session->setRpToken($token); $session->setRpCustomerId($customer->getId()); @@ -404,18 +405,16 @@ public function testEditAction() $this->assertEquals(200, $this->getResponse()->getHttpResponseCode(), $body); $this->assertStringContainsString('<div class="field field-name-firstname required">', $body); // Verify the password check box is not checked - $expectedString = <<<EXPECTED_HTML -<input type="checkbox" name="change_password" id="change-password" data-role="change-password" value="1" - title="Change Password" - class="checkbox" /> -EXPECTED_HTML; - $this->assertStringContainsString($expectedString, $body); + $checkboxXpath = '//input[@type="checkbox"][@name="change_password"][@id="change-password"][not (@checked)]' . + '[@data-role="change-password"][@value="1"][@title="Change Password"][@class="checkbox"]'; + + $this->assertEquals(1, Xpath::getElementsCountForXpath($checkboxXpath, $body)); } /** * @magentoDataFixture Magento/Customer/_files/customer.php */ - public function testChangePasswordEditAction() + public function testChangePasswordEditAction(): void { $this->login(1); @@ -425,12 +424,11 @@ public function testChangePasswordEditAction() $this->assertEquals(200, $this->getResponse()->getHttpResponseCode(), $body); $this->assertStringContainsString('<div class="field field-name-firstname required">', $body); // Verify the password check box is checked - $expectedString = <<<EXPECTED_HTML -<input type="checkbox" name="change_password" id="change-password" data-role="change-password" value="1" - title="Change Password" - checked="checked" class="checkbox" /> -EXPECTED_HTML; - $this->assertStringContainsString($expectedString, $body); + $checkboxXpath = '//input[@type="checkbox"][@name="change_password"][@id="change-password"]' . + '[@data-role="change-password"][@value="1"][@title="Change Password"][@checked="checked"]' . + '[@class="checkbox"]'; + + $this->assertEquals(1, Xpath::getElementsCountForXpath($checkboxXpath, $body)); } /** From f610c71350428f01c7de1b49b245dfa45e6a5c74 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Thu, 20 Aug 2020 13:47:09 -0500 Subject: [PATCH 0313/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - added store config and modified schema --- .../Magento/WishlistGraphQl/etc/graphql/di.xml | 17 +++++++++++++++++ .../Magento/WishlistGraphQl/etc/schema.graphqls | 12 ++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 app/code/Magento/WishlistGraphQl/etc/graphql/di.xml diff --git a/app/code/Magento/WishlistGraphQl/etc/graphql/di.xml b/app/code/Magento/WishlistGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..4d4ce9458fb6c --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/etc/graphql/di.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="magento_wishlist_general_is_enabled" xsi:type="string">wishlist/general/active</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 430e77cc45e96..5fc7ef33f3e22 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -40,8 +40,8 @@ type Mutation { } 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") + 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") @@ -58,8 +58,8 @@ type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's } 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") + 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") @@ -79,3 +79,7 @@ enum WishListUserInputErrorType { PRODUCT_NOT_FOUND UNDEFINED } + +type StoreConfig { + magento_wishlist_general_is_enabled: String @doc(description: "Wishlist functionality status: enabled/disabled") +} From e55b271e2acbf933e91b560ce007fd9de2b5992e Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Thu, 20 Aug 2020 14:33:48 -0500 Subject: [PATCH 0314/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - description change --- app/code/Magento/WishlistGraphQl/etc/schema.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 5fc7ef33f3e22..31bf32fa5ebcf 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -81,5 +81,5 @@ enum WishListUserInputErrorType { } type StoreConfig { - magento_wishlist_general_is_enabled: String @doc(description: "Wishlist functionality status: enabled/disabled") + magento_wishlist_general_is_enabled: String @doc(description: "Indicates whether wishlists are enabled (1) or disabled (0)") } From eb8b56531f1b2af7e3c49660ffe2d1cc122f855d Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Thu, 20 Aug 2020 17:24:25 -0500 Subject: [PATCH 0315/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - Added fix for erroring out on no quantity --- .../Model/Resolver/UpdateProductsInWishlist.php | 9 +++++++-- app/code/Magento/WishlistGraphQl/etc/schema.graphqls | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php index c6ede66fc2b1b..53c4e26d26124 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -101,7 +101,7 @@ public function resolve( } $wishlistItems = $args['wishlistItems']; - $wishlistItems = $this->getWishlistItems($wishlistItems); + $wishlistItems = $this->getWishlistItems($wishlistItems, $wishlist); $wishlistOutput = $this->updateProductsInWishlist->execute($wishlist, $wishlistItems); if (count($wishlistOutput->getErrors()) !== count($wishlistItems)) { @@ -126,14 +126,19 @@ function (Error $error) { * Get DTO wishlist items * * @param array $wishlistItemsData + * @param Wishlist $wishlist * * @return array */ - private function getWishlistItems(array $wishlistItemsData): array + private function getWishlistItems(array $wishlistItemsData, Wishlist $wishlist): array { $wishlistItems = []; foreach ($wishlistItemsData as $wishlistItemData) { + if (!isset($wishlistItemData['quantity'])) { + $wishlistItem = $wishlist->getItem($wishlistItemData['wishlist_item_id']); + $wishlistItemData['quantity'] = (float) $wishlistItem->getQty(); + } $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); } diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index 31bf32fa5ebcf..bf46b269e6421 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -59,7 +59,7 @@ type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's 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") + 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") From 11619df5f58fe65c9d8b81ec61d1e7e5ce4b2a50 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 20 Aug 2020 18:38:07 -0500 Subject: [PATCH 0316/1013] MC-36838: Cannot checkout with automatic customer group assignment --- .../Frontend/Quote/Address/CollectTotalsObserver.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php index a1228903e2323..ccac95b3c0401 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -135,8 +135,10 @@ public function execute(\Magento\Framework\Event\Observer $observer) $address->setPrevQuoteCustomerGroupId($quote->getCustomerGroupId()); $quote->setCustomerGroupId($groupId); $this->customerSession->setCustomerGroupId($groupId); - $customer->setGroupId($groupId); - $quote->setCustomer($customer); + if ($customer->getId() !== null) { + $customer->setGroupId($groupId); + $quote->setCustomer($customer); + } } } } From ed29f16c821bfeb62620c0bbd276d36ec5b08556 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 20 Aug 2020 20:31:59 -0500 Subject: [PATCH 0317/1013] MC-36838: Cannot checkout with automatic customer group assignment --- .../GraphQl/Quote/Guest/PlaceOrderTest.php | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php index f1aa4f02a6922..72d35fdd51b96 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -7,7 +7,7 @@ namespace Magento\GraphQl\Quote\Guest; -use Exception; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Registry; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Sales\Api\OrderRepositoryInterface; @@ -56,7 +56,10 @@ protected function setUp(): void $this->orderCollectionFactory = $objectManager->get(CollectionFactory::class); $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); $this->orderFactory = $objectManager->get(OrderFactory::class); - $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->registry = $objectManager->get(Registry::class); + /** @var ScopeConfigInterface $scopeConfig */ + $scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); } /** @@ -68,6 +71,7 @@ protected function setUp(): void * @magentoConfigFixture default_store payment/cashondelivery/active 1 * @magentoConfigFixture default_store payment/checkmo/active 1 * @magentoConfigFixture default_store payment/purchaseorder/active 1 + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 0 * @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 @@ -94,6 +98,42 @@ public function testPlaceOrder() } /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoConfigFixture default_store carriers/flatrate/active 1 + * @magentoConfigFixture default_store carriers/tablerate/active 1 + * @magentoConfigFixture default_store carriers/freeshipping/active 1 + * @magentoConfigFixture default_store payment/banktransfer/active 1 + * @magentoConfigFixture default_store payment/cashondelivery/active 1 + * @magentoConfigFixture default_store payment/checkmo/active 1 + * @magentoConfigFixture default_store payment/purchaseorder/active 1 + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 1 + * @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 + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_checkmo_payment_method.php + */ + public function testPlaceOrderWithAutoGroup() + { + $reservedOrderId = 'test_quote'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute($reservedOrderId); + + $query = $this->getQuery($maskedQuoteId); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('placeOrder', $response); + self::assertArrayHasKey('order_number', $response['placeOrder']['order']); + self::assertEquals($reservedOrderId, $response['placeOrder']['order']['order_number']); + $orderIncrementId = $response['placeOrder']['order']['order_number']; + $order = $this->orderFactory->create(); + $order->loadByIncrementId($orderIncrementId); + $this->assertNotEmpty($order->getEmailSent()); + } + + /** + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 0 */ public function testPlaceOrderIfCartIdIsEmpty() { @@ -115,6 +155,7 @@ public function testPlaceOrderIfCartIdIsEmpty() * @magentoConfigFixture default_store payment/cashondelivery/active 1 * @magentoConfigFixture default_store payment/checkmo/active 1 * @magentoConfigFixture default_store payment/purchaseorder/active 1 + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 0 * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php @@ -268,6 +309,7 @@ public function testPlaceOrderWithOutOfStockProduct() * @magentoConfigFixture default_store payment/cashondelivery/active 1 * @magentoConfigFixture default_store payment/checkmo/active 1 * @magentoConfigFixture default_store payment/purchaseorder/active 1 + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 0 * @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_shipping_address.php From 814396fd3ae8647a03abbbeca169b85b5d02de40 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 20 Aug 2020 22:15:52 -0500 Subject: [PATCH 0318/1013] MC-36838: Cannot checkout with automatic customer group assignment --- .../Frontend/Quote/Address/CollectTotalsObserverTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index 1920b088b1c0e..d5cef5896c335 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -269,7 +269,7 @@ public function testDispatchWithDefaultCustomerGroupId() $this->quoteMock->expects($this->once()) ->method('getCustomerGroupId') ->willReturn('customerGroupId'); - $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); + $this->customerMock->expects($this->exactly(2))->method('getId')->willReturn('1'); $this->groupManagementMock->expects($this->once()) ->method('getDefaultGroup') ->willReturn($this->groupInterfaceMock); @@ -329,6 +329,7 @@ public function testDispatchWithCustomerCountryInEU() ->method('setPrevQuoteCustomerGroupId') ->with('customerGroupId'); + $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); $this->quoteMock->expects($this->once())->method('setCustomerGroupId')->with('customerGroupId'); $this->quoteMock->expects($this->once())->method('setCustomer')->with($this->customerMock); $this->customerDataFactoryMock->expects($this->any()) @@ -436,6 +437,8 @@ public function testDispatchWithEmptyShippingAddress() ->method('setPrevQuoteCustomerGroupId') ->with('customerGroupId'); + $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); + $this->quoteMock->expects($this->once())->method('setCustomerGroupId')->with('customerGroupId'); $this->quoteMock->expects($this->once())->method('setCustomer')->with($this->customerMock); $this->customerDataFactoryMock->expects($this->any()) From f52921e5adc0c99e2f5658642ad828f2bb7a5d8a Mon Sep 17 00:00:00 2001 From: Valerii Naida <vnayda@adobe.com> Date: Fri, 21 Aug 2020 09:24:30 -0500 Subject: [PATCH 0319/1013] magento2/issues/29687: Revert @api annotation in Magento\LoginAsCustomerAssistance\Block\Adminhtml\NotAllowedPopup --- .../Model/ResourceModel/SaveAuthenticationData.php | 2 +- .../Block/Adminhtml/NotAllowedPopup.php | 2 -- .../Magento/Test/Integrity/_files/whitelist/public_code.txt | 1 + 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 dev/tests/static/testsuite/Magento/Test/Integrity/_files/whitelist/public_code.txt diff --git a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php index 23d707d151487..10d110c8ddf0b 100644 --- a/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php +++ b/app/code/Magento/LoginAsCustomer/Model/ResourceModel/SaveAuthenticationData.php @@ -41,10 +41,10 @@ class SaveAuthenticationData implements SaveAuthenticationDataInterface private $random; /** - * @param EncryptorInterface $encryptor * @param ResourceConnection $resourceConnection * @param DateTime $dateTime * @param Random $random + * @param EncryptorInterface $encryptor */ public function __construct( ResourceConnection $resourceConnection, diff --git a/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php b/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php index 547be1de5a008..f0f4eba026733 100644 --- a/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php +++ b/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php @@ -13,8 +13,6 @@ /** * Pop-up for Login as Customer button then Login as Customer is not allowed. - * - * @api */ class NotAllowedPopup extends Template { diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/whitelist/public_code.txt b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/whitelist/public_code.txt new file mode 100644 index 0000000000000..e48bfda936545 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/whitelist/public_code.txt @@ -0,0 +1 @@ +Magento\LoginAsCustomerAssistance\Block\Adminhtml\NotAllowedPopup From 0ce373e9d2c859ef64de040ee62756a3f0ad6a05 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Fri, 21 Aug 2020 12:31:30 -0500 Subject: [PATCH 0320/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - Added validation for quantity ,description fix and schema description update, --- .../Wishlist/Model/Wishlist/UpdateProductsInWishlist.php | 3 +++ .../Model/Resolver/UpdateProductsInWishlist.php | 4 ++++ app/code/Magento/WishlistGraphQl/etc/schema.graphqls | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php index 4abcada138362..4455b118e7a5e 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -95,6 +95,9 @@ private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wish $wishlistItem = $this->wishlistItemFactory->create(); $this->wishlistItemResource->load($wishlistItem, $wishlistItemData->getId()); $wishlistItem->setDescription($wishlistItemData->getDescription()); + if ($wishlistItemData->getQuantity() == 0) { + throw new LocalizedException(__("The quantity of a wish list item cannot be 0")); + } $resultItem = $wishlist->updateItem($wishlistItem, $options); if (is_string($resultItem)) { diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php index 53c4e26d26124..1d0d7f8b61dc0 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -139,6 +139,10 @@ private function getWishlistItems(array $wishlistItemsData, Wishlist $wishlist): $wishlistItem = $wishlist->getItem($wishlistItemData['wishlist_item_id']); $wishlistItemData['quantity'] = (float) $wishlistItem->getQty(); } + if (!isset($wishlistItemData['description'])) { + $wishlistItem = $wishlist->getItem($wishlistItemData['wishlist_item_id']); + $wishlistItemData['description'] = $wishlistItem->getDescription(); + } $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); } diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index bf46b269e6421..2992f851cf328 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -60,7 +60,7 @@ type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's 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") + description: String @doc(description: "Customer-entered comments about the item") 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") } From 250e3d9f58fd2ab4ae753f6ecd75dee2e1af17e9 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 21 Aug 2020 19:56:41 -0500 Subject: [PATCH 0321/1013] MC-34841: free-payment --- app/code/Magento/Payment/Model/Method/Free.php | 2 +- .../Payment/Test/Unit/Model/Method/FreeTest.php | 3 --- .../Model/Cart/SetShippingAddressesOnCart.php | 12 +++++++++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Payment/Model/Method/Free.php b/app/code/Magento/Payment/Model/Method/Free.php index 1df2f942c885a..a02fa4b18d5e1 100644 --- a/app/code/Magento/Payment/Model/Method/Free.php +++ b/app/code/Magento/Payment/Model/Method/Free.php @@ -103,7 +103,7 @@ public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null) return parent::isAvailable( $quote ) && null !== $quote && $this->priceCurrency->round( - $quote->collectTotals()->getGrandTotal() + $quote->getGrandTotal() ) == 0; } diff --git a/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php b/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php index 4e643f313de18..9fc69a87c5303 100644 --- a/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php +++ b/app/code/Magento/Payment/Test/Unit/Model/Method/FreeTest.php @@ -99,9 +99,6 @@ public function testIsAvailable($grandTotal, $isActive, $notEmptyQuote, $result) $quote = null; if ($notEmptyQuote) { $quote = $this->createMock(Quote::class); - $quote->expects($this->once()) - ->method('collectTotals') - ->willReturn($quote); $quote->expects($this->any()) ->method('__call') ->with('getGrandTotal') diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index e959c19a7cbe4..644c0569cc195 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -10,6 +10,7 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\QuoteRepository; /** * Set single shipping address for a specified shopping cart @@ -26,16 +27,23 @@ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface */ private $getShippingAddress; + /** + * @var QuoteRepository + */ + private $quoteRepository; + /** * @param AssignShippingAddressToCart $assignShippingAddressToCart * @param GetShippingAddress $getShippingAddress */ public function __construct( AssignShippingAddressToCart $assignShippingAddressToCart, - GetShippingAddress $getShippingAddress + GetShippingAddress $getShippingAddress, + QuoteRepository $quoteRepository ) { $this->assignShippingAddressToCart = $assignShippingAddressToCart; $this->getShippingAddress = $getShippingAddress; + $this->quoteRepository = $quoteRepository; } /** @@ -70,5 +78,7 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s throw $e; } $this->assignShippingAddressToCart->execute($cart, $shippingAddress); + // trigger quote re-evaluation after address change + $this->quoteRepository->save($cart); } } From 97ed9c404be74f986619605e84146d4d554581e5 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 21 Aug 2020 23:24:25 -0500 Subject: [PATCH 0322/1013] MC-34841: free-payment --- .../QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 644c0569cc195..0f350be78a945 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -35,6 +35,7 @@ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface /** * @param AssignShippingAddressToCart $assignShippingAddressToCart * @param GetShippingAddress $getShippingAddress + * @param QuoteRepository $quoteRepository */ public function __construct( AssignShippingAddressToCart $assignShippingAddressToCart, From 73760de6f54c4d0976b09c44567a3b79853fb42e Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Sun, 23 Aug 2020 17:03:21 -0500 Subject: [PATCH 0323/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - Added validation for quantity in add products wo wishlist mutation --- .../Wishlist/Model/Wishlist/AddProductsToWishlist.php | 3 +++ .../Model/Resolver/UpdateProductsInWishlist.php | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php index 7acfb503a5ad0..c8eb98b938573 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php @@ -113,6 +113,9 @@ private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistIte } try { + if ($wishlistItem->getQuantity() == 0) { + throw new LocalizedException(__("The quantity of a wish list item cannot be 0")); + } $options = $this->buyRequestBuilder->build($wishlistItem, (int) $product->getId()); $result = $wishlist->addNewItem($product, $options); diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php index 1d0d7f8b61dc0..d9e431223b642 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -137,11 +137,15 @@ private function getWishlistItems(array $wishlistItemsData, Wishlist $wishlist): foreach ($wishlistItemsData as $wishlistItemData) { if (!isset($wishlistItemData['quantity'])) { $wishlistItem = $wishlist->getItem($wishlistItemData['wishlist_item_id']); - $wishlistItemData['quantity'] = (float) $wishlistItem->getQty(); + if (isset($wishlistItem)) { + $wishlistItemData['quantity'] = (float) $wishlistItem->getQty(); + } } if (!isset($wishlistItemData['description'])) { $wishlistItem = $wishlist->getItem($wishlistItemData['wishlist_item_id']); - $wishlistItemData['description'] = $wishlistItem->getDescription(); + if (isset($wishlistItem)) { + $wishlistItemData['description'] = $wishlistItem->getDescription(); + } } $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); } From 482357d0fdbb62a5f2572131a46392cd5a175ab4 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Sun, 23 Aug 2020 22:53:12 -0500 Subject: [PATCH 0324/1013] MC-36897: [GraphQl] Issues with updating wishlist quantity. Adding Recommended Store config changes - added tests to cover bugs --- .../UpdateProductsFromWishlistTest.php | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index 9a9cd424e54ca..900ae1ce16441 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -57,6 +57,57 @@ public function testUpdateSimpleProductFromWishlist(): void self::assertEquals($description, $wishlistResponse['items'][0]['description']); } + /** + * update the wishlist by setting an qty = 0 + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testUpdateProductInWishlistWithZeroQty() + { + $wishlist = $this->getWishlist(); + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $qty = 0; + $description = 'Description for zero quantity'; + $updateWishlistQuery = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); + self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); + self::assertArrayHasKey('user_errors', $response['updateProductsInWishlist']); + self::assertCount(1, $response['updateProductsInWishlist']['user_errors']); + $message = 'The quantity of a wish list item cannot be 0'; + self::assertEquals( + $message, + $response['updateProductsInWishlist']['user_errors'][0]['message'] + ); + } + + /** + * update the wishlist by setting qty to a valid value and no description + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testUpdateProductWithValidQtyAndNoDescription() + { + $wishlist = $this->getWishlist(); + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $qty = 2; + $updateWishlistQuery = $this->getQueryWithNoDescription((int) $wishlistId, (int) $wishlistItem['id'], $qty); + $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); + self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); + $itemsInWishlist = $response['updateProductsInWishlist']['wishlist']['items'][0]; + self::assertEquals($qty, $itemsInWishlist['qty']); + self::assertEquals('simple-1', $itemsInWishlist['product']['sku']); + } + /** * Authentication header map * @@ -121,6 +172,51 @@ private function getQuery( MUTATION; } + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * @param int $qty + * + * @return string + */ + private function getQueryWithNoDescription( + int $wishlistId, + int $wishlistItemId, + int $qty + ): string { + return <<<MUTATION +mutation { + updateProductsInWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + wishlist_item_id: "{$wishlistItemId}" + quantity: {$qty} + + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + items { + id + qty + product{sku name} + } + } + } +} +MUTATION; + } + /** * Get wishlist result * From db80d11594ec72e8b29b8c83b4022218933cff45 Mon Sep 17 00:00:00 2001 From: Hwashiang Yu <hwyu@adobe.com> Date: Mon, 24 Aug 2020 09:44:50 -0500 Subject: [PATCH 0325/1013] MC-36490: Template preview update - Updated template preview for email and newsletter module - Updated preview template tests --- .../Block/Adminhtml/Template/Preview.php | 1 + .../Magento/Email/Model/AbstractTemplate.php | 9 +++- .../PrepareDraftCustomTemplateActionGroup.xml | 25 ++++++++++ .../Test/Mftf/Data/EmailTemplateData.xml | 6 +++ ...mplatePreviewAsDraftWithDirectivesTest.xml | 47 +++++++++++++++++++ .../Test/AdminEmailTemplatePreviewTest.xml | 1 + ...EmailTemplatePreviewWithDirectivesTest.xml | 45 ++++++++++++++++++ .../Block/Adminhtml/Queue/Preview.php | 1 + .../Block/Adminhtml/Template/Preview.php | 1 + .../templates/preview/iframeswitcher.phtml | 2 +- .../Magento/Email/Model/TemplateTest.php | 41 +++++++++++++++- .../Adminhtml/Template/DropTest.php | 43 +++++++++++++++++ .../Magento/Newsletter/Model/TemplateTest.php | 2 + 13 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/Email/Test/Mftf/ActionGroup/PrepareDraftCustomTemplateActionGroup.xml create mode 100644 app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewAsDraftWithDirectivesTest.xml create mode 100644 app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewWithDirectivesTest.xml create mode 100644 dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/Template/DropTest.php diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php b/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php index ec5596e2194a1..58fa4a1d318ff 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Preview.php @@ -67,6 +67,7 @@ protected function _toHtml() $template->setTemplateType($request->getParam('type')); $template->setTemplateText($request->getParam('text')); $template->setTemplateStyles($request->getParam('styles')); + $template->setData('is_legacy', false); } \Magento\Framework\Profiler::start($this->profilerName); diff --git a/app/code/Magento/Email/Model/AbstractTemplate.php b/app/code/Magento/Email/Model/AbstractTemplate.php index c697734b9df0f..1a05f88d8fa8f 100644 --- a/app/code/Magento/Email/Model/AbstractTemplate.php +++ b/app/code/Magento/Email/Model/AbstractTemplate.php @@ -361,8 +361,15 @@ public function getProcessedTemplate(array $variables = []) $variables = $this->addEmailVariables($variables, $storeId); $processor->setVariables($variables); + // Type legacy id strict + // db legacy true numeric false + // db new false numeric true + // filesystem false string false + // preview false null true + $isLegacy = $this->getData('is_legacy'); + $templateId = $this->getTemplateId(); $previousStrictMode = $processor->setStrictMode( - !$this->getData('is_legacy') && is_numeric($this->getTemplateId()) + !$isLegacy && (is_numeric($templateId) || empty($templateId)) ); try { diff --git a/app/code/Magento/Email/Test/Mftf/ActionGroup/PrepareDraftCustomTemplateActionGroup.xml b/app/code/Magento/Email/Test/Mftf/ActionGroup/PrepareDraftCustomTemplateActionGroup.xml new file mode 100644 index 0000000000000..72c7a3ee8e4ca --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/ActionGroup/PrepareDraftCustomTemplateActionGroup.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="PrepareDraftCustomTemplateActionGroup" extends="CreateNewTemplateActionGroup"> + <arguments> + <argument name="template" defaultValue="EmailTemplate"/> + </arguments> + <remove keyForRemoval="selectValueFromTemplateDropDown"/> + <remove keyForRemoval="clickLoadTemplateButton"/> + <remove keyForRemoval="clickSaveTemplateButton"/> + <remove keyForRemoval="waitForSuccessMessage"/> + <remove keyForRemoval="seeSuccessMessage"/> + + <fillField selector="{{AdminEmailTemplateEditSection.templateSubject}}" userInput="{{template.templateSubject}}" after="fillTemplateNameField" stepKey="fillTemplateSubject"/> + <fillField selector="{{AdminEmailTemplateEditSection.templateText}}" userInput="{{template.templateText}}" after="fillTemplateSubject" stepKey="fillTemplateText"/> + </actionGroup> + +</actionGroups> diff --git a/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml b/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml index 7f28e2241761b..06aff6ed6ab7e 100644 --- a/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml +++ b/app/code/Magento/Email/Test/Mftf/Data/EmailTemplateData.xml @@ -13,4 +13,10 @@ <data key="templateSubject" unique="suffix">Template Subject_</data> <data key="templateText" unique="suffix">Template Text_</data> </entity> + <entity name="EmailTemplateWithDirectives" type="template"> + <data key="templateName" unique="suffix">Template</data> + <data key="templateSubject" unique="suffix">Template Subject_</data> + <data key="templateText">Template {{var this.template_id}}:{{var this.getData(template_id)}} Text</data> + <data key="expectedTemplate">Template : Text</data> + </entity> </entities> diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewAsDraftWithDirectivesTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewAsDraftWithDirectivesTest.xml new file mode 100644 index 0000000000000..f61a0bba91046 --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewAsDraftWithDirectivesTest.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="AdminEmailTemplatePreviewAsDraftWithDirectivesTest"> + <annotations> + <features value="Email"/> + <stories value="Create email template with directives and preview as draft"/> + <title value="Check email template preview with directives and preview as draft"/> + <description value="Check if email template preview works correctly with directives in draft mode"/> + <severity value="CRITICAL"/> + <useCaseId value="MC-23058"/> + <group value="email"/> + <group value="WYSIWYGDisabled"/> + <stories value="Email Template Preview"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + + <actionGroup ref="PrepareDraftCustomTemplateActionGroup" stepKey="createDraftTemplate"> + <argument name="template" value="EmailTemplateWithDirectives"/> + </actionGroup> + + <click selector="{{AdminEmailTemplateEditSection.previewTemplateButton}}" stepKey="clickPreviewTemplate"/> + <switchToNextTab stepKey="switchToPreviewTab"/> + <seeInCurrentUrl url="{{AdminEmailTemplatePreviewPage.url}}" stepKey="seePreviewInUrl"/> + <seeElement selector="{{AdminEmailTemplatePreviewSection.iframe}}" stepKey="seeIframeOnPage"/> + <switchToIFrame userInput="preview_iframe" stepKey="switchToIframe"/> + <waitForPageLoad stepKey="waitForPreviewLoaded"/> + + <actionGroup ref="AssertEmailTemplateContentActionGroup" stepKey="assertContent"> + <argument name="expectedContent" value="{{EmailTemplateWithDirectives.expectedTemplate}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewTest.xml index 0d9ca6a2c195a..22e70b72e5401 100644 --- a/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewTest.xml +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewTest.xml @@ -18,6 +18,7 @@ <testCaseId value="MC-15794"/> <useCaseId value="MC-11050"/> <group value="email"/> + <group value="WYSIWYGDisabled"/> </annotations> <before> diff --git a/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewWithDirectivesTest.xml b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewWithDirectivesTest.xml new file mode 100644 index 0000000000000..7683591e8f37f --- /dev/null +++ b/app/code/Magento/Email/Test/Mftf/Test/AdminEmailTemplatePreviewWithDirectivesTest.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="AdminEmailTemplatePreviewWithDirectivesTest"> + <annotations> + <features value="Email"/> + <stories value="Create email template with directives"/> + <title value="Check email template preview with directives"/> + <description value="Check if email template preview works correctly with directives"/> + <severity value="CRITICAL"/> + <useCaseId value="MC-23058"/> + <group value="email"/> + <group value="WYSIWYGDisabled"/> + <stories value="Email Template Preview"/> + </annotations> + + <before> + <!--Login to Admin Area--> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminArea"/> + </before> + + <after> + <!--Delete created Template--> + <actionGroup ref="DeleteEmailTemplateActionGroup" stepKey="deleteTemplate"/> + <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clearFilters"/> + <!--Logout from Admin Area--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + + <actionGroup ref="CreateCustomTemplateActionGroup" stepKey="createTemplate"> + <argument name="template" value="EmailTemplateWithDirectives"/> + </actionGroup> + <actionGroup ref="PreviewEmailTemplateActionGroup" stepKey="previewTemplate"/> + <actionGroup ref="AssertEmailTemplateContentActionGroup" stepKey="assertContent"> + <argument name="expectedContent" value="{{EmailTemplateWithDirectives.expectedTemplate}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php index 69512775f4e93..2ede548b5ebd8 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php @@ -56,6 +56,7 @@ protected function loadTemplate(\Magento\Newsletter\Model\Template $template, $i $template->setTemplateType($queue->getNewsletterType()); $template->setTemplateText($queue->getNewsletterText()); $template->setTemplateStyles($queue->getNewsletterStyles()); + $template->setData('is_legacy', false); return $this; } diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php index 58a904248e978..1eb1c0ff4a15a 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Template/Preview.php @@ -65,6 +65,7 @@ protected function _toHtml() $template->setTemplateType($previewData['type']); $template->setTemplateText($previewData['text']); $template->setTemplateStyles($previewData['styles']); + $template->setData('is_legacy', false); } \Magento\Framework\Profiler::start($this->profilerName); 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 62b368b8911f8..99342fd9d81ba 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -22,7 +22,7 @@ frameborder="0" title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" width="100%" - sandbox="allow-forms allow-pointer-lock" + sandbox="allow-forms allow-pointer-lock allow-same-origin" > </iframe> diff --git a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php index f09a0429979ba..3617c467da659 100644 --- a/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Email/Model/TemplateTest.php @@ -75,7 +75,11 @@ protected function mockModel($filesystem = null) $this->objectManager->get(\Magento\Framework\App\State::class)->setAreaCode('frontend'); $this->model->expects($this->any())->method('_getMail')->willReturnCallback([$this, 'getMail']); - $this->model->setSenderName('sender')->setSenderEmail('sender@example.com')->setTemplateSubject('Subject'); + $this->model + ->setSenderName('sender') + ->setSenderEmail('sender@example.com') + ->setTemplateSubject('Subject') + ->setTemplateId('abc'); } /** @@ -120,6 +124,7 @@ public function testLoadDefault() public function testGetProcessedTemplate() { $this->mockModel(); + $this->model->setTemplateId(null); $this->objectManager->get(\Magento\Framework\App\AreaList::class) ->getArea(Area::AREA_FRONTEND) ->load(); @@ -416,6 +421,40 @@ public function testLegacyTemplateLoadedFromDbIsFilteredInLegacyMode() self::assertEquals('1 - some_unique_code - 1 - some_unique_code', $this->model->getProcessedTemplate()); } + /** + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoComponentsDir Magento/Email/Model/_files/design + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testPreviewTemplateIsFilteredInStrictMode() + { + $this->mockModel(); + + $this->setUpThemeFallback(BackendFrontNameResolver::AREA_CODE); + + $this->model->setTemplateType(TemplateTypesInterface::TYPE_HTML); + $template = '{{var store.isSaveAllowed()}} - {{template config_path="design/email/footer_template"}}'; + $this->model->setTemplateText($template); + + $template = $this->objectManager->create(\Magento\Email\Model\Template::class); + $templateData = [ + 'is_legacy' => '0', + 'template_code' => 'some_unique_code', + 'template_type' => TemplateTypesInterface::TYPE_HTML, + 'template_text' => '{{var this.template_code}}' + . ' - {{var store.isSaveAllowed()}} - {{var this.getTemplateCode()}}', + ]; + $template->setData($templateData); + $template->save(); + + // Store the ID of the newly created template in the system config so that this template will be loaded + $this->objectManager->get(\Magento\Framework\App\Config\MutableScopeConfigInterface::class) + ->setValue('design/email/footer_template', $template->getId(), ScopeInterface::SCOPE_STORE, 'fixturestore'); + + self::assertEquals('1 - some_unique_code - - some_unique_code', $this->model->getProcessedTemplate()); + } + /** * Ensure that the template_styles variable contains styles from either <!--@styles @--> or the "Template Styles" * textarea in backend, depending on whether template was loaded from filesystem or DB. diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/Template/DropTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/Template/DropTest.php new file mode 100644 index 0000000000000..5ad8d1c3328a6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/Template/DropTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Newsletter\Controller\Adminhtml\Template; + +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Data\Form\FormKey; +use Magento\Newsletter\Model\Template; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +class DropTest extends AbstractBackendController +{ + public function testDefaultTemplateAction() + { + $website = $this->_objectManager + ->get(StoreManagerInterface::class) + ->getWebsite(); + + $storeId = $website->getDefaultStore() + ->getId(); + + /** @var $formKey FormKey */ + $formKey = $this->_objectManager->get(FormKey::class); + $post = [ + 'form_key' => $formKey->getFormKey(), + 'type' => Template::TYPE_HTML, + 'preview_store_id' => $storeId, + 'text' => 'Template {{var this.template_id}}:{{var this.getData(template_id)}} Text' + ]; + $this->getRequest()->setPostValue($post); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/newsletter/template/drop'); + $this->assertStringContainsString( + 'Template : Text', + $this->getResponse()->getBody() + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Model/TemplateTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Model/TemplateTest.php index 6c594690d536a..bd6f68fbfb704 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Model/TemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Model/TemplateTest.php @@ -147,6 +147,7 @@ public function testLegacyTemplateFromDbLoadsInLegacyMode() $this->_model->setTemplateType(TemplateTypesInterface::TYPE_HTML); $templateText = '{{var store.isSaveAllowed()}} - {{template config_path="foobar"}}'; $this->_model->setTemplateText($templateText); + $this->_model->setTemplateId('abc'); $template = $objectManager->create(\Magento\Email\Model\Template::class); $templateData = [ @@ -184,6 +185,7 @@ public function testTemplateFromDbLoadsInStrictMode() $this->_model->setTemplateType(TemplateTypesInterface::TYPE_HTML); $templateText = '{{var store.isSaveAllowed()}} - {{template config_path="foobar"}}'; $this->_model->setTemplateText($templateText); + $this->_model->setTemplateId('abc'); $template = $objectManager->create(\Magento\Email\Model\Template::class); $templateData = [ From 67544444dffe09822669b9ed3e6a469d474c5545 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Mon, 24 Aug 2020 17:50:03 +0100 Subject: [PATCH 0326/1013] Added JS to copy mobile account menu back --- app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js | 2 ++ 1 file changed, 2 insertions(+) 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 e4edd3bd8662c..87aceb0b00036 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 @@ -16,5 +16,7 @@ define([ container: '#maincontent' }); + $('.panel.header > .header.links').clone().appendTo('#store\\.links'); + keyboardHandler.apply(); }); From 4ac627ec52ff144288312ed28db112aed69b0f0d Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Mon, 24 Aug 2020 11:52:08 -0500 Subject: [PATCH 0327/1013] MC-36897: [GraphQl] Issues with updating wishlist quantity. Adding Recommended Store config changes - added tests to cover bugs --- .../GraphQl/Wishlist/CustomerWishlistTest.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) 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 0a8e1757a2ce2..b4c8d3149f527 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -124,6 +124,78 @@ public function testGuestCannotGetWishlist() $this->graphQlQuery($query); } + /** + * Add product to wishlist with quantity 0 + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php + */ + public function testAddProductToWishlistWithZeroQty() + { + $customerWishlistQuery = + <<<QUERY +{ + customer { + wishlist { + id + } + } +} +QUERY; + + $response = $this->graphQlQuery( + $customerWishlistQuery, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + $qty = 0; + $sku = 'simple-1'; + $wishlistId = $response['customer']['wishlist']['id']; + $addProductToWishlistQuery = + <<<QUERY +mutation{ + addProductsToWishlist( + wishlistId:{$wishlistId} + wishlistItems:[ + { + sku:"{$sku}" + quantity:{$qty} + } + ]) + { + wishlist{ + id + items_count + items{product{name sku} description qty} + } + user_errors{code message} + } +} + +QUERY; + $addToWishlistResponse = $this->graphQlMutation( + $addProductToWishlistQuery, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + $this->assertArrayHasKey('user_errors', $addToWishlistResponse['addProductsToWishlist']); + $this->assertCount(1, $addToWishlistResponse['addProductsToWishlist']['user_errors']); + $this->assertEmpty($addToWishlistResponse['addProductsToWishlist']['wishlist']['items']); + $this->assertEquals( + 0, + $addToWishlistResponse['addProductsToWishlist']['wishlist']['items_count'], + 'Count is greater than 0' + ); + $message = 'The quantity of a wish list item cannot be 0'; + $this->assertEquals( + $message, + $addToWishlistResponse['addProductsToWishlist']['user_errors'][0]['message'] + ); + } + /** * @magentoConfigFixture default_store wishlist/general/active 0 * @magentoApiDataFixture Magento/Customer/_files/customer.php From 1b08a0c5cef7eca5f7075f6c0c06589aad5c926a Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Mon, 24 Aug 2020 14:25:08 -0500 Subject: [PATCH 0328/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - Added validation for disabled product. --- .../Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php | 4 ++++ .../Wishlist/Model/Wishlist/UpdateProductsInWishlist.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php index c8eb98b938573..f2eeb0a59b0c4 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php @@ -8,6 +8,7 @@ namespace Magento\Wishlist\Model\Wishlist; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Exception\AlreadyExistsException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -116,6 +117,9 @@ private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistIte if ($wishlistItem->getQuantity() == 0) { throw new LocalizedException(__("The quantity of a wish list item cannot be 0")); } + if ($product->getStatus() == Status::STATUS_DISABLED) { + throw new LocalizedException(__("The product is disabled")); + } $options = $this->buyRequestBuilder->build($wishlistItem, (int) $product->getId()); $result = $wishlist->addNewItem($product, $options); diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php index 4455b118e7a5e..1b2bd1e271ff0 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -7,6 +7,7 @@ namespace Magento\Wishlist\Model\Wishlist; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Framework\Exception\LocalizedException; use Magento\Wishlist\Model\Item as WishlistItem; use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; @@ -98,6 +99,9 @@ private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wish if ($wishlistItemData->getQuantity() == 0) { throw new LocalizedException(__("The quantity of a wish list item cannot be 0")); } + if ($wishlistItem->getProduct()->getStatus() == Status::STATUS_DISABLED) { + throw new LocalizedException(__("The product is disabled")); + } $resultItem = $wishlist->updateItem($wishlistItem, $options); if (is_string($resultItem)) { From 048a489eec2e8fa55cdcd452990bffab0f07b04b Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Mon, 24 Aug 2020 15:32:38 -0500 Subject: [PATCH 0329/1013] MC-36897: [GraphQl] Issues with updating wishlist quantity. Adding Recommended Store config changes - added tests to cover bug fix --- .../GraphQl/Wishlist/CustomerWishlistTest.php | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) 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 b4c8d3149f527..dcbb550d66d62 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -126,7 +126,7 @@ public function testGuestCannotGetWishlist() /** * Add product to wishlist with quantity 0 - * + * * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Customer/_files/customer.php * @magentoApiDataFixture Magento/Catalog/_files/product_simple_duplicated.php @@ -196,6 +196,78 @@ public function testAddProductToWishlistWithZeroQty() ); } + /** + * Add disabled product to wishlist + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/simple_product_disabled.php + */ + public function testAddProductToWishlistWithDisabledProduct() + { + $customerWishlistQuery = + <<<QUERY +{ + customer { + wishlist { + id + } + } +} +QUERY; + + $response = $this->graphQlQuery( + $customerWishlistQuery, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + $qty = 2; + $sku = 'product_disabled'; + $wishlistId = $response['customer']['wishlist']['id']; + $addProductToWishlistQuery = + <<<QUERY +mutation{ + addProductsToWishlist( + wishlistId:{$wishlistId} + wishlistItems:[ + { + sku:"{$sku}" + quantity:{$qty} + } + ]) + { + wishlist{ + id + items_count + items{product{name sku} description qty} + } + user_errors{code message} + } +} + +QUERY; + $addToWishlistResponse = $this->graphQlMutation( + $addProductToWishlistQuery, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + $this->assertArrayHasKey('user_errors', $addToWishlistResponse['addProductsToWishlist']); + $this->assertCount(1, $addToWishlistResponse['addProductsToWishlist']['user_errors']); + $this->assertEmpty($addToWishlistResponse['addProductsToWishlist']['wishlist']['items']); + $this->assertEquals( + 0, + $addToWishlistResponse['addProductsToWishlist']['wishlist']['items_count'], + 'Count is greater than 0' + ); + $message = 'The product is disabled'; + $this->assertEquals( + $message, + $addToWishlistResponse['addProductsToWishlist']['user_errors'][0]['message'] + ); + } + /** * @magentoConfigFixture default_store wishlist/general/active 0 * @magentoApiDataFixture Magento/Customer/_files/customer.php From e9f6eed8312051cb4d3cddda6b229591a5bf6b18 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Tue, 25 Aug 2020 13:34:43 -0500 Subject: [PATCH 0330/1013] MC-36977: [GraphQL] Add storeConfig attributes for product reviews - Added store config --- .../Magento/ReviewGraphQl/etc/graphql/di.xml | 17 +++++++++++++++++ .../Magento/ReviewGraphQl/etc/schema.graphqls | 5 +++++ 2 files changed, 22 insertions(+) create mode 100644 app/code/Magento/ReviewGraphQl/etc/graphql/di.xml diff --git a/app/code/Magento/ReviewGraphQl/etc/graphql/di.xml b/app/code/Magento/ReviewGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..34e309f0b151a --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/etc/graphql/di.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="product_reviews_enabled" xsi:type="string">catalog/review/active</item> + <item name="allow_guests_to_write_product_reviews" xsi:type="string">catalog/review/allow_guest</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls index 14b4fc60e8b09..171033d55ed73 100644 --- a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls @@ -76,3 +76,8 @@ input ProductReviewRatingInput { id: String! @doc(description: "Base64 encoded rating ID.") value_id: String! @doc(description: "Base 64 encoded rating value id.") } + +type StoreConfig @doc(description: "The type contains information about a store config") { + product_reviews_enabled : String @doc(description: "Indicates whether product reviews are enabled. Possible values: 1 (Yes) and 0 (No)") + allow_guests_to_write_product_reviews : String @doc(description: "Indicates whether guest users can write product reviews. Possible values: 1 (Yes) and 0 (No)") +} From 377d01d070f228c7e1d1df8bbf7d7cc5a41a3a1f Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Tue, 25 Aug 2020 17:50:55 -0500 Subject: [PATCH 0331/1013] MC-36977: [GraphQL] Add storeConfig attributes for product reviews - remove base 64 references --- app/code/Magento/ReviewGraphQl/etc/schema.graphqls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls index 171033d55ed73..e7b1926f46202 100644 --- a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls @@ -39,13 +39,13 @@ type ProductReviewRatingsMetadata { } type ProductReviewRatingMetadata { - id: String! @doc(description: "Base64 encoded rating ID.") + id: String! @doc(description: "An 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_id: String! @doc(description: "An encoded rating value id.") value: String! @doc(description: "e.g Good, Perfect, 3, 4, 5") } @@ -73,8 +73,8 @@ input CreateProductReviewInput { } input ProductReviewRatingInput { - id: String! @doc(description: "Base64 encoded rating ID.") - value_id: String! @doc(description: "Base 64 encoded rating value id.") + id: String! @doc(description: "An encoded rating ID.") + value_id: String! @doc(description: "An encoded rating value id.") } type StoreConfig @doc(description: "The type contains information about a store config") { From e848f129ec06210de7e0b0b30a3326e852e3e0de Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Tue, 25 Aug 2020 19:21:56 -0500 Subject: [PATCH 0332/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - static fix --- .../Model/Resolver/UpdateProductsInWishlist.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php index d9e431223b642..42b8cd576f7c8 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -137,13 +137,13 @@ private function getWishlistItems(array $wishlistItemsData, Wishlist $wishlist): foreach ($wishlistItemsData as $wishlistItemData) { if (!isset($wishlistItemData['quantity'])) { $wishlistItem = $wishlist->getItem($wishlistItemData['wishlist_item_id']); - if (isset($wishlistItem)) { + if ($wishlistItem !== null) { $wishlistItemData['quantity'] = (float) $wishlistItem->getQty(); } } if (!isset($wishlistItemData['description'])) { $wishlistItem = $wishlist->getItem($wishlistItemData['wishlist_item_id']); - if (isset($wishlistItem)) { + if ($wishlistItem !== null) { $wishlistItemData['description'] = $wishlistItem->getDescription(); } } From 4719c2b045a7e9c043b6a362feeb88a9b77ace62 Mon Sep 17 00:00:00 2001 From: Marjan Petkovski <petkovski.marjan@gmail.com> Date: Wed, 26 Aug 2020 14:58:00 +0200 Subject: [PATCH 0333/1013] magento/magento2#29372: Wrong tags are set for CategoryList query Split categoryList cache test classes --- .../Catalog/CategoryListCacheTest.php | 145 ------------------ .../CategoryListMultipleIdsCacheTest.php | 54 +++++++ .../Catalog/CategoryListSingleIdCacheTest.php | 53 +++++++ 3 files changed, 107 insertions(+), 145 deletions(-) delete mode 100644 dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php create mode 100644 dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListMultipleIdsCacheTest.php create mode 100644 dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListSingleIdCacheTest.php diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php deleted file mode 100644 index a8e9059a84eb6..0000000000000 --- a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListCacheTest.php +++ /dev/null @@ -1,145 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\GraphQlCache\Controller\Catalog; - -use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; - -/** - * Test caching works for categoryList query - * - * @magentoAppArea graphql - * @magentoCache full_page enabled - * @magentoDbIsolation disabled - */ -class CategoryListCacheTest extends AbstractGraphqlCacheTest -{ - /** - * Test cache tags are generated - * - * @magentoDataFixture Magento/Catalog/_files/category_product.php - */ - public function testRequestCacheTagsForCategoryList(): void - { - $categoryId ='333'; - $query - = <<<QUERY - { - categoryList(filters: {ids: {in: ["$categoryId"]}}) { - id - name - url_key - description - product_count - } - } -QUERY; - $response = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } - - /** - * Test request is served from cache - * - * @magentoDataFixture Magento/Catalog/_files/category_product.php - */ - public function testSecondRequestIsServedFromCache() - { - $categoryId ='333'; - $query - = <<<QUERY - { - categoryList(filters: {ids: {in: ["$categoryId"]}}) { - id - name - url_key - description - product_count - } - } -QUERY; - $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; - - $response = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - - $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } - - /** - * Test cache tags are generated - * - * @magentoDataFixture Magento/Catalog/_files/category_tree.php - */ - public function testRequestCacheTagsForCategoryListOnMultipleIds(): void - { - $categoryId1 ='400'; - $categoryId2 = '401'; - $query - = <<<QUERY - { - categoryList(filters: {ids: {in: ["$categoryId1", "$categoryId2"]}}) { - id - name - url_key - description - product_count - } - } -QUERY; - //added the previous category in expected tags as it is cached - $expectedCacheTags = ['cat_c','cat_c_' .'333', 'cat_c_' . $categoryId1, 'cat_c_' . $categoryId2, 'FPC']; - - $response = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } - - /** - * Test request is served from cache - * - * @magentoDataFixture Magento/Catalog/_files/category_tree.php - */ - public function testSecondRequestIsServedFromCacheOnMultipleIds() - { - $categoryId1 ='400'; - $categoryId2 = '401'; - $query - = <<<QUERY - { - categoryList(filters: {ids: {in: ["$categoryId1", "$categoryId2"]}}) { - id - name - url_key - description - product_count - } - } -QUERY; - //added the previous category in expected tags as it is cached - $expectedCacheTags = ['cat_c','cat_c_' .'333', 'cat_c_' . $categoryId1, 'cat_c_' . $categoryId2, 'FPC']; - - $response = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - - $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); - $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); - $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); - $this->assertEquals($expectedCacheTags, $actualCacheTags); - } -} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListMultipleIdsCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListMultipleIdsCacheTest.php new file mode 100644 index 0000000000000..977a0ed5b144c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListMultipleIdsCacheTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Test caching for categoryList query + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class CategoryListMultipleIdsCacheTest extends AbstractGraphqlCacheTest +{ + /** + * Test request is served from cache. Expected cache tags are equal in both MISS and HIT cases. + * + * @magentoDataFixture Magento/Catalog/_files/category_tree.php + */ + public function testSecondRequestIsServedFromCacheOnMultipleIds() + { + $categoryId1 ='400'; + $categoryId2 = '401'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId1", "$categoryId2"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + $expectedCacheTags = ['cat_c', 'cat_c_' . $categoryId1, 'cat_c_' . $categoryId2, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListSingleIdCacheTest.php b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListSingleIdCacheTest.php new file mode 100644 index 0000000000000..51a9218e6a37a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQlCache/Controller/Catalog/CategoryListSingleIdCacheTest.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQlCache\Controller\Catalog; + +use Magento\GraphQlCache\Controller\AbstractGraphqlCacheTest; + +/** + * Test caching for categoryList query + * + * @magentoAppArea graphql + * @magentoCache full_page enabled + * @magentoDbIsolation disabled + */ +class CategoryListSingleIdCacheTest extends AbstractGraphqlCacheTest +{ + /** + * Test request is served from cache. Expected cache tags are equal in both MISS and HIT cases. + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testSecondRequestIsServedFromCache() + { + $categoryId ='333'; + $query + = <<<QUERY + { + categoryList(filters: {ids: {in: ["$categoryId"]}}) { + id + name + url_key + description + product_count + } + } +QUERY; + $expectedCacheTags = ['cat_c','cat_c_' . $categoryId, 'FPC']; + + $response = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('MISS', $response->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $response->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + + $cacheResponse = $this->dispatchGraphQlGETRequest(['query' => $query]); + $this->assertEquals('HIT', $cacheResponse->getHeader('X-Magento-Cache-Debug')->getFieldValue()); + $actualCacheTags = explode(',', $cacheResponse->getHeader('X-Magento-Tags')->getFieldValue()); + $this->assertEquals($expectedCacheTags, $actualCacheTags); + } +} From a152e8ee76a1a617c6b2f0afb5d17f3ef037d9d3 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Wed, 26 Aug 2020 12:53:21 -0500 Subject: [PATCH 0334/1013] MC-36977: [GraphQL] Add storeConfig attributes for product reviews - updated schema description --- app/code/Magento/ReviewGraphQl/etc/schema.graphqls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls index e7b1926f46202..709e25598a737 100644 --- a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls @@ -46,7 +46,7 @@ type ProductReviewRatingMetadata { type ProductReviewRatingValueMetadata { value_id: String! @doc(description: "An encoded rating value id.") - value: String! @doc(description: "e.g Good, Perfect, 3, 4, 5") + value: String! @doc(description: "A ratings scale, such as the number of stars awarded") } type Customer { From c94a2f80bf5615f4961256dd0dad511f3ecd8bf9 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Wed, 26 Aug 2020 14:06:49 -0500 Subject: [PATCH 0335/1013] MC-34841: free-payment --- .../Model/Cart/SetShippingAddressesOnCart.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index 0f350be78a945..71740488c4cea 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -7,6 +7,7 @@ namespace Magento\QuoteGraphQl\Model\Cart; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Quote\Api\Data\CartInterface; @@ -35,16 +36,17 @@ class SetShippingAddressesOnCart implements SetShippingAddressesOnCartInterface /** * @param AssignShippingAddressToCart $assignShippingAddressToCart * @param GetShippingAddress $getShippingAddress - * @param QuoteRepository $quoteRepository + * @param QuoteRepository|null $quoteRepository */ public function __construct( AssignShippingAddressToCart $assignShippingAddressToCart, GetShippingAddress $getShippingAddress, - QuoteRepository $quoteRepository + QuoteRepository $quoteRepository = null ) { $this->assignShippingAddressToCart = $assignShippingAddressToCart; $this->getShippingAddress = $getShippingAddress; - $this->quoteRepository = $quoteRepository; + $this->quoteRepository = $quoteRepository + ?? ObjectManager::getInstance()->get(QuoteRepository::class); } /** From 306cb001d7cbf41a84bebcb74a56f433d5871aa1 Mon Sep 17 00:00:00 2001 From: Soumya Unnikrishnan <sunnikri@adobe.com> Date: Wed, 26 Aug 2020 14:47:57 -0500 Subject: [PATCH 0336/1013] MQE-2271: Release 3.1.0 Delivery Composer update --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1f91e7b8594a1..25be12b5bb72f 100644 --- a/composer.json +++ b/composer.json @@ -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.1", + "magento/magento2-functional-testing-framework": "^3.0", "pdepend/pdepend": "~2.7.1", "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "^2.8.0", diff --git a/composer.lock b/composer.lock index 5b7e6c3da431a..36a42d0c750df 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": "b5562151b3be7e921e3ebc8080da557f", + "content-hash": "197f0388c574f9d40555a95634e439e0", "packages": [ { "name": "colinmollenhour/cache-backend-file", From fe28e234a488ad1ce8219241afb6e9cb16cd97a2 Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Wed, 26 Aug 2020 22:06:28 +0100 Subject: [PATCH 0337/1013] Refactor fields array population per code review --- .../Catalog/Model/Category/DataProvider.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index c8f21780cf6f7..1d4edec299717 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -22,6 +22,7 @@ use Magento\Eav\Model\Entity\Type; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; use Magento\Framework\Config\DataInterfaceFactory; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -33,7 +34,6 @@ use Magento\Ui\Component\Form\Field; use Magento\Ui\DataProvider\EavValidationRules; use Magento\Ui\DataProvider\Modifier\PoolInterface; -use Magento\Framework\AuthorizationInterface; use Magento\Ui\DataProvider\ModifierPoolDataProvider; /** @@ -671,22 +671,21 @@ protected function getFieldsMap() continue; } - $fieldsMap[$group] = []; + $fields = []; foreach ($node['children'] as $childName => $childNode) { if (!empty($childNode['children'])) { // <container/> nodes need special handling - foreach ($childNode['children'] as $grandchildName => $grandchildNode) { - $fieldsMap[$group][] = $grandchildName; + foreach (array_keys($childNode['children']) as $grandchildName) { + $fields[] = $grandchildName; } } else { - $fieldsMap[$group][] = $childName; + $fields[] = $childName; } } - // Remove empty groups - if (empty($fieldsMap[$group])) { - unset($fieldsMap[$group]); + if (count($fields)) { + $fieldsMap[$group] = $fields; } } From e24f416600748abe9b6dffc4c9a1112d69a4de7a Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Wed, 26 Aug 2020 16:20:49 -0500 Subject: [PATCH 0338/1013] MC-36897: [GraphQl] Store config and schema modifications for Wishlist - review fix --- .../Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php | 2 +- .../Wishlist/Model/Wishlist/UpdateProductsInWishlist.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php index f2eeb0a59b0c4..088805ffe76ac 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php @@ -114,7 +114,7 @@ private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistIte } try { - if ($wishlistItem->getQuantity() == 0) { + if ((int)$wishlistItem->getQuantity() === 0) { throw new LocalizedException(__("The quantity of a wish list item cannot be 0")); } if ($product->getStatus() == Status::STATUS_DISABLED) { diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php index 1b2bd1e271ff0..e3f2cd760a1fb 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -96,7 +96,7 @@ private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wish $wishlistItem = $this->wishlistItemFactory->create(); $this->wishlistItemResource->load($wishlistItem, $wishlistItemData->getId()); $wishlistItem->setDescription($wishlistItemData->getDescription()); - if ($wishlistItemData->getQuantity() == 0) { + if ((int)$wishlistItemData->getQuantity() === 0) { throw new LocalizedException(__("The quantity of a wish list item cannot be 0")); } if ($wishlistItem->getProduct()->getStatus() == Status::STATUS_DISABLED) { From 58651f999cc532e931b1a53077be6259737626fd Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Wed, 26 Aug 2020 23:16:07 +0100 Subject: [PATCH 0339/1013] Use optional argument in constructor --- app/code/Magento/Catalog/Model/Category/DataProvider.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 1d4edec299717..8efd5201639b2 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -222,7 +222,6 @@ public function __construct( Config $eavConfig, RequestInterface $request, CategoryFactory $categoryFactory, - DataInterfaceFactory $uiConfigFactory, array $meta = [], array $data = [], PoolInterface $pool = null, @@ -231,7 +230,8 @@ public function __construct( ScopeOverriddenValue $scopeOverriddenValue = null, ArrayManager $arrayManager = null, FileInfo $fileInfo = null, - ?Image $categoryImage = null + ?Image $categoryImage = null, + ?DataInterfaceFactory $uiConfigFactory = null ) { $this->eavValidationRules = $eavValidationRules; $this->collection = $categoryCollectionFactory->create(); @@ -241,7 +241,6 @@ public function __construct( $this->storeManager = $storeManager; $this->request = $request; $this->categoryFactory = $categoryFactory; - $this->uiConfigFactory = $uiConfigFactory; $this->auth = $auth ?? ObjectManager::getInstance()->get(AuthorizationInterface::class); $this->arrayUtils = $arrayUtils ?? ObjectManager::getInstance()->get(ArrayUtils::class); $this->scopeOverriddenValue = $scopeOverriddenValue ?: @@ -249,6 +248,10 @@ public function __construct( $this->arrayManager = $arrayManager ?: ObjectManager::getInstance()->get(ArrayManager::class); $this->fileInfo = $fileInfo ?: ObjectManager::getInstance()->get(FileInfo::class); $this->categoryImage = $categoryImage ?? ObjectManager::getInstance()->get(Image::class); + $this->uiConfigFactory = $uiConfigFactory ?? ObjectManager::getInstance()->create( + DataInterfaceFactory::class, + ['instanceName' => \Magento\Ui\Config\Data::class] + ); parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data, $pool); } From d267e3bfae51f0210b275d76e9842c0597d3b96a Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Thu, 27 Aug 2020 00:00:42 +0100 Subject: [PATCH 0340/1013] Correct constructor docblock --- app/code/Magento/Catalog/Model/Category/DataProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 8efd5201639b2..fe13b9c5e96a4 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -199,7 +199,6 @@ class DataProvider extends ModifierPoolDataProvider * @param Config $eavConfig * @param RequestInterface $request * @param CategoryFactory $categoryFactory - * @param DataInterfaceFactory $uiConfigFactory * @param array $meta * @param array $data * @param PoolInterface|null $pool @@ -209,6 +208,7 @@ class DataProvider extends ModifierPoolDataProvider * @param ArrayManager|null $arrayManager * @param FileInfo|null $fileInfo * @param Image|null $categoryImage + * @param DataInterfaceFactory|null $uiConfigFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( From 977f6ff611743addcb59dfc436c229c477c0ecc9 Mon Sep 17 00:00:00 2001 From: Tu Nguyen <tuna@ecommage.com> Date: Thu, 27 Aug 2020 19:15:54 +0700 Subject: [PATCH 0341/1013] Add some common font weight variable update update --- lib/web/css/source/lib/variables/_typography.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/web/css/source/lib/variables/_typography.less b/lib/web/css/source/lib/variables/_typography.less index 205b1fa9c7a3b..e357b6969dbfd 100644 --- a/lib/web/css/source/lib/variables/_typography.less +++ b/lib/web/css/source/lib/variables/_typography.less @@ -39,11 +39,15 @@ @font-size__xs: floor(.75 * @font-size__base); // 11px // Weights +@font-weight__hairline: 100; +@font-weight__extralight: 200; @font-weight__light: 300; @font-weight__regular: 400; @font-weight__heavier: 500; @font-weight__semibold: 600; @font-weight__bold: 700; +@font-weight__extrabold: 800; +@font-weight__heavy: 900; // Styles @font-style__base: normal; From 4787c826f7d1a38fe2844114fceb84f34a6ef1cb Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Thu, 27 Aug 2020 16:56:08 +0300 Subject: [PATCH 0342/1013] magento/magento2#29752: Stores are not shown in "Login as Customer: Select Store" modal window on order view page --- .../Component/ConfirmationPopup/Options.php | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 4ec2bed41c799..865d4f0bc12e2 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -10,9 +10,14 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Config\Share; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\Data\OptionSourceInterface; use Magento\Framework\Escaper; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Api\ShipmentRepositoryInterface; use Magento\Store\Model\Group; use Magento\Store\Model\System\Store as SystemStore; use Magento\Store\Model\Website; @@ -52,25 +57,61 @@ class Options implements OptionSourceInterface */ private $options; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var ShipmentRepositoryInterface + */ + private $shipmentRepository; + + /** + * @var CreditmemoRepositoryInterface + */ + private $creditmemoRepository; + /** * @param CustomerRepositoryInterface $customerRepository * @param Escaper $escaper * @param RequestInterface $request * @param Share $share * @param SystemStore $systemStore + * @param OrderRepositoryInterface|null $orderRepository + * @param InvoiceRepositoryInterface|null $invoiceRepository + * @param ShipmentRepositoryInterface|null $shipmentRepository + * @param CreditmemoRepositoryInterface|null $creditmemoRepository */ public function __construct( CustomerRepositoryInterface $customerRepository, Escaper $escaper, RequestInterface $request, Share $share, - SystemStore $systemStore + SystemStore $systemStore, + ?OrderRepositoryInterface $orderRepository = null, + ?InvoiceRepositoryInterface $invoiceRepository = null, + ?ShipmentRepositoryInterface $shipmentRepository = null, + ?CreditmemoRepositoryInterface $creditmemoRepository = null ) { $this->customerRepository = $customerRepository; $this->escaper = $escaper; $this->request = $request; $this->share = $share; $this->systemStore = $systemStore; + $this->orderRepository = $orderRepository + ?? ObjectManager::getInstance()->get(OrderRepositoryInterface::class); + $this->invoiceRepository = $invoiceRepository + ?? ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); + $this->shipmentRepository = $shipmentRepository + ?? ObjectManager::getInstance()->get(ShipmentRepositoryInterface::class); + $this->creditmemoRepository = $creditmemoRepository + ?? ObjectManager::getInstance()->get(CreditmemoRepositoryInterface::class); } /** @@ -82,7 +123,7 @@ public function toOptionArray(): array return $this->options; } - $customerId = (int)$this->request->getParam('id'); + $customerId = $this->getCustomerId(); $this->options = $this->generateCurrentOptions($customerId); return $this->options; @@ -167,4 +208,30 @@ private function fillStoreGroupOptions(Website $website, CustomerInterface $cust return $groups; } + + /** + * Get Customer id from request param. + * + * @return int + */ + private function getCustomerId(): int + { + $customerId = $this->request->getParam('id'); + if (!$customerId) { + $orderId = $this->request->getParam('order_id'); + $shipmentId = $this->request->getParam('shipment_id'); + $creditmemoId = $this->request->getParam('creditmemo_id'); + $invoiceId = $this->request->getParam('invoice_id'); + if ($invoiceId) { + $orderId = $this->invoiceRepository->get($invoiceId)->getOrderId(); + } elseif ($shipmentId) { + $orderId = $this->shipmentRepository->get($shipmentId)->getOrderId(); + } elseif ($creditmemoId) { + $orderId = $this->creditmemoRepository->get($creditmemoId)->getOrderId(); + } + $customerId = $this->orderRepository->get($orderId)->getCustomerId(); + } + + return (int)$customerId; + } } From 9f8130417b55da9a94ff58fb0d7b1adc6cae17c0 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Thu, 27 Aug 2020 17:14:20 +0300 Subject: [PATCH 0343/1013] Revert "magento/magento2#29752: Stores are not shown in "Login as Customer: Select Store" modal window on order view page" This reverts commit 4787c826 --- .../Component/ConfirmationPopup/Options.php | 71 +------------------ 1 file changed, 2 insertions(+), 69 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 865d4f0bc12e2..4ec2bed41c799 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -10,14 +10,9 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Config\Share; -use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\Data\OptionSourceInterface; use Magento\Framework\Escaper; -use Magento\Sales\Api\CreditmemoRepositoryInterface; -use Magento\Sales\Api\InvoiceRepositoryInterface; -use Magento\Sales\Api\OrderRepositoryInterface; -use Magento\Sales\Api\ShipmentRepositoryInterface; use Magento\Store\Model\Group; use Magento\Store\Model\System\Store as SystemStore; use Magento\Store\Model\Website; @@ -57,61 +52,25 @@ class Options implements OptionSourceInterface */ private $options; - /** - * @var OrderRepositoryInterface - */ - private $orderRepository; - - /** - * @var InvoiceRepositoryInterface - */ - private $invoiceRepository; - - /** - * @var ShipmentRepositoryInterface - */ - private $shipmentRepository; - - /** - * @var CreditmemoRepositoryInterface - */ - private $creditmemoRepository; - /** * @param CustomerRepositoryInterface $customerRepository * @param Escaper $escaper * @param RequestInterface $request * @param Share $share * @param SystemStore $systemStore - * @param OrderRepositoryInterface|null $orderRepository - * @param InvoiceRepositoryInterface|null $invoiceRepository - * @param ShipmentRepositoryInterface|null $shipmentRepository - * @param CreditmemoRepositoryInterface|null $creditmemoRepository */ public function __construct( CustomerRepositoryInterface $customerRepository, Escaper $escaper, RequestInterface $request, Share $share, - SystemStore $systemStore, - ?OrderRepositoryInterface $orderRepository = null, - ?InvoiceRepositoryInterface $invoiceRepository = null, - ?ShipmentRepositoryInterface $shipmentRepository = null, - ?CreditmemoRepositoryInterface $creditmemoRepository = null + SystemStore $systemStore ) { $this->customerRepository = $customerRepository; $this->escaper = $escaper; $this->request = $request; $this->share = $share; $this->systemStore = $systemStore; - $this->orderRepository = $orderRepository - ?? ObjectManager::getInstance()->get(OrderRepositoryInterface::class); - $this->invoiceRepository = $invoiceRepository - ?? ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); - $this->shipmentRepository = $shipmentRepository - ?? ObjectManager::getInstance()->get(ShipmentRepositoryInterface::class); - $this->creditmemoRepository = $creditmemoRepository - ?? ObjectManager::getInstance()->get(CreditmemoRepositoryInterface::class); } /** @@ -123,7 +82,7 @@ public function toOptionArray(): array return $this->options; } - $customerId = $this->getCustomerId(); + $customerId = (int)$this->request->getParam('id'); $this->options = $this->generateCurrentOptions($customerId); return $this->options; @@ -208,30 +167,4 @@ private function fillStoreGroupOptions(Website $website, CustomerInterface $cust return $groups; } - - /** - * Get Customer id from request param. - * - * @return int - */ - private function getCustomerId(): int - { - $customerId = $this->request->getParam('id'); - if (!$customerId) { - $orderId = $this->request->getParam('order_id'); - $shipmentId = $this->request->getParam('shipment_id'); - $creditmemoId = $this->request->getParam('creditmemo_id'); - $invoiceId = $this->request->getParam('invoice_id'); - if ($invoiceId) { - $orderId = $this->invoiceRepository->get($invoiceId)->getOrderId(); - } elseif ($shipmentId) { - $orderId = $this->shipmentRepository->get($shipmentId)->getOrderId(); - } elseif ($creditmemoId) { - $orderId = $this->creditmemoRepository->get($creditmemoId)->getOrderId(); - } - $customerId = $this->orderRepository->get($orderId)->getCustomerId(); - } - - return (int)$customerId; - } } From b597c7a0a80072a73a4914b1d80644a57445cb46 Mon Sep 17 00:00:00 2001 From: Stanislav Idolov <sidolov@adobe.com> Date: Thu, 27 Aug 2020 09:27:48 -0500 Subject: [PATCH 0344/1013] Added visibility modifiers for constants --- .../Framework/DB/Adapter/Pdo/Mysql.php | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 04b88c9a3ed50..28b5d8da53525 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -47,31 +47,31 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface { // @codingStandardsIgnoreEnd - const TIMESTAMP_FORMAT = 'Y-m-d H:i:s'; - const DATETIME_FORMAT = 'Y-m-d H:i:s'; - const DATE_FORMAT = 'Y-m-d'; + public const TIMESTAMP_FORMAT = 'Y-m-d H:i:s'; + public const DATETIME_FORMAT = 'Y-m-d H:i:s'; + public const DATE_FORMAT = 'Y-m-d'; - const DDL_DESCRIBE = 1; - const DDL_CREATE = 2; - const DDL_INDEX = 3; - const DDL_FOREIGN_KEY = 4; - const DDL_EXISTS = 5; - const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; - const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; + public const DDL_DESCRIBE = 1; + public const DDL_CREATE = 2; + public const DDL_INDEX = 3; + public const DDL_FOREIGN_KEY = 4; + public const DDL_EXISTS = 5; + public const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; + public const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; - const LENGTH_TABLE_NAME = 64; - const LENGTH_INDEX_NAME = 64; - const LENGTH_FOREIGN_NAME = 64; + public const LENGTH_TABLE_NAME = 64; + public const LENGTH_INDEX_NAME = 64; + public const LENGTH_FOREIGN_NAME = 64; /** * MEMORY engine type for MySQL tables */ - const ENGINE_MEMORY = 'MEMORY'; + public const ENGINE_MEMORY = 'MEMORY'; /** * Maximum number of connection retries */ - const MAX_CONNECTION_RETRIES = 10; + public const MAX_CONNECTION_RETRIES = 10; /** * Default class name for a DB statement. From d60cbb9ed28d4b307cd2b82721e0f19e501134b4 Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Fri, 21 Feb 2020 17:11:00 +0100 Subject: [PATCH 0345/1013] Fix for numeric argument conversion --- .../Framework/Amqp/Topology/ArgumentProcessor.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php b/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php index caa5db4e7ef5c..2a7b6b939853f 100644 --- a/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php +++ b/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php @@ -1,10 +1,15 @@ <?php + +declare(strict_types=1); + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ namespace Magento\Framework\Amqp\Topology; +use InvalidArgumentException; + /** * @deprecated 100.0.0 * see: https://github.com/php-amqplib/php-amqplib/issues/405 @@ -17,22 +22,23 @@ trait ArgumentProcessor * @param array $arguments * @return array */ - public function processArguments($arguments) + public function processArguments($arguments): array { $output = []; foreach ($arguments as $key => $value) { if (is_array($value)) { $output[$key] = ['A', $value]; - } elseif (is_int($value)) { - $output[$key] = ['I', $value]; + } elseif (is_numeric($value)) { + $output[$key] = ['I', (int) $value]; } elseif (is_bool($value)) { $output[$key] = ['t', $value]; } elseif (is_string($value)) { $output[$key] = ['S', $value]; } else { - throw new \InvalidArgumentException('Unknown argument type ' . gettype($value)); + throw new InvalidArgumentException('Unknown argument type ' . gettype($value)); } } + return $output; } } From e2c42e638f52b3937526b05ba6642f97fc213983 Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Thu, 27 Aug 2020 23:33:10 +0100 Subject: [PATCH 0346/1013] Fix out-of-scope PHPStan error --- app/code/Magento/Catalog/Model/Category/DataProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index fe13b9c5e96a4..80147260cd822 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -623,7 +623,7 @@ private function convertValues($category, $categoryData): array $categoryData[$attributeCode][0]['url'] = $this->categoryImage->getUrl($category, $attributeCode); - $categoryData[$attributeCode][0]['size'] = isset($stat) ? $stat['size'] : 0; + $categoryData[$attributeCode][0]['size'] = $stat['size']; $categoryData[$attributeCode][0]['type'] = $mime; } } From 749ee559a934ee0ac801645a00171099205bc5ac Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Thu, 27 Aug 2020 23:34:47 +0100 Subject: [PATCH 0347/1013] Replace count() with !empty() --- app/code/Magento/Catalog/Model/Category/DataProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 80147260cd822..6a500c326b358 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -687,7 +687,7 @@ protected function getFieldsMap() } } - if (count($fields)) { + if (!empty($fields)) { $fieldsMap[$group] = $fields; } } From 2cc29b7aafa21aefcb818a7387a6340b92e9c953 Mon Sep 17 00:00:00 2001 From: Timon de Groot <timon@mooore.nl> Date: Fri, 28 Aug 2020 09:12:45 +0200 Subject: [PATCH 0348/1013] Fix requested changes from review --- .../Framework/Image/Adapter/ImageMagick.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 5505ad8dc09e3..a66ba7a8bfd35 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -10,8 +10,6 @@ /** * Image adapter from ImageMagick. - * - * @property \Imagick $_imageHandler */ class ImageMagick extends AbstractAdapter { @@ -38,18 +36,23 @@ class ImageMagick extends AbstractAdapter 'sharpen' => ['radius' => 4, 'deviation' => 1], ]; + /** + * @var \Imagick + */ + protected $_imageHandler; + /** * Colorspace of the image * * @var int */ - protected $colorspace = -1; + private $colorspace = -1; /** * Original colorspace of the image * * @var int */ - protected $originalColorspace = -1; + private $originalColorspace = -1; /** * Set/get background color. Check Imagick::COLOR_* constants @@ -589,7 +592,8 @@ private function addSingleWatermark($positionX, int $positionY, \Imagick $waterm public function getColorspace(): int { if ($this->colorspace === -1) { - $this->originalColorspace = $this->colorspace = $this->_imageHandler->getImageColorspace(); + $this->colorspace = $this->_imageHandler->getImageColorspace(); + $this->originalColorspace = $this->colorspace; } return $this->colorspace; From 6db35d819919d05720333425989b83b0582e8aa7 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Fri, 28 Aug 2020 17:47:22 +0100 Subject: [PATCH 0349/1013] Applied fix for Stores are not shown in Login as Customer --- .../Component/ConfirmationPopup/Options.php | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 4ec2bed41c799..865d4f0bc12e2 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -10,9 +10,14 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Config\Share; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\RequestInterface; use Magento\Framework\Data\OptionSourceInterface; use Magento\Framework\Escaper; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Api\ShipmentRepositoryInterface; use Magento\Store\Model\Group; use Magento\Store\Model\System\Store as SystemStore; use Magento\Store\Model\Website; @@ -52,25 +57,61 @@ class Options implements OptionSourceInterface */ private $options; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var ShipmentRepositoryInterface + */ + private $shipmentRepository; + + /** + * @var CreditmemoRepositoryInterface + */ + private $creditmemoRepository; + /** * @param CustomerRepositoryInterface $customerRepository * @param Escaper $escaper * @param RequestInterface $request * @param Share $share * @param SystemStore $systemStore + * @param OrderRepositoryInterface|null $orderRepository + * @param InvoiceRepositoryInterface|null $invoiceRepository + * @param ShipmentRepositoryInterface|null $shipmentRepository + * @param CreditmemoRepositoryInterface|null $creditmemoRepository */ public function __construct( CustomerRepositoryInterface $customerRepository, Escaper $escaper, RequestInterface $request, Share $share, - SystemStore $systemStore + SystemStore $systemStore, + ?OrderRepositoryInterface $orderRepository = null, + ?InvoiceRepositoryInterface $invoiceRepository = null, + ?ShipmentRepositoryInterface $shipmentRepository = null, + ?CreditmemoRepositoryInterface $creditmemoRepository = null ) { $this->customerRepository = $customerRepository; $this->escaper = $escaper; $this->request = $request; $this->share = $share; $this->systemStore = $systemStore; + $this->orderRepository = $orderRepository + ?? ObjectManager::getInstance()->get(OrderRepositoryInterface::class); + $this->invoiceRepository = $invoiceRepository + ?? ObjectManager::getInstance()->get(InvoiceRepositoryInterface::class); + $this->shipmentRepository = $shipmentRepository + ?? ObjectManager::getInstance()->get(ShipmentRepositoryInterface::class); + $this->creditmemoRepository = $creditmemoRepository + ?? ObjectManager::getInstance()->get(CreditmemoRepositoryInterface::class); } /** @@ -82,7 +123,7 @@ public function toOptionArray(): array return $this->options; } - $customerId = (int)$this->request->getParam('id'); + $customerId = $this->getCustomerId(); $this->options = $this->generateCurrentOptions($customerId); return $this->options; @@ -167,4 +208,30 @@ private function fillStoreGroupOptions(Website $website, CustomerInterface $cust return $groups; } + + /** + * Get Customer id from request param. + * + * @return int + */ + private function getCustomerId(): int + { + $customerId = $this->request->getParam('id'); + if (!$customerId) { + $orderId = $this->request->getParam('order_id'); + $shipmentId = $this->request->getParam('shipment_id'); + $creditmemoId = $this->request->getParam('creditmemo_id'); + $invoiceId = $this->request->getParam('invoice_id'); + if ($invoiceId) { + $orderId = $this->invoiceRepository->get($invoiceId)->getOrderId(); + } elseif ($shipmentId) { + $orderId = $this->shipmentRepository->get($shipmentId)->getOrderId(); + } elseif ($creditmemoId) { + $orderId = $this->creditmemoRepository->get($creditmemoId)->getOrderId(); + } + $customerId = $this->orderRepository->get($orderId)->getCustomerId(); + } + + return (int)$customerId; + } } From 1eb24b7e33c9e08cde7fb73801685b3941696453 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 18 Aug 2020 16:34:56 +0300 Subject: [PATCH 0350/1013] Fix flaky behavior with mftf test --- .../ActionGroup/AdminEditCategoryInGridPageActionGroup.xml | 7 ++++++- .../AdminMediaGalleryCatalogUiCategoryGridSection.xml | 2 +- .../AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml index ccdebccab4e65..50ee9e890ad20 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml @@ -11,7 +11,12 @@ <annotations> <description>Clicks the Edit action from the Media Gallery Category Grid</description> </annotations> - <click selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.edit('2', 'Edit')}}" stepKey="clickOnCategoryRow"/> + + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <click selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.edit(categoryName, 'Edit')}}" stepKey="clickOnCategoryRow"/> <waitForPageLoad time="30" stepKey="waitForCategoryDetailsPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml index ffd3c14c297c3..9e90f2c463adc 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -17,6 +17,6 @@ <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="inMenu" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'In Menu')]/preceding-sibling::th) +1 ]//*[text()='{{inMenuValue}}']" parameterized="true"/> <element name="enabled" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Enabled')]/preceding-sibling::th) +1 ]//*[text()='{{enabledValue}}']" 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"/> + <element name="edit" type="button" selector="//tr[td//text()[contains(., '{{categoryName}}')]]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Action')]/preceding-sibling::th) +1 ]//*[text()='{{actionButton}}']" 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 index b20f63a005279..2a606d8ab6a9e 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml @@ -26,7 +26,9 @@ <deleteData createDataKey="category" stepKey="deleteCategory"/> </after> <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> - <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> <actionGroup ref="AdminAssertCategoryPageTitleActionGroup" stepKey="assertCategoryByName"/> </test> </tests> From 9009ebadba824d4d7f164b103745b108497d6f9b Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 18 Aug 2020 15:13:59 +0300 Subject: [PATCH 0351/1013] cut long folders name to avoid styles issue --- .../Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php index f0998a3e120f2..c152330a39bbd 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -84,8 +84,9 @@ private function getDirectories(): array } $pathArray = explode('/', $path); + $displayName = strlen(end($pathArray)) > 50 ? substr(end($pathArray),0,50)."..." : end($pathArray); $directories[] = [ - 'data' => count($pathArray) > 0 ? end($pathArray) : $path, + 'data' => count($pathArray) > 0 ? $displayName : $path, 'attr' => ['id' => $path], 'metadata' => [ 'path' => $path From 65caa40c13c650d8517a31b699ba189a6c2919f5 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 18 Aug 2020 15:51:17 +0300 Subject: [PATCH 0352/1013] Correctly apply filters from url-applier do avoid dropping other filters --- .../Ui/view/base/web/js/grid/url-filter-applier.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 index 1f870e9e819a1..ab985c51449e8 100644 --- 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 @@ -5,8 +5,9 @@ define([ 'uiComponent', - 'underscore' -], function (Component, _) { + 'underscore', + 'jquery' +], function (Component, _, $) { 'use strict'; return Component.extend({ @@ -36,7 +37,9 @@ define([ * Apply filter */ apply: function () { - var urlFilter = this.getFilterParam(this.searchString); + var urlFilter = this.getFilterParam(this.searchString), + applied, + filters = {}; if (_.isUndefined(this.filterComponent())) { setTimeout(function () { @@ -47,8 +50,9 @@ define([ } if (Object.keys(urlFilter).length) { - this.filterComponent().setData(urlFilter, false); - this.filterComponent().apply(); + applied = this.filterComponent().get('applied'); + filters = $.extend(true, urlFilter, applied); + this.filterComponent().set('applied', filters); } }, From 9bf9072670c9cf9dfe46b210ecadbfbe2557e3a1 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 19 Aug 2020 14:35:23 +0300 Subject: [PATCH 0353/1013] Cover long folder names in media gallery with mftf test --- .../Model/Directories/GetFolderTree.php | 6 ++- .../Mftf/Data/AdminMediaGalleryFolderData.xml | 2 + ...iaGalleryCreateFolderWithLongNamesTest.xml | 43 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.xml diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php index c152330a39bbd..f297d027c13e1 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -17,6 +17,8 @@ */ class GetFolderTree { + private const NAME_LENGTH = 50; + /** * @var Filesystem */ @@ -84,7 +86,9 @@ private function getDirectories(): array } $pathArray = explode('/', $path); - $displayName = strlen(end($pathArray)) > 50 ? substr(end($pathArray),0,50)."..." : end($pathArray); + $displayName = strlen(end($pathArray)) > self::NAME_LENGTH ? + substr(end($pathArray),0, self::NAME_LENGTH) . "..." : + end($pathArray); $directories[] = [ 'data' => count($pathArray) > 0 ? $displayName : $path, 'attr' => ['id' => $path], diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml index e4149acdf58d1..d67c019397ec1 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml @@ -13,5 +13,7 @@ </entity> <entity name="AdminMediaGalleryFolderInvalidData"> <data key="name">,.?/:;'[{]}|~`!@#$%^*()_=+</data> + <data key="longName">mediagallerylongfoldernamemediagallerylongfoldername54</data> + <data key="cutedName">mediagallerylongfoldernamemediagallerylongfolderna...</data> </entity> </entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.xml new file mode 100644 index 0000000000000..c76790e164298 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.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="AdminMediaGalleryCreateFolderWithLongNamesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1758"/> + <stories value="Create new folder with long names in Media Gallery"/> + <title value="Create new folder with long names in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4456547"/> + <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.longName}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="AssertFolderName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.cutedName}}"/> + </actionGroup> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.cutedName}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.cutedName}}"/> + </actionGroup> + </test> +</tests> From 3c4588d06a9c5864a6e9418a47be196edaa83569 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 19 Aug 2020 15:32:11 +0300 Subject: [PATCH 0354/1013] Cover filters functionality --- ...aGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml index 7e0fa6c477c45..fd669926c2f09 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -60,5 +60,14 @@ <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> <argument name="categoryName" value="$$category.name$$"/> </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <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"/> + </test> </tests> From 03746d7cd2396567f80fedd23bdecdc34cfe2b53 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 19 Aug 2020 15:56:27 +0300 Subject: [PATCH 0355/1013] Cover url-filter-applier issue with mftf test --- ...leryCatalogUiVerifyUsedInLinkCategoryGridTest.xml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml index fd669926c2f09..58c270687ab34 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -60,14 +60,16 @@ <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> <argument name="categoryName" value="$$category.name$$"/> </actionGroup> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> - <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPageToVerifyIfFilterCanBeApplied"/> <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> - <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setAssetFilter"> <argument name="filterName" value="Asset"/> - <argument name="optionName" value="{{ImageMetadata.title}}"/> + <argument name="optionName" value="{{UpdatedImageDetails.title}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> - + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterAppliedAfterUrlFilterApplier"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> </test> </tests> From 3d23bad4eeae1e29fa51625abfb0a03ce7575d15 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 19 Aug 2020 16:31:09 +0300 Subject: [PATCH 0356/1013] Correct jasmine tests && fix static tests --- .../Model/Directories/GetFolderTree.php | 2 +- .../base/js/grid/url-filter-applier.test.js | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php index f297d027c13e1..9dba5ec8433af 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -87,7 +87,7 @@ private function getDirectories(): array $pathArray = explode('/', $path); $displayName = strlen(end($pathArray)) > self::NAME_LENGTH ? - substr(end($pathArray),0, self::NAME_LENGTH) . "..." : + substr(end($pathArray), 0 , self::NAME_LENGTH) . "..." : end($pathArray); $directories[] = [ 'data' => count($pathArray) > 0 ? $displayName : $path, 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 index a3d49e382de51..71ca60ef9077f 100644 --- 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 @@ -5,14 +5,16 @@ /*eslint max-nested-callbacks: 0*/ define([ - 'Magento_Ui/js/grid/url-filter-applier' -], function (UrlFilterApplier) { + 'Magento_Ui/js/grid/url-filter-applier', + 'jquery' +], function (UrlFilterApplier, $) { 'use strict'; describe('Magento_Ui/js/grid/url-filter-applier', function () { var urlFilterApplierObj, filterComponentMock = { - setData: jasmine.createSpy(), + set: jasmine.createSpy(), + get: jasmine.createSpy(), apply: jasmine.createSpy() }; @@ -64,11 +66,14 @@ define([ 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(); + expect(urlFilterApplierObj.filterComponent().get).toHaveBeenCalled(); + expect(urlFilterApplierObj.filterComponent().set).toHaveBeenCalledWith( + 'applied', + { + 'name': 'test', + 'qty': '1' + } + ); }); }); }); From a3a8947c1614d4518df86262982fd4a44acbedee Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 19 Aug 2020 18:11:45 +0300 Subject: [PATCH 0357/1013] static tests fix --- .../MediaGalleryUi/Model/Directories/GetFolderTree.php | 2 +- .../code/Magento/Ui/base/js/grid/url-filter-applier.test.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php index 9dba5ec8433af..54c3cac5ecd13 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -87,7 +87,7 @@ private function getDirectories(): array $pathArray = explode('/', $path); $displayName = strlen(end($pathArray)) > self::NAME_LENGTH ? - substr(end($pathArray), 0 , self::NAME_LENGTH) . "..." : + substr(end($pathArray), 0, self::NAME_LENGTH) . "..." : end($pathArray); $directories[] = [ 'data' => count($pathArray) > 0 ? $displayName : $path, 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 index 71ca60ef9077f..1e63f9f61f6d1 100644 --- 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 @@ -5,9 +5,8 @@ /*eslint max-nested-callbacks: 0*/ define([ - 'Magento_Ui/js/grid/url-filter-applier', - 'jquery' -], function (UrlFilterApplier, $) { + 'Magento_Ui/js/grid/url-filter-applier' +], function (UrlFilterApplier) { 'use strict'; describe('Magento_Ui/js/grid/url-filter-applier', function () { From 2d44b78fedfdd7e89838e1f8ac6772efe2d8611f Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 20 Aug 2020 14:08:10 +0300 Subject: [PATCH 0358/1013] Add ablility to scroll folders in case when we have a lot of included one by one folders revert cutting folders from backend --- .../Model/Directories/GetFolderTree.php | 7 +------ .../view/adminhtml/web/css/source/_module.less | 12 +++++++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php index 54c3cac5ecd13..f0998a3e120f2 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -17,8 +17,6 @@ */ class GetFolderTree { - private const NAME_LENGTH = 50; - /** * @var Filesystem */ @@ -86,11 +84,8 @@ private function getDirectories(): array } $pathArray = explode('/', $path); - $displayName = strlen(end($pathArray)) > self::NAME_LENGTH ? - substr(end($pathArray), 0, self::NAME_LENGTH) . "..." : - end($pathArray); $directories[] = [ - 'data' => count($pathArray) > 0 ? $displayName : $path, + 'data' => count($pathArray) > 0 ? end($pathArray) : $path, 'attr' => ['id' => $path], 'metadata' => [ 'path' => $path 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 index fc8bd49126d8e..b1e7966a3cfb4 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -170,8 +170,9 @@ height: 30px; margin: 1px; padding-left: 6px; + padding-right: 10px; padding-top: 6px; - width: 100%; + width: max-content; } .jstree-default .jstree-clicked { @@ -272,8 +273,17 @@ } .media-directory-container { + &::-webkit-scrollbar { + background: transparent; + width: 0px; + } + -ms-overflow-style: none; float: left; + max-width: 50%; + overflow-x: scroll; + overflow-y: hidden; padding-right: 40px; + scrollbar-width: none; } .media-gallery-image-block { From d7b1ba6fdcd117222f11a0762b71aab0048d1ba3 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 20 Aug 2020 14:10:19 +0300 Subject: [PATCH 0359/1013] Rever foders tests as it can't be veryfied by mftf tests --- .../Mftf/Data/AdminMediaGalleryFolderData.xml | 2 - ...iaGalleryCreateFolderWithLongNamesTest.xml | 43 ------------------- 2 files changed, 45 deletions(-) delete mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml index d67c019397ec1..e4149acdf58d1 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml @@ -13,7 +13,5 @@ </entity> <entity name="AdminMediaGalleryFolderInvalidData"> <data key="name">,.?/:;'[{]}|~`!@#$%^*()_=+</data> - <data key="longName">mediagallerylongfoldernamemediagallerylongfoldername54</data> - <data key="cutedName">mediagallerylongfoldernamemediagallerylongfolderna...</data> </entity> </entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.xml deleted file mode 100644 index c76790e164298..0000000000000 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateFolderWithLongNamesTest.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCreateFolderWithLongNamesTest"> - <annotations> - <features value="MediaGallery"/> - <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1758"/> - <stories value="Create new folder with long names in Media Gallery"/> - <title value="Create new folder with long names in Media Gallery"/> - <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4456547"/> - <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.longName}}"/> - </actionGroup> - <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="AssertFolderName"> - <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.cutedName}}"/> - </actionGroup> - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> - - - <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> - <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.cutedName}}"/> - </actionGroup> - <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"> - <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.cutedName}}"/> - </actionGroup> - </test> -</tests> From 95d7d605038aedb621b96253529035ebdb9f0399 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 20 Aug 2020 17:09:45 +0300 Subject: [PATCH 0360/1013] fix static tests --- .../MediaGalleryUi/view/adminhtml/web/css/source/_module.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b1e7966a3cfb4..2b26fc19945d6 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -275,7 +275,7 @@ .media-directory-container { &::-webkit-scrollbar { background: transparent; - width: 0px; + width: 0; } -ms-overflow-style: none; float: left; From 84525d2f230afab81da627558ed97746ff7e3bbb Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 21 Aug 2020 13:21:02 +0300 Subject: [PATCH 0361/1013] update style to match mockups --- .../view/adminhtml/web/css/source/_module.less | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 index 2b26fc19945d6..16a2453b8b542 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -274,16 +274,17 @@ .media-directory-container { &::-webkit-scrollbar { - background: transparent; - width: 0; + background-color: @color-media-gallery-checkbox-background; + } + &::-webkit-scrollbar-thumb { + background-color: @color-masonry-grey; } - -ms-overflow-style: none; float: left; max-width: 50%; overflow-x: scroll; overflow-y: hidden; padding-right: 40px; - scrollbar-width: none; + scrollbar-color: @color-masonry-grey @color-media-gallery-checkbox-background; } .media-gallery-image-block { From d4586540e117f55ed1e9262173b85d9c276b6ab5 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 21 Aug 2020 13:40:52 +0300 Subject: [PATCH 0362/1013] cFix colors --- .../view/adminhtml/web/css/source/_module.less | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 16a2453b8b542..05cdf1d440833 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -18,7 +18,7 @@ @color-media-gallery-buttons-border: #adadad; @color-media-gallery-buttons-text: #514943; @color-media-gallery-checkbox-background: #eee; - +@color-media-gallery-scrollbar-background: white; & when (@media-common = true) { .media-gallery-delete-image-action, @@ -274,7 +274,7 @@ .media-directory-container { &::-webkit-scrollbar { - background-color: @color-media-gallery-checkbox-background; + background-color: @color-media-gallery-scrollbar-background; } &::-webkit-scrollbar-thumb { background-color: @color-masonry-grey; @@ -284,7 +284,7 @@ overflow-x: scroll; overflow-y: hidden; padding-right: 40px; - scrollbar-color: @color-masonry-grey @color-media-gallery-checkbox-background; + scrollbar-color: @color-masonry-grey @color-media-gallery-scrollbar-background; } .media-gallery-image-block { From a83fc986c3cd778391d56ec77ea334f0d15bf31e Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 21 Aug 2020 14:20:18 +0300 Subject: [PATCH 0363/1013] Fix static CSS tests --- .../MediaGalleryUi/view/adminhtml/web/css/source/_module.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 05cdf1d440833..df005319a50d4 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -18,7 +18,7 @@ @color-media-gallery-buttons-border: #adadad; @color-media-gallery-buttons-text: #514943; @color-media-gallery-checkbox-background: #eee; -@color-media-gallery-scrollbar-background: white; +@color-media-gallery-scrollbar-background: #FFFFFF; & when (@media-common = true) { .media-gallery-delete-image-action, From 5fa10d29cc92b68be2789f5740484bc9eb53432b Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 21 Aug 2020 15:11:07 +0300 Subject: [PATCH 0364/1013] again css static tests --- .../MediaGalleryUi/view/adminhtml/web/css/source/_module.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index df005319a50d4..b4529ac9296b1 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -18,7 +18,7 @@ @color-media-gallery-buttons-border: #adadad; @color-media-gallery-buttons-text: #514943; @color-media-gallery-checkbox-background: #eee; -@color-media-gallery-scrollbar-background: #FFFFFF; +@color-media-gallery-scrollbar-background: #ffffff; & when (@media-common = true) { .media-gallery-delete-image-action, From 568a73cffb155cffc352a8f599089ce36de9b651 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 21 Aug 2020 16:33:48 +0300 Subject: [PATCH 0365/1013] css static test fix !! --- .../MediaGalleryUi/view/adminhtml/web/css/source/_module.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b4529ac9296b1..6b3cd610f0348 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -18,7 +18,7 @@ @color-media-gallery-buttons-border: #adadad; @color-media-gallery-buttons-text: #514943; @color-media-gallery-checkbox-background: #eee; -@color-media-gallery-scrollbar-background: #ffffff; +@color-media-gallery-scrollbar-background: #fff; & when (@media-common = true) { .media-gallery-delete-image-action, From ce9e0321afe1358b509627ab3937d345f0b18a5a Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 21 Aug 2020 18:27:42 +0300 Subject: [PATCH 0366/1013] Remove product name assertion as other tests can leave some active filters on product grid --- .../Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index fea4436446da2..d4047cbe51722 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -33,7 +33,6 @@ <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"/> From c44a7d9a506896623711df6624e5724571095682 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Fri, 21 Aug 2020 19:03:57 +0300 Subject: [PATCH 0367/1013] Add ablility to ovveride same filter woth url applier, fix unstable mftf test --- .../Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml | 5 +++-- .../Magento/Ui/view/base/web/js/grid/url-filter-applier.js | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml index d4047cbe51722..2eda7b8d02481 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -31,10 +31,11 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}?filters[name]=$createSimpleProduct.name$" stepKey="navigateToProductGridWithFilters"/> + <amOnPage url="{{AdminProductIndexPage.url}}?filters[sku]=$createSimpleProduct.sku$" 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"/> + <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="SKU: $createSimpleProduct.sku$" stepKey="seeProductNameFilter"/> </test> </tests> 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 index ab985c51449e8..be9044143c5a4 100644 --- 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 @@ -39,7 +39,7 @@ define([ apply: function () { var urlFilter = this.getFilterParam(this.searchString), applied, - filters = {}; + filters; if (_.isUndefined(this.filterComponent())) { setTimeout(function () { @@ -51,7 +51,7 @@ define([ if (Object.keys(urlFilter).length) { applied = this.filterComponent().get('applied'); - filters = $.extend(true, urlFilter, applied); + filters = $.extend({}, applied, urlFilter); this.filterComponent().set('applied', filters); } }, From ac3892f185e056e4e236d90045273b26438406ef Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 19 Aug 2020 18:02:41 +0300 Subject: [PATCH 0368/1013] Correctly revert default view after mass image deletion --- .../view/adminhtml/web/js/grid/massaction/massactions.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 index 4f09854005f23..7799bc00f958c 100644 --- 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 @@ -141,9 +141,7 @@ define([ if (response.status === 'canceled') { return; } - this.imageModel().selected({}); - this.massActionMode(false); - this.switchMode(); + $(window).trigger('terminateMassAction.MediaGallery'); }.bind(this)); } }.bind(this)); From a8e3da4f2dd961f7d36f36d4ece80c7673c447de Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Wed, 19 Aug 2020 19:10:54 +0300 Subject: [PATCH 0369/1013] Fix bindings --- .../view/adminhtml/web/js/grid/massaction/massactions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 7799bc00f958c..03e82e65b5db5 100644 --- 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 @@ -142,7 +142,7 @@ define([ return; } $(window).trigger('terminateMassAction.MediaGallery'); - }.bind(this)); + }); } }.bind(this)); } From 2c6465f011a567b296bbdb24c14aa766cb6facfb Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 20 Aug 2020 10:01:00 +0300 Subject: [PATCH 0370/1013] Cover changes with mftf test --- ...lleryAssertMassActionModeNotActiveActionGroup.xml | 2 +- ...minEnhancedMediaGalleryDeleteImagesInBulkTest.xml | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml index a691f65387e8e..1ec2004b22f24 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml @@ -12,8 +12,8 @@ <annotations> <description>Asserts that massaction mode is terminated</description> </annotations> - + <dontSeeElement selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="verifyAddSelectedButtonNotVisible"/> <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMassAction"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml index 94831b039b53a..63c0fbfeefbbf 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml @@ -19,9 +19,17 @@ <group value="media_gallery_ui"/> </annotations> <before> + <createData entity="SimpleSubCategory" stepKey="category"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> <argument name="image" value="ImageUpload"/> </actionGroup> @@ -34,7 +42,7 @@ <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}}"/> From 83c25bd4b41585c19b7b7aeb8267e4460542644f Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Tue, 18 Aug 2020 23:25:22 +0800 Subject: [PATCH 0371/1013] magento/adobe-stock-integration#1387: Uncaught TypeError: Cannot read property 'complete' of undefined appears in dev console if save Previewed image as a new View and open this View on another page - created separate function for loading of image data in preview --- .../base/web/js/grid/columns/image-preview.js | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index d675bd7a60ab5..8a9df56562a15 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -128,8 +128,6 @@ define([ * @param {Object} record */ show: function (record) { - var img; - if (record._rowIndex === this.visibleRecord()) { this.hide(); @@ -141,19 +139,31 @@ define([ this._selectRow(record.rowNumber || null); this.visibleRecord(record._rowIndex); - img = $(this.previewImageSelector + ' img'); + this.lastOpenedImage(record._rowIndex); + this.updateImageData(); + }, - if (img.get(0).complete) { - this.updateHeight(); - this.scrollToPreview(); + /** + * Update image data + */ + updateImageData: function () { + var img = $(this.previewImageSelector + ' img'); + + if (!img.get(0)) { + setTimeout(function () { + this.updateImageData(); + }.bind(this), 100); } else { - img.load(function () { + if (img.get(0).complete) { this.updateHeight(); this.scrollToPreview(); - }.bind(this)); + } else { + img.load(function () { + this.updateHeight(); + this.scrollToPreview(); + }.bind(this)); + } } - - this.lastOpenedImage(record._rowIndex); }, /** From 5be78ca35dc7e5d005f98f503d9cba5b1a000f33 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 19 Aug 2020 18:47:36 +0800 Subject: [PATCH 0372/1013] magento/adobe-stock-integration#1387: Uncaught TypeError: Cannot read property 'complete' of undefined appears in dev console if save Previewed image as a new View and open this View on another page - suggested modifications --- .../base/web/js/grid/columns/image-preview.js | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index 8a9df56562a15..047e2cbae666b 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -32,7 +32,8 @@ define([ listens: { '${ $.provider }:params.filters': 'hide', '${ $.provider }:params.search': 'hide', - '${ $.provider }:params.paging': 'hide' + '${ $.provider }:params.paging': 'hide', + '${ $.provider }:data.items': 'updateDisplayedRecord' }, exports: { height: '${ $.parentName }.thumbnail_url:previewHeight' @@ -48,6 +49,22 @@ define([ this._super(); $(document).on('keydown', this.handleKeyDown.bind(this)); + this.lastOpenedImage.subscribe(function (newValue) { + if (newValue === false && _.isNull(this.visibleRecord())) { + return; + } + if (newValue === this.visibleRecord()) { + return; + } + + if (newValue === false) { + this.hide(); + return; + } + + this.show(this.masonry().rows()[newValue]); + }.bind(this)); + return this; }, @@ -166,6 +183,17 @@ define([ } }, + /** + * Update displayed record + * + * @param items + */ + updateDisplayedRecord: function (items) { + if (!_.isNull(this.visibleRecord())) { + this.displayedRecord(items[this.visibleRecord()]); + } + }, + /** * Update image preview section height */ From 6186b6183b8196b984ad846386ece9b19ee05fca Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 19 Aug 2020 19:26:49 +0800 Subject: [PATCH 0373/1013] magento/adobe-stock-integration#1387: Uncaught TypeError: Cannot read property 'complete' of undefined appears in dev console if save Previewed image as a new View and open this View on another page - requested modifications --- .../Ui/view/base/web/js/grid/columns/image-preview.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index 047e2cbae666b..ee2d9decdb0bb 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -161,7 +161,7 @@ define([ }, /** - * Update image data + * Update image data when image preview is opened */ updateImageData: function () { var img = $(this.previewImageSelector + ' img'); @@ -184,9 +184,9 @@ define([ }, /** - * Update displayed record + * Update opened image preview contents when the data provider is updated * - * @param items + * @param {Array} items */ updateDisplayedRecord: function (items) { if (!_.isNull(this.visibleRecord())) { From 78f49469bb5aaa4b60389addbb046798577b83da Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Thu, 20 Aug 2020 21:21:04 +0800 Subject: [PATCH 0374/1013] magento/adobe-stock-integration#1387: Uncaught TypeError: Cannot read property 'complete' of undefined appears in dev console if save Previewed image as a new View and open this View on another page - fix static test and unit test --- .../base/web/js/grid/columns/image-preview.js | 15 ++++++++------- .../Ui/base/js/grid/columns/image-preview.test.js | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index ee2d9decdb0bb..1e4ae9df7dc77 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -50,15 +50,18 @@ define([ $(document).on('keydown', this.handleKeyDown.bind(this)); this.lastOpenedImage.subscribe(function (newValue) { + if (newValue === false && _.isNull(this.visibleRecord())) { return; } + if (newValue === this.visibleRecord()) { return; } if (newValue === false) { this.hide(); + return; } @@ -170,16 +173,14 @@ define([ setTimeout(function () { this.updateImageData(); }.bind(this), 100); + } else if (img.get(0).complete) { + this.updateHeight(); + this.scrollToPreview(); } else { - if (img.get(0).complete) { + img.load(function () { this.updateHeight(); this.scrollToPreview(); - } else { - img.load(function () { - this.updateHeight(); - this.scrollToPreview(); - }.bind(this)); - } + }.bind(this)); } }, diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js index 6a466f0c37872..a5b434d956097 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/columns/image-preview.test.js @@ -74,6 +74,7 @@ define([ originMock = $.fn.get; spyOn($.fn, 'get').and.returnValue(imageMock); + imagePreview.lastOpenedImage = jasmine.createSpy().and.returnValue(2); imagePreview.visibleRecord = jasmine.createSpy().and.returnValue(2); imagePreview.displayedRecord = ko.observable(); imagePreview.displayedRecord(recordMock); From e97dd6f7b5ead9a82d0fac69e205f3763e8e2fb4 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Thu, 20 Aug 2020 21:27:56 +0800 Subject: [PATCH 0375/1013] magento/adobe-stock-integration#1387: Uncaught TypeError: Cannot read property 'complete' of undefined appears in dev console if save Previewed image as a new View and open this View on another page - modified function description --- .../Magento/Ui/view/base/web/js/grid/columns/image-preview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index 1e4ae9df7dc77..b561ce2e784b8 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -185,7 +185,7 @@ define([ }, /** - * Update opened image preview contents when the data provider is updated + * Update preview displayed record data from the new items data if the preview is expanded * * @param {Array} items */ From d53aff80eb7d04ad6cad60747e94a1ce519b6ac9 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Tue, 25 Aug 2020 00:26:18 +0800 Subject: [PATCH 0376/1013] magento/adobe-stock-integration#1387: Uncaught TypeError: Cannot read property 'complete' of undefined appears in dev console if save Previewed image as a new View and open this View on another page - fix static test fail --- .../Magento/Ui/view/base/web/js/grid/columns/image-preview.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js index b561ce2e784b8..7dcf0994ef56b 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image-preview.js @@ -2,6 +2,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +/* eslint-disable no-undef */ define([ 'jquery', 'Magento_Ui/js/grid/columns/column', From d05fc905bd1d6de7afb71a161e97a21459367713 Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Tue, 18 Aug 2020 20:01:07 +0800 Subject: [PATCH 0377/1013] magento/adobe-stock-integration#1760: Media Gallery page and Category grid page opened successfully if "Enhanced Media Gallery" disabled - redirect to 404 if enhanced media gallery config is disabled. --- .../Controller/Adminhtml/Category/Index.php | 29 +++++++++++++++++++ .../MediaGalleryCatalogUi/composer.json | 3 +- .../Controller/Adminhtml/Media/Index.php | 29 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php index a541e9999b784..497a65b207353 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php @@ -8,10 +8,13 @@ namespace Magento\MediaGalleryCatalogUi\Controller\Adminhtml\Category; use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Page; +use Magento\Backend\Model\View\Result\Forward; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; +use Magento\MediaContentApi\Model\Config; /** * Controller serving the media gallery content @@ -20,6 +23,24 @@ class Index extends Action implements HttpGetActionInterface { public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + /** + * @var Config + */ + private $config; + + /** + * Index constructor. + * @param Context $context + * @param Config $config + */ + public function __construct( + Context $context, + Config $config + ) { + parent::__construct($context); + $this->config = $config; + } + /** * Get the media gallery layout * @@ -27,6 +48,14 @@ class Index extends Action implements HttpGetActionInterface */ public function execute(): ResultInterface { + if (!$this->config->isEnabled()) { + /** @var Forward $resultForward */ + $resultForward = $this->resultFactory->create(ResultFactory::TYPE_FORWARD); + $resultForward->forward('noroute'); + + return $resultForward; + } + /** @var Page $resultPage */ $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); $resultPage->getConfig()->getTitle()->prepend(__('Categories')); diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json index 985d581beff25..da759e67ddf54 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/composer.json +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -8,7 +8,8 @@ "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-store": "*", - "magento/module-ui": "*" + "magento/module-ui": "*", + "magento/module-media-content-api": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php index 3660374243d16..8c5b3d4d3a9ac 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -12,6 +12,9 @@ use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; +use Magento\MediaContentApi\Model\Config; +use Magento\Backend\App\Action\Context; +use Magento\Backend\Model\View\Result\Forward; /** * Controller serving the media gallery content @@ -20,6 +23,24 @@ class Index extends Action implements HttpGetActionInterface { public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + /** + * @var Config + */ + private $config; + + /** + * Index constructor. + * @param Context $context + * @param Config $config + */ + public function __construct( + Context $context, + Config $config + ) { + parent::__construct($context); + $this->config = $config; + } + /** * Get the media gallery layout * @@ -27,6 +48,14 @@ class Index extends Action implements HttpGetActionInterface */ public function execute(): ResultInterface { + if (!$this->config->isEnabled()) { + /** @var Forward $resultForward */ + $resultForward = $this->resultFactory->create(ResultFactory::TYPE_FORWARD); + $resultForward->forward('noroute'); + + return $resultForward; + } + /** @var Page $resultPage */ $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); $resultPage->setActiveMenu('Magento_MediaGalleryUi::media_gallery') From 58ac51f0b23bd1ce03fe230d350a5df43ad41934 Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Wed, 19 Aug 2020 17:04:46 +0800 Subject: [PATCH 0378/1013] magento/adobe-stock-integration#1760: Media Gallery page and Category grid page opened successfully if "Enhanced Media Gallery" disabled - revert category grid no route redirect when media gallery is disabled, MFTF test coverage --- .../Controller/Adminhtml/Category/Index.php | 29 ------------------- .../MediaGalleryCatalogUi/composer.json | 3 +- ...daloneMediaGalleryPageIs404ActionGroup.xml | 18 ++++++++++++ ...dminStandaloneMediaGalleryDisabledTest.xml | 28 ++++++++++++++++++ 4 files changed, 47 insertions(+), 31 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php index 497a65b207353..a541e9999b784 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php @@ -8,13 +8,10 @@ namespace Magento\MediaGalleryCatalogUi\Controller\Adminhtml\Category; use Magento\Backend\App\Action; -use Magento\Backend\App\Action\Context; use Magento\Backend\Model\View\Result\Page; -use Magento\Backend\Model\View\Result\Forward; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; -use Magento\MediaContentApi\Model\Config; /** * Controller serving the media gallery content @@ -23,24 +20,6 @@ class Index extends Action implements HttpGetActionInterface { public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; - /** - * @var Config - */ - private $config; - - /** - * Index constructor. - * @param Context $context - * @param Config $config - */ - public function __construct( - Context $context, - Config $config - ) { - parent::__construct($context); - $this->config = $config; - } - /** * Get the media gallery layout * @@ -48,14 +27,6 @@ public function __construct( */ public function execute(): ResultInterface { - if (!$this->config->isEnabled()) { - /** @var Forward $resultForward */ - $resultForward = $this->resultFactory->create(ResultFactory::TYPE_FORWARD); - $resultForward->forward('noroute'); - - return $resultForward; - } - /** @var Page $resultPage */ $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); $resultPage->getConfig()->getTitle()->prepend(__('Categories')); diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json index da759e67ddf54..985d581beff25 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/composer.json +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -8,8 +8,7 @@ "magento/module-backend": "*", "magento/module-catalog": "*", "magento/module-store": "*", - "magento/module-ui": "*", - "magento/module-media-content-api": "*" + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.xml new file mode 100644 index 0000000000000..868477910f52b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.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="AssertAdminStandaloneMediaGalleryPageIs404ActionGroup"> + <annotations> + <description>Validates that the '404 Error' message is present and correct in the Admin Standalone Media Gallery Page Header.</description> + </annotations> + + <see userInput="404 Error" selector="{{AdminHeaderSection.pageHeading}}" stepKey="see404PageHeading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml new file mode 100644 index 0000000000000..0d2c8207f90c5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AdminMediaGalleryDisabledPageTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1760"/> + <title value="Standalone Media Gallery Page should return 404 if Media Gallery is disabled"/> + <stories value="#1760 Media Gallery Page opened successfully if Enhanced Media Gallery disabled"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/5106786"/> + <description value="Standalone Media Gallery Page should return 404 if Media Gallery is disabled"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <actionGroup ref="AssertAdminStandaloneMediaGalleryPageIs404ActionGroup" stepKey="see404Page"/> + </test> +</tests> From 8d92a2966baaa189af3eac34cc9b3c4c746e0ebc Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Wed, 19 Aug 2020 20:12:00 +0800 Subject: [PATCH 0379/1013] magento/adobe-stock-integration#1760: Media Gallery page and Category grid page opened successfully if "Enhanced Media Gallery" disabled - MFTF Apply PR suggestions --- ...p.xml => AssertAdminPageIs404ActionGroup.xml} | 4 ++-- .../Mftf/Suite/MediaGalleryUiDisabledSuite.xml | 16 ++++++++++++++++ .../AdminStandaloneMediaGalleryDisabledTest.xml | 6 +++--- 3 files changed, 21 insertions(+), 5 deletions(-) rename app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/{AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.xml => AssertAdminPageIs404ActionGroup.xml} (77%) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiDisabledSuite.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml similarity index 77% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.xml rename to app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml index 868477910f52b..09b0bdcc146ae 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminStandaloneMediaGalleryPageIs404ActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml @@ -8,9 +8,9 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AssertAdminStandaloneMediaGalleryPageIs404ActionGroup"> + <actionGroup name="AssertAdminPageIs404ActionGroup"> <annotations> - <description>Validates that the '404 Error' message is present and correct in the Admin Standalone Media Gallery Page Header.</description> + <description>Validates that the '404 Error' message is present in the current Admin Page Header.</description> </annotations> <see userInput="404 Error" selector="{{AdminHeaderSection.pageHeading}}" stepKey="see404PageHeading"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiDisabledSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiDisabledSuite.xml new file mode 100644 index 0000000000000..727fbde3f17b6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiDisabledSuite.xml @@ -0,0 +1,16 @@ +<?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="MediaGalleryUiDisabledSuite"> + <include> + <group name="media_gallery_ui_disabled"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml index 0d2c8207f90c5..767a0004872c4 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryDisabledPageTest"> + <test name="AdminStandaloneMediaGalleryDisabledTest"> <annotations> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1760"/> @@ -16,13 +16,13 @@ <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/5106786"/> <description value="Standalone Media Gallery Page should return 404 if Media Gallery is disabled"/> <severity value="CRITICAL"/> - <group value="media_gallery_ui"/> + <group value="media_gallery_ui_disabled"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> </before> - <actionGroup ref="AssertAdminStandaloneMediaGalleryPageIs404ActionGroup" stepKey="see404Page"/> + <actionGroup ref="AssertAdminPageIs404ActionGroup" stepKey="see404Page"/> </test> </tests> From df69ee15c6c6fd3be833ccf401d35ebac3bf9192 Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Fri, 21 Aug 2020 00:56:30 +0800 Subject: [PATCH 0380/1013] magento/adobe-stock-integration#1760: Media Gallery page and Category grid page opened successfully if "Enhanced Media Gallery" disabled - MFTF Apply PR suggestions --- .../Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml | 0 .../Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) rename app/code/Magento/{MediaGalleryUi => Backend}/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml (100%) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml b/app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml similarity index 100% rename from app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml rename to app/code/Magento/Backend/Test/Mftf/ActionGroup/AssertAdminPageIs404ActionGroup.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml index 767a0004872c4..8b0c984c1df77 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryDisabledTest.xml @@ -20,9 +20,8 @@ </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> </before> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> <actionGroup ref="AssertAdminPageIs404ActionGroup" stepKey="see404Page"/> </test> </tests> From 35c0706cdbbe3a32836ae8300f9504c13820aad0 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 25 Aug 2020 18:13:32 +0300 Subject: [PATCH 0381/1013] Set path column as text type --- app/code/Magento/MediaGallery/etc/db_schema.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml index 1001737daa8a7..a1662ba63f199 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema.xml +++ b/app/code/Magento/MediaGallery/etc/db_schema.xml @@ -8,7 +8,7 @@ <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="media_gallery_asset" resource="default" engine="innodb" comment="Media Gallery Asset"> <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="text" name="path" 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"/> From 99c87e09786eb79ac0d2c532b02022985d48b8b6 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 25 Aug 2020 18:05:16 +0100 Subject: [PATCH 0382/1013] magento/magento2#29742: Removed path unique constraint --- app/code/Magento/MediaGallery/etc/db_schema.xml | 3 --- app/code/Magento/MediaGallery/etc/db_schema_whitelist.json | 1 - 2 files changed, 4 deletions(-) diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml index a1662ba63f199..1a9b0dc96a655 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema.xml +++ b/app/code/Magento/MediaGallery/etc/db_schema.xml @@ -25,9 +25,6 @@ <index referenceId="MEDIA_GALLERY_ID" indexType="btree"> <column name="id"/> </index> - <constraint xsi:type="unique" referenceId="MEDIA_GALLERY_ID_PATH_TITLE_CONTENT_TYPE_WIDTH_HEIGHT"> - <column name="path"/> - </constraint> <index referenceId="MEDIA_GALLERY_ASSET_TITLE" indexType="fulltext"> <column name="title"/> </index> diff --git a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json index b32dfbf082175..e958d630b7e3f 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json +++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json @@ -20,7 +20,6 @@ "MEDIA_GALLERY_ASSET_TITLE": true }, "constraint": { - "MEDIA_GALLERY_ID_PATH_TITLE_CONTENT_TYPE_WIDTH_HEIGHT": true, "PRIMARY": true, "MEDIA_GALLERY_ASSET_PATH": true } From ccdf0ad1ff72ac18abecc9c43509210f4a613817 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 25 Aug 2020 18:50:58 +0300 Subject: [PATCH 0383/1013] Skip segment reader if there is any kind of exception --- .../MediaGalleryMetadata/Model/File/ExtractMetadata.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php index 00f2b07f5bb81..f5efd25bca041 100644 --- a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php @@ -7,7 +7,6 @@ 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; @@ -90,7 +89,12 @@ private function readSegments(FileInterface $file): MetadataInterface ); } - $data = $segmentReader->execute($file); + try { + $data = $segmentReader->execute($file); + } catch (\Exception $exception) { + continue; + } + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; $description = !empty($data->getDescription()) ? $data->getDescription() : $description; From 6a48523082a40052beb490d49702dbf76e2d2e2b Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Wed, 26 Aug 2020 18:28:23 +0100 Subject: [PATCH 0384/1013] Intoroduced MediaGallerySynchronizationMetadata module --- .../Model/GetAssetFromPath.php | 1 + .../Model/SynchronizeFilesTest.php | 70 +-- .../MediaGallerySynchronization/composer.json | 3 +- .../MediaGallerySynchronization/etc/di.xml | 3 +- .../Model/CreateAssetFromFile.php | 16 +- .../LICENSE.txt | 48 ++ .../LICENSE_AFL.txt | 48 ++ .../Model/ImportKeywords.php} | 14 +- .../Plugin/CreateAssetFromFileMetadata.php | 81 +++ .../README.md | 3 + .../composer.json | 24 + .../etc/di.xml | 19 + .../etc/module.xml | 10 + .../registration.php | 14 + composer.json | 1 + composer.lock | 568 +----------------- .../Model/SynchronizeFilesTest.php | 151 +++++ .../_files/magento.jpg | Bin 0 -> 55303 bytes .../_files/magento_metadata.jpg | Bin 19 files changed, 425 insertions(+), 649 deletions(-) rename app/code/Magento/{MediaGallerySynchronization => MediaGallerySynchronizationApi}/Model/CreateAssetFromFile.php (84%) create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE.txt create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE_AFL.txt rename app/code/Magento/{MediaGallerySynchronization/Model/ImportImageFileKeywords.php => MediaGallerySynchronizationMetadata/Model/ImportKeywords.php} (93%) create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/README.md create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/composer.json create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/etc/module.xml create mode 100644 app/code/Magento/MediaGallerySynchronizationMetadata/registration.php create mode 100644 dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/Model/SynchronizeFilesTest.php create mode 100644 dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento.jpg rename {app/code/Magento/MediaGallerySynchronization/Test/Integration => dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata}/_files/magento_metadata.jpg (100%) diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php index 533d814c9f1d0..ef23e09dfa1fa 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php @@ -12,6 +12,7 @@ use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFile; /** * Create media asset object based on the file information diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php index 6c4338c0935dc..8a44307298065 100644 --- a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/SynchronizeFilesTest.php @@ -9,14 +9,12 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\DriverInterface; -use Magento\MediaGalleryApi\Api\Data\AssetInterface; -use Magento\MediaGalleryApi\Api\Data\KeywordInterface; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; -use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -45,11 +43,6 @@ class SynchronizeFilesTest extends TestCase */ private $mediaDirectory; - /** - * @var GetAssetsKeywordsInterface - */ - private $getAssetKeywords; - /** * @inheritdoc */ @@ -58,7 +51,6 @@ protected function setUp(): void $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); $this->synchronizeFiles = Bootstrap::getObjectManager()->get(SynchronizeFilesInterface::class); $this->getAssetsByPath = Bootstrap::getObjectManager()->get(GetAssetsByPathsInterface::class); - $this->getAssetKeywords = Bootstrap::getObjectManager()->get(GetAssetsKeywordsInterface::class); $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) ->getDirectoryWrite(DirectoryList::MEDIA); } @@ -67,18 +59,16 @@ protected function setUp(): void * Test for SynchronizeFiles::execute * * @dataProvider filesProvider - * @param null|string $file - * @param null|string $title - * @param null|string $description - * @param null|array $keywords + * @param string $file + * @param string $title + * @param string $source * @throws FileSystemException - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function testExecute( - ?string $file, - ?string $title, - ?string $description, - ?array $keywords + string $file, + string $title, + string $source ): void { $path = realpath(__DIR__ . '/../_files/' . $file); $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($file); @@ -89,12 +79,10 @@ public function testExecute( $this->synchronizeFiles->execute([$file]); - $loadedAssets = $this->getAssetsByPath->execute([$file])[0]; - $loadedKeywords = $this->getKeywords($loadedAssets) ?: null; + $loadedAsset = $this->getAssetsByPath->execute([$file])[0]; - $this->assertEquals($title, $loadedAssets->getTitle()); - $this->assertEquals($description, $loadedAssets->getDescription()); - $this->assertEquals($keywords, $loadedKeywords); + $this->assertEquals($title, $loadedAsset->getTitle()); + $this->assertEquals($source, $loadedAsset->getSource()); $this->driver->deleteFile($modifiableFilePath); } @@ -110,42 +98,8 @@ public function filesProvider(): array [ '/magento.jpg', 'magento', - null, - null - ], - [ - '/magento_metadata.jpg', - 'Title of the magento image', - 'Description of the magento image', - [ - 'magento', - 'mediagallerymetadata' - ] + 'Local' ] ]; } - - /** - * 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 - ); - } } diff --git a/app/code/Magento/MediaGallerySynchronization/composer.json b/app/code/Magento/MediaGallerySynchronization/composer.json index e1d4962366978..f9d642dd02568 100644 --- a/app/code/Magento/MediaGallerySynchronization/composer.json +++ b/app/code/Magento/MediaGallerySynchronization/composer.json @@ -6,8 +6,7 @@ "magento/framework": "*", "magento/module-media-gallery-api": "*", "magento/module-media-gallery-synchronization-api": "*", - "magento/framework-message-queue": "*", - "magento/module-media-gallery-metadata-api": "*" + "magento/framework-message-queue": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml index 47a4360575b2e..4b9ffcbe63c76 100644 --- a/app/code/Magento/MediaGallerySynchronization/etc/di.xml +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -12,8 +12,7 @@ <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> + <item name="10" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportMediaAsset</item> </argument> </arguments> </type> diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFile.php similarity index 84% rename from app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php rename to app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFile.php index 80b334733ed43..0e11487ecfa73 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFile.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\MediaGallerySynchronization\Model; +namespace Magento\MediaGallerySynchronizationApi\Model; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; @@ -14,7 +14,6 @@ use Magento\Framework\Filesystem\Driver\File; use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; -use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; use Magento\MediaGallerySynchronization\Model\GetContentHash; @@ -43,11 +42,6 @@ class CreateAssetFromFile */ private $getContentHash; - /** - * @var ExtractMetadataInterface - */ - private $extractMetadata; - /** * @var GetFileInfo */ @@ -58,7 +52,6 @@ class CreateAssetFromFile * @param File $driver * @param AssetInterfaceFactory $assetFactory * @param GetContentHash $getContentHash - * @param ExtractMetadataInterface $extractMetadata * @param GetFileInfo $getFileInfo */ public function __construct( @@ -66,14 +59,12 @@ public function __construct( File $driver, AssetInterfaceFactory $assetFactory, GetContentHash $getContentHash, - ExtractMetadataInterface $extractMetadata, GetFileInfo $getFileInfo ) { $this->filesystem = $filesystem; $this->driver = $driver; $this->assetFactory = $assetFactory; $this->getContentHash = $getContentHash; - $this->extractMetadata = $extractMetadata; $this->getFileInfo = $getFileInfo; } @@ -90,14 +81,11 @@ public function execute(string $path): AssetInterface $file = $this->getFileInfo->execute($absolutePath); [$width, $height] = getimagesize($absolutePath); - $metadata = $this->extractMetadata->execute($absolutePath); - return $this->assetFactory->create( [ 'id' => null, 'path' => $path, - 'title' => $metadata->getTitle() ?: $file->getBasename(), - 'description' => $metadata->getDescription(), + 'title' => $file->getBasename(), 'width' => $width, 'height' => $height, 'hash' => $this->getHash($path), diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE.txt b/app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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/MediaGallerySynchronizationMetadata/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronizationMetadata/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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/ImportImageFileKeywords.php b/app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php similarity index 93% rename from app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php rename to app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php index 361137ad27686..a9910157f27c7 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/Model/ImportKeywords.php @@ -5,12 +5,11 @@ */ declare(strict_types=1); -namespace Magento\MediaGallerySynchronization\Model; +namespace Magento\MediaGallerySynchronizationMetadata\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; @@ -22,18 +21,13 @@ /** * import image keywords from file metadata */ -class ImportImageFileKeywords implements ImportFilesInterface +class ImportKeywords implements ImportFilesInterface { /** * @var Filesystem */ private $filesystem; - /** - * @var File - */ - private $driver; - /** * @var KeywordInterfaceFactory */ @@ -60,7 +54,6 @@ class ImportImageFileKeywords implements ImportFilesInterface private $getAssetsByPaths; /** - * @param File $driver * @param Filesystem $filesystem * @param KeywordInterfaceFactory $keywordFactory * @param ExtractMetadataInterface $extractMetadata @@ -69,7 +62,6 @@ class ImportImageFileKeywords implements ImportFilesInterface * @param GetAssetsByPathsInterface $getAssetsByPaths */ public function __construct( - File $driver, Filesystem $filesystem, KeywordInterfaceFactory $keywordFactory, ExtractMetadataInterface $extractMetadata, @@ -77,7 +69,6 @@ public function __construct( AssetKeywordsInterfaceFactory $assetKeywordsFactory, GetAssetsByPathsInterface $getAssetsByPaths ) { - $this->driver = $driver; $this->filesystem = $filesystem; $this->keywordFactory = $keywordFactory; $this->extractMetadata = $extractMetadata; @@ -123,6 +114,7 @@ private function getMetadataKeywords(string $path): ?array { $metadataKeywords = $this->extractMetadata->execute($this->getMediaDirectory()->getAbsolutePath($path)) ->getKeywords(); + if ($metadataKeywords === null) { return null; } diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php b/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php new file mode 100644 index 0000000000000..1f67a871b57ae --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationMetadata\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFile; + +/** + * Add metadata to the asset created from file + */ +class CreateAssetFromFileMetadata +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @param Filesystem $filesystem + * @param AssetInterfaceFactory $assetFactory + * @param ExtractMetadataInterface $extractMetadata + */ + public function __construct( + Filesystem $filesystem, + AssetInterfaceFactory $assetFactory, + ExtractMetadataInterface $extractMetadata + ) { + $this->filesystem = $filesystem; + $this->assetFactory = $assetFactory; + $this->extractMetadata = $extractMetadata; + } + + /** + * Add metadata to the asset + * + * @param CreateAssetFromFile $createAssetFromFile + * @param AssetInterface $asset + * @return AssetInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(CreateAssetFromFile $createAssetFromFile, AssetInterface $asset): AssetInterface + { + $metadata = $this->extractMetadata->execute( + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($asset->getPath()) + ); + + return $this->assetFactory->create( + [ + 'id' => $asset->getId(), + 'path' => $asset->getPath(), + 'title' => $metadata->getTitle() ?: $asset->getTitle(), + 'description' => $metadata->getDescription(), + 'width' => $asset->getWidth(), + 'height' => $asset->getHeight(), + 'hash' => $asset->getHash(), + 'size' => $asset->getSize(), + 'contentType' => $asset->getContentType(), + 'source' => $asset->getSource() + ] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/README.md b/app/code/Magento/MediaGallerySynchronizationMetadata/README.md new file mode 100644 index 0000000000000..64988dd543fe4 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGallerySynchronizationMetadata + +The purpose of this module is to include assets metadata to media gallery synchronization process diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json new file mode 100644 index 0000000000000..0674014026b24 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-gallery-synchronization-metadata", + "description": "Magento module responsible for images metadata synchronization", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-synchronization-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronizationMetadata\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml b/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml new file mode 100644 index 0000000000000..c82350f617b5b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <arguments> + <argument name="importers" xsi:type="array"> + <item name="20" xsi:type="object">Magento\MediaGallerySynchronizationMetadata\Model\ImportKeywords</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFile"> + <plugin name="addMetadataToAssetCreatedFromFile" type="Magento\MediaGallerySynchronizationMetadata\Plugin\CreateAssetFromFileMetadata"/> + </type> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/etc/module.xml b/app/code/Magento/MediaGallerySynchronizationMetadata/etc/module.xml new file mode 100644 index 0000000000000..f92c370496d2d --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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_MediaGallerySynchronizationMetadata"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/registration.php b/app/code/Magento/MediaGallerySynchronizationMetadata/registration.php new file mode 100644 index 0000000000000..82315db519f82 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/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_MediaGallerySynchronizationMetadata', + __DIR__ +); diff --git a/composer.json b/composer.json index 25be12b5bb72f..1af86e438882c 100644 --- a/composer.json +++ b/composer.json @@ -215,6 +215,7 @@ "magento/module-media-content-synchronization-api": "*", "magento/module-media-content-synchronization-catalog": "*", "magento/module-media-content-synchronization-cms": "*", + "magento/module-media-gallery-synchronization-metadata": "*", "magento/module-media-gallery-metadata": "*", "magento/module-media-gallery-metadata-api": "*", "magento/module-media-gallery-catalog-ui": "*", diff --git a/composer.lock b/composer.lock index 36a42d0c750df..9f079570cc2ac 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": "197f0388c574f9d40555a95634e439e0", + "content-hash": "aadcf8a265dd7ecbb86dd3dd4e49bc28", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -7315,555 +7315,6 @@ ], "time": "2020-06-27T23:57:46+00:00" }, - { - "name": "hoa/consistency", - "version": "1.17.05.02", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Consistency.git", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", - "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", - "shasum": "" - }, - "require": { - "hoa/exception": "~1.0", - "php": ">=5.5.0" - }, - "require-dev": { - "hoa/stream": "~1.0", - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Consistency\\": "." - }, - "files": [ - "Prelude.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Consistency library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "autoloader", - "callable", - "consistency", - "entity", - "flex", - "keyword", - "library" - ], - "time": "2017-05-02T12:18:12+00:00" - }, - { - "name": "hoa/console", - "version": "3.17.05.02", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Console.git", - "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Console/zipball/e231fd3ea70e6d773576ae78de0bdc1daf331a66", - "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/file": "~1.0", - "hoa/protocol": "~1.0", - "hoa/stream": "~1.0", - "hoa/ustring": "~4.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "suggest": { - "ext-pcntl": "To enable hoa://Event/Console/Window:resize.", - "hoa/dispatcher": "To use the console kit.", - "hoa/router": "To use the console kit." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Console\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Console library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "autocompletion", - "chrome", - "cli", - "console", - "cursor", - "getoption", - "library", - "option", - "parser", - "processus", - "readline", - "terminfo", - "tput", - "window" - ], - "time": "2017-05-02T12:26:19+00:00" - }, - { - "name": "hoa/event", - "version": "1.17.01.13", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Event.git", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", - "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Event\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Event library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "event", - "library", - "listener", - "observer" - ], - "time": "2017-01-13T15:30:50+00:00" - }, - { - "name": "hoa/exception", - "version": "1.17.01.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Exception.git", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", - "reference": "091727d46420a3d7468ef0595651488bfc3a458f", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Exception\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Exception library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "exception", - "library" - ], - "time": "2017-01-16T07:53:27+00:00" - }, - { - "name": "hoa/file", - "version": "1.17.07.11", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/File.git", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", - "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/iterator": "~2.0", - "hoa/stream": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\File\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\File library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "Socket", - "directory", - "file", - "finder", - "library", - "link", - "temporary" - ], - "time": "2017-07-11T07:42:15+00:00" - }, - { - "name": "hoa/iterator", - "version": "2.17.01.10", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Iterator.git", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", - "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Iterator\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Iterator library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "iterator", - "library" - ], - "time": "2017-01-10T10:34:47+00:00" - }, - { - "name": "hoa/protocol", - "version": "1.17.01.14", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Protocol.git", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", - "reference": "5c2cf972151c45f373230da170ea015deecf19e2", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Protocol\\": "." - }, - "files": [ - "Wrapper.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Protocol library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "protocol", - "resource", - "stream", - "wrapper" - ], - "time": "2017-01-14T12:26:10+00:00" - }, - { - "name": "hoa/stream", - "version": "1.17.02.21", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Stream.git", - "reference": "3293cfffca2de10525df51436adf88a559151d82" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", - "reference": "3293cfffca2de10525df51436adf88a559151d82", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/event": "~1.0", - "hoa/exception": "~1.0", - "hoa/protocol": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Stream\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Stream library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "Context", - "bucket", - "composite", - "filter", - "in", - "library", - "out", - "protocol", - "stream", - "wrapper" - ], - "time": "2017-02-21T16:01:06+00:00" - }, - { - "name": "hoa/ustring", - "version": "4.17.01.16", - "source": { - "type": "git", - "url": "https://github.com/hoaproject/Ustring.git", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", - "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", - "shasum": "" - }, - "require": { - "hoa/consistency": "~1.0", - "hoa/exception": "~1.0" - }, - "require-dev": { - "hoa/test": "~2.0" - }, - "suggest": { - "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", - "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.x-dev" - } - }, - "autoload": { - "psr-4": { - "Hoa\\Ustring\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Ivan Enderlin", - "email": "ivan.enderlin@hoa-project.net" - }, - { - "name": "Hoa community", - "homepage": "https://hoa-project.net/" - } - ], - "description": "The Hoa\\Ustring library.", - "homepage": "https://hoa-project.net/", - "keywords": [ - "library", - "search", - "string", - "unicode" - ], - "time": "2017-01-16T07:08:25+00:00" - }, { "name": "jms/metadata", "version": "1.7.0", @@ -8120,12 +7571,6 @@ "sftp", "storage" ], - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], "time": "2020-05-18T15:13:39+00:00" }, { @@ -8236,16 +7681,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.1.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "8a106ea029f222f4354854636861273c7577bee9" + "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", - "reference": "8a106ea029f222f4354854636861273c7577bee9", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8d98efa7434a30ab9e82ef128c430ef8e3a50699", + "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699", "shasum": "" }, "require": { @@ -8263,7 +7708,6 @@ "ext-intl": "*", "ext-json": "*", "ext-openssl": "*", - "hoa/console": "~3.0", "monolog/monolog": "^1.17", "mustache/mustache": "~2.5", "php": "^7.3", @@ -8323,7 +7767,7 @@ "magento", "testing" ], - "time": "2020-08-19T19:57:27+00:00" + "time": "2020-07-09T21:26:19+00:00" }, { "name": "mikey179/vfsstream", diff --git a/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/Model/SynchronizeFilesTest.php b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/Model/SynchronizeFilesTest.php new file mode 100644 index 0000000000000..52e7191a97226 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/Model/SynchronizeFilesTest.php @@ -0,0 +1,151 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationMetadata\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for SynchronizeFiles. + */ +class SynchronizeFilesTest extends TestCase +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPath; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->synchronizeFiles = Bootstrap::getObjectManager()->get(SynchronizeFilesInterface::class); + $this->getAssetsByPath = Bootstrap::getObjectManager()->get(GetAssetsByPathsInterface::class); + $this->getAssetKeywords = Bootstrap::getObjectManager()->get(GetAssetsKeywordsInterface::class); + $this->mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * Test for SynchronizeFiles::execute + * + * @dataProvider filesProvider + * @param null|string $file + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @throws FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function testExecute( + ?string $file, + ?string $title, + ?string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../_files/' . $file); + $modifiableFilePath = $this->mediaDirectory->getAbsolutePath($file); + $this->driver->copy( + $path, + $modifiableFilePath + ); + + $this->synchronizeFiles->execute([$file]); + + $loadedAssets = $this->getAssetsByPath->execute([$file])[0]; + $loadedKeywords = $this->getKeywords($loadedAssets) ?: null; + + $this->assertEquals($title, $loadedAssets->getTitle()); + $this->assertEquals($description, $loadedAssets->getDescription()); + $this->assertEquals($keywords, $loadedKeywords); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + '/magento.jpg', + 'magento', + null, + null + ], + [ + '/magento_metadata.jpg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ] + ]; + } + + /** + * 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 + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento.jpg b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c377daf8fb0b390d89aa4f6f111715fc9118ca1b GIT binary patch literal 55303 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 Hh5vs5{BvNq literal 0 HcmV?d00001 diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_metadata.jpg b/dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento_metadata.jpg similarity index 100% rename from app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_metadata.jpg rename to dev/tests/integration/testsuite/Magento/MediaGallerySynchronizationMetadata/_files/magento_metadata.jpg From 31938d47ce83541bc02edc6667ce4d049cb06ed6 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Wed, 26 Aug 2020 21:19:49 +0100 Subject: [PATCH 0385/1013] magento/magento2#29761: Added CreateAssetFromFileInterface --- .../Model/CreateAssetFromFile.php | 12 +++------ .../Model/GetAssetFromPath.php | 8 +++--- .../MediaGallerySynchronization/etc/di.xml | 1 + .../Model/CreateAssetFromFileInterface.php | 26 +++++++++++++++++++ .../composer.json | 3 ++- .../Plugin/CreateAssetFromFileMetadata.php | 6 ++--- .../etc/di.xml | 2 +- 7 files changed, 41 insertions(+), 17 deletions(-) rename app/code/Magento/{MediaGallerySynchronizationApi => MediaGallerySynchronization}/Model/CreateAssetFromFile.php (91%) create mode 100644 app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFileInterface.php diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php similarity index 91% rename from app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFile.php rename to app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index 0e11487ecfa73..5bfc2fd54ed2a 100644 --- a/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -5,7 +5,7 @@ */ declare(strict_types=1); -namespace Magento\MediaGallerySynchronizationApi\Model; +namespace Magento\MediaGallerySynchronization\Model; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; @@ -15,12 +15,12 @@ use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; use Magento\MediaGallerySynchronization\Model\Filesystem\GetFileInfo; -use Magento\MediaGallerySynchronization\Model\GetContentHash; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface; /** * Create media asset object based on the file information */ -class CreateAssetFromFile +class CreateAssetFromFile implements CreateAssetFromFileInterface { /** * @var Filesystem @@ -69,11 +69,7 @@ public function __construct( } /** - * Create and format media asset object - * - * @param string $path - * @return AssetInterface - * @throws FileSystemException + * @inheridoc */ public function execute(string $path): AssetInterface { diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php index ef23e09dfa1fa..be672666786dd 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php @@ -12,7 +12,7 @@ use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; -use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFile; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface; /** * Create media asset object based on the file information @@ -30,19 +30,19 @@ class GetAssetFromPath private $assetFactory; /** - * @var CreateAssetFromFile + * @var CreateAssetFromFileInterface */ private $createAssetFromFile; /** * @param AssetInterfaceFactory $assetFactory * @param GetAssetsByPathsInterface $getMediaGalleryAssetByPath - * @param CreateAssetFromFile $createAssetFromFile + * @param CreateAssetFromFileInterface $createAssetFromFile */ public function __construct( AssetInterfaceFactory $assetFactory, GetAssetsByPathsInterface $getMediaGalleryAssetByPath, - CreateAssetFromFile $createAssetFromFile + CreateAssetFromFileInterface $createAssetFromFile ) { $this->assetFactory = $assetFactory; $this->getAssetsByPaths = $getMediaGalleryAssetByPath; diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml index 4b9ffcbe63c76..82bd1303eda74 100644 --- a/app/code/Magento/MediaGallerySynchronization/etc/di.xml +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -9,6 +9,7 @@ <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"/> + <preference for="Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface" type="Magento\MediaGallerySynchronization\Model\CreateAssetFromFile"/> <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> <arguments> <argument name="importers" xsi:type="array"> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFileInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFileInterface.php new file mode 100644 index 0000000000000..667c2e68a27d8 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/CreateAssetFromFileInterface.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; + +use Magento\Framework\Exception\FileSystemException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Create media asset object from the media file + */ +interface CreateAssetFromFileInterface +{ + /** + * Create media asset object from the media file + * + * @param string $path + * @return AssetInterface + * @throws FileSystemException + */ + public function execute(string $path): AssetInterface; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/composer.json b/app/code/Magento/MediaGallerySynchronizationApi/composer.json index 427bd2bd4aca7..19bab75dd5f42 100644 --- a/app/code/Magento/MediaGallerySynchronizationApi/composer.json +++ b/app/code/Magento/MediaGallerySynchronizationApi/composer.json @@ -3,7 +3,8 @@ "description": "Magento module responsible for the media gallery synchronization implementation API", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-media-gallery-api": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php b/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php index 1f67a871b57ae..59604c0b3e501 100644 --- a/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/Plugin/CreateAssetFromFileMetadata.php @@ -12,7 +12,7 @@ use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; -use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFile; +use Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface; /** * Add metadata to the asset created from file @@ -52,12 +52,12 @@ public function __construct( /** * Add metadata to the asset * - * @param CreateAssetFromFile $createAssetFromFile + * @param CreateAssetFromFileInterface $subject * @param AssetInterface $asset * @return AssetInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterExecute(CreateAssetFromFile $createAssetFromFile, AssetInterface $asset): AssetInterface + public function afterExecute(CreateAssetFromFileInterface $subject, AssetInterface $asset): AssetInterface { $metadata = $this->extractMetadata->execute( $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($asset->getPath()) diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml b/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml index c82350f617b5b..ed66fd08cabfc 100644 --- a/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/etc/di.xml @@ -13,7 +13,7 @@ </argument> </arguments> </type> - <type name="Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFile"> + <type name="Magento\MediaGallerySynchronizationApi\Model\CreateAssetFromFileInterface"> <plugin name="addMetadataToAssetCreatedFromFile" type="Magento\MediaGallerySynchronizationMetadata\Plugin\CreateAssetFromFileMetadata"/> </type> </config> From 08f1648d3eadfe046fa9bfaebb28f1c9da0c59f0 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 27 Aug 2020 17:48:12 +0100 Subject: [PATCH 0386/1013] Fixed typo --- .../MediaGallerySynchronization/Model/CreateAssetFromFile.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index 5bfc2fd54ed2a..b4c360c3e0538 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -69,7 +69,7 @@ public function __construct( } /** - * @inheridoc + * @inheritdoc */ public function execute(string $path): AssetInterface { From 9ce2ba54138a98edd7e7e59362116adafce9432d Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Thu, 27 Aug 2020 19:56:06 +0800 Subject: [PATCH 0387/1013] magento/adobe-stock-integration#1783: Change the position of the main action buttons on View Details - change button order --- .../view/adminhtml/templates/image_details.phtml | 12 ++++++------ .../templates/image_details_standalone.phtml | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml index ba2033478afa1..a6da20a255192 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -74,12 +74,6 @@ use Magento\Framework\Escaper; "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", @@ -92,6 +86,12 @@ use Magento\Framework\Escaper; "name": "delete", "classes": "action-default scalable delete action-quaternary" }, + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" + }, { "title": "<?= $escaper->escapeJs(__('Add Image')); ?>", "handler": "addImage", 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 index 9fc0e749ac888..288a2eaee5221 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -72,12 +72,6 @@ use Magento\Backend\Block\Template; "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", @@ -89,6 +83,12 @@ use Magento\Backend\Block\Template; "handler": "deleteImageAction", "name": "delete", "classes": "action-default scalable delete action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" } ] } From c561c96d31880b74afc0e5d3ab3fea3811edcefb Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 27 Aug 2020 19:07:10 +0300 Subject: [PATCH 0388/1013] Fix filter placeholders for ui-select filter (frontend implementation) --- .../Adminhtml/Product/GetSelected.php | 82 +++++++------ .../web/js/components/product-ui-select.js | 3 + .../Listing/Filters/UsedInProducts.php | 115 ------------------ .../media_gallery_category_listing.xml | 3 +- .../ui_component/media_gallery_listing.xml | 3 +- .../standalone_media_gallery_listing.xml | 3 +- .../Adminhtml/Block/GetSelected.php | 83 +++++++++++++ .../Controller/Adminhtml/Page/GetSelected.php | 83 +++++++++++++ .../Listing/Filters/UsedInBlocks.php | 115 ------------------ .../Component/Listing/Filters/UsedInPages.php | 114 ----------------- .../ui_component/media_gallery_listing.xml | 6 +- .../standalone_media_gallery_listing.xml | 8 +- .../Adminhtml/Asset/GetSelected.php | 99 +++++++++++++++ .../Ui/Component/Listing/Filters/Asset.php | 112 +---------------- .../ui_component/cms_block_listing.xml | 3 +- .../ui_component/cms_page_listing.xml | 3 +- .../ui_component/product_listing.xml | 3 +- 17 files changed, 333 insertions(+), 505 deletions(-) delete mode 100644 app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php create mode 100644 app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php delete mode 100644 app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php delete mode 100644 app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php create mode 100644 app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php index 841715180a229..c6e39b048c40b 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php @@ -8,67 +8,77 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; -use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Backend\App\Action\Context; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Backend\App\Action; -/** Returns information about selected product by product id. Returns empty array if product don't exist */ -class GetSelected extends \Magento\Backend\App\Action +/** + * Returns selected product by product id. for ui-select filter + */ +class GetSelected extends Action implements HttpGetActionInterface { /** - * Authorization level of a basic admin session - * * @see _isAllowed() */ const ADMIN_RESOURCE = 'Magento_Catalog::products'; /** - * @var \Magento\Framework\Controller\Result\JsonFactory + * @var JsonFactory */ private $resultJsonFactory; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory + * @var ProductRepositoryInterface */ - private $productCollectionFactory; + private $productRepository; /** - * Search constructor. - * @param \Magento\Framework\Controller\Result\JsonFactory $jsonFactory - * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory - * @param \Magento\Backend\App\Action\Context $context + * GetSelected constructor. + * + * @param JsonFactory $jsonFactory + * @param ProductRepositoryInterface $productRepository + * @param Context $context */ public function __construct( - \Magento\Framework\Controller\Result\JsonFactory $jsonFactory, - \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, - \Magento\Backend\App\Action\Context $context + JsonFactory $jsonFactory, + ProductRepositoryInterface $productRepository, + Context $context ) { $this->resultJsonFactory = $jsonFactory; - $this->productCollectionFactory = $productCollectionFactory; + $this->productRepository = $productRepository; parent::__construct($context); } /** - * @return \Magento\Framework\Controller\ResultInterface + * + * @return ResultInterface */ - public function execute() : \Magento\Framework\Controller\ResultInterface + public function execute() : ResultInterface { - $productId = $this->getRequest()->getParam('productId'); - /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ - $productCollection = $this->productCollectionFactory->create(); - $productCollection->addAttributeToSelect(ProductInterface::NAME); - $productCollection->addIdFilter($productId); - $option = []; - /** @var ProductInterface $product */ - if (!empty($productCollection->getFirstItem()->getData())) { - $product = $productCollection->getFirstItem(); - $option = [ - 'value' => $productId, - 'label' => $product->getName(), - 'is_active' => $product->getStatus(), - 'path' => $product->getSku(), - ]; + $productIds = $this->getRequest()->getParam('ids'); + $options = []; + + + if (!is_array($productIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); } - /** @var \Magento\Framework\Controller\Result\Json $resultJson */ - $resultJson = $this->resultJsonFactory->create(); - return $resultJson->setData($option); + foreach ($productIds as $id) { + try { + $product = $this->productRepository->getById($id); + $options[] = [ + 'value' => $product->getId(), + 'label' => $product->getName(), + 'is_active' => $product->getSatus(), + 'path' => $product->getSku() + ]; + } catch (\Exception $e) { + continue; + } + } + + return $this->resultJsonFactory->create()->setData($options); } } diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js index fb7ea7a5bcd69..c04daf07db3dd 100644 --- a/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/product-ui-select.js @@ -3,6 +3,9 @@ * See COPYING.txt for license details. */ +/** + * @deprecated see Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js + */ define([ 'Magento_Ui/js/form/element/ui-select', 'jquery', diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php deleted file mode 100644 index d86617e12b8f8..0000000000000 --- a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php +++ /dev/null @@ -1,115 +0,0 @@ -<?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 = []; - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - if ($bookmark === null) { - parent::prepare(); - return; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $productIds = $applied[$this->getName()]; - } - - foreach ($productIds as $id) { - try { - $product = $this->productRepository->getById($id); - $options[] = [ - 'value' => $id, - 'label' => $product->getName(), - 'is_active' => $product->getStatus(), - 'path' => $product->getSku(), - 'optgroup' => false - ]; - } catch (\Exception $e) { - continue; - } - } - $this->optionsProvider = $options; - parent::prepare(); - } -} 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 index e12d90b95303b..17fe33e5b2bf5 100644 --- 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 @@ -58,7 +58,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -74,6 +74,7 @@ <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="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> 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 index 2ca58b6020fa7..a5491e3450451 100644 --- 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 @@ -13,8 +13,7 @@ name="product_id" provider="${ $.parentName }" sortOrder="110" - class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters\UsedInProducts" - component="Magento_Catalog/js/components/product-ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> 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 index 2ca58b6020fa7..a5491e3450451 100644 --- 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 @@ -13,8 +13,7 @@ name="product_id" provider="${ $.parentName }" sortOrder="110" - class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters\UsedInProducts" - component="Magento_Catalog/js/components/product-ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php new file mode 100644 index 0000000000000..a8beeb791f74a --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php @@ -0,0 +1,83 @@ +<?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\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to get selected block for ui-select component + */ +class GetSelected 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; + + /** + * @param JsonFactory $resultFactory + * @param BlockRepositoryInterface $blockRepository + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + BlockRepositoryInterface $blockRepository, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->blockRepository = $blockRepository; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $options = []; + $blockIds = $this->getRequest()->getParam('ids'); + + if (!is_array($blockIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + foreach ($blockIds as $id) { + try { + $block = $this->blockRepository->getById($id); + $options[] = [ + 'value' => $block->getId(), + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } catch (\Exception $e) { + continue; + } + } + + return $this->resultJsonFactory->create()->setData($options); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php new file mode 100644 index 0000000000000..33f87b93c01e1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php @@ -0,0 +1,83 @@ +<?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\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to get selected page for ui-select component + */ +class GetSelected 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; + + /** + * @param JsonFactory $resultFactory + * @param PageRepositoryInterface $pageRepository + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + PageRepositoryInterface $pageRepository, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->pageRepository = $pageRepository; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $options = []; + $pageIds = $this->getRequest()->getParam('ids'); + + if (!is_array($pageIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + foreach ($pageIds as $id) { + try { + $page = $this->pageRepository->getById($id); + $options[] = [ + 'value' => $page->getId(), + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } catch (\Exception $e) { + continue; + } + } + + return $this->resultJsonFactory->create()->setData($options); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php deleted file mode 100644 index 66f8caa71d70a..0000000000000 --- a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php +++ /dev/null @@ -1,115 +0,0 @@ -<?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 = []; - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - if ($bookmark === null) { - parent::prepare(); - return; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $blockIds = $applied[$this->getName()]; - } - - foreach ($blockIds as $id) { - try { - $block = $this->blockRepository->getById($id); - $options[] = [ - 'value' => $id, - 'label' => $block->getTitle(), - 'is_active' => $block->isActive(), - 'optgroup' => false - ]; - } catch (\Exception $e) { - continue; - } - } - - $this->optionsProvider = $options; - 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 deleted file mode 100644 index 78ab1b63d32d1..0000000000000 --- a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php +++ /dev/null @@ -1,114 +0,0 @@ -<?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 = []; - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - if ($bookmark === null) { - parent::prepare(); - return; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $pageIds = $applied[$this->getName()]; - } - - foreach ($pageIds as $id) { - try { - $page = $this->pageRepository->getById($id); - $options[] = [ - 'value' => $id, - 'label' => $page->getTitle(), - 'is_active' => $page->isActive(), - 'optgroup' => false - ]; - } catch (\Exception $e) { - continue; - } - } - $this->optionsProvider = $options; - parent::prepare(); - } -} 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 index 506a6cad5b68e..402866852711d 100644 --- 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 @@ -14,7 +14,7 @@ provider="${ $.parentName }" sortOrder="120" class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInPages" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -27,6 +27,7 @@ <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 name="validationUrl" xsi:type="url" path="media_gallery_cms/page/getSelected"/> </item> </argument> <settings> @@ -39,7 +40,7 @@ provider="${ $.parentName }" sortOrder="130" class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInBlocks" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -52,6 +53,7 @@ <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 name="validationUrl" xsi:type="url" path="media_gallery_cms/block/getSelected"/> </item> </argument> <settings> 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 index 506a6cad5b68e..e49ba7a98c8ce 100644 --- 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 @@ -13,8 +13,7 @@ name="page_id" provider="${ $.parentName }" sortOrder="120" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInPages" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -27,6 +26,7 @@ <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 name="validationUrl" xsi:type="url" path="media_gallery_cms/page/getSelected"/> </item> </argument> <settings> @@ -38,8 +38,7 @@ name="block_id" provider="${ $.parentName }" sortOrder="130" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInBlocks" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -52,6 +51,7 @@ <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 name="validationUrl" xsi:type="url" path="media_gallery_cms/block/getSelected"/> </item> </argument> <settings> diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php new file mode 100644 index 0000000000000..725c67185ce77 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Asset; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; + +/** + * Controller to get selected asset for ui-select component + */ +class GetSelected extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetById; + + /** + * @var Images + */ + private $images; + + /** + * @var Storage + */ + private $storage; + + /** + * @param JsonFactory $resultFactory + * @param GetAssetsByIdsInterface $getAssetById + * @param Context $context + * @param Images $images + * @param Storage $storage + * + */ + public function __construct( + JsonFactory $resultFactory, + GetAssetsByIdsInterface $getAssetById, + Context $context, + Images $images, + Storage $storage + ) { + $this->resultJsonFactory = $resultFactory; + $this->getAssetById = $getAssetById; + $this->images = $images; + $this->storage = $storage; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $options = []; + $assetIds = $this->getRequest()->getParam('ids'); + + if (!is_array($assetIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + $assets = $this->getAssetById->execute($assetIds); + + foreach ($assets as $asset) { + $assetPath = $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()); + $options[] = [ + 'value' => (string) $asset->getId(), + 'label' => $asset->getTitle(), + 'src' => $assetPath + ]; + } + + return $this->resultJsonFactory->create()->setData($options); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php index e8dc232584adb..190a99cac55b6 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -16,9 +16,6 @@ use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Select; use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; -use Magento\Cms\Helper\Wysiwyg\Images; -use Magento\Cms\Model\Wysiwyg\Images\Storage; -use Magento\Ui\Api\BookmarkManagementInterface; /** * Asset filter @@ -35,21 +32,6 @@ class Asset extends Select */ private $getAssetsByIds; - /** - * @var Images - */ - private $images; - - /** - * @var Storage - */ - private $storage; - - /** - * @var BookmarkManagementInterface - */ - private $bookmarkManagement; - /** * Constructor * @@ -58,11 +40,6 @@ class Asset extends Select * @param FilterBuilder $filterBuilder * @param FilterModifier $filterModifier * @param OptionSourceInterface $optionsProvider - * @param GetContentByAssetIdsInterface $getContentIdentities - * @param GetAssetsByIdsInterface $getAssetsByIds - * @param BookmarkManagementInterface $bookmarkManagement - * @param Images $images - * @param Storage $storage * @param array $components * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -74,10 +51,6 @@ public function __construct( FilterModifier $filterModifier, OptionSourceInterface $optionsProvider = null, GetContentByAssetIdsInterface $getContentIdentities, - GetAssetsByIdsInterface $getAssetsByIds, - BookmarkManagementInterface $bookmarkManagement, - Images $images, - Storage $storage, array $components = [], array $data = [] ) { @@ -93,89 +66,6 @@ public function __construct( $data ); $this->getContentIdentities = $getContentIdentities; - $this->getAssetsByIds = $getAssetsByIds; - $this->bookmarkManagement = $bookmarkManagement; - $this->images = $images; - $this->storage = $storage; - } - - /** - * Prepare component configuration - * - * @return void - */ - public function prepare() - { - $options = []; - $assetIds = $this->getAssetIds(); - - if (empty($assetIds)) { - parent::prepare(); - return; - } - - $assets = $this->getAssetsByIds->execute($assetIds); - - foreach ($assets as $asset) { - $assetPath = $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()); - $options[] = [ - 'value' => (string) $asset->getId(), - 'label' => $asset->getTitle(), - 'src' => $assetPath - ]; - } - - $this->optionsProvider = $options; - parent::prepare(); - } - - /** - * Get asset ids from filterData or from bookmarks - */ - private function getAssetIds(): array - { - $assetIds = []; - - if (isset($this->filterData[$this->getName()])) { - $assetIds = $this->filterData[$this->getName()]; - - if (!is_array($assetIds)) { - $assetIds = $this->stringToArray($assetIds); - } - - return $assetIds; - } - - $bookmark = $this->bookmarkManagement->getByIdentifierNamespace( - 'current', - $this->context->getNameSpace() - ); - - if ($bookmark === null) { - return $assetIds; - } - - $applied = $bookmark->getConfig()['current']['filters']['applied']; - - if (isset($applied[$this->getName()])) { - $assetIds = $applied[$this->getName()]; - } - - if (!is_array($assetIds)) { - $assetIds = $this->stringToArray($assetIds); - } - - return $assetIds; - } - - /** - * Converts string array from url-applier to array - * - * @param string $string - */ - private function stringToArray(string $string): array - { - return explode(',', str_replace(['[', ']'], '', $string)); } /** @@ -191,7 +81,7 @@ public function applyFilter() $assetIds = $this->filterData[$this->getName()]; if (!is_array($assetIds)) { - $assetIds = $this->stringToArray($assetIds); + $assetIds = explode(',', str_replace(['[', ']'], '', $assetIds)); } $filter = $this->filterBuilder->setConditionType('in') 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 index 86c8590bb4860..20988fad5ff35 100644 --- 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 @@ -13,7 +13,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -24,6 +24,7 @@ <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="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> 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 index 58881a8c9de6c..4abeb71679bde 100644 --- 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 @@ -13,7 +13,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -24,6 +24,7 @@ <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="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> 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 index 2b7d9fde3b9ff..4634652e9cc19 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml @@ -13,7 +13,7 @@ provider="${ $.parentName }" sortOrder="10" class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" - component="Magento_Ui/js/form/element/ui-select" + component="Magento_Ui/js/grid/filters/elements/ui-select" template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> @@ -24,6 +24,7 @@ <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="validationUrl" xsi:type="url" path="media_gallery/asset/getSelected"/> <item name="levelsVisibility" xsi:type="number">1</item> </item> </argument> From 301ec2ee51dd277cb74abe8a95f4567435597c76 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 27 Aug 2020 19:12:51 +0300 Subject: [PATCH 0389/1013] Introduce ui-select filter with validation init values --- .../web/js/grid/filters/elements/ui-select.js | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js new file mode 100644 index 0000000000000..c45ec348103c1 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js @@ -0,0 +1,82 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/ui-select', + 'jquery', + 'underscore' +], function (Select, $, _) { + 'use strict'; + + return Select.extend({ + defaults: { + bookmarkProvider: 'ns = ${ $.ns }, index = bookmarks', + filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', + validationUrl: false, + loadedOption: [], + validationLoading: true, + imports: { + activeIndex: '${ $.bookmarkProvider }:activeIndex' + }, + modules: { + filterChips: '${ $.filterChipsProvider }' + }, + listens: { + activeIndex: 'validateInitialValue' + } + + }, + + /** @inheritdoc */ + initialize: function () { + this._super(); + + this.validateInitialValue(); + + return this; + }, + + /** + * Validate initial value actually exists + */ + validateInitialValue: function () { + if (!_.isEmpty(this.value())) { + $.ajax({ + url: this.validationUrl, + type: 'GET', + dataType: 'json', + context: this, + data: { + ids: this.value() + }, + + /** @param {Object} response */ + success: function (response) { + if (!_.isEmpty(response)) { + this.options([]); + this.success({ + options: response + }); + } + this.filterChips().updateActive(); + }, + + /** set empty array if error occurs */ + error: function () { + this.options([]); + }, + + /** stop loader */ + complete: function () { + this.validationLoading(false); + this.setCaption(); + } + }); + } else { + this.validationLoading(false); + } + } + }); +}); From 4850009a4c7f7ce8e2277366e85d859c78bc62a3 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 27 Aug 2020 22:03:46 +0300 Subject: [PATCH 0390/1013] Codereview suggestions, static test fixes --- .../Adminhtml/Product/GetSelected.php | 82 ++++++++---------- .../Adminhtml/Product/GetSelected.php | 84 +++++++++++++++++++ .../ui_component/media_gallery_listing.xml | 2 +- .../standalone_media_gallery_listing.xml | 2 +- .../ui_component/media_gallery_listing.xml | 2 - .../Ui/Component/Listing/Filters/Asset.php | 7 +- .../web/js/grid/filters/elements/ui-select.js | 65 +++++++------- 7 files changed, 157 insertions(+), 87 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php index c6e39b048c40b..841715180a229 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/GetSelected.php @@ -8,77 +8,67 @@ namespace Magento\Catalog\Controller\Adminhtml\Product; -use Magento\Framework\Controller\ResultInterface; -use Magento\Backend\App\Action\Context; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\Controller\Result\JsonFactory; -use Magento\Framework\App\Action\HttpGetActionInterface; -use Magento\Backend\App\Action; +use Magento\Catalog\Api\Data\ProductInterface; -/** - * Returns selected product by product id. for ui-select filter - */ -class GetSelected extends Action implements HttpGetActionInterface +/** Returns information about selected product by product id. Returns empty array if product don't exist */ +class GetSelected extends \Magento\Backend\App\Action { /** + * Authorization level of a basic admin session + * * @see _isAllowed() */ const ADMIN_RESOURCE = 'Magento_Catalog::products'; /** - * @var JsonFactory + * @var \Magento\Framework\Controller\Result\JsonFactory */ private $resultJsonFactory; /** - * @var ProductRepositoryInterface + * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */ - private $productRepository; + private $productCollectionFactory; /** - * GetSelected constructor. - * - * @param JsonFactory $jsonFactory - * @param ProductRepositoryInterface $productRepository - * @param Context $context + * Search constructor. + * @param \Magento\Framework\Controller\Result\JsonFactory $jsonFactory + * @param \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory + * @param \Magento\Backend\App\Action\Context $context */ public function __construct( - JsonFactory $jsonFactory, - ProductRepositoryInterface $productRepository, - Context $context + \Magento\Framework\Controller\Result\JsonFactory $jsonFactory, + \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory, + \Magento\Backend\App\Action\Context $context ) { $this->resultJsonFactory = $jsonFactory; - $this->productRepository = $productRepository; + $this->productCollectionFactory = $productCollectionFactory; parent::__construct($context); } /** - * - * @return ResultInterface + * @return \Magento\Framework\Controller\ResultInterface */ - public function execute() : ResultInterface + public function execute() : \Magento\Framework\Controller\ResultInterface { - $productIds = $this->getRequest()->getParam('ids'); - $options = []; - - - if (!is_array($productIds)) { - return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + $productId = $this->getRequest()->getParam('productId'); + /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection */ + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addAttributeToSelect(ProductInterface::NAME); + $productCollection->addIdFilter($productId); + $option = []; + /** @var ProductInterface $product */ + if (!empty($productCollection->getFirstItem()->getData())) { + $product = $productCollection->getFirstItem(); + $option = [ + 'value' => $productId, + 'label' => $product->getName(), + 'is_active' => $product->getStatus(), + 'path' => $product->getSku(), + ]; } - foreach ($productIds as $id) { - try { - $product = $this->productRepository->getById($id); - $options[] = [ - 'value' => $product->getId(), - 'label' => $product->getName(), - 'is_active' => $product->getSatus(), - 'path' => $product->getSku() - ]; - } catch (\Exception $e) { - continue; - } - } - - return $this->resultJsonFactory->create()->setData($options); + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setData($option); } } diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php new file mode 100644 index 0000000000000..3fb3234482efd --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogUi\Controller\Adminhtml\Product; + +use Magento\Framework\Controller\ResultInterface; +use Magento\Backend\App\Action\Context; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Backend\App\Action; + +/** + * Returns selected product by product id. for ui-select filter + */ +class GetSelected extends Action implements HttpGetActionInterface +{ + /** + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Catalog::products'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * GetSelected constructor. + * + * @param JsonFactory $jsonFactory + * @param ProductRepositoryInterface $productRepository + * @param Context $context + */ + public function __construct( + JsonFactory $jsonFactory, + ProductRepositoryInterface $productRepository, + Context $context + ) { + $this->resultJsonFactory = $jsonFactory; + $this->productRepository = $productRepository; + parent::__construct($context); + } + + /** + * + * @return ResultInterface + */ + public function execute() : ResultInterface + { + $productIds = $this->getRequest()->getParam('ids'); + $options = []; + + + if (!is_array($productIds)) { + return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); + } + foreach ($productIds as $id) { + try { + $product = $this->productRepository->getById($id); + $options[] = [ + 'value' => $product->getId(), + 'label' => $product->getName(), + 'is_active' => $product->getSatus(), + 'path' => $product->getSku() + ]; + } catch (\Exception $e) { + continue; + } + } + + return $this->resultJsonFactory->create()->setData($options); + } +} 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 index a5491e3450451..6976584c2e36c 100644 --- 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 @@ -29,7 +29,7 @@ <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 name="validationUrl" xsi:type="url" path="media_gallery_catalog/product/getSelected"/> </item> </argument> <settings> 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 index a5491e3450451..6976584c2e36c 100644 --- 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 @@ -29,7 +29,7 @@ <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 name="validationUrl" xsi:type="url" path="media_gallery_catalog/product/getSelected"/> </item> </argument> <settings> 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 index 402866852711d..e49ba7a98c8ce 100644 --- 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 @@ -13,7 +13,6 @@ name="page_id" provider="${ $.parentName }" sortOrder="120" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInPages" component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> @@ -39,7 +38,6 @@ name="block_id" provider="${ $.parentName }" sortOrder="130" - class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInBlocks" component="Magento_Ui/js/grid/filters/elements/ui-select" template="ui/grid/filters/elements/ui-select"> <argument name="data" xsi:type="array"> diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php index 190a99cac55b6..f61e34512bfe3 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -15,7 +15,6 @@ use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; use Magento\Ui\Component\Filters\FilterModifier; use Magento\Ui\Component\Filters\Type\Select; -use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; /** * Asset filter @@ -27,11 +26,6 @@ class Asset extends Select */ private $getContentIdentities; - /** - * @var GetAssetsByIdsInterface - */ - private $getAssetsByIds; - /** * Constructor * @@ -40,6 +34,7 @@ class Asset extends Select * @param FilterBuilder $filterBuilder * @param FilterModifier $filterModifier * @param OptionSourceInterface $optionsProvider + * @param GetContentByAssetIdsInterface $getContentIdentities * @param array $components * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js index c45ec348103c1..5e46360b59e7d 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js @@ -42,41 +42,44 @@ define([ * Validate initial value actually exists */ validateInitialValue: function () { - if (!_.isEmpty(this.value())) { - $.ajax({ - url: this.validationUrl, - type: 'GET', - dataType: 'json', - context: this, - data: { - ids: this.value() - }, + if (_.isEmpty(this.value())) { + this.validationLoading(false); - /** @param {Object} response */ - success: function (response) { - if (!_.isEmpty(response)) { - this.options([]); - this.success({ - options: response - }); - } - this.filterChips().updateActive(); - }, + return; + } - /** set empty array if error occurs */ - error: function () { - this.options([]); - }, + $.ajax({ + url: this.validationUrl, + type: 'GET', + dataType: 'json', + context: this, + data: { + ids: this.value() + }, - /** stop loader */ - complete: function () { - this.validationLoading(false); - this.setCaption(); + /** @param {Object} response */ + success: function (response) { + if (!_.isEmpty(response)) { + this.options([]); + this.success({ + options: response + }); } - }); - } else { - this.validationLoading(false); - } + this.filterChips().updateActive(); + }, + + /** set empty array if error occurs */ + error: function () { + this.options([]); + }, + + /** stop loader */ + complete: function () { + this.validationLoading(false); + this.setCaption(); + } + }); + } }); }); From a44d3affa9768f0b6d10138819f23ca12c166813 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 27 Aug 2020 23:14:51 +0300 Subject: [PATCH 0391/1013] Fix static tests --- .../Controller/Adminhtml/Product/GetSelected.php | 2 +- .../Controller/Adminhtml/Block/GetSelected.php | 2 +- .../Controller/Adminhtml/Page/GetSelected.php | 2 +- app/code/Magento/MediaGalleryCmsUi/composer.json | 3 +-- .../Controller/Adminhtml/Asset/GetSelected.php | 2 +- .../Ui/view/base/web/js/grid/filters/elements/ui-select.js | 6 +++++- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php index 3fb3234482efd..f70d4584547a3 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Product/GetSelected.php @@ -53,6 +53,7 @@ public function __construct( } /** + * Return selected products options * * @return ResultInterface */ @@ -61,7 +62,6 @@ public function execute() : ResultInterface $productIds = $this->getRequest()->getParam('ids'); $options = []; - if (!is_array($productIds)) { return $this->resultJsonFactory->create()->setData('parameter ids must be type of array'); } diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php index a8beeb791f74a..a686f0e7b3ace 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/GetSelected.php @@ -52,7 +52,7 @@ public function __construct( } /** - * Execute pages search. + * Return selected blocks options. * * @return ResultInterface */ diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php index 33f87b93c01e1..be6eb9fd9de9f 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/GetSelected.php @@ -52,7 +52,7 @@ public function __construct( } /** - * Execute pages search. + * Return selected pages options. * * @return ResultInterface */ diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json index 73747a669c051..1ecfb9a3c8855 100644 --- a/app/code/Magento/MediaGalleryCmsUi/composer.json +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -5,8 +5,7 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-cms": "*", - "magento/module-backend": "*", - "magento/module-ui": "*" + "magento/module-backend": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php index 725c67185ce77..09837c301c367 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/GetSelected.php @@ -71,7 +71,7 @@ public function __construct( } /** - * Execute pages search. + * Return selected asset options. * * @return ResultInterface */ diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js index 5e46360b59e7d..9f14639194fa7 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js @@ -29,7 +29,11 @@ define([ }, - /** @inheritdoc */ + /** + * Initializes UiSelect component. + * + * @returns {UiSelect} Chainable. + */ initialize: function () { this._super(); From a63ba82db187db80a92262dfbb0ea1a49d4705cc Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Thu, 27 Aug 2020 23:15:36 +0300 Subject: [PATCH 0392/1013] remove empty line --- .../Ui/view/base/web/js/grid/filters/elements/ui-select.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js index 9f14639194fa7..a913f3fa4a042 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js @@ -83,7 +83,6 @@ define([ this.setCaption(); } }); - } }); }); From 9775a1dbbe19203662de3e22be99845cced5f2cc Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Sat, 29 Aug 2020 17:03:03 +0100 Subject: [PATCH 0393/1013] Updated composer lock --- composer.lock | 6 ------ 1 file changed, 6 deletions(-) diff --git a/composer.lock b/composer.lock index 36a42d0c750df..e1ce66005223d 100644 --- a/composer.lock +++ b/composer.lock @@ -8120,12 +8120,6 @@ "sftp", "storage" ], - "funding": [ - { - "url": "https://offset.earth/frankdejonge", - "type": "other" - } - ], "time": "2020-05-18T15:13:39+00:00" }, { From 98fe5093b90cfa6b4b44bb04db9d9b014901758a Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Sun, 30 Aug 2020 14:12:10 +0100 Subject: [PATCH 0394/1013] Corrected MFTF version in composer.lock --- composer.lock | 560 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 555 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index 9f079570cc2ac..551167152be4d 100644 --- a/composer.lock +++ b/composer.lock @@ -7315,6 +7315,555 @@ ], "time": "2020-06-27T23:57:46+00:00" }, + { + "name": "hoa/consistency", + "version": "1.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Consistency.git", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Consistency/zipball/fd7d0adc82410507f332516faf655b6ed22e4c2f", + "reference": "fd7d0adc82410507f332516faf655b6ed22e4c2f", + "shasum": "" + }, + "require": { + "hoa/exception": "~1.0", + "php": ">=5.5.0" + }, + "require-dev": { + "hoa/stream": "~1.0", + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Consistency\\": "." + }, + "files": [ + "Prelude.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Consistency library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autoloader", + "callable", + "consistency", + "entity", + "flex", + "keyword", + "library" + ], + "time": "2017-05-02T12:18:12+00:00" + }, + { + "name": "hoa/console", + "version": "3.17.05.02", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Console.git", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Console/zipball/e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "reference": "e231fd3ea70e6d773576ae78de0bdc1daf331a66", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/file": "~1.0", + "hoa/protocol": "~1.0", + "hoa/stream": "~1.0", + "hoa/ustring": "~4.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-pcntl": "To enable hoa://Event/Console/Window:resize.", + "hoa/dispatcher": "To use the console kit.", + "hoa/router": "To use the console kit." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Console\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Console library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "autocompletion", + "chrome", + "cli", + "console", + "cursor", + "getoption", + "library", + "option", + "parser", + "processus", + "readline", + "terminfo", + "tput", + "window" + ], + "time": "2017-05-02T12:26:19+00:00" + }, + { + "name": "hoa/event", + "version": "1.17.01.13", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Event.git", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Event/zipball/6c0060dced212ffa3af0e34bb46624f990b29c54", + "reference": "6c0060dced212ffa3af0e34bb46624f990b29c54", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Event\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Event library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "event", + "library", + "listener", + "observer" + ], + "time": "2017-01-13T15:30:50+00:00" + }, + { + "name": "hoa/exception", + "version": "1.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Exception.git", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Exception/zipball/091727d46420a3d7468ef0595651488bfc3a458f", + "reference": "091727d46420a3d7468ef0595651488bfc3a458f", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Exception\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Exception library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "exception", + "library" + ], + "time": "2017-01-16T07:53:27+00:00" + }, + { + "name": "hoa/file", + "version": "1.17.07.11", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/File.git", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/File/zipball/35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "reference": "35cb979b779bc54918d2f9a4e02ed6c7a1fa67ca", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/iterator": "~2.0", + "hoa/stream": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\File\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\File library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Socket", + "directory", + "file", + "finder", + "library", + "link", + "temporary" + ], + "time": "2017-07-11T07:42:15+00:00" + }, + { + "name": "hoa/iterator", + "version": "2.17.01.10", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Iterator.git", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Iterator/zipball/d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "reference": "d1120ba09cb4ccd049c86d10058ab94af245f0cc", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Iterator\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Iterator library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "iterator", + "library" + ], + "time": "2017-01-10T10:34:47+00:00" + }, + { + "name": "hoa/protocol", + "version": "1.17.01.14", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Protocol.git", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Protocol/zipball/5c2cf972151c45f373230da170ea015deecf19e2", + "reference": "5c2cf972151c45f373230da170ea015deecf19e2", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Protocol\\": "." + }, + "files": [ + "Wrapper.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Protocol library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "protocol", + "resource", + "stream", + "wrapper" + ], + "time": "2017-01-14T12:26:10+00:00" + }, + { + "name": "hoa/stream", + "version": "1.17.02.21", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Stream.git", + "reference": "3293cfffca2de10525df51436adf88a559151d82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Stream/zipball/3293cfffca2de10525df51436adf88a559151d82", + "reference": "3293cfffca2de10525df51436adf88a559151d82", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/event": "~1.0", + "hoa/exception": "~1.0", + "hoa/protocol": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Stream\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Stream library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "Context", + "bucket", + "composite", + "filter", + "in", + "library", + "out", + "protocol", + "stream", + "wrapper" + ], + "time": "2017-02-21T16:01:06+00:00" + }, + { + "name": "hoa/ustring", + "version": "4.17.01.16", + "source": { + "type": "git", + "url": "https://github.com/hoaproject/Ustring.git", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hoaproject/Ustring/zipball/e6326e2739178799b1fe3fdd92029f9517fa17a0", + "reference": "e6326e2739178799b1fe3fdd92029f9517fa17a0", + "shasum": "" + }, + "require": { + "hoa/consistency": "~1.0", + "hoa/exception": "~1.0" + }, + "require-dev": { + "hoa/test": "~2.0" + }, + "suggest": { + "ext-iconv": "ext/iconv must be present (or a third implementation) to use Hoa\\Ustring::transcode().", + "ext-intl": "To get a better Hoa\\Ustring::toAscii() and Hoa\\Ustring::compareTo()." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Hoa\\Ustring\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net" + }, + { + "name": "Hoa community", + "homepage": "https://hoa-project.net/" + } + ], + "description": "The Hoa\\Ustring library.", + "homepage": "https://hoa-project.net/", + "keywords": [ + "library", + "search", + "string", + "unicode" + ], + "time": "2017-01-16T07:08:25+00:00" + }, { "name": "jms/metadata", "version": "1.7.0", @@ -7681,16 +8230,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699" + "reference": "8a106ea029f222f4354854636861273c7577bee9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8d98efa7434a30ab9e82ef128c430ef8e3a50699", - "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", + "reference": "8a106ea029f222f4354854636861273c7577bee9", "shasum": "" }, "require": { @@ -7708,6 +8257,7 @@ "ext-intl": "*", "ext-json": "*", "ext-openssl": "*", + "hoa/console": "~3.0", "monolog/monolog": "^1.17", "mustache/mustache": "~2.5", "php": "^7.3", @@ -7767,7 +8317,7 @@ "magento", "testing" ], - "time": "2020-07-09T21:26:19+00:00" + "time": "2020-08-19T19:57:27+00:00" }, { "name": "mikey179/vfsstream", From 55f06e6fd09d5a5c0988eb9d96d18e4b26798f90 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Mon, 31 Aug 2020 13:25:33 +0100 Subject: [PATCH 0395/1013] Added tests coverage and small improvements --- ...inFromOrderPageManualChooseActionGroup.xml | 30 ++++++ ...sCustomerManualChooseFromOrderPageTest.xml | 101 ++++++++++++++++++ .../Component/ConfirmationPopup/Options.php | 50 ++++++--- 3 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml create mode 100644 app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml new file mode 100644 index 0000000000000..e778ede05f9a5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.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="AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is works properly from Order page with manual Store View choose.</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + <argument name="storeName" type="string" defaultValue="default"/> + </arguments> + + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <click selector="{{AdminOrderDetailsMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Login as Customer: Select Store" stepKey="seeModal"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <selectOption selector="{{AdminLoginAsCustomerConfirmationModalSection.store}}" userInput="{{storeName}}" stepKey="selectStore"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml new file mode 100644 index 0000000000000..372e89771b1e5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerManualChooseFromOrderPageTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Select Store 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 on Order Page"/> + <description + value="Verify admin user can directly login into customer account to Custom store view when Store View To Login In = Manual Choose on Order Page"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <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="AdminCreateNewStoreGroupActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createFirstCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createSecondCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + </before> + <after> + <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> + <argument name="storeGroupName" value="customStoreGroup.name"/> + </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"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + <argument name="storeName" value="{{customStoreGroup.name}}"/> + </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"/> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup" + stepKey="loginAsCustomerFromOrderPage"> + <argument name="orderId" value="{$grabOrderId}"/> + <argument name="storeName" value="{{customStoreGroup.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="{{customStoreEN.name}}"/> + </actionGroup> + + <!-- Log out Customer and close tab --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 865d4f0bc12e2..4d9e61fffdb21 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -14,6 +14,7 @@ use Magento\Framework\App\RequestInterface; use Magento\Framework\Data\OptionSourceInterface; use Magento\Framework\Escaper; +use Magento\Framework\Exception\LocalizedException; use Magento\Sales\Api\CreditmemoRepositoryInterface; use Magento\Sales\Api\InvoiceRepositoryInterface; use Magento\Sales\Api\OrderRepositoryInterface; @@ -116,6 +117,7 @@ public function __construct( /** * @inheritdoc + * @throws LocalizedException */ public function toOptionArray(): array { @@ -213,25 +215,45 @@ private function fillStoreGroupOptions(Website $website, CustomerInterface $cust * Get Customer id from request param. * * @return int + * @throws LocalizedException */ private function getCustomerId(): int { $customerId = $this->request->getParam('id'); - if (!$customerId) { - $orderId = $this->request->getParam('order_id'); - $shipmentId = $this->request->getParam('shipment_id'); - $creditmemoId = $this->request->getParam('creditmemo_id'); - $invoiceId = $this->request->getParam('invoice_id'); - if ($invoiceId) { - $orderId = $this->invoiceRepository->get($invoiceId)->getOrderId(); - } elseif ($shipmentId) { - $orderId = $this->shipmentRepository->get($shipmentId)->getOrderId(); - } elseif ($creditmemoId) { - $orderId = $this->creditmemoRepository->get($creditmemoId)->getOrderId(); - } - $customerId = $this->orderRepository->get($orderId)->getCustomerId(); + if ($customerId) { + return (int)$customerId; + } + try { + $orderId = $this->getOrderId(); + } catch (LocalizedException $exception) { + throw new LocalizedException(__('Unable to get Customer ID.')); } - return (int)$customerId; + return (int)$this->orderRepository->get($orderId)->getCustomerId(); + } + + /** + * Get Order id from request param + * + * @return int + * @throws LocalizedException + */ + private function getOrderId(): int + { + $orderId = $this->request->getParam('order_id'); + if ($orderId) { + return (int)$orderId; + } + $shipmentId = $this->request->getParam('shipment_id'); + $creditmemoId = $this->request->getParam('creditmemo_id'); + $invoiceId = $this->request->getParam('invoice_id'); + if ($invoiceId) { + return $this->invoiceRepository->get($invoiceId)->getOrderId(); + } elseif ($shipmentId) { + return $this->shipmentRepository->get($shipmentId)->getOrderId(); + } elseif ($creditmemoId) { + return $this->creditmemoRepository->get($creditmemoId)->getOrderId(); + } + throw new LocalizedException(__('Unable to get Order ID.')); } } From dd26af242e7fffc3f2e5ad47d632480872aa628e Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Mon, 31 Aug 2020 16:31:09 +0300 Subject: [PATCH 0396/1013] MFTF test update. --- ...LoginAsCustomerLoginFromOrderPageActionGroup.xml | 13 +++++++++++-- ...merLoginFromOrderPageManualChooseActionGroup.xml | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml index a478f8e9d18cd..68eaad37ccd91 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml @@ -16,8 +16,17 @@ <argument name="orderId" type="string"/> </arguments> - <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> + <waitForPageLoad stepKey="waitForOrdersPage"/> + <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> + <waitForPageLoad stepKey="waitForClearFilters"/> + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> + <waitForPageLoad stepKey="waitForClickFilters"/> + <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> + <click selector="{{AdminOrdersGridSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> + <waitForPageLoad stepKey="waitForApplyFilters"/> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageOpened"/> <click selector="{{AdminOrderDetailsMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> <see selector="{{AdminConfirmationModalSection.title}}" userInput="You are about to Login as Customer" stepKey="seeModal"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml index e778ede05f9a5..f06c87db0e6ae 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml @@ -17,8 +17,17 @@ <argument name="storeName" type="string" defaultValue="default"/> </arguments> - <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> - <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> + <waitForPageLoad stepKey="waitForOrdersPage"/> + <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> + <waitForPageLoad stepKey="waitForClearFilters"/> + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> + <waitForPageLoad stepKey="waitForClickFilters"/> + <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> + <click selector="{{AdminOrdersGridSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> + <waitForPageLoad stepKey="waitForApplyFilters"/> + <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> + <waitForPageLoad stepKey="waitForOrderPageOpened"/> <click selector="{{AdminOrderDetailsMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> <see selector="{{AdminConfirmationModalSection.title}}" userInput="Login as Customer: Select Store" stepKey="seeModal"/> <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> From 7c24ca030e2cc72af5b467d959fb4c547e43d598 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Mon, 31 Aug 2020 17:36:15 -0500 Subject: [PATCH 0397/1013] MC-37250: [GraphQl] Change exception message when wishlist is not enabled - updated exception messages --- .../WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php | 2 +- .../WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php | 2 +- .../Model/Resolver/RemoveProductsFromWishlist.php | 2 +- .../WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php | 2 +- .../Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php | 2 +- .../GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php | 2 +- .../testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php index 3489585cd17d7..c98c2376255dc 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -97,7 +97,7 @@ public function resolve( $wishlist = $this->getWishlist($wishlistId, $customerId); if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { - throw new GraphQlInputException(__('The wishlist was not found.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $wishlistItems = $this->getWishlistItems($args['wishlistItems']); diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php index cad574ef56ed2..b73afe27883dd 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php @@ -54,7 +54,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } if (false === $context->getExtensionAttributes()->getIsCustomer()) { diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php index a59c5ccdb0f70..66a6c7b86ea37 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php @@ -83,7 +83,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php index 42b8cd576f7c8..465ab33744984 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -83,7 +83,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php index 09c0a8a935a6c..f31b403a514fb 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -63,7 +63,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php index 489a960056f1b..bccb6a7cb9fd6 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -110,7 +110,7 @@ public function testAddDownloadableProductOnDisabledWishlist(): void json_encode($itemOptions) ), '{}'); $query = $this->getQuery($qty, $sku, $productOptionsQuery); - $this->expectExceptionMessage('The wishlist is not currently available.'); + $this->expectExceptionMessage('The wishlist configuration is currently disabled.'); $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } 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 dcbb550d66d62..a38f8f8015d66 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -275,7 +275,7 @@ public function testAddProductToWishlistWithDisabledProduct() public function testCustomerCannotGetWishlistWhenDisabled() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The wishlist is not currently available.'); + $this->expectExceptionMessage('The wishlist configuration is currently disabled.'); $query = <<<QUERY From 08e7e4faffca9330aaed009dbe6ddebb9f6c70ee Mon Sep 17 00:00:00 2001 From: Vadim Malesh <51680850+engcom-Charlie@users.noreply.github.com> Date: Tue, 1 Sep 2020 09:58:08 +0300 Subject: [PATCH 0398/1013] add testCaseId --- ...torefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml index 0959962d50d81..1055ff25edaef 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithPurchaseOrderNumberPressKeyEnterTest.xml @@ -15,6 +15,7 @@ <title value="Create Checkout with purchase order payment method test. Press key Enter on field Purchase Order Number for create Order."/> <description value="Create Checkout with purchase order payment method. Press key Enter on field Purchase Order Number for create Order."/> <severity value="MAJOR"/> + <testCaseId value="MC-37227"/> <group value="checkout"/> </annotations> From c7e2deb4e9f648f5f42f70acbbc24e27fc2e5ec8 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Tue, 1 Sep 2020 09:51:40 +0100 Subject: [PATCH 0399/1013] Applied bug fix on getOrderId --- .../Ui/Customer/Component/ConfirmationPopup/Options.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 4d9e61fffdb21..e9b0bf26a9375 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -248,11 +248,11 @@ private function getOrderId(): int $creditmemoId = $this->request->getParam('creditmemo_id'); $invoiceId = $this->request->getParam('invoice_id'); if ($invoiceId) { - return $this->invoiceRepository->get($invoiceId)->getOrderId(); + return (int)$this->invoiceRepository->get($invoiceId)->getOrderId(); } elseif ($shipmentId) { - return $this->shipmentRepository->get($shipmentId)->getOrderId(); + return (int)$this->shipmentRepository->get($shipmentId)->getOrderId(); } elseif ($creditmemoId) { - return $this->creditmemoRepository->get($creditmemoId)->getOrderId(); + return (int)$this->creditmemoRepository->get($creditmemoId)->getOrderId(); } throw new LocalizedException(__('Unable to get Order ID.')); } From 28efcf2117504cf2538400bc366e62b9ad5b4727 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Tue, 1 Sep 2020 14:53:50 -0500 Subject: [PATCH 0400/1013] MC-37250: [GraphQl] Change exception message when wishlist is not enabled - updated exception messages --- .../WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php index c98c2376255dc..3cfb95e766d0e 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -83,7 +83,7 @@ public function resolve( array $args = null ) { if (!$this->wishlistConfig->isEnabled()) { - throw new GraphQlInputException(__('The wishlist is not currently available.')); + throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); } $customerId = $context->getUserId(); From de25cec0be91766dea3b0cb2838fe09326031deb Mon Sep 17 00:00:00 2001 From: Maksym Aposov <maposov@magento.com> Date: Tue, 1 Sep 2020 20:56:26 -0500 Subject: [PATCH 0401/1013] MC-37307: Code generation failed for *ExtensionInterfaceFactory --- .../Magento/Framework/Code/GeneratorTest.php | 42 +++++ .../SourceClassWithNestedNamespace.php | 170 ++++++++++++++++++ ...ourceClassWithNestedNamespaceExtension.php | 14 ++ ...espaceExtensionInterfaceFactory.php.sample | 48 +++++ ...ClassWithNestedNamespaceFactory.php.sample | 48 +++++ ...ionAttributesInterfaceFactoryGenerator.php | 24 +-- .../ObjectManager/Code/Generator/Factory.php | 15 +- 7 files changed, 339 insertions(+), 22 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespaceExtension.php create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample create mode 100644 dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceFactory.php.sample diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php index fe92c295b47fa..d2c4b1dd70d75 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php @@ -16,6 +16,8 @@ require_once __DIR__ . '/GeneratorTest/SourceClassWithNamespace.php'; require_once __DIR__ . '/GeneratorTest/ParentClassWithNamespace.php'; require_once __DIR__ . '/GeneratorTest/SourceClassWithNamespaceExtension.php'; +require_once __DIR__ . '/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php'; +require_once __DIR__ . '/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespaceExtension.php'; /** * @magentoAppIsolation enabled @@ -24,6 +26,7 @@ class GeneratorTest extends TestCase { const CLASS_NAME_WITH_NAMESPACE = GeneratorTest\SourceClassWithNamespace::class; + const CLASS_NAME_WITH_NESTED_NAMESPACE = GeneratorTest\NestedNamespace\SourceClassWithNestedNamespace::class; /** * @var Generator @@ -116,6 +119,45 @@ public function testGenerateClassFactoryWithNamespace() $this->assertEquals($expectedContent, $content); } + /** + * Generates a new file with Factory class and compares with the sample from the + * SourceClassWithNestedNamespaceFactory.php.sample file. + */ + public function testGenerateClassFactoryWithNestedNamespace() + { + $factoryClassName = self::CLASS_NAME_WITH_NESTED_NAMESPACE . 'Factory'; + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); + $factory = Bootstrap::getObjectManager()->create($factoryClassName); + $this->assertInstanceOf(self::CLASS_NAME_WITH_NESTED_NAMESPACE, $factory->create()); + $content = $this->_clearDocBlock( + file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) + ); + $expectedContent = $this->_clearDocBlock( + file_get_contents(__DIR__ . '/_expected/SourceClassWithNestedNamespaceFactory.php.sample') + ); + $this->assertEquals($expectedContent, $content); + } + + /** + * Generates a new file with ExtensionInterfaceFactory class and compares with the sample from the + * SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample file. + */ + public function testGenerateClassExtensionAttributesInterfaceFactoryWithNestedNamespace() + { + $factoryClassName = self::CLASS_NAME_WITH_NESTED_NAMESPACE . 'ExtensionInterfaceFactory'; + $this->generatedDirectory->create($this->testRelativePath); + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); + $factory = Bootstrap::getObjectManager()->create($factoryClassName); + $this->assertInstanceOf(self::CLASS_NAME_WITH_NESTED_NAMESPACE . 'Extension', $factory->create()); + $content = $this->_clearDocBlock( + file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) + ); + $expectedContent = $this->_clearDocBlock( + file_get_contents(__DIR__ . '/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample') + ); + $this->assertEquals($expectedContent, $content); + } + /** * Generates a new file with Proxy class and compares with the sample from the * SourceClassWithNamespaceProxy.php.sample file. diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php new file mode 100644 index 0000000000000..6471a198b31f9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespace.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Code\GeneratorTest\NestedNamespace; + +use Laminas\Code\Generator\ClassGenerator; + +/** + * phpcs:ignoreFile + */ +class SourceClassWithNestedNamespace extends \Magento\Framework\Code\GeneratorTest\ParentClassWithNamespace +{ + /** + * Public child constructor + * + * @param string $param1 + * @param string $param2 + * @param string $param3 + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __construct($param1 = '', $param2 = '\\', $param3 = '\'') + { + } + + /** + * Public child method + * + * @param \Laminas\Code\Generator\ClassGenerator $classGenerator + * @param string $param1 + * @param string $param2 + * @param string $param3 + * @param array $array + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function publicChildMethod( + ClassGenerator $classGenerator, + $param1 = '', + $param2 = '\\', + $param3 = '\'', + array $array = [] + ) { + } + + /** + * Public child method with reference + * + * @param \Laminas\Code\Generator\ClassGenerator $classGenerator + * @param string $param1 + * @param array $array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function publicMethodWithReference(ClassGenerator &$classGenerator, &$param1, array &$array) + { + } + + /** + * Protected child method + * + * @param \Laminas\Code\Generator\ClassGenerator $classGenerator + * @param string $param1 + * @param string $param2 + * @param string $param3 + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _protectedChildMethod( + ClassGenerator $classGenerator, + $param1 = '', + $param2 = '\\', + $param3 = '\'' + ) { + } + + /** + * Private child method + * + * @param \Laminas\Code\Generator\ClassGenerator $classGenerator + * @param string $param1 + * @param string $param2 + * @param string $param3 + * @param array $array + * @return void + * + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + private function _privateChildMethod( + ClassGenerator $classGenerator, + $param1 = '', + $param2 = '\\', + $param3 = '\'', + array $array = [] + ) { + } + + /** + * Test method + */ + public function publicChildWithoutParameters() + { + } + + /** + * Test method + */ + public static function publicChildStatic() + { + } + + /** + * Test method + */ + final public function publicChildFinal() + { + } + + /** + * Test method + * + * @param mixed $arg1 + * @param string $arg2 + * @param int|null $arg3 + * @param int|null $arg4 + * + * @return void + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function public71( + $arg1, + string $arg2, + ?int $arg3, + ?int $arg4 = null + ): void { + } + + /** + * Test method + * + * @param \DateTime|null $arg1 + * @param mixed $arg2 + * + * @return null|string + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function public71Another(?\DateTime $arg1, $arg2 = false): ?string + { + // phpstan:ignore + } + + /** + * Test method + * + * @param bool $arg + * @return SourceClassWithNamespace + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function publicWithSelf($arg = false): self + { + // phpstan:ignore + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespaceExtension.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespaceExtension.php new file mode 100644 index 0000000000000..24a1dd8c328d3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest/NestedNamespace/SourceClassWithNestedNamespaceExtension.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Code\GeneratorTest\NestedNamespace; + +/** + * Source class for ExtensionInterfaceFactory generator. + */ +class SourceClassWithNestedNamespaceExtension extends \Magento\Framework\Code\GeneratorTest\ParentClassWithNamespace +{ +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample new file mode 100644 index 0000000000000..f51d3a850d4e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample @@ -0,0 +1,48 @@ +<?php +namespace Magento\Framework\Code\GeneratorTest\NestedNamespace; + +/** + * Factory class for @see \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespaceExtension + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +class SourceClassWithNestedNamespaceExtensionInterfaceFactory +{ + /** + * Object Manager instance + * + * @var \Magento\Framework\ObjectManagerInterface + */ + protected $_objectManager = null; + + /** + * Instance name to create + * + * @var string + */ + protected $_instanceName = null; + + /** + * ExtensionInterfaceFactory constructor + * + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param string $instanceName + */ + public function __construct(\Magento\Framework\ObjectManagerInterface $objectManager, $instanceName = '\\Magento\\Framework\\Code\\GeneratorTest\\NestedNamespace\\SourceClassWithNestedNamespaceExtension') + { + $this->_objectManager = $objectManager; + $this->_instanceName = $instanceName; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return \Magento\Framework\Code\GeneratorTest\NestedNamespace\SourceClassWithNestedNamespaceExtension + */ + public function create(array $data = []) + { + return $this->_objectManager->create($this->_instanceName, $data); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceFactory.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceFactory.php.sample new file mode 100644 index 0000000000000..1913968d199af --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/_expected/SourceClassWithNestedNamespaceFactory.php.sample @@ -0,0 +1,48 @@ +<?php +namespace Magento\Framework\Code\GeneratorTest\NestedNamespace; + +/** + * Factory class for @see \Magento\Framework\Code\GeneratorTest\SourceClassWithNamespace + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +class SourceClassWithNestedNamespaceFactory +{ + /** + * Object Manager instance + * + * @var \Magento\Framework\ObjectManagerInterface + */ + protected $_objectManager = null; + + /** + * Instance name to create + * + * @var string + */ + protected $_instanceName = null; + + /** + * Factory constructor + * + * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param string $instanceName + */ + public function __construct(\Magento\Framework\ObjectManagerInterface $objectManager, $instanceName = '\\Magento\\Framework\\Code\\GeneratorTest\\NestedNamespace\\SourceClassWithNestedNamespace') + { + $this->_objectManager = $objectManager; + $this->_instanceName = $instanceName; + } + + /** + * Create class instance with specified parameters + * + * @param array $data + * @return \Magento\Framework\Code\GeneratorTest\NestedNamespace\SourceClassWithNestedNamespace + */ + public function create(array $data = []) + { + return $this->_objectManager->create($this->_instanceName, $data); + } +} diff --git a/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php b/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php index 12af882a46760..017f76e780a45 100644 --- a/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php +++ b/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php @@ -6,10 +6,10 @@ namespace Magento\Framework\Api\Code\Generator; -use Magento\Framework\ObjectManager\Code\Generator\Factory; +use Magento\Framework\Code\Generator\CodeGeneratorInterface; use Magento\Framework\Code\Generator\DefinedClasses; use Magento\Framework\Code\Generator\Io; -use Magento\Framework\Code\Generator\CodeGeneratorInterface; +use Magento\Framework\ObjectManager\Code\Generator\Factory; class ExtensionAttributesInterfaceFactoryGenerator extends Factory { @@ -18,11 +18,6 @@ class ExtensionAttributesInterfaceFactoryGenerator extends Factory */ const ENTITY_TYPE = 'extensionInterfaceFactory'; - /** - * @var string - */ - private static $suffix = 'InterfaceFactory'; - /** * Initialize dependencies. * @@ -52,19 +47,8 @@ public function __construct( /** * {@inheritdoc} */ - protected function _validateData() + protected function getResultClassSuffix() { - $result = true; - $sourceClassName = $this->getSourceClassName(); - $resultClassName = $this->_getResultClassName(); - - if ($resultClassName !== $sourceClassName . self::$suffix) { - $this->_addError( - 'Invalid Factory class name [' . $resultClassName . ']. Use ' . $sourceClassName . self::$suffix - ); - $result = false; - } - - return $result; + return 'InterfaceFactory'; } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php index 6186bffd4ca68..2f9ff0533f88d 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php @@ -100,13 +100,24 @@ protected function _validateData() $sourceClassName = $this->getSourceClassName(); $resultClassName = $this->_getResultClassName(); - if ($resultClassName !== $sourceClassName . 'Factory') { + if ($resultClassName !== $sourceClassName . $this->getResultClassSuffix()) { $this->_addError( - 'Invalid Factory class name [' . $resultClassName . ']. Use ' . $sourceClassName . 'Factory' + 'Invalid Factory class name [' . $resultClassName . ']. Use ' . + $sourceClassName . $this->getResultClassSuffix() ); $result = false; } } return $result; } + + /** + * Suffix for generated class + * + * @return string + */ + protected function getResultClassSuffix() + { + return 'Factory'; + } } From 1778c0f73570f19f9f368a18ff0808a6177f96e2 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Wed, 2 Sep 2020 09:34:16 +0100 Subject: [PATCH 0402/1013] MC-37112: Reverting partially contribution because of bug with fastly --- app/code/Magento/PageCache/Model/Config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/PageCache/Model/Config.php b/app/code/Magento/PageCache/Model/Config.php index bf144cc46637e..10ae41be21d4d 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 (int)$this->_scopeConfig->getValue(self::XML_PAGECACHE_TYPE); + return $this->_scopeConfig->getValue(self::XML_PAGECACHE_TYPE); } /** From 89c00cea4a2e26bfd23d12329a1c0d9044e807e8 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Wed, 2 Sep 2020 12:01:55 +0300 Subject: [PATCH 0403/1013] MC-37121: Deleted category still shown as available Category during product creation. --- .../Framework/App/Cache/FlushCacheByTags.php | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php index 8f8dfd3baf1b6..f53d7f70e300b 100644 --- a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -55,37 +55,21 @@ public function __construct( $this->tagResolver = $tagResolver; } - /** - * Clean cache on save object - * - * @param AbstractResource $subject - * @param \Closure $proceed - * @param AbstractModel $object - * @return AbstractResource - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function aroundSave(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource - { - $result = $proceed($object); - $tags = $this->tagResolver->getTags($object); - $this->cleanCacheByTags($tags); - - return $result; - } - /** * Clean cache on delete object * * @param AbstractResource $subject - * @param \Closure $proceed + * @param AbstractResource $result * @param AbstractModel $object * @return AbstractResource * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundDelete(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource - { + public function afterDelete( + AbstractResource $subject, + AbstractResource $result, + AbstractModel $object + ): AbstractResource { $tags = $this->tagResolver->getTags($object); - $result = $proceed($object); $this->cleanCacheByTags($tags); return $result; From 256cedd9ece42f4b0e848f7cea0697c0beff426c Mon Sep 17 00:00:00 2001 From: siimm <siim.medijainen@vaimo.com> Date: Wed, 2 Sep 2020 11:04:53 +0000 Subject: [PATCH 0404/1013] ensure tags uniqueness --- lib/internal/Magento/Framework/Indexer/CacheContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Indexer/CacheContext.php b/lib/internal/Magento/Framework/Indexer/CacheContext.php index 4a6964477ebd5..1374a8c3f2089 100644 --- a/lib/internal/Magento/Framework/Indexer/CacheContext.php +++ b/lib/internal/Magento/Framework/Indexer/CacheContext.php @@ -73,6 +73,6 @@ public function getIdentities() $identities[] = $cacheTag . '_' . $id; } } - return array_merge($identities, array_unique($this->tags)); + return array_unique(array_merge($identities, array_unique($this->tags)); } } From 3efc95a409120fa2724be02bf2b5222c17b4ee23 Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Wed, 2 Sep 2020 15:10:15 +0300 Subject: [PATCH 0405/1013] remove wrong tags --- app/code/Magento/AdminNotification/Block/ToolbarEntry.php | 1 - app/code/Magento/AdminNotification/Model/Feed.php | 1 - app/code/Magento/AdminNotification/Model/InboxInterface.php | 1 - .../Magento/AdminNotification/Model/NotificationService.php | 1 - .../AdminNotification/Model/ResourceModel/Grid/Collection.php | 2 -- .../AdminNotification/Model/ResourceModel/Inbox/Collection.php | 2 -- .../Model/ResourceModel/Inbox/Collection/Unread.php | 2 -- .../Observer/PredispatchAdminActionControllerObserver.php | 2 -- 8 files changed, 12 deletions(-) diff --git a/app/code/Magento/AdminNotification/Block/ToolbarEntry.php b/app/code/Magento/AdminNotification/Block/ToolbarEntry.php index c097edfd8af65..42ca68177cb83 100644 --- a/app/code/Magento/AdminNotification/Block/ToolbarEntry.php +++ b/app/code/Magento/AdminNotification/Block/ToolbarEntry.php @@ -10,7 +10,6 @@ * Toolbar entry that shows latest notifications * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class ToolbarEntry extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/AdminNotification/Model/Feed.php b/app/code/Magento/AdminNotification/Model/Feed.php index b99a8bbbc9031..ac1e631cc3f33 100644 --- a/app/code/Magento/AdminNotification/Model/Feed.php +++ b/app/code/Magento/AdminNotification/Model/Feed.php @@ -12,7 +12,6 @@ /** * AdminNotification Feed model * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 diff --git a/app/code/Magento/AdminNotification/Model/InboxInterface.php b/app/code/Magento/AdminNotification/Model/InboxInterface.php index 4e87822763fc3..5e61c3dd680c9 100644 --- a/app/code/Magento/AdminNotification/Model/InboxInterface.php +++ b/app/code/Magento/AdminNotification/Model/InboxInterface.php @@ -8,7 +8,6 @@ /** * AdminNotification Inbox interface * - * @author Magento Core Team <core@magentocommerce.com> * @api * @since 100.0.2 */ diff --git a/app/code/Magento/AdminNotification/Model/NotificationService.php b/app/code/Magento/AdminNotification/Model/NotificationService.php index d44e98aaf2203..a13efe2136a6f 100644 --- a/app/code/Magento/AdminNotification/Model/NotificationService.php +++ b/app/code/Magento/AdminNotification/Model/NotificationService.php @@ -8,7 +8,6 @@ /** * Notification service model * - * @author Magento Core Team <core@magentocommerce.com> * @api * @since 100.0.2 */ diff --git a/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php b/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php index e12419155d52b..1a59d15e40c7a 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/Grid/Collection.php @@ -6,8 +6,6 @@ /** * AdminNotification Inbox model - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\AdminNotification\Model\ResourceModel\Grid; diff --git a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php index 44ec765b9d0a2..bf4f91cc6ae80 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection.php @@ -9,8 +9,6 @@ * AdminNotification Inbox model * * @api - * @author Magento Core Team <core@magentocommerce.com> - * @api * @since 100.0.2 */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection diff --git a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php index b9e77f8a35295..9504c2f2d10f7 100644 --- a/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php +++ b/app/code/Magento/AdminNotification/Model/ResourceModel/Inbox/Collection/Unread.php @@ -6,8 +6,6 @@ /** * Collection of unread notifications - * - * @author Magento Core Team <core@magentocommerce.com> */ namespace Magento\AdminNotification\Model\ResourceModel\Inbox\Collection; diff --git a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php index 24ef712c0f61f..5c40ec88f0906 100644 --- a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php +++ b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php @@ -9,8 +9,6 @@ /** * AdminNotification observer - * - * @author Magento Core Team <core@magentocommerce.com> */ class PredispatchAdminActionControllerObserver implements ObserverInterface { From cd9e834a2f1b9066a8961dbbada25f2301f7b4b7 Mon Sep 17 00:00:00 2001 From: Alex Taranovskyi <a.taranovskyi@atwix.com> Date: Wed, 2 Sep 2020 15:12:17 +0300 Subject: [PATCH 0406/1013] magento/magento2#: Remove a redundant getMappedNums from a loop --- lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php index fb4d9d40b05fa..4afeac9238623 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php @@ -52,7 +52,7 @@ public function getEnumValueFromField(string $enumName, string $fieldValue) : st $mappedValues = $this->enumDataMapper->getMappedEnums($enumName); foreach ($enumObject->getValues() as $enumItem) { - if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] == $fieldValue) { + if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] === $fieldValue) { return $enumItem->getValue(); } } From abff48fd8d26ab455154da5b1a102c5ec99a27b8 Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Wed, 2 Sep 2020 15:24:27 +0300 Subject: [PATCH 0407/1013] Remove wrong tag --- .../AdvancedPricingImportExport/Model/Export/AdvancedPricing.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index 27e2713995653..a11f53aeb67b8 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -14,7 +14,6 @@ /** * Export Advanced Pricing * - * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) From 61b3c24f25a8b6833ae54fcb95229e599584115f Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Wed, 2 Sep 2020 15:30:36 +0300 Subject: [PATCH 0408/1013] removes wrong tags --- app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php | 1 - app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php | 1 - .../AdvancedSearch/Model/ResourceModel/Recommendations.php | 1 - 3 files changed, 3 deletions(-) diff --git a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php index 403a4d12cc17b..401e9d666103e 100644 --- a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php +++ b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Edit.php @@ -9,7 +9,6 @@ * Search queries relations grid container * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Edit extends \Magento\Backend\Block\Widget\Grid\Container diff --git a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php index 6bdfd3b0dd143..add3e244be851 100644 --- a/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php +++ b/app/code/Magento/AdvancedSearch/Block/Adminhtml/Search/Grid.php @@ -9,7 +9,6 @@ * Search query relations edit grid * * @api - * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ class Grid extends \Magento\Backend\Block\Widget\Grid diff --git a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php index c19c1d67d81f7..9be5d0c201841 100644 --- a/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php +++ b/app/code/Magento/AdvancedSearch/Model/ResourceModel/Recommendations.php @@ -8,7 +8,6 @@ /** * Catalog search recommendations resource model * - * @author Magento Core Team <core@magentocommerce.com> * @api * @since 100.0.2 */ From 4666ac046457f733b79bf47dd1aede3b0ae4c76d Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Wed, 2 Sep 2020 16:19:37 +0300 Subject: [PATCH 0409/1013] remove some wrongs tags --- .../Setup/Patch/Data/InitializeAuthRoles.php | 25 ++++++++++--------- .../Model/Indexer/Stock/AbstractAction.php | 2 -- .../Model/Indexer/Stock/Action/Full.php | 2 -- .../Model/Indexer/Stock/Action/Row.php | 4 --- .../Model/Indexer/Stock/Action/Rows.php | 4 --- .../Model/Indexer/Stock/CacheCleaner.php | 2 -- .../Model/Indexer/Stock/Processor.php | 2 -- .../Model/Indexer/Stock/Action/FullTest.php | 3 --- .../Model/Indexer/Stock/Action/RowTest.php | 3 --- .../Model/Indexer/Stock/Action/RowsTest.php | 3 --- .../Indexer/Stock/Plugin/StoreGroupTest.php | 3 --- .../Sales/Block/Adminhtml/Order/View.php | 2 -- 12 files changed, 13 insertions(+), 42 deletions(-) diff --git a/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php index 84992badf65db..c133bae98f1c5 100644 --- a/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php +++ b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php @@ -6,6 +6,9 @@ namespace Magento\Authorization\Setup\Patch\Data; +use Magento\Authorization\Model\ResourceModel\Role; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Setup\AuthorizationFactory; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; @@ -14,8 +17,7 @@ use Magento\Authorization\Model\UserContextInterface; /** - * Class InitializeAuthRoles - * @package Magento\Authorization\Setup\Patch + * Class for Initialize Auth Roles */ class InitializeAuthRoles implements DataPatchInterface, PatchVersionInterface { @@ -25,25 +27,24 @@ class InitializeAuthRoles implements DataPatchInterface, PatchVersionInterface private $moduleDataSetup; /** - * @var \Magento\Authorization\Setup\AuthorizationFactory + * @var AuthorizationFactory */ private $authFactory; /** - * InitializeAuthRoles constructor. * @param ModuleDataSetupInterface $moduleDataSetup - * @param \Magento\Authorization\Setup\AuthorizationFactory $authorizationFactory + * @param AuthorizationFactory $authorizationFactory */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, - \Magento\Authorization\Setup\AuthorizationFactory $authorizationFactory + AuthorizationFactory $authorizationFactory ) { $this->moduleDataSetup = $moduleDataSetup; $this->authFactory = $authorizationFactory; } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -68,7 +69,7 @@ public function apply() ] )->save(); } else { - /** @var \Magento\Authorization\Model\ResourceModel\Role $item */ + /** @var Role $item */ foreach ($roleCollection as $item) { $admGroupRole = $item; break; @@ -89,7 +90,7 @@ public function apply() ] )->save(); } else { - /** @var \Magento\Authorization\Model\Rules $rule */ + /** @var Rules $rule */ foreach ($rulesCollection as $rule) { $rule->setData('resource_id', 'Magento_Backend::all')->save(); } @@ -108,7 +109,7 @@ public function apply() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -116,7 +117,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -124,7 +125,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php index 85fee62eb4303..54d92cf12e2b8 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php index 43a5aabee9779..e345ef2ee752b 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php index c7dfcffee3d31..9e5e39e4aeb53 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Row.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,8 +8,6 @@ /** * Class Row reindex action - * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action */ class Row extends \Magento\CatalogInventory\Model\Indexer\Stock\AbstractAction { diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php index f107955f0201e..a6176df3b107e 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Rows.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ @@ -10,8 +8,6 @@ /** * Class Rows reindex action for mass actions - * - * @package Magento\CatalogInventory\Model\Indexer\Stock\Action */ class Rows extends \Magento\CatalogInventory\Model\Indexer\Stock\AbstractAction { diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php index b3fa07479a712..055185239e404 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/CacheCleaner.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php index 403f64e7f77f8..e59f81414f102 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php index ca89ac01f280f..c888d522d2e8b 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/FullTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php index 25b0c2ef33ebe..c9f60bd61c2fb 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php index e01f371b829d6..42d578ec88ea8 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Action/RowsTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php index 0e2b6b2f329c1..a81a4cd34b87f 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Indexer/Stock/Plugin/StoreGroupTest.php @@ -1,8 +1,5 @@ <?php /** - * @category Magento - * @package Magento_CatalogInventory - * @subpackage unit_tests * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View.php index e4b12c30e71b4..d70df80038193 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View.php @@ -1,7 +1,5 @@ <?php /** - * @category Magento - * @package Magento_Sales * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ From 18c36a6baede9f7f49b83ce2e235adee0ef61c90 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Wed, 2 Sep 2020 16:35:34 +0300 Subject: [PATCH 0410/1013] MC-37097: Stores are not shown in "Login as Customer: Select Store" modal window on order view page - MFTF tests fix. --- ...sCustomerLoginFromOrderPageActionGroup.xml | 13 ++--------- ...inFromOrderPageManualChooseActionGroup.xml | 13 ++--------- ...sCustomerManualChooseFromOrderPageTest.xml | 23 +++++-------------- ...minUIShownIfLoginAsCustomerEnabledTest.xml | 5 +--- 4 files changed, 11 insertions(+), 43 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml index 68eaad37ccd91..a478f8e9d18cd 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml @@ -16,17 +16,8 @@ <argument name="orderId" type="string"/> </arguments> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> - <waitForPageLoad stepKey="waitForOrdersPage"/> - <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> - <waitForPageLoad stepKey="waitForClearFilters"/> - <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> - <waitForPageLoad stepKey="waitForClickFilters"/> - <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> - <click selector="{{AdminOrdersGridSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForApplyFilters"/> - <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> - <waitForPageLoad stepKey="waitForOrderPageOpened"/> + <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"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml index f06c87db0e6ae..e778ede05f9a5 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup.xml @@ -17,17 +17,8 @@ <argument name="storeName" type="string" defaultValue="default"/> </arguments> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderGridPage"/> - <waitForPageLoad stepKey="waitForOrdersPage"/> - <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilters"/> - <waitForPageLoad stepKey="waitForClearFilters"/> - <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openOrderGridFilters"/> - <waitForPageLoad stepKey="waitForClickFilters"/> - <fillField selector="{{AdminOrdersGridSection.idFilter}}" userInput="{{orderId}}" stepKey="fillOrderIdFilter"/> - <click selector="{{AdminOrdersGridSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForApplyFilters"/> - <click selector="{{AdminDataGridTableSection.firstRow}}" stepKey="clickOrderRow"/> - <waitForPageLoad stepKey="waitForOrderPageOpened"/> + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> <click selector="{{AdminOrderDetailsMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> <see selector="{{AdminConfirmationModalSection.title}}" userInput="Login as Customer: Select Store" stepKey="seeModal"/> <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml index 372e89771b1e5..e4f0209c55233 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseFromOrderPageTest.xml @@ -60,26 +60,15 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> - <!-- Login as Customer from Customer page --> - <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup" - stepKey="loginAsCustomerFromCustomerPage"> - <argument name="customerId" value="$$createCustomer.id$$"/> - <argument name="storeName" value="{{customStoreGroup.name}}"/> - </actionGroup> - - <!-- Place Order as Customer --> - <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> - <argument name="productUrl" value="$$createProduct.sku$$"/> - </actionGroup> - <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <!-- Create order --> + <actionGroup ref="CreateOrderInStoreActionGroup" stepKey="createOrder"> <argument name="product" value="$$createProduct$$"/> - <argument name="productCount" value="1"/> + <argument name="customer" value="$$createCustomer$$"/> + <argument name="storeView" value="customStoreFR"/> </actionGroup> - <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openCart"/> - <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"/> - <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderId"/> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> - <!-- Login as Customer from Customer page --> + <!-- Login as Customer from Order page --> <actionGroup ref="AdminLoginAsCustomerLoginFromOrderPageManualChooseActionGroup" stepKey="loginAsCustomerFromOrderPage"> <argument name="orderId" value="{$grabOrderId}"/> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml index ea06263901b9e..9b415d9914fdb 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml @@ -59,10 +59,7 @@ <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> + <grabFromCurrentUrl regex="~/order_id/(\d+)/~" stepKey="grabOrderId"/> <!-- Verify Login as Customer Login action works correctly from Order page --> <actionGroup ref="AdminLoginAsCustomerLoginFromOrderPageActionGroup" From 2c70ad89cab02cdd543f37b02073692f243096da Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Wed, 2 Sep 2020 16:47:37 +0300 Subject: [PATCH 0411/1013] MC-37121: Deleted category still shown as available Category during product creation - restore deleted method. --- .../Framework/App/Cache/FlushCacheByTags.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php index f53d7f70e300b..c6c43f7053443 100644 --- a/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php +++ b/lib/internal/Magento/Framework/App/Cache/FlushCacheByTags.php @@ -55,6 +55,24 @@ public function __construct( $this->tagResolver = $tagResolver; } + /** + * Clean cache on save object + * + * @param AbstractResource $subject + * @param \Closure $proceed + * @param AbstractModel $object + * @return AbstractResource + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundSave(AbstractResource $subject, \Closure $proceed, AbstractModel $object): AbstractResource + { + $result = $proceed($object); + $tags = $this->tagResolver->getTags($object); + $this->cleanCacheByTags($tags); + + return $result; + } + /** * Clean cache on delete object * From 30aebc39fb59cc76456e48198ee285c1b86f4e96 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Wed, 2 Sep 2020 10:52:16 -0500 Subject: [PATCH 0412/1013] MC-37250: [GraphQl] Change exception message when wishlist is not enabled - updated exception messages --- .../WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php index 3cfb95e766d0e..840c4638614c4 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -97,7 +97,7 @@ public function resolve( $wishlist = $this->getWishlist($wishlistId, $customerId); if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { - throw new GraphQlInputException(__('The wishlist configuration is currently disabled.')); + throw new GraphQlInputException(__('The wishlist was not found.')); } $wishlistItems = $this->getWishlistItems($args['wishlistItems']); From 5154cda61341c8ec5cb3a48f23da4f53990abf9f Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 2 Sep 2020 17:20:38 -0500 Subject: [PATCH 0413/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../Product/Initialization/Helper.php | 38 ++++++++- .../Controller/Adminhtml/Product/Save.php | 4 - app/code/Magento/Catalog/Model/Product.php | 5 +- .../Product/Initialization/HelperTest.php | 56 ++++++------- .../Catalog/Test/Unit/Model/ProductTest.php | 58 ++++++++++--- .../Product/Initialization/HelperTest.php | 84 +++++++++++++++++++ 6 files changed, 200 insertions(+), 45 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php 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 d948daed1c7d9..f0e0ff73b838c 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; use Magento\Backend\Helper\Js; +use Magento\Catalog\Api\Data\CategoryLinkInterfaceFactory; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory as CustomOptionFactory; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory as ProductLinkFactory; use Magento\Catalog\Api\Data\ProductLinkTypeInterface; @@ -115,6 +116,11 @@ class Helper */ private $dateTimeFilter; + /** + * @var CategoryLinkInterfaceFactory + */ + private $categoryLinkFactory; + /** * Constructor * @@ -132,6 +138,7 @@ class Helper * @param FormatInterface|null $localeFormat * @param ProductAuthorization|null $productAuthorization * @param DateTimeFilter|null $dateTimeFilter + * @param CategoryLinkInterfaceFactory|null $categoryLinkFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -148,7 +155,8 @@ public function __construct( AttributeFilter $attributeFilter = null, FormatInterface $localeFormat = null, ?ProductAuthorization $productAuthorization = null, - ?DateTimeFilter $dateTimeFilter = null + ?DateTimeFilter $dateTimeFilter = null, + ?CategoryLinkInterfaceFactory $categoryLinkFactory = null ) { $this->request = $request; $this->storeManager = $storeManager; @@ -166,6 +174,7 @@ public function __construct( $this->localeFormat = $localeFormat ?: $objectManager->get(FormatInterface::class); $this->productAuthorization = $productAuthorization ?? $objectManager->get(ProductAuthorization::class); $this->dateTimeFilter = $dateTimeFilter ?? $objectManager->get(DateTimeFilter::class); + $this->categoryLinkFactory = $categoryLinkFactory ?? $objectManager->get(CategoryLinkInterfaceFactory::class); } /** @@ -238,6 +247,7 @@ public function initializeFromData(Product $product, array $productData) $product = $this->setProductLinks($product); $product = $this->fillProductOptions($product, $productOptions); + $this->setCategoryLinks($product); $product->setCanSaveCustomOptions( !empty($productData['affect_product_custom_options']) && !$product->getOptionsReadonly() @@ -484,4 +494,30 @@ function ($valueData) { return $product->setOptions($customOptions); } + + /** + * Set category links based on initialized category ids + * + * @param Product $product + */ + private function setCategoryLinks(Product $product): void + { + $extensionAttributes = $product->getExtensionAttributes(); + $categoryLinks = []; + foreach ((array) $extensionAttributes->getCategoryLinks() as $categoryLink) { + $categoryLinks[$categoryLink->getCategoryId()] = $categoryLink; + } + + $newCategoryLinks = []; + foreach ($product->getCategoryIds() as $categoryId) { + $categoryLink = $categoryLinks[$categoryId] ?? + $this->categoryLinkFactory->create() + ->setCategoryId($categoryId) + ->setPosition(0); + $newCategoryLinks[] = $categoryLink; + } + + $extensionAttributes->setCategoryLinks(!empty($newCategoryLinks) ? $newCategoryLinks : null); + $product->setExtensionAttributes($extensionAttributes); + } } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index 5c3e27334cb66..3ad3ac470bc65 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -141,10 +141,6 @@ public function execute() $canSaveCustomOptions = $product->getCanSaveCustomOptions(); $product->save(); $this->handleImageRemoveError($data, $product->getId()); - $this->categoryLinkManagement->assignProductToCategories( - $product->getSku(), - $product->getCategoryIds() - ); $productId = $product->getEntityId(); $productAttributeSetId = $product->getAttributeSetId(); $productTypeId = $product->getTypeId(); diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 04ad0c0fef1ce..6b409583ddaab 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -2367,14 +2367,15 @@ public function getIdentities() { $identities = [self::CACHE_TAG . '_' . $this->getId()]; - if (!$this->isObjectNew() || $this->getStatus() == Status::STATUS_ENABLED) { + $isStatusChanged = $this->getOrigData(self::STATUS) != $this->getData(self::STATUS) && !$this->isObjectNew(); + if ($isStatusChanged || $this->getStatus() == Status::STATUS_ENABLED) { if ($this->getIsChangedCategories()) { foreach ($this->getAffectedCategoryIds() as $categoryId) { $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; } } - if (($this->getOrigData('status') != $this->getData('status')) || $this->isStockStatusChanged()) { + if ($isStatusChanged || $this->isStockStatusChanged()) { foreach ($this->getCategoryIds() as $categoryId) { $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index 521468cd82927..5aeec9ae1f596 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -7,7 +7,10 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Initialization; +use Magento\Catalog\Api\Data\CategoryLinkInterface; +use Magento\Catalog\Api\Data\CategoryLinkInterfaceFactory; use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Api\Data\ProductLinkInterfaceFactory; use Magento\Catalog\Api\Data\ProductLinkTypeInterface; use Magento\Catalog\Api\ProductRepositoryInterface as ProductRepository; @@ -122,20 +125,16 @@ protected function setUp(): void { $this->objectManager = new ObjectManager($this); $this->productLinkFactoryMock = $this->getMockBuilder(ProductLinkInterfaceFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - $this->productRepositoryMock = $this->getMockBuilder(ProductRepository::class) + ->onlyMethods(['create']) ->disableOriginalConstructor() ->getMock(); + $this->productRepositoryMock = $this->createMock(ProductRepository::class); $this->requestMock = $this->getMockBuilder(RequestInterface::class) ->setMethods(['getPost']) ->getMockForAbstractClass(); - $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) - ->getMockForAbstractClass(); - $this->stockFilterMock = $this->getMockBuilder(StockDataFilter::class) - ->disableOriginalConstructor() - ->getMock(); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->stockFilterMock = $this->createMock(StockDataFilter::class); + $this->productMock = $this->getMockBuilder(Product::class) ->setMethods( [ @@ -150,30 +149,32 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $productExtensionAttributes = $this->createMock(ProductExtensionInterface::class); + $this->productMock->setExtensionAttributes($productExtensionAttributes); + $this->customOptionFactoryMock = $this->getMockBuilder(ProductCustomOptionInterfaceFactory::class) ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $this->productLinksMock = $this->getMockBuilder(ProductLinks::class) - ->disableOriginalConstructor() - ->getMock(); - $this->linkTypeProviderMock = $this->getMockBuilder(LinkTypeProvider::class) - ->disableOriginalConstructor() + ->onlyMethods(['create']) ->getMock(); + $this->productLinksMock = $this->createMock(ProductLinks::class); + $this->linkTypeProviderMock = $this->createMock(LinkTypeProvider::class); $this->productLinksMock->expects($this->any()) ->method('initializeLinks') ->willReturn($this->productMock); - $this->attributeFilterMock = $this->getMockBuilder(AttributeFilter::class) - ->setMethods(['prepareProductAttributes']) - ->disableOriginalConstructor() - ->getMock(); - $this->localeFormatMock = $this->getMockBuilder(Format::class) - ->setMethods(['getNumber']) - ->disableOriginalConstructor() - ->getMock(); + $this->attributeFilterMock = $this->createMock(AttributeFilter::class); + $this->localeFormatMock = $this->createMock(Format::class); $this->dateTimeFilterMock = $this->createMock(DateTime::class); + $categoryLinkFactoryMock = $this->getMockBuilder(CategoryLinkInterfaceFactory::class) + ->onlyMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $categoryLinkFactoryMock->method('create') + ->willReturnCallback(function() { + return $this->createMock(CategoryLinkInterface::class); + }); + $this->helper = $this->objectManager->getObject( Helper::class, [ @@ -187,13 +188,12 @@ protected function setUp(): void 'linkTypeProvider' => $this->linkTypeProviderMock, 'attributeFilter' => $this->attributeFilterMock, 'localeFormat' => $this->localeFormatMock, - 'dateTimeFilter' => $this->dateTimeFilterMock + 'dateTimeFilter' => $this->dateTimeFilterMock, + 'categoryLinkFactory' => $categoryLinkFactoryMock, ] ); - $this->linkResolverMock = $this->getMockBuilder(Resolver::class) - ->disableOriginalConstructor() - ->getMock(); + $this->linkResolverMock = $this->createMock(Resolver::class); $helperReflection = new \ReflectionClass(get_class($this->helper)); $resolverProperty = $helperReflection->getProperty('linkResolver'); $resolverProperty->setAccessible(true); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php index c97c4f6acb7fa..42d0778daa4af 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/ProductTest.php @@ -831,18 +831,36 @@ public function getIdentitiesProvider(): array 'is_changed_categories' => true ], ], - 'status change only' => [ + 'category change for disabled product' => [ + [0 => 'cat_p_1'], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_DISABLED], + [ + 'id' => 1, + 'name' => 'value', + 'category_ids' => [2], + 'status' => Status::STATUS_DISABLED, + 'affected_category_ids' => [1, 2], + 'is_changed_categories' => true + ], + ], + 'status change to disabled' => [ [0 => 'cat_p_1', 1 => 'cat_c_p_7'], ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_ENABLED], ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_DISABLED], ], + 'status change to enabled' => [ + [0 => 'cat_p_1', 1 => 'cat_c_p_7'], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_DISABLED], + ['id' => 1, 'name' => 'value', 'category_ids' => [7], 'status' => Status::STATUS_ENABLED], + ], 'status changed, category unassigned' => $this->getStatusAndCategoryChangesData(), 'no status changes' => [ [0 => 'cat_p_1'], ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], ], - 'no stock status changes' => [ + 'no stock status changes' => $this->getNoStockStatusChangesData($extensionAttributesMock), + 'no stock status data 1' => [ [0 => 'cat_p_1'], ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ @@ -850,11 +868,10 @@ public function getIdentitiesProvider(): array 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED, - 'stock_data' => ['is_in_stock' => true], ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], ], - 'no stock status data 1' => [ + 'no stock status data 2' => [ [0 => 'cat_p_1'], ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], [ @@ -862,21 +879,22 @@ public function getIdentitiesProvider(): array 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED, - ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, + 'stock_data' => ['is_in_stock' => true], ], ], - 'no stock status data 2' => [ + 'stock status changes for enabled product' => $this->getStatusStockProviderData($extensionAttributesMock), + 'stock status changes for disabled product' => [ [0 => 'cat_p_1'], - ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_DISABLED], [ 'id' => 1, 'name' => 'value', 'category_ids' => [1], - 'status' => Status::STATUS_ENABLED, - 'stock_data' => ['is_in_stock' => true], + 'status' => Status::STATUS_DISABLED, + 'stock_data' => ['is_in_stock' => false], + ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, ], ], - 'stock status changes' => $this->getStatusStockProviderData($extensionAttributesMock), ]; } @@ -899,6 +917,26 @@ private function getStatusAndCategoryChangesData(): array ]; } + /** + * @param MockObject $extensionAttributesMock + * @return array + */ + private function getNoStockStatusChangesData($extensionAttributesMock): array + { + return [ + [0 => 'cat_p_1'], + ['id' => 1, 'name' => 'value', 'category_ids' => [1], 'status' => Status::STATUS_ENABLED], + [ + 'id' => 1, + 'name' => 'value', + 'category_ids' => [1], + 'status' => Status::STATUS_ENABLED, + 'stock_data' => ['is_in_stock' => true], + ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => $extensionAttributesMock, + ], + ]; + } + /** * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php new file mode 100644 index 0000000000000..a52ecb6df24fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; + +use Magento\Catalog\Api\Data\CategoryLinkInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +class HelperTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Helper + */ + private $initializationHelper; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->initializationHelper = Bootstrap::getObjectManager()->create(Helper::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/categories.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @dataProvider initializeCategoriesFromDataProvider + * @param string $sku + * @param array $categoryIds + */ + public function testInitializeCategoriesFromData(string $sku, array $categoryIds): void + { + $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + /** @var \Magento\Catalog\Model\Product $product */ + $product = $productRepository->get($sku); + $productData = $product->getData(); + $productData['category_ids'] = $categoryIds; + + $product = $this->initializationHelper->initializeFromData($product, $productData); + $extensionAttributes = $product->getExtensionAttributes(); + $linkedCategoryIds = array_map(function(CategoryLinkInterface $categoryLink) { + return $categoryLink->getCategoryId(); + }, (array) $extensionAttributes->getCategoryLinks()); + $this->assertEquals($categoryIds, $linkedCategoryIds); + } + + /** + * @return array + */ + public function initializeCategoriesFromDataProvider(): array + { + return [ + 'assign categories' => [ + 'simple', + [2, 3, 4, 11, 12, 13], + ], + 'unassign categories' => [ + 'simple-4', + [11, 12], + ], + 'update categories' => [ + 'simple-3', + [10, 12, 13], + ], + 'unassign all categories' => [ + 'simple-3', + [], + ], + 'assign new category' => [ + 'simple2', + [11], + ], + 'assign new categories' => [ + 'simple2', + [11, 13], + ], + ]; + } +} From e7deac0af461652a87defd9ebe0eadfdce383d8f Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 2 Sep 2020 23:50:06 -0500 Subject: [PATCH 0414/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../Controller/Adminhtml/Product/Save.php | 4 +++ app/code/Magento/Catalog/Model/Product.php | 30 +++++++++++++++---- .../Product/Initialization/HelperTest.php | 12 ++++---- .../Product/Initialization/HelperTest.php | 6 +++- .../Magento/Catalog/_files/categories.php | 6 +++- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index 3ad3ac470bc65..5c3e27334cb66 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -141,6 +141,10 @@ public function execute() $canSaveCustomOptions = $product->getCanSaveCustomOptions(); $product->save(); $this->handleImageRemoveError($data, $product->getId()); + $this->categoryLinkManagement->assignProductToCategories( + $product->getSku(), + $product->getCategoryIds() + ); $productId = $product->getEntityId(); $productAttributeSetId = $product->getAttributeSetId(); $productTypeId = $product->getTypeId(); diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 6b409583ddaab..1f3ecf8f967b5 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -2358,6 +2358,22 @@ public function getImage() return parent::getImage(); } + /** + * Get identities for related to product categories + * + * @param array $categoryIds + * @return array + */ + private function getProductCategoryIdentities(array $categoryIds): array + { + $identities = []; + foreach ($categoryIds as $categoryId) { + $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; + } + + return $identities; + } + /** * Get identities * @@ -2370,15 +2386,17 @@ public function getIdentities() $isStatusChanged = $this->getOrigData(self::STATUS) != $this->getData(self::STATUS) && !$this->isObjectNew(); if ($isStatusChanged || $this->getStatus() == Status::STATUS_ENABLED) { if ($this->getIsChangedCategories()) { - foreach ($this->getAffectedCategoryIds() as $categoryId) { - $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; - } + $identities = array_merge( + $identities, + $this->getProductCategoryIdentities($this->getAffectedCategoryIds()) + ); } if ($isStatusChanged || $this->isStockStatusChanged()) { - foreach ($this->getCategoryIds() as $categoryId) { - $identities[] = self::CACHE_PRODUCT_CATEGORY_TAG . '_' . $categoryId; - } + $identities = array_merge( + $identities, + $this->getProductCategoryIdentities($this->getCategoryIds()) + ); } } diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php index 5aeec9ae1f596..886b03e0f3c1e 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -125,7 +125,7 @@ protected function setUp(): void { $this->objectManager = new ObjectManager($this); $this->productLinkFactoryMock = $this->getMockBuilder(ProductLinkInterfaceFactory::class) - ->onlyMethods(['create']) + ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); $this->productRepositoryMock = $this->createMock(ProductRepository::class); @@ -149,12 +149,14 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $productExtensionAttributes = $this->createMock(ProductExtensionInterface::class); + $productExtensionAttributes = $this->getMockBuilder(ProductExtensionInterface::class) + ->setMethods(['getCategoryLinks', 'setCategoryLinks']) + ->getMockForAbstractClass(); $this->productMock->setExtensionAttributes($productExtensionAttributes); $this->customOptionFactoryMock = $this->getMockBuilder(ProductCustomOptionInterfaceFactory::class) ->disableOriginalConstructor() - ->onlyMethods(['create']) + ->setMethods(['create']) ->getMock(); $this->productLinksMock = $this->createMock(ProductLinks::class); $this->linkTypeProviderMock = $this->createMock(LinkTypeProvider::class); @@ -167,11 +169,11 @@ protected function setUp(): void $this->dateTimeFilterMock = $this->createMock(DateTime::class); $categoryLinkFactoryMock = $this->getMockBuilder(CategoryLinkInterfaceFactory::class) - ->onlyMethods(['create']) + ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); $categoryLinkFactoryMock->method('create') - ->willReturnCallback(function() { + ->willReturnCallback(function () { return $this->createMock(CategoryLinkInterface::class); }); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php index a52ecb6df24fa..8b93e0cf41a2c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -43,7 +43,7 @@ public function testInitializeCategoriesFromData(string $sku, array $categoryIds $product = $this->initializationHelper->initializeFromData($product, $productData); $extensionAttributes = $product->getExtensionAttributes(); - $linkedCategoryIds = array_map(function(CategoryLinkInterface $categoryLink) { + $linkedCategoryIds = array_map(function (CategoryLinkInterface $categoryLink) { return $categoryLink->getCategoryId(); }, (array) $extensionAttributes->getCategoryLinks()); $this->assertEquals($categoryIds, $linkedCategoryIds); @@ -67,6 +67,10 @@ public function initializeCategoriesFromDataProvider(): array 'simple-3', [10, 12, 13], ], + 'change all categories' => [ + 'simple-3', + [4, 5] + ], 'unassign all categories' => [ 'simple-3', [], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index 4255d7d3c98e5..b1a7fc89eb073 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -12,11 +12,15 @@ $productRepository = $objectManager->create( \Magento\Catalog\Api\ProductRepositoryInterface::class ); +$categoryRepository = $objectManager->create( + \Magento\Catalog\Api\CategoryRepositoryInterface::class +); $categoryLinkRepository = $objectManager->create( \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, [ - 'productRepository' => $productRepository + 'productRepository' => $productRepository, + 'categoryRepository' => $categoryRepository, ] ); From 835be0d6c4d2631484f27e074c872ad388c3c4bf Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Thu, 3 Sep 2020 10:56:39 +0300 Subject: [PATCH 0415/1013] MC-37121: Deleted category still shown as available Category during product creation - restore deleted method - unit test fix. --- .../App/Test/Unit/Cache/FlushCacheByTagsTest.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php index 6e550cd4fbde4..ba0a6b9b8e4c7 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Cache/FlushCacheByTagsTest.php @@ -85,7 +85,7 @@ function () use ($resource) { /** * @return void */ - public function testAroundDelete(): void + public function testAfterDelete(): void { $resource = $this->getMockBuilder(AbstractResource::class) ->disableOriginalConstructor() @@ -95,11 +95,9 @@ public function testAroundDelete(): void ->getMockForAbstractClass(); $this->tagResolver->expects($this->atLeastOnce())->method('getTags')->with($model)->willReturn([]); - $result = $this->plugin->aroundDelete( + $result = $this->plugin->afterDelete( + $resource, $resource, - function () use ($resource) { - return $resource; - }, $model ); From 40512d421c1bb1fb32fe3d3c5214f16257493679 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Thu, 3 Sep 2020 03:37:33 -0500 Subject: [PATCH 0416/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../Magento/Catalog/Controller/Adminhtml/Product/Save.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php index 5c3e27334cb66..97b57317851fc 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Save.php @@ -15,7 +15,8 @@ use Magento\Framework\App\Request\DataPersistorInterface; /** - * Class Save + * Product save controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Catalog\Controller\Adminhtml\Product implements HttpPostActionInterface @@ -141,10 +142,6 @@ public function execute() $canSaveCustomOptions = $product->getCanSaveCustomOptions(); $product->save(); $this->handleImageRemoveError($data, $product->getId()); - $this->categoryLinkManagement->assignProductToCategories( - $product->getSku(), - $product->getCategoryIds() - ); $productId = $product->getEntityId(); $productAttributeSetId = $product->getAttributeSetId(); $productTypeId = $product->getTypeId(); From 7bbd5de5261436eb412c73a08b6fcdd1e251be6d Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Thu, 3 Sep 2020 10:59:08 +0100 Subject: [PATCH 0417/1013] Fixed static tests --- .../Ui/Customer/Component/ConfirmationPopup/Options.php | 1 + app/code/Magento/LoginAsCustomerAdminUi/composer.json | 1 + 2 files changed, 2 insertions(+) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index e9b0bf26a9375..16ff6ec6df99d 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -117,6 +117,7 @@ public function __construct( /** * @inheritdoc + * * @throws LocalizedException */ public function toOptionArray(): array diff --git a/app/code/Magento/LoginAsCustomerAdminUi/composer.json b/app/code/Magento/LoginAsCustomerAdminUi/composer.json index 8bbe0e2bd6c9e..b6291226827a8 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/composer.json +++ b/app/code/Magento/LoginAsCustomerAdminUi/composer.json @@ -8,6 +8,7 @@ "magento/module-login-as-customer-frontend-ui": "*", "magento/module-backend": "*", "magento/module-customer": "*", + "magento/module-sales": "*", "magento/module-store": "*" }, "suggest": { From 189c93bf5c8b1bf823fda8466550dbba8029e2ba Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Thu, 3 Sep 2020 12:07:33 +0100 Subject: [PATCH 0418/1013] Added suppress for excessive coupling --- .../Ui/Customer/Component/ConfirmationPopup/Options.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php index 16ff6ec6df99d..791ed35519850 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/ConfirmationPopup/Options.php @@ -25,6 +25,8 @@ /** * Store group options for Login As Customer confirmation pop-up. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Options implements OptionSourceInterface { From 727d322b63a5f54054fb7179e61835d322fe22bc Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Thu, 3 Sep 2020 14:13:10 +0300 Subject: [PATCH 0419/1013] MC-37121: Deleted category still shown as available Category during product creation - restore deleted method - MFTF test added. --- ...ryNotShownAsAvailableOnProductPageTest.xml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml new file mode 100644 index 0000000000000..4207f88ecd97d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeletedCategoryNotShownAsAvailableOnProductPageTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeletedCategoryNotShownAsAvailableOnProductPageTest"> + <annotations> + <features value="Catalog"/> + <stories value="Delete categories"/> + <title value="Deleted Category not shown as available on Product page"/> + <description value="Deleted category not shown as available Category on Product edit page"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37121"/> + <group value="Catalog"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="logInAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + </after> + + <!-- Create Category --> + <actionGroup ref="GoToCreateCategoryPageActionGroup" stepKey="goToCreateCategoryPage"/> + <actionGroup ref="FillCategoryNameAndUrlKeyAndSaveActionGroup" stepKey="fillCategoryForm"> + <argument name="categoryName" value="additional"/> + <argument name="categoryUrlKey" value=""/> + </actionGroup> + + <!-- Check if Category present on Product page --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToProductPage"> + <argument name="productId" value="$$createProduct.id$$"/> + </actionGroup> + <waitForPageLoad time="60" stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductFormCategoryExistInCategoryListActionGroup" stepKey="checkExistCategoryInList"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + + <!-- Delete Category --> + <actionGroup ref="AdminDeleteCategoryByNameActionGroup" stepKey="deleteAdditionalCategory"> + <argument name="categoryName" value="additional"/> + </actionGroup> + + <!-- Check if Category absent on Product page --> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToProductPageAfterDelete"> + <argument name="productId" value="$$createProduct.id$$"/> + </actionGroup> + <waitForPageLoad time="60" stepKey="waitForProductPageLoadAfterDelete"/> + <actionGroup ref="AdminProductFormCategoryNotExistInCategoryListActionGroup" stepKey="checkNotExistCategoryInList"> + <argument name="categoryName" value="additional"/> + </actionGroup> + </test> +</tests> From a6c98da6c26f15636754d76aad25d2833ebc7812 Mon Sep 17 00:00:00 2001 From: Marjan <petkovski.marjan@gmail.com> Date: Thu, 3 Sep 2020 13:41:04 +0200 Subject: [PATCH 0420/1013] magento/magento2#29880: GraphQL categories and categoryList do not consider Category Permissions configuration Introduce collection processors, pass the context so to obtain customer group --- .../Model/Category/CategoryFilter.php | 123 ++++++++---------- .../Model/Category/Filter/SearchCriteria.php | 105 +++++++++++++++ .../CollectionProcessor/CatalogProcessor.php | 50 +++++++ .../Category/CollectionProcessorInterface.php | 32 +++++ .../Category/CompositeCollectionProcessor.php | 53 ++++++++ .../Model/Resolver/CategoriesQuery.php | 2 +- .../Model/Resolver/CategoryList.php | 2 +- app/code/Magento/CatalogGraphQl/etc/di.xml | 13 ++ 8 files changed, 312 insertions(+), 68 deletions(-) create mode 100644 app/code/Magento/CatalogGraphQl/Model/Category/Filter/SearchCriteria.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php create mode 100644 app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php index dc93005983776..9d2a175b97fb5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -7,17 +7,17 @@ namespace Magento\CatalogGraphQl\Model\Category; -use Magento\Catalog\Api\CategoryListInterface; -use Magento\Catalog\Api\Data\CategoryInterface; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategorySearchResultsInterface; +use Magento\Catalog\Api\Data\CategorySearchResultsInterfaceFactory; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; +use Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface; +use Magento\CatalogGraphQl\Model\Category\Filter\SearchCriteria; +use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; 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\GraphQl\Model\Query\ContextInterface; use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; /** * Category filter allows filtering category results by attributes. @@ -25,38 +25,57 @@ class CategoryFilter { /** - * @var string + * @var CollectionFactory */ - private const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; + private $categoryCollectionFactory; /** - * @var ScopeConfigInterface + * @var CollectionProcessorInterface */ - private $scopeConfig; + private $collectionProcessor; /** - * @var CategoryListInterface + * @var JoinProcessorInterface */ - private $categoryList; + private $extensionAttributesJoinProcessor; /** - * @var Builder + * @var CategorySearchResultsInterfaceFactory */ - private $searchCriteriaBuilder; + private $categorySearchResultsFactory; /** - * @param ScopeConfigInterface $scopeConfig - * @param CategoryListInterface $categoryList - * @param Builder $searchCriteriaBuilder + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @var SearchCriteria + */ + private $searchCriteria; + + /** + * @param CollectionFactory $categoryCollectionFactory + * @param CollectionProcessorInterface $collectionProcessor + * @param JoinProcessorInterface $extensionAttributesJoinProcessor + * @param CategorySearchResultsInterfaceFactory $categorySearchResultsFactory + * @param CategoryRepositoryInterface $categoryRepository + * @param SearchCriteria $searchCriteria */ public function __construct( - ScopeConfigInterface $scopeConfig, - CategoryListInterface $categoryList, - Builder $searchCriteriaBuilder + CollectionFactory $categoryCollectionFactory, + CollectionProcessorInterface $collectionProcessor, + JoinProcessorInterface $extensionAttributesJoinProcessor, + CategorySearchResultsInterfaceFactory $categorySearchResultsFactory, + CategoryRepositoryInterface $categoryRepository, + SearchCriteria $searchCriteria ) { - $this->scopeConfig = $scopeConfig; - $this->categoryList = $categoryList; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->categoryCollectionFactory = $categoryCollectionFactory; + $this->collectionProcessor = $collectionProcessor; + $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; + $this->categorySearchResultsFactory = $categorySearchResultsFactory; + $this->categoryRepository = $categoryRepository; + $this->searchCriteria = $searchCriteria; } /** @@ -64,21 +83,24 @@ public function __construct( * * @param array $criteria * @param StoreInterface $store + * @param ContextInterface $context * @return int[] * @throws InputException */ - public function getResult(array $criteria, StoreInterface $store) + public function getResult(array $criteria, StoreInterface $store, ContextInterface $context) { - $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; - $searchCriteria->setPageSize($pageSize)->setCurrentPage($currentPage); + $searchCriteria = $this->searchCriteria->buildCriteria($criteria, $store); + $collection = $this->categoryCollectionFactory->create(); + $this->extensionAttributesJoinProcessor->process($collection); + $this->collectionProcessor->process($collection, $searchCriteria, $context); + + /** @var CategorySearchResultsInterface $searchResult */ + $categories = $this->categorySearchResultsFactory->create(); + $categories->setSearchCriteria($searchCriteria); + $categories->setItems($collection->getItems()); + $categories->setTotalCount($collection->getSize()); - $categories = $this->categoryList->getList($searchCriteria); + $categoryIds = []; foreach ($categories->getItems() as $category) { $categoryIds[] = (int)$category->getId(); } @@ -106,35 +128,4 @@ public function getResult(array $criteria, StoreInterface $store) ] ]; } - - /** - * Format match filters to behave like fuzzy match - * - * @param array $filters - * @param StoreInterface $store - * @return array - * @throws InputException - */ - private function formatMatchFilters(array $filters, StoreInterface $store): array - { - $minQueryLength = $this->scopeConfig->getValue( - Query::XML_PATH_MIN_QUERY_LENGTH, - ScopeInterface::SCOPE_STORE, - $store - ); - - foreach ($filters as $filter => $condition) { - $conditionType = current(array_keys($condition)); - if ($conditionType === 'match') { - $searchValue = trim(str_replace(self::SPECIAL_CHARACTERS, '', $condition[$conditionType])); - $matchLength = strlen($searchValue); - if ($matchLength < $minQueryLength) { - throw new InputException(__('Invalid match filter. Minimum length is %1.', $minQueryLength)); - } - unset($filters[$filter]['match']); - $filters[$filter]['like'] = '%' . $searchValue . '%'; - } - } - return $filters; - } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/Filter/SearchCriteria.php b/app/code/Magento/CatalogGraphQl/Model/Category/Filter/SearchCriteria.php new file mode 100644 index 0000000000000..aea34f19fea16 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Category/Filter/SearchCriteria.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Category\Filter; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Filter; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Sort; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\Builder; +use Magento\Search\Model\Query; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Utility to help transform raw criteria data into SearchCriteriaInterface + */ +class SearchCriteria +{ + /** + * @var string + */ + private const SPECIAL_CHARACTERS = '-+~/\\<>\'":*$#@()!,.?`=%&^'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var Builder + */ + private $searchCriteriaBuilder; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param Builder $searchCriteriaBuilder + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Builder $searchCriteriaBuilder + ) { + $this->scopeConfig = $scopeConfig; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Transform raw criteria data into SearchCriteriaInterface + * + * @param array $criteria + * @param StoreInterface $store + * @return SearchCriteriaInterface + * @throws InputException + */ + public function buildCriteria(array $criteria, StoreInterface $store): SearchCriteriaInterface + { + $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; + $searchCriteria->setPageSize($pageSize)->setCurrentPage($currentPage); + + return $searchCriteria; + } + + /** + * Format match filters to behave like fuzzy match + * + * @param array $filters + * @param StoreInterface $store + * @return array + * @throws InputException + */ + private function formatMatchFilters(array $filters, StoreInterface $store): array + { + $minQueryLength = $this->scopeConfig->getValue( + Query::XML_PATH_MIN_QUERY_LENGTH, + ScopeInterface::SCOPE_STORE, + $store + ); + + foreach ($filters as $filter => $condition) { + $conditionType = current(array_keys($condition)); + if ($conditionType === 'match') { + $searchValue = trim(str_replace(self::SPECIAL_CHARACTERS, '', $condition[$conditionType])); + $matchLength = strlen($searchValue); + if ($matchLength < $minQueryLength) { + throw new InputException(__('Invalid match filter. Minimum length is %1.', $minQueryLength)); + } + unset($filters[$filter]['match']); + $filters[$filter]['like'] = '%' . $searchValue . '%'; + } + } + return $filters; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php new file mode 100644 index 0000000000000..b28831623d195 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessor; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface as SearchCriteriaCollectionProcessor; + +/** + * Apply pre-defined catalog filtering + * + * {@inheritdoc} + */ +class CatalogProcessor implements CollectionProcessorInterface +{ + /** @var SearchCriteriaCollectionProcessor */ + private $collectionProcessor; + + public function __construct( + SearchCriteriaCollectionProcessor $collectionProcessor + ) { + $this->collectionProcessor = $collectionProcessor; + } + + /** + * Process collection to add additional joins, attributes, and clauses to a category collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param ContextInterface|null $context + * @return Collection + */ + public function process( + Collection $collection, + SearchCriteriaInterface $searchCriteria, + ContextInterface $context = null + ): Collection + { + $this->collectionProcessor->process($searchCriteria, $collection); + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php new file mode 100644 index 0000000000000..a9e35c6f7970f --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Add additional joins, attributes, and clauses to a category collection. + */ +interface CollectionProcessorInterface +{ + /** + * Process collection to add additional joins, attributes, and clauses to a category collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param ContextInterface|null $context + * @return Collection + */ + public function process( + Collection $collection, + SearchCriteriaInterface $searchCriteria, + ContextInterface $context = null + ): Collection; +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php new file mode 100644 index 0000000000000..ce42280efec07 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category; + +use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Composite collection processor + * + * {@inheritdoc} + */ +class CompositeCollectionProcessor implements CollectionProcessorInterface +{ + /** + * @var CollectionProcessorInterface[] + */ + private $collectionProcessors; + + /** + * @param CollectionProcessorInterface[] $collectionProcessors + */ + public function __construct(array $collectionProcessors = []) + { + $this->collectionProcessors = $collectionProcessors; + } + + /** + * Process collection to add additional joins, attributes, and clauses to a category collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param ContextInterface|null $context + * @return Collection + */ + public function process( + Collection $collection, + SearchCriteriaInterface $searchCriteria, + ContextInterface $context = null + ): Collection { + foreach ($this->collectionProcessors as $collectionProcessor) { + $collection = $collectionProcessor->process($collection, $searchCriteria, $context); + } + + return $collection; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php index eb6708dc48f01..eb5a2f6c90861 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php @@ -70,7 +70,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } try { - $filterResult = $this->categoryFilter->getResult($args, $store); + $filterResult = $this->categoryFilter->getResult($args, $store, $context); } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php index f32c5a1f38425..bc474686bead1 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -65,7 +65,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $args['filters']['ids'] = ['eq' => $store->getRootCategoryId()]; } try { - $filterResults = $this->categoryFilter->getResult($args, $store); + $filterResults = $this->categoryFilter->getResult($args, $store, $context); $rootCategoryIds = $filterResults['category_ids']; } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 03f9d7ad03f04..fd3a834bff160 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface" type="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CompositeCollectionProcessor"/> + <preference for="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessorInterface" type="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CompositeCollectionProcessor"/> <type name="Magento\EavGraphQl\Model\Resolver\Query\Type"> <arguments> <argument name="customTypes" xsi:type="array"> @@ -53,6 +54,13 @@ </argument> </arguments> </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CompositeCollectionProcessor"> + <arguments> + <argument name="collectionProcessors" xsi:type="array"> + <item name="catalog" xsi:type="object">Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessor\CatalogProcessor</item> + </argument> + </arguments> + </type> <type name="Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessor\SearchCriteriaProcessor"> <arguments> <argument name="searchCriteriaApplier" xsi:type="object">Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor</argument> @@ -84,4 +92,9 @@ </argument> </arguments> </type> + <type name="Magento\CatalogGraphQl\Model\Resolver\Categories\DataProvider\Category\CollectionProcessor\CatalogProcessor"> + <arguments> + <argument name="collectionProcessor" xsi:type="object">Magento\Eav\Model\Api\SearchCriteria\CollectionProcessor</argument> + </arguments> + </type> </config> From 6be6b0477d12ad908281144667d8dcd4fa0bf554 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 3 Sep 2020 11:59:45 -0500 Subject: [PATCH 0421/1013] MC-37350: revert graphql cors --- .../CorsAllowCredentialsHeaderProvider.php | 71 ---------- .../Cors/CorsAllowHeadersHeaderProvider.php | 71 ---------- .../Cors/CorsAllowMethodsHeaderProvider.php | 71 ---------- .../Cors/CorsAllowOriginHeaderProvider.php | 71 ---------- .../Cors/CorsMaxAgeHeaderProvider.php | 71 ---------- .../GraphQl/Model/Cors/Configuration.php | 96 ------------- .../Model/Cors/ConfigurationInterface.php | 56 -------- .../Magento/GraphQl/etc/adminhtml/system.xml | 65 --------- app/code/Magento/GraphQl/etc/config.xml | 22 --- app/code/Magento/GraphQl/etc/di.xml | 27 ---- app/code/Magento/GraphQl/etc/graphql/di.xml | 11 -- .../Magento/GraphQl/CorsHeadersTest.php | 126 ------------------ 12 files changed, 758 deletions(-) delete mode 100644 app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php delete mode 100644 app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php delete mode 100644 app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php delete mode 100644 app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php delete mode 100644 app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php delete mode 100644 app/code/Magento/GraphQl/Model/Cors/Configuration.php delete mode 100644 app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php delete mode 100644 app/code/Magento/GraphQl/etc/adminhtml/system.xml delete mode 100644 app/code/Magento/GraphQl/etc/config.xml delete mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php deleted file mode 100644 index ba2e995d4f704..0000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php +++ /dev/null @@ -1,71 +0,0 @@ -<?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 deleted file mode 100644 index 68760de543daa..0000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php +++ /dev/null @@ -1,71 +0,0 @@ -<?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 deleted file mode 100644 index 233839b9deb74..0000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php +++ /dev/null @@ -1,71 +0,0 @@ -<?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 deleted file mode 100644 index 21850f18db1f2..0000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php +++ /dev/null @@ -1,71 +0,0 @@ -<?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 deleted file mode 100644 index e30209ae25e68..0000000000000 --- a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php +++ /dev/null @@ -1,71 +0,0 @@ -<?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 deleted file mode 100644 index dd5a0b426e22d..0000000000000 --- a/app/code/Magento/GraphQl/Model/Cors/Configuration.php +++ /dev/null @@ -1,96 +0,0 @@ -<?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 deleted file mode 100644 index b40b64f48e51f..0000000000000 --- a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php +++ /dev/null @@ -1,56 +0,0 @@ -<?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/etc/adminhtml/system.xml b/app/code/Magento/GraphQl/etc/adminhtml/system.xml deleted file mode 100644 index ddee7596eca3e..0000000000000 --- a/app/code/Magento/GraphQl/etc/adminhtml/system.xml +++ /dev/null @@ -1,65 +0,0 @@ -<?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 deleted file mode 100644 index 39caacbec42d2..0000000000000 --- a/app/code/Magento/GraphQl/etc/config.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?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 fca6c425e2507..b356f33c4f4bf 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -98,31 +98,4 @@ <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 23d49124d1a02..77fce336374dd 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -30,15 +30,4 @@ </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/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php deleted file mode 100644 index 25c808a549e80..0000000000000 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php +++ /dev/null @@ -1,126 +0,0 @@ -<?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'] ?? []; - } -} From ff096a8770b29ab8ebcd2cfc93f0fcde2e2e0ffb Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Thu, 3 Sep 2020 19:01:15 -0500 Subject: [PATCH 0422/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../Product/Initialization/HelperTest.php | 42 ++++---------- .../Magento/Catalog/_files/categories.php | 6 +- .../Catalog/_files/products_in_categories.php | 57 +++++++++++++++++++ .../products_in_categories_rollback.php | 31 ++++++++++ 4 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php index 8b93e0cf41a2c..5bf2797805cb9 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php @@ -27,15 +27,14 @@ protected function setUp(): void } /** - * @magentoDataFixture Magento/Catalog/_files/categories.php - * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @magentoDataFixture Magento/Catalog/_files/products_in_categories.php * @dataProvider initializeCategoriesFromDataProvider * @param string $sku * @param array $categoryIds */ public function testInitializeCategoriesFromData(string $sku, array $categoryIds): void { - $productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + $productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); /** @var \Magento\Catalog\Model\Product $product */ $product = $productRepository->get($sku); $productData = $product->getData(); @@ -55,34 +54,15 @@ public function testInitializeCategoriesFromData(string $sku, array $categoryIds public function initializeCategoriesFromDataProvider(): array { return [ - 'assign categories' => [ - 'simple', - [2, 3, 4, 11, 12, 13], - ], - 'unassign categories' => [ - 'simple-4', - [11, 12], - ], - 'update categories' => [ - 'simple-3', - [10, 12, 13], - ], - 'change all categories' => [ - 'simple-3', - [4, 5] - ], - 'unassign all categories' => [ - 'simple-3', - [], - ], - 'assign new category' => [ - 'simple2', - [11], - ], - 'assign new categories' => [ - 'simple2', - [11, 13], - ], + 'assign category' => ['simple1', [3, 4]], + 'assign categories' => ['simple1', [3, 5, 6]], + 'unassign category' => ['simple2', [3, 6]], + 'unassign categories' => ['simple2', [3]], + 'update categories' => ['simple2', [3, 4, 6]], + 'change all categories' => ['simple2', [4]], + 'unassign all categories' => ['simple2', []], + 'assign new category' => ['simple3', [4]], + 'assign new categories' => ['simple3', [4, 5]], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index b1a7fc89eb073..4255d7d3c98e5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -12,15 +12,11 @@ $productRepository = $objectManager->create( \Magento\Catalog\Api\ProductRepositoryInterface::class ); -$categoryRepository = $objectManager->create( - \Magento\Catalog\Api\CategoryRepositoryInterface::class -); $categoryLinkRepository = $objectManager->create( \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, [ - 'productRepository' => $productRepository, - 'categoryRepository' => $categoryRepository, + 'productRepository' => $productRepository ] ); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php new file mode 100644 index 0000000000000..7957aa560eaa5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +foreach (range(3, 7) as $categoryId) { + $category = $objectManager->create(\Magento\Catalog\Model\Category::class); + $category->isObjectNew(true); + $category->setId($categoryId) + ->setName('Category ' . $categoryId) + ->setParentId(2) + ->setPath('1/2/' . $categoryId) + ->setLevel(2) + ->setIsActive(true) + ->save(); +} + +foreach (range(1, 3) as $productId) { + $product = $objectManager->create(\Magento\Catalog\Model\Product::class); + $product->isObjectNew(true); + $product->setId($productId) + ->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setAttributeSetId(4) + ->setStoreId(1) + ->setWebsiteIds([1]) + ->setName('Simple Product ' . $productId) + ->setSku('simple' . $productId) + ->setPrice(10) + ->setWeight(1) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->save(); +} + +$categoryRepository = $objectManager->create(\Magento\Catalog\Api\CategoryRepositoryInterface::class); +$categoryLinkRepository = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + [ + 'categoryRepository' => $categoryRepository, + ] +); +$categoryLinkManagement = $objectManager->create( + \Magento\Catalog\Api\CategoryLinkManagementInterface::class, + [ + 'categoryRepository' => $categoryRepository, + ] +); +$reflectionClass = new \ReflectionClass(get_class($categoryLinkManagement)); +$reflectionProperty = $reflectionClass->getProperty('categoryLinkRepository'); +$reflectionProperty->setAccessible(true); +$reflectionProperty->setValue($categoryLinkManagement, $categoryLinkRepository); +$categoryLinkManagement->assignProductToCategories('simple1', [3]); +$categoryLinkManagement->assignProductToCategories('simple2', [3, 5, 6]); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php new file mode 100644 index 0000000000000..707419610245a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productsToDelete = ['simple1', 'simple2', 'simple3']; +foreach ($productsToDelete as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); +foreach ($collection->addAttributeToFilter('level', 2) as $category) { + /** @var \Magento\Catalog\Model\Category $category */ + $category->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 0630e5962d5f9177b05aeb12b17749bb93d30d4a Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Fri, 4 Sep 2020 03:08:56 -0500 Subject: [PATCH 0423/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- app/code/Magento/CatalogSearch/etc/mview.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/CatalogSearch/etc/mview.xml b/app/code/Magento/CatalogSearch/etc/mview.xml index e5580d86d1ef8..494b97a816886 100644 --- a/app/code/Magento/CatalogSearch/etc/mview.xml +++ b/app/code/Magento/CatalogSearch/etc/mview.xml @@ -19,6 +19,7 @@ <table name="catalog_product_bundle_selection" entity_column="parent_product_id" /> <table name="catalog_product_super_link" entity_column="product_id" /> <table name="catalog_product_link" entity_column="product_id" /> + <table name="catalog_category_product" entity_column="product_id" /> </subscriptions> </view> </config> From f17148f902309ddfd6868b17b7183099222084d8 Mon Sep 17 00:00:00 2001 From: siimm <siim.medijainen@vaimo.com> Date: Fri, 4 Sep 2020 08:25:34 +0000 Subject: [PATCH 0424/1013] add simple unit test for #29890 --- .../Indexer/Test/Unit/CacheContextTest.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php diff --git a/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php b/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php new file mode 100644 index 0000000000000..90606435a4487 --- /dev/null +++ b/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php @@ -0,0 +1,59 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Indexer\Test\Unit; + +use Magento\Framework\Indexer\CacheContext; +use PHPUnit\Framework\TestCase; + +class CacheContextTest extends TestCase +{ + /** + * @var Batch + */ + private $object; + + protected function setUp(): void + { + $this->object = new CacheContext(); + } + + /** + * @param array $tagsData + * @param array $expected + * @dataProvider getTagsDataProvider + */ + public function testUniqueTags($tagsData, $expected) + { + foreach ($tagsData as $tagSet) { + foreach ($tagSet as $cacheTag => $ids) { + $this->object->registerEntities($cacheTag, $ids); + } + } + + $this->assertEquals($this->object->getIdentities(), $expected); + } + + /** + * @return array + */ + public function getTagsDataProvider() + { + return [ + 'same entities and ids' => [ + [['cat_p' => [1]], ['cat_p' => [1]]], + ['cat_p_1'] + ], + 'same entities with overlapping ids' => [ + [['cat_p' => [1, 2, 3]], ['cat_p' => [3]]], + ['cat_p_1', 'cat_p_2', 'cat_p_3'] + ] + ]; + } +} + From e90df20ec0edbe36e52aa2e76d4e46c972c10df4 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 4 Sep 2020 12:06:29 +0300 Subject: [PATCH 0425/1013] MC-37313: [Create UPS shipping label] Error message appears "Failed to send items" if Signature Confirmation is set to Not Required --- app/code/Magento/Ups/Model/Carrier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index b6e539bdadcb9..ac9764d8c908d 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1538,7 +1538,7 @@ protected function _formShipmentRequest(DataObject $request) } } - if (isset($deliveryConfirmation) && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_SHIPMENT) { + if (!empty($deliveryConfirmation) && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_SHIPMENT) { $serviceOptionsNode = $shipmentPart->addChild('ShipmentServiceOptions'); $serviceOptionsNode->addChild( 'DeliveryConfirmation' From bfd31751733b7ea2264fb345401750a53472e297 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 4 Sep 2020 16:39:46 +0300 Subject: [PATCH 0426/1013] MC-37313: [Create UPS shipping label] Error message appears "Failed to send items" if Signature Confirmation is set to Not Required --- app/code/Magento/Ups/Model/Carrier.php | 4 ++++ .../integration/testsuite/Magento/Ups/Model/CarrierTest.php | 3 +++ .../testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index ac9764d8c908d..4dcc832d1a7db 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1118,6 +1118,7 @@ protected function _getXmlTracking($trackings) /** @var HttpResponseDeferredInterface[] $trackingResponses */ $trackingResponses = []; + $tracking = ''; foreach ($trackings as $tracking) { /** * RequestOption==>'1' to request all activities @@ -1362,6 +1363,7 @@ public function getAllowedMethods() protected function _formShipmentRequest(DataObject $request) { $packages = $request->getPackages(); + $shipmentItems = []; foreach ($packages as $package) { $shipmentItems[] = $package['items']; } @@ -1624,6 +1626,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) $xmlResponse = ''; } + $response = ''; try { $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); } catch (Throwable $e) { @@ -1800,6 +1803,7 @@ protected function _doShipmentRequest(DataObject $request) $this->setXMLAccessRequest(); $xmlRequest = $this->_xmlAccessRequest . $rawXmlRequest; $xmlResponse = $this->_getCachedQuotes($xmlRequest); + $debugData = []; if ($xmlResponse === null) { $debugData['request'] = $this->filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest; diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index 4909a14f6f7d1..25633229516bb 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -252,6 +252,8 @@ public function testRequestToShipment(): void 'weight' => '0.55', 'customs_value' => '20.00', 'container' => 'Large Express Box', + 'delivery_confirmation_level' => 1, + 'delivery_confirmation' => 0, ], 'items' => [ 'item2' => [ @@ -262,6 +264,7 @@ public function testRequestToShipment(): void ] ] ); + $request->setRecipientAddressCountryCode('UK'); $result = $this->carrier->requestToShipment($request); diff --git a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml index 63463c087726c..8caf02a5160a2 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml +++ b/dev/tests/integration/testsuite/Magento/Ups/_files/ShipmentConfirmRequest.xml @@ -27,7 +27,7 @@ <AddressLine1 /> <AddressLine2 /> <City /> - <CountryCode /> + <CountryCode>UK</CountryCode> <PostalCode /> <ResidentialAddress /> </Address> From 49b2bd0784130517bff20845453c8679b191bce8 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 4 Sep 2020 16:52:46 +0300 Subject: [PATCH 0427/1013] MC-37313: [Create UPS shipping label] Error message appears "Failed to send items" if Signature Confirmation is set to Not Required --- .../integration/testsuite/Magento/Ups/Model/CarrierTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php index 25633229516bb..345c572b92861 100644 --- a/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php +++ b/dev/tests/integration/testsuite/Magento/Ups/Model/CarrierTest.php @@ -252,7 +252,6 @@ public function testRequestToShipment(): void 'weight' => '0.55', 'customs_value' => '20.00', 'container' => 'Large Express Box', - 'delivery_confirmation_level' => 1, 'delivery_confirmation' => 0, ], 'items' => [ From b31408ca215d0e21898bdb499dbed4e922cc4452 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Fri, 4 Sep 2020 09:27:21 -0500 Subject: [PATCH 0428/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- app/code/Magento/Elasticsearch/etc/indexer.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Elasticsearch/etc/indexer.xml b/app/code/Magento/Elasticsearch/etc/indexer.xml index f22eb8f0bd39b..30103d7da87ce 100644 --- a/app/code/Magento/Elasticsearch/etc/indexer.xml +++ b/app/code/Magento/Elasticsearch/etc/indexer.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Indexer/etc/indexer.xsd"> <indexer id="catalogsearch_fulltext"> <dependencies> - <indexer id="catalog_category_product" /> + <indexer id="catalog_product_category" /> <indexer id="cataloginventory_stock" /> <indexer id="catalog_product_price" /> </dependencies> From bce4a8cf95e55c6e6482332e0c9087d8ee453cfb Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Fri, 4 Sep 2020 21:41:54 +0300 Subject: [PATCH 0429/1013] fixed static issue --- .../Model/Export/AdvancedPricing.php | 4 +-- .../Setup/Patch/Data/InitializeAuthRoles.php | 25 +++++++++---------- .../Model/Indexer/Stock/AbstractAction.php | 4 +-- .../Model/Indexer/Stock/Action/Full.php | 2 ++ .../Model/Indexer/Stock/Processor.php | 2 +- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index a11f53aeb67b8..60f79987932ad 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -514,6 +514,7 @@ private function fetchTierPrices(array $productIds): array */ protected function getTierPrices(array $listSku, $table) { + $selectFields = []; if (isset($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP])) { $exportFilter = $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP]; } @@ -570,12 +571,11 @@ protected function getTierPrices(array $listSku, $table) if (isset($updatedAtTo) && !empty($updatedAtTo)) { $select->where('cpe.updated_at <= ?', $updatedAtTo); } - $exportData = $this->_connection->fetchAll($select); + return $this->_connection->fetchAll($select); } catch (\Exception $e) { return false; } } - return $exportData; } /** diff --git a/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php index c133bae98f1c5..c450dc7127d4e 100644 --- a/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php +++ b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php @@ -6,9 +6,6 @@ namespace Magento\Authorization\Setup\Patch\Data; -use Magento\Authorization\Model\ResourceModel\Role; -use Magento\Authorization\Model\Rules; -use Magento\Authorization\Setup\AuthorizationFactory; use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; @@ -17,7 +14,8 @@ use Magento\Authorization\Model\UserContextInterface; /** - * Class for Initialize Auth Roles + * Class InitializeAuthRoles + * @package Magento\Authorization\Setup\Patch */ class InitializeAuthRoles implements DataPatchInterface, PatchVersionInterface { @@ -27,24 +25,25 @@ class InitializeAuthRoles implements DataPatchInterface, PatchVersionInterface private $moduleDataSetup; /** - * @var AuthorizationFactory + * @var \Magento\Authorization\Setup\AuthorizationFactor */ private $authFactory; /** + * InitializeAuthRoles constructor. * @param ModuleDataSetupInterface $moduleDataSetup - * @param AuthorizationFactory $authorizationFactory + * @param \Magento\Authorization\Setup\AuthorizationFactor $authorizationFactory */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, - AuthorizationFactory $authorizationFactory + \Magento\Authorization\Setup\AuthorizationFactor $authorizationFactory ) { $this->moduleDataSetup = $moduleDataSetup; $this->authFactory = $authorizationFactory; } /** - * @inheritdoc + * {@inheritdoc} */ public function apply() { @@ -69,7 +68,7 @@ public function apply() ] )->save(); } else { - /** @var Role $item */ + /** @var \Magento\Authorization\Model\ResourceModel\Role $item */ foreach ($roleCollection as $item) { $admGroupRole = $item; break; @@ -90,7 +89,7 @@ public function apply() ] )->save(); } else { - /** @var Rules $rule */ + /** @var \Magento\Authorization\Model\Rules $rule */ foreach ($rulesCollection as $rule) { $rule->setData('resource_id', 'Magento_Backend::all')->save(); } @@ -109,7 +108,7 @@ public function apply() } /** - * @inheritdoc + * {@inheritdoc} */ public static function getDependencies() { @@ -117,7 +116,7 @@ public static function getDependencies() } /** - * @inheritdoc + * {@inheritdoc} */ public static function getVersion() { @@ -125,7 +124,7 @@ public static function getVersion() } /** - * @inheritdoc + * {@inheritdoc} */ public function getAliases() { diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php index 54d92cf12e2b8..4ea6b6bcfde9a 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/AbstractAction.php @@ -12,8 +12,6 @@ /** * Abstract action reindex class - * - * @package Magento\CatalogInventory\Model\Indexer\Stock */ abstract class AbstractAction { @@ -281,6 +279,8 @@ private function doReindex($productIds = []) } /** + * Get cache cleaner object + * * @return CacheCleaner */ private function getCacheCleaner() diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php index e345ef2ee752b..43a5aabee9779 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Action/Full.php @@ -1,5 +1,7 @@ <?php /** + * @category Magento + * @package Magento_CatalogInventory * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ diff --git a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php index e59f81414f102..73c4a8833e433 100644 --- a/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php +++ b/app/code/Magento/CatalogInventory/Model/Indexer/Stock/Processor.php @@ -9,7 +9,7 @@ class Processor extends \Magento\Framework\Indexer\AbstractProcessor { /** - * Indexer ID + * Get Indexer ID for cataloginventory_stock */ const INDEXER_ID = 'cataloginventory_stock'; } From 9e248695ced252adbfd6127577786d5b8b28ae22 Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Fri, 4 Sep 2020 21:43:30 +0300 Subject: [PATCH 0430/1013] minor changes --- .../Authorization/Setup/Patch/Data/InitializeAuthRoles.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php index c450dc7127d4e..84992badf65db 100644 --- a/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php +++ b/app/code/Magento/Authorization/Setup/Patch/Data/InitializeAuthRoles.php @@ -25,18 +25,18 @@ class InitializeAuthRoles implements DataPatchInterface, PatchVersionInterface private $moduleDataSetup; /** - * @var \Magento\Authorization\Setup\AuthorizationFactor + * @var \Magento\Authorization\Setup\AuthorizationFactory */ private $authFactory; /** * InitializeAuthRoles constructor. * @param ModuleDataSetupInterface $moduleDataSetup - * @param \Magento\Authorization\Setup\AuthorizationFactor $authorizationFactory + * @param \Magento\Authorization\Setup\AuthorizationFactory $authorizationFactory */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, - \Magento\Authorization\Setup\AuthorizationFactor $authorizationFactory + \Magento\Authorization\Setup\AuthorizationFactory $authorizationFactory ) { $this->moduleDataSetup = $moduleDataSetup; $this->authFactory = $authorizationFactory; From ff97fe619d211ce80c4ecf73517f896c5a2cc2de Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Fri, 4 Sep 2020 22:30:39 +0300 Subject: [PATCH 0431/1013] fix some static issues --- .../Observer/PredispatchAdminActionControllerObserver.php | 1 + .../Model/Export/AdvancedPricing.php | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php index 5c40ec88f0906..a244ad1fb9a0f 100644 --- a/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php +++ b/app/code/Magento/AdminNotification/Observer/PredispatchAdminActionControllerObserver.php @@ -9,6 +9,7 @@ /** * AdminNotification observer + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class PredispatchAdminActionControllerObserver implements ObserverInterface { diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index 60f79987932ad..27e2713995653 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -14,6 +14,7 @@ /** * Export Advanced Pricing * + * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -514,7 +515,6 @@ private function fetchTierPrices(array $productIds): array */ protected function getTierPrices(array $listSku, $table) { - $selectFields = []; if (isset($this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP])) { $exportFilter = $this->_parameters[\Magento\ImportExport\Model\Export::FILTER_ELEMENT_GROUP]; } @@ -571,11 +571,12 @@ protected function getTierPrices(array $listSku, $table) if (isset($updatedAtTo) && !empty($updatedAtTo)) { $select->where('cpe.updated_at <= ?', $updatedAtTo); } - return $this->_connection->fetchAll($select); + $exportData = $this->_connection->fetchAll($select); } catch (\Exception $e) { return false; } } + return $exportData; } /** From 3f7a1639827797a3d2e3382064e6b9b5648af35f Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Sat, 29 Feb 2020 11:12:43 +0100 Subject: [PATCH 0432/1013] Adding failure callback to ui file uploader --- .../base/web/js/form/element/file-uploader.js | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 73bef62910644..6099c61bd3afc 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -54,13 +54,14 @@ define([ this.$fileInput = fileInput; _.extend(this.uploaderConfig, { - dropZone: $(fileInput).closest(this.dropZone), - change: this.onFilesChoosed.bind(this), - drop: this.onFilesChoosed.bind(this), - add: this.onBeforeFileUpload.bind(this), - done: this.onFileUploaded.bind(this), - start: this.onLoadingStart.bind(this), - stop: this.onLoadingStop.bind(this) + dropZone: $(fileInput).closest(this.dropZone), + change: this.onFilesChoosed.bind(this), + drop: this.onFilesChoosed.bind(this), + add: this.onBeforeFileUpload.bind(this), + fail: this.onFail.bind(this), + done: this.onFileUploaded.bind(this), + start: this.onLoadingStart.bind(this), + stop: this.onLoadingStop.bind(this) }); $(fileInput).fileupload(this.uploaderConfig); @@ -328,11 +329,11 @@ define([ * May be used for implementation of additional validation rules, * e.g. total files and a total size rules. * - * @param {Event} e - Event object. + * @param {Event} event - Event object. * @param {Object} data - File data that will be uploaded. */ - onFilesChoosed: function (e, data) { - // no option exists in fileuploader for restricting upload chains to single files; this enforces that policy + onFilesChoosed: function (event, data) { + // no option exists in file uploader for restricting upload chains to single files; this enforces that policy if (!this.isMultipleFiles) { data.files.splice(1); } @@ -341,13 +342,13 @@ define([ /** * Handler which is invoked prior to the start of a file upload. * - * @param {Event} e - Event object. + * @param {Event} event - Event object. * @param {Object} data - File data that will be uploaded. */ - onBeforeFileUpload: function (e, data) { - var file = data.files[0], - allowed = this.isFileAllowed(file), - target = $(e.target); + onBeforeFileUpload: function (event, data) { + var file = data.files[0], + allowed = this.isFileAllowed(file), + target = $(event.target); if (this.disabled()) { this.notifyError($t('The file upload field is disabled.')); @@ -356,7 +357,7 @@ define([ } if (allowed.passed) { - target.on('fileuploadsend', function (event, postData) { + target.on('fileuploadsend', function (eventBound, postData) { postData.data.append('param_name', this.paramName); }.bind(data)); @@ -386,16 +387,25 @@ define([ }); }, + /** + * @param {Event} event + * @param {Object} data + */ + onFail: function (event, data) { + console.error(data.jqXHR.responseText); + console.error(data.jqXHR.status); + }, + /** * Handler of the file upload complete event. * - * @param {Event} e + * @param {Event} event * @param {Object} data */ - onFileUploaded: function (e, data) { + onFileUploaded: function (event, data) { var uploadedFilename = data.files[0].name, - file = data.result, - error = file.error; + file = data.result, + error = file.error; error ? this.aggregateError(uploadedFilename, error) : @@ -469,10 +479,10 @@ define([ * Handler of the preview image load event. * * @param {Object} file - File associated with an image. - * @param {Event} e + * @param {Event} event */ - onPreviewLoad: function (file, e) { - var img = e.currentTarget; + onPreviewLoad: function (file, event) { + var img = event.currentTarget; file.previewWidth = img.naturalWidth; file.previewHeight = img.naturalHeight; From 314ec92a7c82b70ac7460c21b2c82085e380d292 Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Sat, 5 Sep 2020 22:23:44 +0200 Subject: [PATCH 0433/1013] Adding unit test --- .../Unit/Topology/ArgumentProcessorTest.php | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php diff --git a/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php new file mode 100644 index 0000000000000..ef1098da318ab --- /dev/null +++ b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\Amqp\Test\Unit\Topology; + +use InvalidArgumentException; +use Magento\Framework\Amqp\Topology\ArgumentProcessor; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class ArgumentProcessorTest + */ +class ArgumentProcessorTest extends TestCase +{ + /** + * @var ArgumentProcessor|MockObject + */ + private $argumentProcessor; + + /** + * @return void + */ + public function testProcessArgumentsWhenAnyArgumentIsIncorrect(): void + { + $arguments = [ + 'test' => new class { + } + ]; + + $this->expectException(InvalidArgumentException::class); + $this->argumentProcessor->processArguments($arguments); + } + + /** + * @return void + */ + public function testProcessArgumentsWhenAllArgumentAreCorrect(): void + { + $arguments = [ + 'array_type' => ['some_key' => 'some_value'], + 'numeric_value' => '25', + 'integer_value' => 26, + 'boolean_value' => false, + 'string_value' => 'test' + ]; + + $expected = [ + 'array_type' => ['A', ['some_key' => 'some_value']], + 'numeric_value' => ['I', 25], + 'integer_value' => ['I', 26], + 'boolean_value' => ['t', false], + 'string_value' => ['S', 'test'] + ]; + + $this->assertSame($expected, $this->argumentProcessor->processArguments($arguments)); + } + + /** + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->argumentProcessor = $this->getMockForTrait(ArgumentProcessor::class); + } +} From cec8daf468bf486ee5e70645683661c5d08a96ab Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Sun, 6 Sep 2020 01:48:08 -0500 Subject: [PATCH 0434/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../CatalogSearch/Model/Indexer/Fulltext.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index e226bdc6900e6..9f557412f50b9 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -6,11 +6,13 @@ namespace Magento\CatalogSearch\Model\Indexer; +use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\FullFactory; use Magento\CatalogSearch\Model\Indexer\Scope\State; use Magento\CatalogSearch\Model\Indexer\Scope\StateFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Indexer\CacheContext; use Magento\Framework\Indexer\DimensionProviderInterface; use Magento\Store\Model\StoreDimensionProvider; use Magento\Indexer\Model\ProcessManager; @@ -77,6 +79,11 @@ class Fulltext implements */ private $processManager; + /** + * @var CacheContext + */ + private $cacheContext; + /** * @param FullFactory $fullActionFactory * @param IndexerHandlerFactory $indexerHandlerFactory @@ -86,6 +93,7 @@ class Fulltext implements * @param DimensionProviderInterface $dimensionProvider * @param array $data * @param ProcessManager $processManager + * @param CacheContext $cacheContext|null * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -96,7 +104,8 @@ public function __construct( StateFactory $indexScopeStateFactory, DimensionProviderInterface $dimensionProvider, array $data, - ProcessManager $processManager = null + ProcessManager $processManager = null, + CacheContext $cacheContext = null ) { $this->fullAction = $fullActionFactory->create(['data' => $data]); $this->indexerHandlerFactory = $indexerHandlerFactory; @@ -106,6 +115,7 @@ public function __construct( $this->indexScopeState = ObjectManager::getInstance()->get(State::class); $this->dimensionProvider = $dimensionProvider; $this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class); + $this->cacheContext = $cacheContext ?? ObjectManager::getInstance()->get(CacheContext::class); } /** @@ -145,6 +155,8 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId)); $this->fulltextResource->resetSearchResultsByStore($storeId); + + $this->cacheContext->registerTags([Product::CACHE_TAG]); } else { // internal implementation works only with array $entityIds = iterator_to_array($entityIds); @@ -155,6 +167,8 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); } + + $this->cacheContext->registerEntities(Product::CACHE_TAG, $productIds); } } From 47761ec37af3422a807faf9ce52e682502832502 Mon Sep 17 00:00:00 2001 From: siimm <siim.medijainen@vaimo.com> Date: Sun, 6 Sep 2020 19:55:52 +0000 Subject: [PATCH 0435/1013] fix typo, remove empty line in test --- lib/internal/Magento/Framework/Indexer/CacheContext.php | 2 +- .../Magento/Framework/Indexer/Test/Unit/CacheContextTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Indexer/CacheContext.php b/lib/internal/Magento/Framework/Indexer/CacheContext.php index 1374a8c3f2089..b29aafa054997 100644 --- a/lib/internal/Magento/Framework/Indexer/CacheContext.php +++ b/lib/internal/Magento/Framework/Indexer/CacheContext.php @@ -73,6 +73,6 @@ public function getIdentities() $identities[] = $cacheTag . '_' . $id; } } - return array_unique(array_merge($identities, array_unique($this->tags)); + return array_unique(array_merge($identities, array_unique($this->tags))); } } diff --git a/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php b/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php index 90606435a4487..a13d5d54aafe7 100644 --- a/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php +++ b/lib/internal/Magento/Framework/Indexer/Test/Unit/CacheContextTest.php @@ -56,4 +56,3 @@ public function getTagsDataProvider() ]; } } - From 4b832de25424bd0b6049f6de43167edf79a8968c Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Wed, 5 Aug 2020 22:53:45 +0800 Subject: [PATCH 0436/1013] magento/adobe-stock-integration#1523: Switching between Views does not change the selected folder. [Media Gallery] - modified function to select folder when switching view --- .../web/js/directory/directoryTree.js | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) 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 index decc337e1b83c..f8d129e9d0489 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -26,7 +26,7 @@ define([ filterChips: '${ $.filterChipsProvider }' }, listens: { - '${ $.provider }:params.filters.path': 'clearFiltersHandle' + '${ $.provider }:params.filters.path': 'updateSelectedDirectory' }, viewConfig: [{ component: 'Magento_MediaGalleryUi/js/directory/directories', @@ -220,7 +220,7 @@ define([ this.firejsTreeEvents(); }.bind(this)); } else { - this.checkChipFiltersState(); + this.updateSelectedDirectory(); } }.bind(this)); }.bind(this)); @@ -239,7 +239,7 @@ define([ }.bind(this)); $(this.directoryTreeSelector).on('loaded.jstree', function () { - this.checkChipFiltersState(); + this.updateSelectedDirectory(); }.bind(this)); }, @@ -247,7 +247,7 @@ define([ /** * Verify directory filter on init event, select folder per directory filter state */ - checkChipFiltersState: function () { + updateSelectedDirectory: function () { var currentFilterPath = this.filterChips().filters.path, isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), currentTreePath; @@ -260,6 +260,12 @@ define([ } else { this.selectStorageRoot(); } + + if (_.isUndefined(currentFilterPath)) { + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); + } }, /** @@ -281,8 +287,7 @@ define([ * @param {String} currentFilterPath */ isFiltersApplied: function (currentFilterPath) { - return !_.isUndefined(currentFilterPath) && currentFilterPath !== '' && - currentFilterPath !== 'wysiwyg' && currentFilterPath !== 'catalog/category'; + return !_.isUndefined(currentFilterPath); }, /** @@ -302,17 +307,6 @@ define([ }, - /** - * 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 * From 86b76a07cef9c299127fccc8f315b3a11212b572 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Thu, 6 Aug 2020 01:05:56 +0800 Subject: [PATCH 0437/1013] magento/adobe-stock-integration#1523: Switching between Views does not change the selected folder. [Media Gallery] - modified functions --- .../web/js/directory/directoryTree.js | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) 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 index f8d129e9d0489..5981a7f09355d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -252,19 +252,17 @@ define([ isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), currentTreePath; - currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : - Base64.idDecode(window.MediabrowserUtility.pathId); - - if (this.folderExistsInTree(currentTreePath)) { - this.locateNode(currentTreePath); + if (_.isUndefined(currentFilterPath)) { + this.clearFiltersHandle(); } else { - this.selectStorageRoot(); - } + currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : + Base64.idDecode(window.MediabrowserUtility.pathId); - if (_.isUndefined(currentFilterPath)) { - $(this.directoryTreeSelector).jstree('deselect_all'); - this.activeNode(null); - this.directories().setInActive(); + if (this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); + } } }, @@ -287,7 +285,8 @@ define([ * @param {String} currentFilterPath */ isFiltersApplied: function (currentFilterPath) { - return !_.isUndefined(currentFilterPath); + return !_.isUndefined(currentFilterPath) && currentFilterPath !== '' + && currentFilterPath !== 'catalog/category'; }, /** @@ -307,6 +306,12 @@ define([ }, + clearFiltersHandle: function () { + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); + }, + /** * Set active node filter, or deselect if the same node clicked * From 6623424de1310608c24589d305b5c0ed2741396e Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Fri, 7 Aug 2020 04:15:43 +0800 Subject: [PATCH 0438/1013] magento/adobe-stock-integration#1523: Switching between Views does not change the selected folder. [Media Gallery] - implement request change and mftf test --- .../AssertFolderIsChangedActionGroup.xml | 25 ++++++++ ...nMediaGallerySwitchingBetweenViewsTest.xml | 63 +++++++++++++++++++ .../web/js/directory/directoryTree.js | 23 ++++--- 3 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertFolderIsChangedActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySwitchingBetweenViewsTest.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertFolderIsChangedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertFolderIsChangedActionGroup.xml new file mode 100644 index 0000000000000..090dbed8b4f78 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertFolderIsChangedActionGroup.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="AssertFolderIsChangedActionGroup"> + <annotations> + <description>Assert that folder is changed</description> + </annotations> + <arguments> + <argument name="newSelectedFolder" type="string"/> + <argument name="oldSelectedFolder" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + + <assertNotEquals stepKey="assertNotEqual"> + <actualResult type="string">{{newSelectedFolder}}</actualResult> + <expectedResult type="string">{{oldSelectedFolder}}</expectedResult> + </assertNotEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySwitchingBetweenViewsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySwitchingBetweenViewsTest.xml new file mode 100644 index 0000000000000..01b8c27b7371d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySwitchingBetweenViewsTest.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="AdminMediaGallerySwitchingBetweenViewsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1523"/> + <title value="User switches between Views and checks if the folder is changed"/> + <stories value="User switches between Views and checks if the folder is changed"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/5060037"/> + <description value="User switches between Views and checks if the folder is changed"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeleteGridViewActionGroup" stepKey="deleteView"> + <argument name="viewToDelete" value="New View"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + <actionGroup ref="AdminEnhancedMediaGallerySaveCustomViewActionGroup" stepKey="saveCustomView"> + <argument name="viewName" value="New View"/> + </actionGroup> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="selectDefaultView"> + <argument name="selectView" value="Default View"/> + </actionGroup> + <actionGroup ref="AssertFolderIsChangedActionGroup" stepKey="assertFolderIsChanged"> + <argument name="newSelectedFolder" value="category" /> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="switchBackToNewView"> + <argument name="selectView" value="New View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup" stepKey="assertFilterApplied"> + <argument name="resultValue" value="{{AdminMediaGalleryFolderData.name}}"/> + </actionGroup> + </test> +</tests> 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 index 5981a7f09355d..a0959fa90c7ba 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -254,15 +254,16 @@ define([ if (_.isUndefined(currentFilterPath)) { this.clearFiltersHandle(); - } else { - currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : - Base64.idDecode(window.MediabrowserUtility.pathId); + return; + } - if (this.folderExistsInTree(currentTreePath)) { - this.locateNode(currentTreePath); - } else { - this.selectStorageRoot(); - } + currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : + Base64.idDecode(window.MediabrowserUtility.pathId); + + if (this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); } }, @@ -285,8 +286,7 @@ define([ * @param {String} currentFilterPath */ isFiltersApplied: function (currentFilterPath) { - return !_.isUndefined(currentFilterPath) && currentFilterPath !== '' - && currentFilterPath !== 'catalog/category'; + return !_.isUndefined(currentFilterPath) && currentFilterPath !== ''; }, /** @@ -306,6 +306,9 @@ define([ }, + /** + * Clear filters + */ clearFiltersHandle: function () { $(this.directoryTreeSelector).jstree('deselect_all'); this.activeNode(null); From 8cda8d43b302f4cbc491119e9e2d150cf4ed438a Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Fri, 7 Aug 2020 22:17:49 +0800 Subject: [PATCH 0439/1013] magento/adobe-stock-integration#1523: Switching between Views does not change the selected folder. [Media Gallery] - fix failed static test --- .../view/adminhtml/web/js/directory/directoryTree.js | 1 + 1 file changed, 1 insertion(+) 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 index a0959fa90c7ba..7f0973a543761 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -254,6 +254,7 @@ define([ if (_.isUndefined(currentFilterPath)) { this.clearFiltersHandle(); + return; } From 567f37d7c3fdb52cb870fab7917e4f169b1a3ade Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Thu, 13 Aug 2020 17:20:59 +0100 Subject: [PATCH 0440/1013] magento/magento2#29411: Directory Tree refactoring --- .../Model/Directory/Command/CreateByPaths.php | 2 +- ...{DirectoriesTree.php => DirectoryTree.php} | 8 +- .../ui_component/media_gallery_listing.xml | 2 +- .../standalone_media_gallery_listing.xml | 2 +- .../deleteImageWithDetailConfirmation.js | 23 +- .../adminhtml/web/js/directory/directories.js | 95 +++--- .../web/js/directory/directoryTree.js | 289 +++++++----------- 7 files changed, 177 insertions(+), 244 deletions(-) rename app/code/Magento/MediaGalleryUi/Ui/Component/{DirectoriesTree.php => DirectoryTree.php} (81%) diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php index f33c22a18b4b8..d0ba786c7084e 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php @@ -78,7 +78,7 @@ public function execute(array $paths): void if (!empty($failedPaths)) { throw new CouldNotSaveException( __( - 'Could not save directories: %paths', + 'Could not create directories: %paths', [ 'paths' => implode(' ,', $failedPaths) ] diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php similarity index 81% rename from app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php rename to app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php index 4047a4fcb98d8..269bc1f8bcba7 100644 --- a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoryTree.php @@ -14,7 +14,7 @@ /** * Directories tree component */ -class DirectoriesTree extends Container +class DirectoryTree extends Container { /** * @var UrlInterface @@ -50,9 +50,9 @@ public function prepare(): void 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") + '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/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml index 49206043725f9..66731b1cbae6f 100644 --- 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 @@ -219,7 +219,7 @@ </container> </listingToolbar> <container name="media_gallery_directories" - class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + class="Magento\MediaGalleryUi\Ui\Component\DirectoryTree" template="Magento_MediaGalleryUi/grid/directories/directoryTree" component="Magento_MediaGalleryUi/js/directory/directoryTree"/> <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> 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 index 655178c104492..3656a8ea25f74 100644 --- 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 @@ -206,7 +206,7 @@ </container> </listingToolbar> <container name="media_gallery_directories" - class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + class="Magento\MediaGalleryUi\Ui\Component\DirectoryTree" template="Magento_MediaGalleryUi/grid/directories/directoryTree" component="Magento_MediaGalleryUi/js/directory/directoryTree"/> <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> 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 index ed40674df20f0..28c021fe4728f 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -21,25 +21,24 @@ define([ * @param {String} deleteImageUrl */ deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { - var confirmationContent = $t('%1 Are you sure you want to delete "%2" image(s)?') + var confirmationContent = $t('%1Are you sure you want to delete "%2" image(s)?') .replace('%2', Object.keys(recordsIds).length), deferred = $.Deferred(); - getDetails(imageDetailsUrl, recordsIds) - .then(function (imageDetails) { + getDetails(imageDetailsUrl, recordsIds).then(function (images) { confirmationContent = confirmationContent.replace( '%1', - this.getRecordRelatedContentMessage(imageDetails) + this.getRecordRelatedContentMessage(images) + ' ' ); }.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); - }); - }); + 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(); }, 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 index d7f756d8bbd90..6d8d38a1ca1d6 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -23,6 +23,7 @@ define([ deleteButtonSelector: '#delete_folder', createFolderButtonSelector: '#create_folder', messageDelay: 5, + selectedFolder: null, messagesName: 'media_gallery_listing.media_gallery_listing.messages', modules: { directoryTree: '${ $.parentName }.media_gallery_directories', @@ -47,51 +48,57 @@ define([ */ initEvents: function () { $(this.deleteButtonSelector).on('delete_folder', function () { - this.getConfirmationPopupDeleteFolder(); + this.deleteFolder(); }.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)); + this.createFolder(); + }.bind(this)); + }, - }.bind(this)).fail(function (error) { - uiAlert({ - content: error - }); + /** + * Show confirmation popup and create folder based on user input + */ + createFolder: 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', + }); + }.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)); + /** + * Close modal + */ + click: function () { + this.closeModal(); + } + }, { + text: $t('Confirm'), + class: 'action-primary action-accept' + }] + }); }, /** @@ -101,11 +108,11 @@ define([ * @returns {String} */ getNewFolderPath: function (folderName) { - var selectedFolder = _.isUndefined(this.selectedFolder()) || - _.isNull(this.selectedFolder()) ? '/' : this.selectedFolder(), - folderToCreate = selectedFolder !== '/' ? selectedFolder + '/' + folderName : folderName; + if (_.isUndefined(this.selectedFolder()) || _.isNull(this.selectedFolder())) { + return folderName; + } - return folderToCreate; + return this.selectedFolder() + '/' + folderName; }, /** @@ -136,7 +143,7 @@ define([ /** * Confirmation popup for delete folder action. */ - getConfirmationPopupDeleteFolder: function () { + deleteFolder: function () { confirm({ title: $t('Are you sure you want to delete this folder?'), modalClass: 'delete-folder-confirmation-popup', 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 index 7f0973a543761..9cf5144808f7d 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -20,13 +20,14 @@ define([ filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', directoryTreeSelector: '#media-gallery-directory-tree', getDirectoryTreeUrl: 'media_gallery/directories/gettree', - jsTreeReloaded: null, + createDirectoryUrl: 'media_gallery/directories/create', + activeNode: null, modules: { directories: '${ $.name }_directories', filterChips: '${ $.filterChipsProvider }' }, listens: { - '${ $.provider }:params.filters.path': 'updateSelectedDirectory' + '${ $.provider }:params.filters.path': 'selectTreeFolder' }, viewConfig: [{ component: 'Magento_MediaGalleryUi/js/directory/directories', @@ -49,7 +50,8 @@ define([ this.renderDirectoryTree().then(function () { this.initEvents(); }.bind(this)); - }.bind(this)); + }.bind(this) + ); return this; }, @@ -58,66 +60,57 @@ define([ * 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 { + if (!isFolderCreated) { this.createTree(data); + return; } + + this.getJsonTree().then(function (newData) { + this.createTree(newData); + }.bind(this)); }.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, + var requestedDirectoryPath = this.getRequestedDirectory(), deferred = $.Deferred(), - decodedPath, pathArray; - if (currentTreePath) { - decodedPath = Base64.idDecode(currentTreePath); - - if (!this.isDirectoryExist(directories[0], decodedPath)) { - pathArray = this.convertPathToPathsArray(decodedPath); + if (!requestedDirectoryPath) { + deferred.resolve(false); - $.each(pathArray, function (i, val) { - if (this.isDirectoryExist(directories[0], val)) { - pathArray.splice(i, 1); - } - }.bind(this)); + return deferred.promise(); + } - createDirectory( - this.createDirectoryUrl, - pathArray - ).then(function () { - deferred.resolve(true); - }); - } else { - deferred.resolve(false); - } - } else { + if (this.isDirectoryExist(directories[0], requestedDirectoryPath)) { deferred.resolve(false); + + return deferred.promise(); } + pathArray = this.convertPathToPathsArray(requestedDirectoryPath); + + $.each(pathArray, function (index, directoryId) { + if (this.isDirectoryExist(directories[0], directoryId)) { + pathArray.splice(index, 1); + } + }.bind(this)); + + createDirectory( + this.createDirectoryUrl, + pathArray + ).then(function () { + deferred.resolve(true); + }); + return deferred.promise(); }, @@ -125,33 +118,22 @@ define([ * Verify if directory exists in array * * @param {Array} directories - * @param {String} directoryId + * @param {String} path */ - 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); - } + isDirectoryExist: function (directories, path) { + var i; + + for (i = 0; i < directories.length; i++) { + if (directories[i].attr.id === path + || directories[i].children + && directories[i].children.length + && this.isDirectoryExist(directories[i].children, path) + ) { + return true; } } - recurse(directories, directoryId); - - return found; + return false; }, /** @@ -199,7 +181,7 @@ define([ /** * Remove ability to multiple select on nodes */ - overrideMultiselectBehavior: function () { + disableMultiselectBehavior: function () { $.jstree.defaults.ui['select_range_modifier'] = false; $.jstree.defaults.ui['select_multiple_modifier'] = false; }, @@ -208,21 +190,12 @@ define([ * Handle jstree events */ initEvents: function () { - this.firejsTreeEvents(); - this.overrideMultiselectBehavior(); + this.initJsTreeEvents(); + this.disableMultiselectBehavior(); $(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.updateSelectedDirectory(); - } - }.bind(this)); + this.renderDirectoryTree().then(function () { + this.initJsTreeEvents(); }.bind(this)); }.bind(this)); }, @@ -230,64 +203,46 @@ define([ /** * Fire event for jstree component */ - firejsTreeEvents: function () { + initJsTreeEvents: function () { $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { - var path = $(data.rslt.obj).data('path'); - - this.setActiveNodeFilter(path); - this.setJsTreeReloaded(false); + this.toggleSelectedDirectory($(data.rslt.obj).data('path')); }.bind(this)); $(this.directoryTreeSelector).on('loaded.jstree', function () { - this.updateSelectedDirectory(); - }.bind(this)); + var path = this.getRequestedDirectory() || this.filterChips().filters.path; + if (this.activeNode() !== path) { + this.selectFolder(path); + } + }.bind(this)); }, /** * Verify directory filter on init event, select folder per directory filter state */ - updateSelectedDirectory: function () { - var currentFilterPath = this.filterChips().filters.path, - isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), - currentTreePath; - - if (_.isUndefined(currentFilterPath)) { - this.clearFiltersHandle(); - - return; - } - - currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : - Base64.idDecode(window.MediabrowserUtility.pathId); - - if (this.folderExistsInTree(currentTreePath)) { - this.locateNode(currentTreePath); - } else { - this.selectStorageRoot(); - } + selectTreeFolder: function (path) { + this.isFolderRendered(path) ? this.locateNode(path) : this.selectStorageRoot(); }, /** * Verify if directory exists in folder tree * * @param {String} path + * @return {Boolean} */ - folderExistsInTree: function (path) { - if (!_.isUndefined(path)) { - return $('#' + path.replace(/\//g, '\\/')).length === 1; - } - - return false; + isFolderRendered: function (path) { + return _.isUndefined(path) ? false : $('#' + path.replace(/\//g, '\\/')).length === 1; }, /** - * Check if need to select directory by filters state + * Get directory requested from MediabrowserUtility * - * @param {String} currentFilterPath + * @return {String|Null} */ - isFiltersApplied: function (currentFilterPath) { - return !_.isUndefined(currentFilterPath) && currentFilterPath !== ''; + getRequestedDirectory: function () { + return !_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '' + ? Base64.idDecode(window.MediabrowserUtility.pathId) + : null; }, /** @@ -296,62 +251,40 @@ define([ * @param {String} path */ locateNode: function (path) { - var selectedId = $(this.directoryTreeSelector).jstree('get_selected').attr('id'); - - if (path === selectedId) { + if (path === $(this.directoryTreeSelector).jstree('get_selected').attr('id')) { return; } path = path.replace(/\//g, '\\/'); $(this.directoryTreeSelector).jstree('open_node', '#' + path); $(this.directoryTreeSelector).jstree('select_node', '#' + path, true); - - }, - - /** - * Clear filters - */ - clearFiltersHandle: function () { - $(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 + * @param {String} path */ - setActiveNodeFilter: function (nodePath) { - - if (this.activeNode() === nodePath && !this.jsTreeReloaded) { - this.selectStorageRoot(); - } else { - this.selectFolder(nodePath); - } + toggleSelectedDirectory: function (path) { + this.activeNode() === path ? this.selectStorageRoot() : this.selectFolder(path); }, /** * 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) - ); + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setInActive(); + }.bind(this) + ); + this.dropFilter(); }, /** @@ -360,7 +293,9 @@ define([ * @param {String} path */ selectFolder: function (path) { - this.activeNode(path); + if (_.isUndefined(path) || _.isNull(path)) { + return; + } this.waitForCondition( function () { @@ -372,11 +307,12 @@ define([ ); this.applyFilter(path); + this.activeNode(path); }, /** - * Remove active node from directory tree, and select next - */ + * Remove active node from directory tree, and select next + */ removeNode: function () { $(this.directoryTreeSelector).jstree('remove'); }, @@ -387,13 +323,29 @@ define([ * @param {String} path */ applyFilter: function (path) { + this.filterChips().set( + 'applied', + $.extend( + true, + {}, + this.filterChips().get('applied'), + { + path: path + } + ) + ); + }, + + /** + * Drop path filter + */ + dropFilter: function () { var filters = {}, applied = this.filterChips().get('applied'); filters = $.extend(true, filters, applied); - filters.path = path; + delete filters.path; this.filterChips().set('applied', filters); - }, /** @@ -404,7 +356,6 @@ define([ this.getJsonTree().then(function (data) { this.createTree(data); - this.setJsTreeReloaded(true); this.initEvents(); deferred.resolve(); }.bind(this)); @@ -416,35 +367,11 @@ define([ * Get json data for jstree */ getJsonTree: function () { - var deferred = $.Deferred(); - - $.ajax({ + return $.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; - } + dataType: 'json' }); - - return deferred.promise(); }, /** @@ -454,7 +381,7 @@ define([ */ createTree: function (data) { $(this.directoryTreeSelector).jstree({ - plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], + plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], vcheckbox: { 'two_state': true, 'real_checkboxes': true From 17ea5667e61c730ff53f062f97e4d542c9e5b0ed Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 18 Aug 2020 13:23:24 +0100 Subject: [PATCH 0441/1013] magento/magento2#29411: Naming update --- .../Adminhtml/Directories/GetTree.php | 16 ++--- ...GetFolderTree.php => GetDirectoryTree.php} | 59 ++++++------------- app/code/Magento/MediaGalleryUi/etc/di.xml | 5 -- 3 files changed, 28 insertions(+), 52 deletions(-) rename app/code/Magento/MediaGalleryUi/Model/Directories/{GetFolderTree.php => GetDirectoryTree.php} (77%) diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php index d4885cae055dd..d9a38895e1fa0 100644 --- a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php @@ -11,7 +11,7 @@ use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\Controller\Result\Json; use Magento\Framework\Controller\ResultFactory; -use Magento\MediaGalleryUi\Model\Directories\GetFolderTree; +use Magento\MediaGalleryUi\Model\Directories\GetDirectoryTree; use Psr\Log\LoggerInterface; /** @@ -33,25 +33,25 @@ class GetTree extends Action implements HttpGetActionInterface private $logger; /** - * @var GetFolderTree + * @var GetDirectoryTree */ - private $getFolderTree; + private $getDirectoryTree; /** * Constructor * * @param Action\Context $context * @param LoggerInterface $logger - * @param GetFolderTree $getFolderTree + * @param GetDirectoryTree $getDirectoryTree */ public function __construct( Action\Context $context, LoggerInterface $logger, - GetFolderTree $getFolderTree + GetDirectoryTree $getDirectoryTree ) { parent::__construct($context); $this->logger = $logger; - $this->getFolderTree = $getFolderTree; + $this->getDirectoryTree = $getDirectoryTree; } /** * @inheritdoc @@ -59,7 +59,9 @@ public function __construct( public function execute() { try { - $responseContent[] = $this->getFolderTree->execute(); + $responseContent = [ + $this->getDirectoryTree->execute() + ]; $responseCode = self::HTTP_OK; } catch (\Exception $exception) { $this->logger->critical($exception); diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetDirectoryTree.php similarity index 77% rename from app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php rename to app/code/Magento/MediaGalleryUi/Model/Directories/GetDirectoryTree.php index f0998a3e120f2..35e34a7e5532c 100644 --- a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetDirectoryTree.php @@ -7,58 +7,61 @@ namespace Magento\MediaGalleryUi\Model\Directories; +use Magento\Framework\App\Filesystem\DirectoryList; 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 + * Build media gallery folder tree structure by path */ -class GetFolderTree +class GetDirectoryTree { /** * @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 + public function execute(): array { - return $this->buildFolderTree($this->getDirectories(), $skipRoot); + $tree = [ + 'name' => 'root', + 'path' => '/', + 'children' => [] + ]; + $directories = $this->getDirectories(); + foreach ($directories as $idx => &$node) { + $node['children'] = []; + $result = $this->findParent($node, $tree); + $parent = &$result['treeNode']; + + $parent['children'][] = &$directories[$idx]; + } + return $tree['children']; } /** @@ -72,7 +75,7 @@ private function getDirectories(): array $directories = []; /** @var Read $directory */ - $directory = $this->filesystem->getDirectoryRead($this->path); + $directory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); if (!$directory->isDirectory()) { return $directories; @@ -96,30 +99,6 @@ private function getDirectories(): array 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 * diff --git a/app/code/Magento/MediaGalleryUi/etc/di.xml b/app/code/Magento/MediaGalleryUi/etc/di.xml index a8c4e2a8d8963..6ed3a98bbf03a 100644 --- a/app/code/Magento/MediaGalleryUi/etc/di.xml +++ b/app/code/Magento/MediaGalleryUi/etc/di.xml @@ -28,11 +28,6 @@ </argument> </arguments> </type> - <type name="Magento\MediaGalleryUi\Model\Directories\GetFolderTree"> - <arguments> - <argument name="path" xsi:type="string">media</argument> - </arguments> - </type> <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> <plugin name="createMediaGalleryThumbnails" type="Magento\MediaGalleryUi\Plugin\CreateThumbnails"/> </type> From c918a54a77aee4abdec9c53a30fac8833542b81c Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 18 Aug 2020 14:21:19 +0100 Subject: [PATCH 0442/1013] magento/magento2#29411: Updated requested directory selection behaviour --- .../web/js/directory/directoryTree.js | 244 ++++++++++++------ 1 file changed, 163 insertions(+), 81 deletions(-) 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 index 9cf5144808f7d..a5c5de1ccdf73 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -21,13 +21,13 @@ define([ directoryTreeSelector: '#media-gallery-directory-tree', getDirectoryTreeUrl: 'media_gallery/directories/gettree', createDirectoryUrl: 'media_gallery/directories/create', - activeNode: null, + jsTreeReloaded: null, modules: { directories: '${ $.name }_directories', filterChips: '${ $.filterChipsProvider }' }, listens: { - '${ $.provider }:params.filters.path': 'selectTreeFolder' + '${ $.provider }:params.filters.path': 'updateSelectedDirectory' }, viewConfig: [{ component: 'Magento_MediaGalleryUi/js/directory/directories', @@ -62,45 +62,57 @@ define([ renderDirectoryTree: function () { return this.getJsonTree().then(function (data) { this.createFolderIfNotExists(data).then(function (isFolderCreated) { - if (!isFolderCreated) { + if (isFolderCreated) { + this.getJsonTree().then(function (newData) { + this.createTree(newData); + }.bind(this)); + } else { this.createTree(data); - return; } - - this.getJsonTree().then(function (newData) { - this.createTree(newData); - }.bind(this)); }.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 requestedDirectoryPath = this.getRequestedDirectory(), + var isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + currentTreePath = isMediaBrowser ? window.MediabrowserUtility.pathId : null, deferred = $.Deferred(), + decodedPath, pathArray; - if (!requestedDirectoryPath) { + if (!currentTreePath) { deferred.resolve(false); return deferred.promise(); } - if (this.isDirectoryExist(directories[0], requestedDirectoryPath)) { + decodedPath = Base64.idDecode(currentTreePath); + + if (this.isDirectoryExist(directories[0], decodedPath)) { deferred.resolve(false); return deferred.promise(); } - pathArray = this.convertPathToPathsArray(requestedDirectoryPath); + pathArray = this.convertPathToPathsArray(decodedPath); - $.each(pathArray, function (index, directoryId) { - if (this.isDirectoryExist(directories[0], directoryId)) { - pathArray.splice(index, 1); + $.each(pathArray, function (i, val) { + if (this.isDirectoryExist(directories[0], val)) { + pathArray.splice(i, 1); } }.bind(this)); @@ -118,22 +130,33 @@ define([ * Verify if directory exists in array * * @param {Array} directories - * @param {String} path + * @param {String} directoryId */ - isDirectoryExist: function (directories, path) { - var i; - - for (i = 0; i < directories.length; i++) { - if (directories[i].attr.id === path - || directories[i].children - && directories[i].children.length - && this.isDirectoryExist(directories[i].children, path) - ) { - return true; + 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); + } } } - return false; + recurse(directories, directoryId); + + return found; }, /** @@ -181,7 +204,7 @@ define([ /** * Remove ability to multiple select on nodes */ - disableMultiselectBehavior: function () { + overrideMultiselectBehavior: function () { $.jstree.defaults.ui['select_range_modifier'] = false; $.jstree.defaults.ui['select_multiple_modifier'] = false; }, @@ -190,12 +213,21 @@ define([ * Handle jstree events */ initEvents: function () { - this.initJsTreeEvents(); - this.disableMultiselectBehavior(); + this.firejsTreeEvents(); + this.overrideMultiselectBehavior(); $(window).on('reload.MediaGallery', function () { - this.renderDirectoryTree().then(function () { - this.initJsTreeEvents(); + 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.updateSelectedDirectory(); + } + }.bind(this)); }.bind(this)); }.bind(this)); }, @@ -203,78 +235,124 @@ define([ /** * Fire event for jstree component */ - initJsTreeEvents: function () { + firejsTreeEvents: function () { $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { - this.toggleSelectedDirectory($(data.rslt.obj).data('path')); + this.setActiveNodeFilter($(data.rslt.obj).data('path')); + this.setJsTreeReloaded(false); }.bind(this)); $(this.directoryTreeSelector).on('loaded.jstree', function () { - var path = this.getRequestedDirectory() || this.filterChips().filters.path; - - if (this.activeNode() !== path) { - this.selectFolder(path); - } + this.updateSelectedDirectory(); }.bind(this)); + }, /** * Verify directory filter on init event, select folder per directory filter state */ - selectTreeFolder: function (path) { - this.isFolderRendered(path) ? this.locateNode(path) : this.selectStorageRoot(); + updateSelectedDirectory: function () { + var currentFilterPath = this.filterChips().filters.path, + requestedDirectory = this.getRequestedDirectory(), + currentTreePath; + + if (_.isUndefined(currentFilterPath)) { + this.clearFiltersHandle(); + + return; + } + + currentTreePath = this.isFilterApplied(currentFilterPath) || _.isNull(requestedDirectory) + ? currentFilterPath + : requestedDirectory; + + if (this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); + } }, /** * Verify if directory exists in folder tree * * @param {String} path - * @return {Boolean} */ - isFolderRendered: function (path) { - return _.isUndefined(path) ? false : $('#' + path.replace(/\//g, '\\/')).length === 1; + folderExistsInTree: function (path) { + if (!_.isUndefined(path)) { + return $('#' + path.replace(/\//g, '\\/')).length === 1; + } + + return false; }, - /** - * Get directory requested from MediabrowserUtility - * - * @return {String|Null} - */ getRequestedDirectory: function () { - return !_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '' + return (!_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '') ? Base64.idDecode(window.MediabrowserUtility.pathId) : null; }, + /** + * Check if need to select directory by filters state + * + * @param {String} currentFilterPath + */ + isFilterApplied: function (currentFilterPath) { + return !_.isUndefined(currentFilterPath) && currentFilterPath !== ''; + }, + /** * Locate and higlight node in jstree by path id. * * @param {String} path */ locateNode: function (path) { - if (path === $(this.directoryTreeSelector).jstree('get_selected').attr('id')) { + 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); + + }, + + /** + * Clear filters + */ + clearFiltersHandle: function () { + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); }, /** * Set active node filter, or deselect if the same node clicked * - * @param {String} path + * @param {String} nodePath */ - toggleSelectedDirectory: function (path) { - this.activeNode() === path ? this.selectStorageRoot() : this.selectFolder(path); + 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'); - this.activeNode(null); + filters = $.extend(true, filters, applied); + delete filters.path; + this.filterChips().set('applied', filters); + this.activeNode(null); this.waitForCondition( function () { return _.isUndefined(this.directories()); @@ -284,7 +362,6 @@ define([ }.bind(this) ); - this.dropFilter(); }, /** @@ -293,9 +370,7 @@ define([ * @param {String} path */ selectFolder: function (path) { - if (_.isUndefined(path) || _.isNull(path)) { - return; - } + this.activeNode(path); this.waitForCondition( function () { @@ -307,7 +382,6 @@ define([ ); this.applyFilter(path); - this.activeNode(path); }, /** @@ -323,28 +397,11 @@ define([ * @param {String} path */ applyFilter: function (path) { - this.filterChips().set( - 'applied', - $.extend( - true, - {}, - this.filterChips().get('applied'), - { - path: path - } - ) - ); - }, - - /** - * Drop path filter - */ - dropFilter: function () { var filters = {}, applied = this.filterChips().get('applied'); filters = $.extend(true, filters, applied); - delete filters.path; + filters.path = path; this.filterChips().set('applied', filters); }, @@ -356,6 +413,7 @@ define([ this.getJsonTree().then(function (data) { this.createTree(data); + this.setJsTreeReloaded(true); this.initEvents(); deferred.resolve(); }.bind(this)); @@ -367,11 +425,35 @@ define([ * Get json data for jstree */ getJsonTree: function () { - return $.ajax({ + var deferred = $.Deferred(); + + $.ajax({ url: this.getDirectoryTreeUrl, type: 'GET', - dataType: 'json' + 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(); }, /** @@ -381,7 +463,7 @@ define([ */ createTree: function (data) { $(this.directoryTreeSelector).jstree({ - plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], + plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], vcheckbox: { 'two_state': true, 'real_checkboxes': true From 3f02497278ef8b0cce764d7b43ad72c6500c3bbf Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 18 Aug 2020 14:33:35 +0100 Subject: [PATCH 0443/1013] magento/magento2#29411: Formatting fixes --- .../web/js/directory/directoryTree.js | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) 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 index a5c5de1ccdf73..4749593f08d4f 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -21,6 +21,7 @@ define([ directoryTreeSelector: '#media-gallery-directory-tree', getDirectoryTreeUrl: 'media_gallery/directories/gettree', createDirectoryUrl: 'media_gallery/directories/create', + deleteDirectoryUrl: 'media_gallery/directories/delete', jsTreeReloaded: null, modules: { directories: '${ $.name }_directories', @@ -88,27 +89,23 @@ define([ * @param {Array} directories */ createFolderIfNotExists: function (directories) { - var isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), - currentTreePath = isMediaBrowser ? window.MediabrowserUtility.pathId : null, + var requestedDirectory = this.getRequestedDirectory(), deferred = $.Deferred(), - decodedPath, pathArray; - if (!currentTreePath) { + if (_.isNull(requestedDirectory)) { deferred.resolve(false); return deferred.promise(); } - decodedPath = Base64.idDecode(currentTreePath); - - if (this.isDirectoryExist(directories[0], decodedPath)) { + if (this.isDirectoryExist(directories[0], requestedDirectory)) { deferred.resolve(false); return deferred.promise(); } - pathArray = this.convertPathToPathsArray(decodedPath); + pathArray = this.convertPathToPathsArray(requestedDirectory); $.each(pathArray, function (i, val) { if (this.isDirectoryExist(directories[0], val)) { @@ -204,7 +201,7 @@ define([ /** * Remove ability to multiple select on nodes */ - overrideMultiselectBehavior: function () { + disableMultiselectBehavior : function () { $.jstree.defaults.ui['select_range_modifier'] = false; $.jstree.defaults.ui['select_multiple_modifier'] = false; }, @@ -213,8 +210,8 @@ define([ * Handle jstree events */ initEvents: function () { - this.firejsTreeEvents(); - this.overrideMultiselectBehavior(); + this.initJsTreeEvents(); + this.disableMultiselectBehavior(); $(window).on('reload.MediaGallery', function () { this.getJsonTree().then(function (data) { @@ -222,7 +219,7 @@ define([ if (isCreated) { this.renderDirectoryTree().then(function () { this.setJsTreeReloaded(true); - this.firejsTreeEvents(); + this.initJsTreeEvents(); }.bind(this)); } else { this.updateSelectedDirectory(); @@ -235,7 +232,7 @@ define([ /** * Fire event for jstree component */ - firejsTreeEvents: function () { + initJsTreeEvents: function () { $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { this.setActiveNodeFilter($(data.rslt.obj).data('path')); this.setJsTreeReloaded(false); @@ -244,7 +241,6 @@ define([ $(this.directoryTreeSelector).on('loaded.jstree', function () { this.updateSelectedDirectory(); }.bind(this)); - }, /** @@ -285,6 +281,11 @@ define([ return false; }, + /** + * Get requested directory from MediabrowserUtility + * + * @returns {String|null} + */ getRequestedDirectory: function () { return (!_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '') ? Base64.idDecode(window.MediabrowserUtility.pathId) @@ -306,9 +307,7 @@ define([ * @param {String} path */ locateNode: function (path) { - var selectedId = $(this.directoryTreeSelector).jstree('get_selected').attr('id'); - - if (path === selectedId) { + if (path === $(this.directoryTreeSelector).jstree('get_selected').attr('id')) { return; } path = path.replace(/\//g, '\\/'); @@ -332,7 +331,6 @@ define([ * @param {String} nodePath */ setActiveNodeFilter: function (nodePath) { - if (this.activeNode() === nodePath && !this.jsTreeReloaded) { this.selectStorageRoot(); } else { @@ -361,7 +359,6 @@ define([ this.directories().setInActive(); }.bind(this) ); - }, /** From fe307294d112c79f58fb55564e60e7aaef75776c Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 18 Aug 2020 18:24:51 +0100 Subject: [PATCH 0444/1013] magento/magento2#29411: Fixed static tests --- .../view/adminhtml/web/js/directory/directoryTree.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 4749593f08d4f..0309b11296181 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -201,7 +201,7 @@ define([ /** * Remove ability to multiple select on nodes */ - disableMultiselectBehavior : function () { + disableMultiselectBehavior: function () { $.jstree.defaults.ui['select_range_modifier'] = false; $.jstree.defaults.ui['select_multiple_modifier'] = false; }, @@ -287,7 +287,7 @@ define([ * @returns {String|null} */ getRequestedDirectory: function () { - return (!_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '') + return !_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '' ? Base64.idDecode(window.MediabrowserUtility.pathId) : null; }, From faceaacb768d9dec1030208f3b054f16b4e3796b Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 24 Aug 2020 17:56:31 +0100 Subject: [PATCH 0445/1013] magento/magento2#29411: Fixed MFTF test --- .../Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml index ca7a71258fead..3dd294fa50605 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml @@ -36,6 +36,7 @@ <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> <argument name="title" value="ProductImage.filename"/> </actionGroup> From 950a18b68e76d1f2286cd8ee5893b32fbd483c96 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 24 Aug 2020 20:11:29 +0100 Subject: [PATCH 0446/1013] Fixed static tests --- .../view/adminhtml/web/js/directory/directoryTree.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index 0309b11296181..00a23e4a5fe08 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -257,16 +257,15 @@ define([ return; } - currentTreePath = this.isFilterApplied(currentFilterPath) || _.isNull(requestedDirectory) - ? currentFilterPath - : requestedDirectory; + currentTreePath = this.isFilterApplied(currentFilterPath) || _.isNull(requestedDirectory) ? + currentFilterPath : requestedDirectory; if (this.folderExistsInTree(currentTreePath)) { this.locateNode(currentTreePath); } else { this.selectStorageRoot(); } - }, + },g /** * Verify if directory exists in folder tree From 7e4fc5d181479ef2755138e06d2194e64d5d21a5 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 25 Aug 2020 12:00:17 +0100 Subject: [PATCH 0447/1013] Fixed static tests --- .../view/adminhtml/web/js/directory/directoryTree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 00a23e4a5fe08..cf894a7395b31 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -265,7 +265,7 @@ define([ } else { this.selectStorageRoot(); } - },g + }, /** * Verify if directory exists in folder tree From 66e67c3afbf2bdddbc885aee06fe97d300bf3fec Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 25 Aug 2020 13:27:02 +0100 Subject: [PATCH 0448/1013] Fixed static tests --- .../view/adminhtml/web/js/directory/directoryTree.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index cf894a7395b31..2e1e9a980cd59 100644 --- a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -286,9 +286,8 @@ define([ * @returns {String|null} */ getRequestedDirectory: function () { - return !_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '' - ? Base64.idDecode(window.MediabrowserUtility.pathId) - : null; + return !_.isUndefined(window.MediabrowserUtility) && window.MediabrowserUtility.pathId !== '' ? + Base64.idDecode(window.MediabrowserUtility.pathId) : null; }, /** From 8d92b990968ddb43b9bb388ce3ae511d988cbbb6 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Sun, 6 Sep 2020 19:15:58 +0100 Subject: [PATCH 0449/1013] magento/adobe-stock-integration#1810: Fixed saving asset keywords links for db prefix --- .../Model/ResourceModel/Keyword/SaveAssetLinks.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php index eb6bd2aad236c..87f9359d4fc37 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php @@ -133,7 +133,7 @@ private function deleteAssetKeywords(int $assetId, array $obsoleteKeywordIds): v /** @var Mysql $connection */ $connection = $this->resourceConnection->getConnection(); $connection->delete( - $connection->getTableName( + $this->resourceConnection->getTableName( self::TABLE_ASSET_KEYWORD ), [ @@ -196,7 +196,7 @@ private function setAssetUpdatedAt(int $assetId): void try { $connection = $this->resourceConnection->getConnection(); $connection->update( - $connection->getTableName(self::TABLE_MEDIA_ASSET), + $this->resourceConnection->getTableName(self::TABLE_MEDIA_ASSET), ['updated_at' => null], ['id =?' => $assetId] ); From c04c4f3e886d3551448b8e56c627d22969d614a4 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 7 Sep 2020 11:32:31 +0100 Subject: [PATCH 0450/1013] magento/magento2#29921: Fixed unit tests --- .../Model/ResourceModel/Keyword/SaveAssetLinksTest.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 d027f0ed21b53..6531cddf628df 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 @@ -78,10 +78,14 @@ public function testAssetKeywordsSave(int $assetId, array $keywordIds, array $va $this->resourceConnectionMock->expects($this->exactly(2)) ->method('getConnection') ->willReturn($this->connectionMock); - $this->resourceConnectionMock->expects($this->once()) + $this->resourceConnectionMock->expects($this->any()) ->method('getTableName') - ->with('media_gallery_asset_keyword') - ->willReturn('prefix_media_gallery_asset_keyword'); + ->willReturnMap( + [ + ['media_gallery_asset_keyword', 'default', 'prefix_media_gallery_asset_keyword'], + ['media_gallery_asset', 'default', 'prefix_media_gallery_asset'] + ] + ); $this->connectionMock->expects($this->once()) ->method('insertArray') ->with( From 7b63b6c9cb59f2a068687512ebd0f96f35fbc7e5 Mon Sep 17 00:00:00 2001 From: joweecaquicla <joie@abovethefray.io> Date: Fri, 4 Sep 2020 02:38:33 +0800 Subject: [PATCH 0451/1013] magento/adobe-stock-integration#1798: Move Add Selected button to be the last in Old Media Gallery - moved add selected button --- app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php index 9efd24e5003ca..d1c6b0fe7956d 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Content.php @@ -102,7 +102,7 @@ protected function _construct() 'type' => 'button' ], 0, - 0, + 100, 'header' ); } From a9f2a5df20ab796feb90d4c086daff70bb7c4639 Mon Sep 17 00:00:00 2001 From: janmonteros <janraymonteros@gmail.com> Date: Wed, 26 Aug 2020 22:01:08 +0800 Subject: [PATCH 0452/1013] magento/adobe-stock-integration#1784: Remove area emulation from media-content:sync command - remove area emulation --- .../Console/Command/Synchronize.php | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php index 55f99697c289b..e591b4f2339b1 100644 --- a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php +++ b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php @@ -7,8 +7,6 @@ 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; @@ -25,21 +23,13 @@ class Synchronize extends Command */ private $synchronizeContent; - /** - * @var State $state - */ - private $state; - /** * @param SynchronizeInterface $synchronizeContent - * @param State $state */ public function __construct( - SynchronizeInterface $synchronizeContent, - State $state + SynchronizeInterface $synchronizeContent ) { $this->synchronizeContent = $synchronizeContent; - $this->state = $state; parent::__construct(); } @@ -58,12 +48,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln('Synchronizing content with assets...'); - $this->state->emulateAreaCode( - Area::AREA_ADMINHTML, - function () { - $this->synchronizeContent->execute(); - } - ); + $this->synchronizeContent->execute(); $output->writeln('Completed content synchronization.'); return Cli::RETURN_SUCCESS; } From b140c963491fb645c63bdc6e085c0091f158ad60 Mon Sep 17 00:00:00 2001 From: Nazar Klovanych <nazarn96@gmail.com> Date: Tue, 1 Sep 2020 21:54:04 +0300 Subject: [PATCH 0453/1013] Fix issue with saving filters from url-applier to default view of bookmark --- .../Ui/view/base/web/js/grid/url-filter-applier.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 index be9044143c5a4..3c5e72d4d66ed 100644 --- 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 @@ -13,10 +13,12 @@ define([ return Component.extend({ defaults: { listingNamespace: null, + bookmarkProvider: 'componentType = bookmark, ns = ${ $.listingNamespace }', filterProvider: 'componentType = filters, ns = ${ $.listingNamespace }', filterKey: 'filters', searchString: location.search, modules: { + bookmarks: '${ $.bookmarkProvider }', filterComponent: '${ $.filterProvider }' } }, @@ -49,6 +51,16 @@ define([ return; } + if (!_.isUndefined(this.bookmarks())) { + if (!_.size(this.bookmarks().getViewData(this.bookmarks().defaultIndex))) { + setTimeout(function () { + this.apply(); + }.bind(this), 500); + + return; + } + } + if (Object.keys(urlFilter).length) { applied = this.filterComponent().get('applied'); filters = $.extend({}, applied, urlFilter); From fff396a9e010f7ae22cee21ea7b7df7811a399e3 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Mon, 7 Sep 2020 17:17:24 +0100 Subject: [PATCH 0454/1013] MC-37236: Skipped PayPal tests --- .../Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml | 3 +++ ...torefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index d27ac4c4a92f5..b8adcc3630351 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -16,6 +16,9 @@ <description value="Users are able to place order using Paypal Smart Button on Checkout Page, payment action is Sale"/> <severity value="CRITICAL"/> <testCaseId value="MC-13690"/> + <skip> + <issueId value="MC-37236"/> + </skip> <group value="paypalExpress"/> </annotations> <before> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml index 3fd5f44d5a4b6..a4d99ecbf7e61 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml @@ -16,6 +16,9 @@ <description value="Users are able to place order using Paypal Smart Button using Euro currency and merchant country is France"/> <severity value="MAJOR"/> <testCaseId value="MC-33274"/> + <skip> + <issueId value="MC-37236"/> + </skip> <group value="paypalExpress"/> </annotations> <before> From ece5d58d95b69f2d7f0c71899bd4798976f12042 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 7 Sep 2020 13:40:59 -0500 Subject: [PATCH 0455/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 9f557412f50b9..45c91dadaa089 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -93,7 +93,7 @@ class Fulltext implements * @param DimensionProviderInterface $dimensionProvider * @param array $data * @param ProcessManager $processManager - * @param CacheContext $cacheContext|null + * @param CacheContext|null $cacheContext * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( From 2ce21168514a6f03410cedb730bb6ad083891394 Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Mon, 7 Sep 2020 23:10:19 +0100 Subject: [PATCH 0456/1013] Fix bug introduced by previous commit Now instead of using the helper, we are only catching the exception thrown. See previous commit for full description. --- .../Payment/Model/PaymentMethodList.php | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Payment/Model/PaymentMethodList.php b/app/code/Magento/Payment/Model/PaymentMethodList.php index d1d5fec2a88e6..007555fe10c1f 100644 --- a/app/code/Magento/Payment/Model/PaymentMethodList.php +++ b/app/code/Magento/Payment/Model/PaymentMethodList.php @@ -6,6 +6,7 @@ namespace Magento\Payment\Model; use Magento\Payment\Api\Data\PaymentMethodInterface; +use UnexpectedValueException; /** * Payment method list class. @@ -39,12 +40,29 @@ public function __construct( */ public function getList($storeId) { - $methodsInstances = $this->helper->getStoreMethods($storeId); + $methodsCodes = array_keys($this->helper->getPaymentMethods()); + $methodsInstances = array_map( + function ($code) { + try { + return $this->helper->getMethodInstance($code); + } catch (UnexpectedValueException $e) { + return null; + } + }, + $methodsCodes + ); - $methodsInstances = array_filter($methodsInstances, function (MethodInterface $method) { - return !($method instanceof \Magento\Payment\Model\Method\Substitution); + $methodsInstances = array_filter($methodsInstances, function ($method) { + return $method && !($method instanceof \Magento\Payment\Model\Method\Substitution); }); + @uasort( + $methodsInstances, + function (MethodInterface $a, MethodInterface $b) use ($storeId) { + return (int)$a->getConfigData('sort_order', $storeId) - (int)$b->getConfigData('sort_order', $storeId); + } + ); + $methodList = array_map( function (MethodInterface $methodInstance) use ($storeId) { From ef204858ea63983f83080f2b83ca5b3bd57b2016 Mon Sep 17 00:00:00 2001 From: Marjan <petkovski.marjan@gmail.com> Date: Tue, 8 Sep 2020 13:44:26 +0200 Subject: [PATCH 0457/1013] magento/magento2#29880: GraphQL categories and categoryList do not consider Category Permissions configuration Address static tests --- .../Category/CollectionProcessor/CatalogProcessor.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php index b28831623d195..f40ae6210e442 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php @@ -23,6 +23,9 @@ class CatalogProcessor implements CollectionProcessorInterface /** @var SearchCriteriaCollectionProcessor */ private $collectionProcessor; + /** + * @param SearchCriteriaCollectionProcessor $collectionProcessor + */ public function __construct( SearchCriteriaCollectionProcessor $collectionProcessor ) { @@ -41,8 +44,7 @@ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, ContextInterface $context = null - ): Collection - { + ): Collection { $this->collectionProcessor->process($searchCriteria, $collection); return $collection; From 9bf90cdeeccd27724f3de66f4e02f6d4fff5a6f3 Mon Sep 17 00:00:00 2001 From: Andrii Kalinich <51681435+engcom-Echo@users.noreply.github.com> Date: Tue, 8 Sep 2020 16:01:19 +0300 Subject: [PATCH 0458/1013] Update StorefrontPrintOrderFindByZipGuestTest.xml Changed testCaseId and severity --- .../Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml index 85d2a229c5446..c99a02750d6ce 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderFindByZipGuestTest.xml @@ -13,8 +13,8 @@ <stories value="Print Order"/> <title value="Print Order from Guest on Frontend using Zip for search"/> <description value="Print Order from Guest on Frontend"/> - <severity value="BLOCKER"/> - <testCaseId value="MC-16225"/> + <severity value="MINOR"/> + <testCaseId value="MC-37449"/> <group value="sales"/> </annotations> From 3487aff4175bdc5af289094503d9ef44f57723dc Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 8 Sep 2020 16:54:02 +0300 Subject: [PATCH 0459/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/Configurable.php | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 6031ab6f8f8ae..2e0e7d82eb050 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -6,6 +6,7 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; +use Magento\Framework\DB\Select; use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; @@ -140,6 +141,30 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) $this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds)); } + /** + * Filter select by inventory + * + * @param Select $select + * @return Select + */ + public function filterSelectByInventory(Select $select) + { + $select->join( + ['si' => $this->getTable('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->join( + ['si_parent' => $this->getTable('cataloginventory_stock_item')], + 'si_parent.product_id = l.parent_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + $select->orWhere('si_parent.is_in_stock = ?', Stock::STOCK_OUT_OF_STOCK); + + return $select; + } + /** * Apply configurable option * @@ -200,12 +225,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar // Does not make sense to extend query if out of stock products won't appear in tables for indexing if ($this->isConfigShowOutOfStock()) { - $select->join( - ['si' => $this->getTable('cataloginventory_stock_item')], - 'si.product_id = l.product_id', - [] - ); - $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + $select = $this->filterSelectByInventory($select); } $select->columns( From bd343f0e15b913eee15866e998eb7c58cfa997b2 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 8 Sep 2020 09:23:05 -0500 Subject: [PATCH 0460/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../CatalogSearch/Model/Indexer/Fulltext.php | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index 45c91dadaa089..e226bdc6900e6 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -6,13 +6,11 @@ namespace Magento\CatalogSearch\Model\Indexer; -use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\FullFactory; use Magento\CatalogSearch\Model\Indexer\Scope\State; use Magento\CatalogSearch\Model\Indexer\Scope\StateFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Indexer\CacheContext; use Magento\Framework\Indexer\DimensionProviderInterface; use Magento\Store\Model\StoreDimensionProvider; use Magento\Indexer\Model\ProcessManager; @@ -79,11 +77,6 @@ class Fulltext implements */ private $processManager; - /** - * @var CacheContext - */ - private $cacheContext; - /** * @param FullFactory $fullActionFactory * @param IndexerHandlerFactory $indexerHandlerFactory @@ -93,7 +86,6 @@ class Fulltext implements * @param DimensionProviderInterface $dimensionProvider * @param array $data * @param ProcessManager $processManager - * @param CacheContext|null $cacheContext * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -104,8 +96,7 @@ public function __construct( StateFactory $indexScopeStateFactory, DimensionProviderInterface $dimensionProvider, array $data, - ProcessManager $processManager = null, - CacheContext $cacheContext = null + ProcessManager $processManager = null ) { $this->fullAction = $fullActionFactory->create(['data' => $data]); $this->indexerHandlerFactory = $indexerHandlerFactory; @@ -115,7 +106,6 @@ public function __construct( $this->indexScopeState = ObjectManager::getInstance()->get(State::class); $this->dimensionProvider = $dimensionProvider; $this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class); - $this->cacheContext = $cacheContext ?? ObjectManager::getInstance()->get(CacheContext::class); } /** @@ -155,8 +145,6 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId)); $this->fulltextResource->resetSearchResultsByStore($storeId); - - $this->cacheContext->registerTags([Product::CACHE_TAG]); } else { // internal implementation works only with array $entityIds = iterator_to_array($entityIds); @@ -167,8 +155,6 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); } - - $this->cacheContext->registerEntities(Product::CACHE_TAG, $productIds); } } From 69eca0706fdfd74222b8a5aaab5a57bf0c4f0ea7 Mon Sep 17 00:00:00 2001 From: Stanislav Idolov <sidolov@adobe.com> Date: Tue, 8 Sep 2020 09:40:50 -0500 Subject: [PATCH 0461/1013] Remove useless doc block --- .../Amqp/Test/Unit/Topology/ArgumentProcessorTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php index ef1098da318ab..8aaf57bf36543 100644 --- a/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php +++ b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/ArgumentProcessorTest.php @@ -14,9 +14,6 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -/** - * Class ArgumentProcessorTest - */ class ArgumentProcessorTest extends TestCase { /** From 6dfe101979e6ac077d4d1ad1a8d4f93580e2103c Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 9 Sep 2020 00:57:33 -0500 Subject: [PATCH 0462/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml index ac9c0206f4e24..1e79d8d23e27e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml @@ -33,6 +33,9 @@ <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchProductCategory"> <argument name="indexerValue" value="catalog_product_category"/> </actionGroup> + <actionGroup ref="AdminSwitchIndexerToActionModeActionGroup" stepKey="switchCatalogSearch"> + <argument name="indexerValue" value="catalogsearch_fulltext"/> + </actionGroup> </before> <after> From 87f5b98dc7d556f2662c75238f7a4267637ac0a7 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Wed, 9 Sep 2020 17:12:09 +0300 Subject: [PATCH 0463/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Indexer/Price/ConfigurableTest.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) 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 ffa84ca740e62..6c0a92372ae3d 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 @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\ProductRepositoryInterface; @@ -157,6 +159,41 @@ public function testReindexWithCorrectPriority() $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } + /** + * Test get product minimal price if all children is out of stock + * + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testReindexIfAllChildrenIsOutOfStock(): void + { + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct1 = $this->productRepository->getById(10, false, null, true); + $stockItem = $childProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $childProduct2 = $this->productRepository->getById(20, false, null, true); + $stockItem = $childProduct2->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); + $priceIndexerProcessor->reindexList( + [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], + true + ); + + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + } + /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php From ed139a6f357b8ab29ad1322b6c718938f51f4c0f Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Wed, 9 Sep 2020 11:44:55 -0500 Subject: [PATCH 0464/1013] MC-37417: [Magento Cloud] Redis cache getting full / Generating errors - Add cache lifetime for layout caches --- .../Magento/Framework/View/Layout.php | 17 ++++++++++++++-- .../Framework/View/Model/Layout/Merge.php | 20 +++++++++++++++---- .../Framework/View/Test/Unit/LayoutTest.php | 2 +- .../View/Test/Unit/Model/Layout/MergeTest.php | 15 +++++++++++++- 4 files changed, 46 insertions(+), 8 deletions(-) diff --git a/lib/internal/Magento/Framework/View/Layout.php b/lib/internal/Magento/Framework/View/Layout.php index ce8b086dc7b84..eeba7485e0469 100644 --- a/lib/internal/Magento/Framework/View/Layout.php +++ b/lib/internal/Magento/Framework/View/Layout.php @@ -35,6 +35,11 @@ class Layout extends \Magento\Framework\Simplexml\Config implements \Magento\Fra */ const LAYOUT_NODE = '<layout/>'; + /** + * Default cache life time + */ + private const DEFAULT_CACHE_LIFETIME = 31536000; + /** * Layout Update module * @@ -172,6 +177,10 @@ class Layout extends \Magento\Framework\Simplexml\Config implements \Magento\Fra * @var SerializerInterface */ private $serializer; + /** + * @var int + */ + private $cacheLifetime; /** * @param Layout\ProcessorFactory $processorFactory @@ -188,6 +197,7 @@ class Layout extends \Magento\Framework\Simplexml\Config implements \Magento\Fra * @param \Psr\Log\LoggerInterface $logger * @param bool $cacheable * @param SerializerInterface|null $serializer + * @param int|null $cacheLifetime */ public function __construct( Layout\ProcessorFactory $processorFactory, @@ -203,7 +213,8 @@ public function __construct( AppState $appState, Logger $logger, $cacheable = true, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ?int $cacheLifetime = null ) { $this->_elementClass = \Magento\Framework\View\Layout\Element::class; $this->_renderingOutput = new \Magento\Framework\DataObject(); @@ -222,6 +233,7 @@ public function __construct( $this->generatorContextFactory = $generatorContextFactory; $this->appState = $appState; $this->logger = $logger; + $this->cacheLifetime = $cacheLifetime ?? self::DEFAULT_CACHE_LIFETIME; } /** @@ -338,7 +350,8 @@ public function generateElements() 'pageConfigStructure' => $this->getReaderContext()->getPageConfigStructure()->__toArray(), 'scheduledStructure' => $this->getReaderContext()->getScheduledStructure()->__toArray(), ]; - $this->cache->save($this->serializer->serialize($data), $cacheId, $this->getUpdate()->getHandles()); + $handles = $this->getUpdate()->getHandles(); + $this->cache->save($this->serializer->serialize($data), $cacheId, $handles, $this->cacheLifetime); } $generatorContext = $this->generatorContextFactory->create( diff --git a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php index fe79976039a9c..239d4167043c4 100644 --- a/lib/internal/Magento/Framework/View/Model/Layout/Merge.php +++ b/lib/internal/Magento/Framework/View/Model/Layout/Merge.php @@ -47,6 +47,11 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface */ const PAGE_LAYOUT_CACHE_SUFFIX = 'page_layout_merged'; + /** + * Default cache life time + */ + private const DEFAULT_CACHE_LIFETIME = 31536000; + /** * @var \Magento\Framework\View\Design\ThemeInterface */ @@ -169,6 +174,10 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface * @var ReadFactory */ private $readFactory; + /** + * @var int + */ + private $cacheLifetime; /** * Init merge model @@ -182,10 +191,11 @@ class Merge implements \Magento\Framework\View\Layout\ProcessorInterface * @param \Magento\Framework\View\Model\Layout\Update\Validator $validator * @param \Psr\Log\LoggerInterface $logger * @param ReadFactory $readFactory - * @param \Magento\Framework\View\Design\ThemeInterface $theme Non-injectable theme instance + * @param \Magento\Framework\View\Design\ThemeInterface|null $theme Non-injectable theme instance * @param string $cacheSuffix - * @param LayoutCacheKeyInterface $layoutCacheKey + * @param LayoutCacheKeyInterface|null $layoutCacheKey * @param SerializerInterface|null $serializer + * @param int|null $cacheLifetime * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -201,7 +211,8 @@ public function __construct( \Magento\Framework\View\Design\ThemeInterface $theme = null, $cacheSuffix = '', LayoutCacheKeyInterface $layoutCacheKey = null, - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ?int $cacheLifetime = null ) { $this->theme = $theme ?: $design->getDesignTheme(); $this->scope = $scopeResolver->getScope(); @@ -216,6 +227,7 @@ public function __construct( $this->layoutCacheKey = $layoutCacheKey ?: \Magento\Framework\App\ObjectManager::getInstance()->get(LayoutCacheKeyInterface::class); $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); + $this->cacheLifetime = $cacheLifetime ?? self::DEFAULT_CACHE_LIFETIME; } /** @@ -749,7 +761,7 @@ protected function _loadCache($cacheId) */ protected function _saveCache($data, $cacheId, array $cacheTags = []) { - $this->cache->save($data, $cacheId, $cacheTags, null); + $this->cache->save($data, $cacheId, $cacheTags, $this->cacheLifetime); } /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php b/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php index 31606b55f6519..8e4a907011a69 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/LayoutTest.php @@ -944,7 +944,7 @@ public function testGenerateElementsWithoutCache(): void $this->cacheMock->expects($this->once()) ->method('save') - ->with(json_encode($data), 'structure_' . $layoutCacheId, $handles) + ->with(json_encode($data), 'structure_' . $layoutCacheId, $handles, 31536000) ->willReturn(true); $generatorContextMock = $this->getMockBuilder(Context::class) diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php index a34e9a840ea75..cdd3d2c4dc65c 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Model/Layout/MergeTest.php @@ -14,6 +14,7 @@ use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Url\ScopeInterface; +use Magento\Framework\View\Design\ThemeInterface; use Magento\Framework\View\Layout\LayoutCacheKeyInterface; use Magento\Framework\View\Model\Layout\Merge; use Magento\Framework\View\Model\Layout\Update\Validator; @@ -67,6 +68,10 @@ class MergeTest extends TestCase * @var LayoutCacheKeyInterface|MockObject */ protected $layoutCacheKeyMock; + /** + * @var ThemeInterface|MockObject + */ + private $theme; protected function setUp(): void { @@ -88,6 +93,8 @@ protected function setUp(): void ->method('getCacheKeys') ->willReturn([]); + $this->theme = $this->createMock(ThemeInterface::class); + $this->model = $this->objectManagerHelper->getObject( Merge::class, [ @@ -98,6 +105,7 @@ protected function setUp(): void 'appState' => $this->appState, 'layoutCacheKey' => $this->layoutCacheKeyMock, 'serializer' => $this->serializer, + 'theme' => $this->theme, ] ); } @@ -133,7 +141,12 @@ public function testValidateMergedLayoutThrowsException() public function testSaveToCache() { $this->scope->expects($this->once())->method('getId')->willReturn(1); - $this->cache->expects($this->once())->method('save'); + $this->theme->method('getArea')->willReturn('frontend'); + $this->theme->method('getId')->willReturn(1); + $cacheKey = 'LAYOUT_frontend_STORE1_1d41d8cd98f00b204e9800998ecf8427e_page_layout_merged'; + $this->cache->expects($this->once()) + ->method('save') + ->with(null, $cacheKey, [], 31536000); $this->model->load(); } From 167c3acc0fdb28384c2f8ccda08b20aa0a5f34f9 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 9 Sep 2020 18:17:49 -0500 Subject: [PATCH 0465/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- app/code/Magento/Elasticsearch/etc/indexer.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Elasticsearch/etc/indexer.xml b/app/code/Magento/Elasticsearch/etc/indexer.xml index 30103d7da87ce..f22eb8f0bd39b 100644 --- a/app/code/Magento/Elasticsearch/etc/indexer.xml +++ b/app/code/Magento/Elasticsearch/etc/indexer.xml @@ -8,7 +8,7 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Indexer/etc/indexer.xsd"> <indexer id="catalogsearch_fulltext"> <dependencies> - <indexer id="catalog_product_category" /> + <indexer id="catalog_category_product" /> <indexer id="cataloginventory_stock" /> <indexer id="catalog_product_price" /> </dependencies> From 7442aece17d4f3d5e341595b060babc2a8ae112d Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 10 Sep 2020 10:58:12 +0300 Subject: [PATCH 0466/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/ConfigurableTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 6c0a92372ae3d..700746ebbd3ad 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 @@ -155,7 +155,7 @@ public function testReindexWithCorrectPriority() true ); - $configurableProduct = $this->getConfigurableProductFromCollection($configurableProduct->getId()); + $configurableProduct = $this->getConfigurableProductFromCollection((int)$configurableProduct->getId()); $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } @@ -184,6 +184,11 @@ public function testReindexIfAllChildrenIsOutOfStock(): void $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); $this->stockRepository->save($stockItem); + $configurableProduct1 = $this->productRepository->getById(1, false, null, true); + $stockItem = $configurableProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); $priceIndexerProcessor->reindexList( [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], From 7ee934df7e141f0b9285fc8ec3f1532e6f8f95ef Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 10 Sep 2020 12:19:10 +0300 Subject: [PATCH 0467/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/Configurable.php | 36 +++++----- .../Price/StockStatusBaseSelectProcessor.php | 68 +++++++++++++++++++ .../Magento/ConfigurableProduct/etc/di.xml | 1 + 3 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 2e0e7d82eb050..903aa837f2d3b 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; use Magento\Framework\DB\Select; use Magento\Framework\Indexer\DimensionalIndexerInterface; @@ -14,10 +17,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\ObjectManager; use Magento\CatalogInventory\Model\Stock; -use Magento\CatalogInventory\Model\Configuration; /** * Configurable Products Price Indexer Resource model @@ -76,6 +77,11 @@ class Configurable implements DimensionalIndexerInterface */ private $scopeConfig; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param BaseFinalPrice $baseFinalPrice * @param IndexTableStructureFactory $indexTableStructureFactory @@ -86,6 +92,9 @@ class Configurable implements DimensionalIndexerInterface * @param bool $fullReindexAction * @param string $connectionName * @param ScopeConfigInterface $scopeConfig + * @param BaseSelectProcessorInterface|null $baseSelectProcessor + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( BaseFinalPrice $baseFinalPrice, @@ -96,7 +105,8 @@ public function __construct( BasePriceModifier $basePriceModifier, $fullReindexAction = false, $connectionName = 'indexer', - ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + ?BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->baseFinalPrice = $baseFinalPrice; $this->indexTableStructureFactory = $indexTableStructureFactory; @@ -107,6 +117,8 @@ public function __construct( $this->fullReindexAction = $fullReindexAction; $this->basePriceModifier = $basePriceModifier; $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->baseSelectProcessor = $baseSelectProcessor ?: + ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -223,10 +235,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar [] ); - // Does not make sense to extend query if out of stock products won't appear in tables for indexing - if ($this->isConfigShowOutOfStock()) { - $select = $this->filterSelectByInventory($select); - } + $this->baseSelectProcessor->process($select); $select->columns( [ @@ -315,17 +324,4 @@ private function getTable($tableName) { return $this->resource->getTableName($tableName, $this->connectionName); } - - /** - * Is flag Show Out Of Stock setted - * - * @return bool - */ - private function isConfigShowOutOfStock(): bool - { - return $this->scopeConfig->isSetFlag( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - ScopeInterface::SCOPE_STORE - ); - } } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php new file mode 100644 index 0000000000000..bd4a5dd5fa39a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; + +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; + +/** + * A Select object processor. + * + * Adds stock status limitations to a given Select object. + */ +class StockStatusBaseSelectProcessor implements BaseSelectProcessorInterface +{ + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var StockConfigurationInterface + */ + private $stockConfig; + + /** + * @param ResourceConnection $resource + * @param StockConfigurationInterface $stockConfig + */ + public function __construct( + ResourceConnection $resource, + StockConfigurationInterface $stockConfig + ) { + $this->resource = $resource; + $this->stockConfig = $stockConfig; + } + + /** + * {@inheritdoc} + */ + public function process(Select $select) + { + // Does not make sense to extend query if out of stock products won't appear in tables for indexing + if ($this->stockConfig->isShowOutOfStock()) { + $select->join( + ['si' => $this->resource->getTableName('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->join( + ['si_parent' => $this->resource->getTableName('cataloginventory_stock_item')], + 'si_parent.product_id = l.parent_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + $select->orWhere('si_parent.is_in_stock = ?', Stock::STOCK_OUT_OF_STOCK); + } + + return $select; + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index c8a278df92dc6..9f01af66f9713 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -198,6 +198,7 @@ <arguments> <argument name="tableStrategy" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy</argument> <argument name="connectionName" xsi:type="string">indexer</argument> + <argument name="baseSelectProcessor" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\StockStatusBaseSelectProcessor</argument> </arguments> </type> <type name="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product"> From 5a93a3a7dacf94b5a7f7cf4e5ecca151c175daca Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Wed, 9 Sep 2020 18:31:24 -0500 Subject: [PATCH 0468/1013] MC-37137: Advance Reporting generated csv files are not properly escaped which causes reports to fail on MBI side (continuation) - Fix escaped quotes with backslash are not properly escaped in the generated csv --- .../Magento/Analytics/Model/ReportWriter.php | 8 +- .../Test/Unit/Model/ReportWriterTest.php | 79 ++++++++++++++++--- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php index d5bd36d068d20..c1df4e1af508b 100644 --- a/app/code/Magento/Analytics/Model/ReportWriter.php +++ b/app/code/Magento/Analytics/Model/ReportWriter.php @@ -103,14 +103,14 @@ public function write(WriteInterface $directory, $path) /** * Replace wrong symbols in row * + * Strip backslashes before double quotes so they will be properly escaped in the generated csv + * + * @see fputcsv() * @param array $row * @return array */ private function prepareRow(array $row): array { - $row = preg_replace('/(?<!\\\\)"/', '\\"', $row); - $row = preg_replace('/[\\\\]+/', '\\', $row); - - return $row; + return preg_replace('/\\\+(?=\")/', '', $row); } } diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php index 8fb135fb4d9ed..1cf69cd33db7b 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php @@ -103,7 +103,7 @@ protected function setUp(): void * @param array $expectedFileData * @return void * - * @dataProvider configDataProvider + * @dataProvider writeDataProvider */ public function testWrite(array $configData, array $fileData, array $expectedFileData): void { @@ -162,7 +162,7 @@ public function testWrite(array $configData, array $fileData, array $expectedFil * @param array $configData * @return void * - * @dataProvider configDataProvider + * @dataProvider writeErrorFileDataProvider */ public function testWriteErrorFile(array $configData): void { @@ -195,10 +195,75 @@ public function testWriteEmptyReports(): void /** * @return array */ - public function configDataProvider(): array + public function writeDataProvider(): array + { + $configData = [ + 'providers' => [ + [ + 'name' => $this->providerName, + 'class' => $this->providerClass, + 'parameters' => [ + 'name' => $this->reportName + ], + ] + ] + ]; + return [ + [ + 'configData' => $configData, + 'fileData' => [ + ['number' => 1, 'type' => 'Shoes\"" Usual\\\\"'] + ], + 'expectedFileData' => [ + ['number' => 1, 'type' => 'Shoes"" Usual"'] + ] + ], + [ + 'configData' => $configData, + 'fileData' => [ + ['number' => 1, 'type' => 'hello "World"'] + ], + 'expectedFileData' => [ + ['number' => 1, 'type' => 'hello "World"'] + ] + ], + [ + 'configData' => $configData, + 'fileData' => [ + ['number' => 1, 'type' => 'hello \"World\"'] + ], + 'expectedFileData' => [ + ['number' => 1, 'type' => 'hello "World"'] + ] + ], + [ + 'configData' => $configData, + 'fileData' => [ + ['number' => 1, 'type' => 'hello \\"World\\"'] + ], + 'expectedFileData' => [ + ['number' => 1, 'type' => 'hello "World"'] + ] + ], + [ + 'configData' => $configData, + 'fileData' => [ + ['number' => 1, 'type' => 'hello \\\"World\\\"'] + ], + 'expectedFileData' => [ + ['number' => 1, 'type' => 'hello "World"'] + ] + ], + ]; + } + + /** + * @return array + */ + public function writeErrorFileDataProvider(): array { return [ - 'reportProvider' => [ + [ 'configData' => [ 'providers' => [ [ @@ -210,12 +275,6 @@ public function configDataProvider(): array ] ] ], - 'fileData' => [ - ['number' => 1, 'type' => 'Shoes\"" Usual\\\\"'] - ], - 'expectedFileData' => [ - ['number' => 1, 'type' => 'Shoes\"\" Usual\\"'] - ] ], ]; } From 4ec639803c33d0d020c40ec20fdf41083b764400 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 10 Sep 2020 15:59:47 +0300 Subject: [PATCH 0469/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Indexer/Price/ConfigurableTest.php | 40 ------------------- 1 file changed, 40 deletions(-) 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 700746ebbd3ad..ba3d5e46b98fb 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 @@ -159,46 +159,6 @@ public function testReindexWithCorrectPriority() $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } - /** - * Test get product minimal price if all children is out of stock - * - * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 - * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php - * @magentoDbIsolation disabled - * - * @return void - * @throws \Magento\Framework\Exception\NoSuchEntityException - */ - public function testReindexIfAllChildrenIsOutOfStock(): void - { - $configurableProduct = $this->getConfigurableProductFromCollection(1); - $this->assertEquals(10, $configurableProduct->getMinimalPrice()); - - $childProduct1 = $this->productRepository->getById(10, false, null, true); - $stockItem = $childProduct1->getExtensionAttributes()->getStockItem(); - $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); - $this->stockRepository->save($stockItem); - - $childProduct2 = $this->productRepository->getById(20, false, null, true); - $stockItem = $childProduct2->getExtensionAttributes()->getStockItem(); - $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); - $this->stockRepository->save($stockItem); - - $configurableProduct1 = $this->productRepository->getById(1, false, null, true); - $stockItem = $configurableProduct1->getExtensionAttributes()->getStockItem(); - $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); - $this->stockRepository->save($stockItem); - - $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); - $priceIndexerProcessor->reindexList( - [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], - true - ); - - $configurableProduct = $this->getConfigurableProductFromCollection(1); - $this->assertEquals(10, $configurableProduct->getMinimalPrice()); - } - /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php From 7e88be51a739a595d54bde02ceb3e38cfbd5d7a9 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Thu, 10 Sep 2020 15:39:21 -0500 Subject: [PATCH 0470/1013] MC-37006: Adding/removing disabled products to Magento flushes categories cache --- .../Product/Initialization/HelperTest.php | 68 ------------------- .../Catalog/_files/products_in_categories.php | 57 ---------------- .../products_in_categories_rollback.php | 31 --------- 3 files changed, 156 deletions(-) delete mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php delete mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php delete mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php deleted file mode 100644 index 5bf2797805cb9..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Initialization/HelperTest.php +++ /dev/null @@ -1,68 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Controller\Adminhtml\Product\Initialization; - -use Magento\Catalog\Api\Data\CategoryLinkInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\TestFramework\Helper\Bootstrap; - -class HelperTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var Helper - */ - private $initializationHelper; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->initializationHelper = Bootstrap::getObjectManager()->create(Helper::class); - } - - /** - * @magentoDataFixture Magento/Catalog/_files/products_in_categories.php - * @dataProvider initializeCategoriesFromDataProvider - * @param string $sku - * @param array $categoryIds - */ - public function testInitializeCategoriesFromData(string $sku, array $categoryIds): void - { - $productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $product */ - $product = $productRepository->get($sku); - $productData = $product->getData(); - $productData['category_ids'] = $categoryIds; - - $product = $this->initializationHelper->initializeFromData($product, $productData); - $extensionAttributes = $product->getExtensionAttributes(); - $linkedCategoryIds = array_map(function (CategoryLinkInterface $categoryLink) { - return $categoryLink->getCategoryId(); - }, (array) $extensionAttributes->getCategoryLinks()); - $this->assertEquals($categoryIds, $linkedCategoryIds); - } - - /** - * @return array - */ - public function initializeCategoriesFromDataProvider(): array - { - return [ - 'assign category' => ['simple1', [3, 4]], - 'assign categories' => ['simple1', [3, 5, 6]], - 'unassign category' => ['simple2', [3, 6]], - 'unassign categories' => ['simple2', [3]], - 'update categories' => ['simple2', [3, 4, 6]], - 'change all categories' => ['simple2', [4]], - 'unassign all categories' => ['simple2', []], - 'assign new category' => ['simple3', [4]], - 'assign new categories' => ['simple3', [4, 5]], - ]; - } -} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php deleted file mode 100644 index 7957aa560eaa5..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - -foreach (range(3, 7) as $categoryId) { - $category = $objectManager->create(\Magento\Catalog\Model\Category::class); - $category->isObjectNew(true); - $category->setId($categoryId) - ->setName('Category ' . $categoryId) - ->setParentId(2) - ->setPath('1/2/' . $categoryId) - ->setLevel(2) - ->setIsActive(true) - ->save(); -} - -foreach (range(1, 3) as $productId) { - $product = $objectManager->create(\Magento\Catalog\Model\Product::class); - $product->isObjectNew(true); - $product->setId($productId) - ->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setAttributeSetId(4) - ->setStoreId(1) - ->setWebsiteIds([1]) - ->setName('Simple Product ' . $productId) - ->setSku('simple' . $productId) - ->setPrice(10) - ->setWeight(1) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); -} - -$categoryRepository = $objectManager->create(\Magento\Catalog\Api\CategoryRepositoryInterface::class); -$categoryLinkRepository = $objectManager->create( - \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, - [ - 'categoryRepository' => $categoryRepository, - ] -); -$categoryLinkManagement = $objectManager->create( - \Magento\Catalog\Api\CategoryLinkManagementInterface::class, - [ - 'categoryRepository' => $categoryRepository, - ] -); -$reflectionClass = new \ReflectionClass(get_class($categoryLinkManagement)); -$reflectionProperty = $reflectionClass->getProperty('categoryLinkRepository'); -$reflectionProperty->setAccessible(true); -$reflectionProperty->setValue($categoryLinkManagement, $categoryLinkRepository); -$categoryLinkManagement->assignProductToCategories('simple1', [3]); -$categoryLinkManagement->assignProductToCategories('simple2', [3, 5, 6]); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php deleted file mode 100644 index 707419610245a..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_in_categories_rollback.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - -$registry = $objectManager->get(\Magento\Framework\Registry::class); -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', true); - -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); -$productsToDelete = ['simple1', 'simple2', 'simple3']; -foreach ($productsToDelete as $sku) { - try { - $product = $productRepository->get($sku, false, null, true); - $productRepository->delete($product); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { - //Product already removed - } -} - -$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); -foreach ($collection->addAttributeToFilter('level', 2) as $category) { - /** @var \Magento\Catalog\Model\Category $category */ - $category->delete(); -} - -$registry->unregister('isSecureArea'); -$registry->register('isSecureArea', false); From f5d690232c5fc9fd163844cddc1dc8b43811fb93 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Fri, 11 Sep 2020 10:23:31 +0300 Subject: [PATCH 0471/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Product/Indexer/Price/Configurable.php | 24 ------------------- .../Price/StockStatusBaseSelectProcessor.php | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 903aa837f2d3b..d00e5c72a4622 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -153,30 +153,6 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds) $this->applyConfigurableOption($temporaryPriceTable, $dimensions, iterator_to_array($entityIds)); } - /** - * Filter select by inventory - * - * @param Select $select - * @return Select - */ - public function filterSelectByInventory(Select $select) - { - $select->join( - ['si' => $this->getTable('cataloginventory_stock_item')], - 'si.product_id = l.product_id', - [] - ); - $select->join( - ['si_parent' => $this->getTable('cataloginventory_stock_item')], - 'si_parent.product_id = l.parent_id', - [] - ); - $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); - $select->orWhere('si_parent.is_in_stock = ?', Stock::STOCK_OUT_OF_STOCK); - - return $select; - } - /** * Apply configurable option * diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php index bd4a5dd5fa39a..b5cbaa57858c9 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php @@ -43,7 +43,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function process(Select $select) { From 36790c4635549a020e771db0857772f893fb5f89 Mon Sep 17 00:00:00 2001 From: Marjan <petkovski.marjan@gmail.com> Date: Fri, 11 Sep 2020 12:19:06 +0200 Subject: [PATCH 0472/1013] magento/magento2#29926: Prices should be possibly hidden from products query results Initial draft --- .../Model/Resolver/PriceTiers.php | 5 +++ .../Model/Resolver/Product/PriceRange.php | 42 ++++++++++++++++++- .../Model/Resolver/Product/SpecialPrice.php | 5 ++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php index e78224ba0af38..9bc0d87061a01 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -110,6 +110,11 @@ public function resolve( } $product = $value['model']; + + if ($product->hasData('can_show_price') && $product->getData('can_show_price') === false) { + return []; + } + $productId = $product->getId(); $this->tiers->addProductFilter($productId); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php index dbb52f2010930..805571d58d634 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/PriceRange.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\Discount; use Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderPool as PriceProviderPool; use Magento\Framework\GraphQl\Query\ResolverInterface; @@ -66,10 +67,12 @@ public function resolve( $returnArray = []; if (isset($requestedFields['minimum_price'])) { - $returnArray['minimum_price'] = $this->getMinimumProductPrice($product, $store); + $returnArray['minimum_price'] = $this->canShowPrice($product) ? + $this->getMinimumProductPrice($product, $store) : $this->formatEmptyResult(); } if (isset($requestedFields['maximum_price'])) { - $returnArray['maximum_price'] = $this->getMaximumProductPrice($product, $store); + $returnArray['maximum_price'] = $this->canShowPrice($product) ? + $this->getMaximumProductPrice($product, $store) : $this->formatEmptyResult(); } return $returnArray; } @@ -130,4 +133,39 @@ private function formatPrice(float $regularPrice, float $finalPrice, StoreInterf 'discount' => $this->discount->getDiscountByDifference($regularPrice, $finalPrice), ]; } + + /** + * Check if the product is allowed to show price + * + * @param ProductInterface $product + * @return bool + */ + private function canShowPrice($product): bool + { + if ($product->hasData('can_show_price') && $product->getData('can_show_price') === false) { + return false; + } + + return true; + } + + /** + * Format empty result + * + * @return array + */ + private function formatEmptyResult(): array + { + return [ + 'regular_price' => [ + 'value' => null, + 'currency' => null + ], + 'final_price' => [ + 'value' => null, + 'currency' => null + ], + 'discount' => null + ]; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php index 1b42b0fde2bcb..c80cc3744876f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php @@ -28,7 +28,10 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var PricingSpecialPrice $specialPrice */ $specialPrice = $product->getPriceInfo()->getPrice(PricingSpecialPrice::PRICE_CODE); - if ($specialPrice->getValue()) { + if ((!$product->hasData('can_show_price') + || ($product->hasData('can_show_price') && $product->getData('can_show_price') === true) + ) + && $specialPrice->getValue()) { return $specialPrice->getValue(); } From 2d5c3b24567eb9d590536a6ca285d5f9fa8562f7 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Fri, 11 Sep 2020 13:44:31 -0500 Subject: [PATCH 0473/1013] MC-35855: CSP cannot be configured from a theme --- .../CspWhitelistXml/FileResolver.php | 95 ++++++++++++++++ app/code/Magento/Csp/etc/di.xml | 1 + .../Config/CompositeFileIterator.php | 102 ++++++++++++++++++ .../Test/Unit/CompositeFileIteratorTest.php | 73 +++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php create mode 100644 lib/internal/Magento/Framework/Config/CompositeFileIterator.php create mode 100644 lib/internal/Magento/Framework/Config/Test/Unit/CompositeFileIteratorTest.php diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php new file mode 100644 index 0000000000000..bbab95fcf7ab9 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php @@ -0,0 +1,95 @@ +<?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\Config\FileResolverInterface; +use Magento\Framework\Filesystem; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\Design\Theme\CustomizationInterface; +use Magento\Framework\View\Design\Theme\CustomizationInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\ReadInterface as DirectoryRead; +use Magento\Framework\Config\CompositeFileIteratorFactory; + +/** + * Combines configuration files from both modules and current theme. + */ +class FileResolver implements FileResolverInterface +{ + /** + * @var FileResolverInterface + */ + private $moduleFileResolver; + + /** + * @var ThemeInterface + */ + private $theme; + + /** + * @var CustomizationInterfaceFactory + */ + private $themeInfoFactory; + + /** + * @var DirectoryRead + */ + private $rootDir; + + /** + * @var CompositeFileIteratorFactory + */ + private $iteratorFactory; + + /** + * @param FileResolverInterface $moduleFileResolver + * @param DesignInterface $design + * @param CustomizationInterfaceFactory $customizationFactory + * @param Filesystem $filesystem + * @param CompositeFileIteratorFactory $iteratorFactory + */ + public function __construct( + FileResolverInterface $moduleFileResolver, + DesignInterface $design, + CustomizationInterfaceFactory $customizationFactory, + Filesystem $filesystem, + CompositeFileIteratorFactory $iteratorFactory + ) { + $this->moduleFileResolver = $moduleFileResolver; + $this->theme = $design->getDesignTheme(); + $this->themeInfoFactory = $customizationFactory; + $this->rootDir = $filesystem->getDirectoryRead(DirectoryList::ROOT); + $this->iteratorFactory = $iteratorFactory; + } + + /** + * @inheritDoc + */ + public function get($filename, $scope) + { + $configs = $this->moduleFileResolver->get($filename, $scope); + if ($scope === 'global') { + $files = []; + $theme = $this->theme; + while ($theme) { + /** @var CustomizationInterface $info */ + $info = $this->themeInfoFactory->create(['theme' => $theme]); + $file = $info->getThemeFilesPath() .'/etc/' .$filename; + if ($this->rootDir->isExist($file)) { + $files[] = $file; + } + $theme = $theme->getParentTheme(); + } + $configs = $this->iteratorFactory->create(['paths' => $files, 'existingIterator' => $configs]); + } + + return $configs; + } +} diff --git a/app/code/Magento/Csp/etc/di.xml b/app/code/Magento/Csp/etc/di.xml index 7b1129a0e1a41..049259752a0b5 100644 --- a/app/code/Magento/Csp/etc/di.xml +++ b/app/code/Magento/Csp/etc/di.xml @@ -46,6 +46,7 @@ <arguments> <argument name="converter" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\Converter</argument> <argument name="schemaLocator" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\SchemaLocator</argument> + <argument name="fileResolver" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\FileResolver</argument> <argument name="fileName" xsi:type="string">csp_whitelist.xml</argument> </arguments> </type> diff --git a/lib/internal/Magento/Framework/Config/CompositeFileIterator.php b/lib/internal/Magento/Framework/Config/CompositeFileIterator.php new file mode 100644 index 0000000000000..904afa4f00c49 --- /dev/null +++ b/lib/internal/Magento/Framework/Config/CompositeFileIterator.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Config; + +use Magento\Framework\Filesystem\File\ReadFactory; + +/** + * Combine existing file iterator and new files. + */ +class CompositeFileIterator extends FileIterator +{ + /** + * @var FileIterator + */ + private $existingIterator; + + /** + * @param ReadFactory $readFactory + * @param array $paths + * @param FileIterator $existingIterator + */ + public function __construct(ReadFactory $readFactory, array $paths, FileIterator $existingIterator) + { + parent::__construct($readFactory, $paths); + $this->existingIterator = $existingIterator; + } + + /** + * @inheritDoc + */ + public function rewind() + { + $this->existingIterator->rewind(); + parent::rewind(); + } + + /** + * @inheritDoc + */ + public function current() + { + if ($this->existingIterator->valid()) { + return $this->existingIterator->current(); + } + + return parent::current(); + } + + /** + * @inheritDoc + */ + public function key() + { + if ($this->existingIterator->valid()) { + return $this->existingIterator->key(); + } + + return parent::key(); + } + + /** + * @inheritDoc + */ + public function next() + { + if ($this->existingIterator->valid()) { + $this->existingIterator->next(); + } else { + parent::next(); + } + } + + /** + * @inheritDoc + */ + public function valid() + { + return $this->existingIterator->valid() || parent::valid(); + } + + /** + * @inheritDoc + */ + public function toArray() + { + return array_merge($this->existingIterator->toArray(), parent::toArray()); + } + + /** + * @inheritDoc + */ + public function count() + { + return $this->existingIterator->count() + parent::count(); + } +} diff --git a/lib/internal/Magento/Framework/Config/Test/Unit/CompositeFileIteratorTest.php b/lib/internal/Magento/Framework/Config/Test/Unit/CompositeFileIteratorTest.php new file mode 100644 index 0000000000000..eece996f91d5d --- /dev/null +++ b/lib/internal/Magento/Framework/Config/Test/Unit/CompositeFileIteratorTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\Config\Test\Unit; + +use Magento\Framework\Config\CompositeFileIterator; +use Magento\Framework\Config\FileIterator; +use Magento\Framework\Filesystem\File\ReadFactory; +use Magento\Framework\Filesystem\File\Read; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test composition of file iterators. + */ +class CompositeFileIteratorTest extends TestCase +{ + /** + * @var ReadFactory|MockObject + */ + private $readFactoryMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->readFactoryMock = $this->createMock(ReadFactory::class); + $this->readFactoryMock->method('create') + ->willReturnCallback( + function (string $file): Read { + $readMock = $this->createMock(Read::class); + $readMock->method('readAll')->willReturn('Content of ' .$file); + + return $readMock; + } + ); + } + + /** + * Test the composite. + */ + public function testComposition(): void + { + $existingFiles = [ + '/etc/magento/somefile.ext', + '/etc/magento/somefile2.ext', + '/etc/magento/somefile3.ext' + ]; + $newFiles = [ + '/etc/magento/some-other-file.ext', + '/etc/magento/some-other-file2.ext' + ]; + + $existing = new FileIterator($this->readFactoryMock, $existingFiles); + $composite = new CompositeFileIterator($this->readFactoryMock, $newFiles, $existing); + $found = []; + foreach ($composite as $file => $content) { + $this->assertNotEmpty($content); + $found[] = $file; + } + $this->assertEquals(array_merge($existingFiles, $newFiles), $found); + $this->assertEquals(count($existingFiles) + count($newFiles), $composite->count()); + $this->assertEquals(array_merge($existingFiles, $newFiles), array_keys($composite->toArray())); + } +} From efc9b2190a3870eaed76bf2876a50ab8e4ea2ecf Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Sat, 12 Sep 2020 15:16:01 -0500 Subject: [PATCH 0474/1013] MC-35855: CSP cannot be configured from a theme --- .../Csp/Model/Collector/CspWhitelistXml/FileResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php index bbab95fcf7ab9..bfbc88219ca94 100644 --- a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php @@ -61,7 +61,7 @@ public function __construct( CustomizationInterfaceFactory $customizationFactory, Filesystem $filesystem, CompositeFileIteratorFactory $iteratorFactory - ) { + ) { $this->moduleFileResolver = $moduleFileResolver; $this->theme = $design->getDesignTheme(); $this->themeInfoFactory = $customizationFactory; From 6c41266af14c282d4e4cbfe7a4e4e5472527649c Mon Sep 17 00:00:00 2001 From: Marjan <petkovski.marjan@gmail.com> Date: Sun, 13 Sep 2020 17:10:59 +0200 Subject: [PATCH 0475/1013] magento/magento2#29930: Add to Cart mutations in GraphQl should consider Catalog Permissions Initial draft --- .../Processor/ItemDataCompositeProcessor.php | 45 +++++++++++++++++++ .../Processor/ItemDataProcessorInterface.php | 25 +++++++++++ .../Model/Resolver/AddProductsToCart.php | 32 ++++++++++++- app/code/Magento/QuoteGraphQl/etc/di.xml | 1 + 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataCompositeProcessor.php create mode 100644 app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataProcessorInterface.php diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataCompositeProcessor.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataCompositeProcessor.php new file mode 100644 index 0000000000000..73a22471584ec --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataCompositeProcessor.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * {@inheritdoc} + */ +class ItemDataCompositeProcessor implements ItemDataProcessorInterface +{ + /** + * @var ItemDataProcessorInterface[] + */ + private $itemDataProcessors; + + /** + * @param ItemDataProcessorInterface[] $itemDataProcessors + */ + public function __construct(array $itemDataProcessors = []) + { + $this->itemDataProcessors = $itemDataProcessors; + } + + /** + * Process cart item data + * + * @param array $cartItemData + * @param ContextInterface $context + * @return array + */ + public function process(array $cartItemData, ContextInterface $context): array + { + foreach ($this->itemDataProcessors as $itemDataProcessor) { + $cartItemData = $itemDataProcessor->process($cartItemData, $context); + } + + return $cartItemData; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataProcessorInterface.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataProcessorInterface.php new file mode 100644 index 0000000000000..33f40bd28c1d3 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/Processor/ItemDataProcessorInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor; + +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Process Cart Item Data + */ +interface ItemDataProcessorInterface +{ + /** + * Process cart item data + * + * @param array $cartItemData + * @param ContextInterface $context + * @return array + */ + public function process(array $cartItemData, ContextInterface $context): array; +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php index d5e554f096ec1..c7ab7596741e0 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php @@ -11,11 +11,13 @@ use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; 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; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataProcessorInterface; /** * Resolver for addProductsToCart mutation @@ -34,16 +36,24 @@ class AddProductsToCart implements ResolverInterface */ private $addProductsToCartService; + /** + * @var ItemDataProcessorInterface + */ + private $itemDataProcessor; + /** * @param GetCartForUser $getCartForUser * @param AddProductsToCartService $addProductsToCart + * @param ItemDataProcessorInterface $itemDataProcessor */ public function __construct( GetCartForUser $getCartForUser, - AddProductsToCartService $addProductsToCart + AddProductsToCartService $addProductsToCart, + ItemDataProcessorInterface $itemDataProcessor ) { $this->getCartForUser = $getCartForUser; $this->addProductsToCartService = $addProductsToCart; + $this->itemDataProcessor = $itemDataProcessor; } /** @@ -68,6 +78,9 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $cartItems = []; foreach ($cartItemsData as $cartItemData) { + if (!$this->itemIsAllowedToCart($cartItemData, $context)) { + continue; + } $cartItems[] = (new CartItemFactory())->create($cartItemData); } @@ -90,4 +103,21 @@ function (Error $error) { ) ]; } + + /** + * Check if the item can be added to cart + * + * @param array $cartItemData + * @param ContextInterface $context + * @return bool + */ + private function itemIsAllowedToCart(array $cartItemData, ContextInterface $context): bool + { + $cartItemData = $this->itemDataProcessor->process($cartItemData, $context); + if (isset($cartItemData['grant_checkout']) && $cartItemData['grant_checkout'] === false) { + return false; + } + + return true; + } } diff --git a/app/code/Magento/QuoteGraphQl/etc/di.xml b/app/code/Magento/QuoteGraphQl/etc/di.xml index d230df253221b..35b52dd495c5a 100644 --- a/app/code/Magento/QuoteGraphQl/etc/di.xml +++ b/app/code/Magento/QuoteGraphQl/etc/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <preference for="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValueInterface" type="Magento\QuoteGraphQl\Model\CartItem\DataProvider\CustomizableOptionValue\Composite" /> + <preference for="Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataProcessorInterface" type="Magento\QuoteGraphQl\Model\CartItem\DataProvider\Processor\ItemDataCompositeProcessor" /> <type name="Magento\QuoteGraphQl\Model\Resolver\CartItemTypeResolver"> <arguments> <argument name="supportedTypes" xsi:type="array"> From d4931f79648bfd1677a861257697d5f8a57cecd6 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Mon, 14 Sep 2020 11:45:49 +0300 Subject: [PATCH 0476/1013] fix duplicate image during import products, refactor tests --- .../Model/Import/Product.php | 136 +++++++----------- .../Import/Product/MediaGalleryProcessor.php | 20 --- .../Catalog/_files/magento_image_2.jpg | Bin 12137 -> 0 bytes .../Model/Import/ProductTest.php | 46 +++--- ...mport_media_additional_long_name_image.csv | 2 + .../_files/import_media_update_images.csv | 2 - .../_files/import_with_filesystem_images.php | 4 - 7 files changed, 75 insertions(+), 135 deletions(-) delete mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image_2.jpg create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_long_name_image.csv delete mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index b11f1951b49cb..4384cec88bc46 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -23,6 +23,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; use Magento\Framework\Intl\DateTimeFactory; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; @@ -766,6 +767,11 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity */ private $linkProcessor; + /** + * @var File + */ + private $fileDriver; + /** * @param \Magento\Framework\Json\Helper\Data $jsonHelper * @param \Magento\ImportExport\Helper\Data $importExportData @@ -814,6 +820,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity * @param StatusProcessor|null $statusProcessor * @param StockProcessor|null $stockProcessor * @param LinkProcessor|null $linkProcessor + * @param File|null $fileDriver * @throws LocalizedException * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -866,7 +873,8 @@ public function __construct( ProductRepositoryInterface $productRepository = null, StatusProcessor $statusProcessor = null, StockProcessor $stockProcessor = null, - LinkProcessor $linkProcessor = null + LinkProcessor $linkProcessor = null, + ?File $fileDriver = null ) { $this->_eventManager = $eventManager; $this->stockRegistry = $stockRegistry; @@ -930,6 +938,7 @@ public function __construct( $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class); $this->productRepository = $productRepository ?? ObjectManager::getInstance() ->get(ProductRepositoryInterface::class); + $this->fileDriver = $fileDriver ?: ObjectManager::getInstance()->get(File::class); } /** @@ -1148,6 +1157,7 @@ protected function _replaceProducts() * Save products data. * * @return $this + * @throws LocalizedException */ protected function _saveProductsData() { @@ -1155,7 +1165,7 @@ protected function _saveProductsData() foreach ($this->_productTypeModels as $productTypeModel) { $productTypeModel->saveData(); } - $this->_saveLinks(); + $this->linkProcessor->saveLinks($this, $this->_dataSourceModel, $this->getProductEntityLinkField()); $this->_saveStockItem(); if ($this->_replaceFlag) { $this->getOptionEntity()->clearProductsSkuToId(); @@ -1563,15 +1573,13 @@ protected function _saveProducts() $this->categoriesCache = []; $tierPrices = []; $mediaGallery = []; - $uploadedFiles = []; - $galleryItemsToRemove = []; $labelsForUpdate = []; $imagesForChangeVisibility = []; $uploadedImages = []; $previousType = null; $prevAttributeSet = null; - $importDir = $this->_mediaDirectory->getAbsolutePath($this->getImportDir()); + $importDir = $this->_mediaDirectory->getAbsolutePath($this->getUploader()->getTmpDir()); $existingImages = $this->getExistingImages($bunch); $this->addImageHashes($existingImages); @@ -1721,11 +1729,12 @@ protected function _saveProducts() foreach (array_keys($imageHiddenStates) as $image) { //Mark image as uploaded if it exists if (array_key_exists($image, $rowExistingImages)) { - $rowImages[self::COL_MEDIA_IMAGE][] = $image; $uploadedImages[$image] = $image; } - - if (empty($rowImages)) { + //Add image to hide to images list if it does not exist + if (empty($rowImages[self::COL_MEDIA_IMAGE]) + || !in_array($image, $rowImages[self::COL_MEDIA_IMAGE]) + ) { $rowImages[self::COL_MEDIA_IMAGE][] = $image; } } @@ -1743,8 +1752,11 @@ protected function _saveProducts() $hash = ''; if (filter_var($columnImage, FILTER_VALIDATE_URL) === false) { $filename = $importDir . DIRECTORY_SEPARATOR . $columnImage; - if (file_exists($filename)) { - $hash = hash_file('sha256', $importDir . DIRECTORY_SEPARATOR . $columnImage); + if ($this->fileDriver->isExists($filename)) { + $hash = hash_file( + 'sha256', + $importDir . DIRECTORY_SEPARATOR . $columnImage + ); } } else { $hash = hash_file('sha256', $columnImage); @@ -1760,11 +1772,11 @@ function ($exists, $file) use ($hash) { if ($exists) { return $exists; } - if (isset($file['hash']) && - !empty($file['hash']) && - $file['hash'] === $hash) { + + if (isset($file['hash']) && $file['hash'] === $hash) { return $file['value']; } + return $exists; }, '' @@ -1773,35 +1785,29 @@ function ($exists, $file) use ($hash) { if ($imageAlreadyExists) { $uploadedFile = $imageAlreadyExists; - } else { - if (!isset($uploadedImages[$columnImage])) { - $uploadedFile = $this->uploadMediaFiles($columnImage); - $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); - if ($uploadedFile) { - $uploadedImages[$columnImage] = $uploadedFile; - } else { - unset($rowData[$column]); - $this->addRowError( - ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, - $rowNum, - null, - null, - ProcessingError::ERROR_LEVEL_NOT_CRITICAL - ); - } + } elseif (!isset($uploadedImages[$columnImage])) { + $uploadedFile = $this->uploadMediaFiles($columnImage); + $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); + if ($uploadedFile) { + $uploadedImages[$columnImage] = $uploadedFile; } else { - $uploadedFile = $uploadedImages[$columnImage]; + unset($rowData[$column]); + $this->addRowError( + ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE, + $rowNum, + null, + null, + ProcessingError::ERROR_LEVEL_NOT_CRITICAL + ); } + } else { + $uploadedFile = $uploadedImages[$columnImage]; } if ($uploadedFile && $column !== self::COL_MEDIA_IMAGE) { $rowData[$column] = $uploadedFile; } - if ($uploadedFile) { - $uploadedFiles[] = $uploadedFile; - } - if (!$uploadedFile || isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { continue; } @@ -1823,8 +1829,7 @@ function ($exists, $file) use ($hash) { } if (isset($rowLabels[$column][$columnImageKey]) - && $rowLabels[$column][$columnImageKey] != - $currentFileData['label'] + && $rowLabels[$column][$columnImageKey] !== $currentFileData['label'] ) { $labelsForUpdate[] = [ 'label' => $rowLabels[$column][$columnImageKey], @@ -1833,7 +1838,7 @@ function ($exists, $file) use ($hash) { ]; } } else { - if ($column == self::COL_MEDIA_IMAGE) { + if ($column === self::COL_MEDIA_IMAGE) { $rowData[$column][] = $uploadedFile; } $mediaGallery[$storeId][$rowSku][$uploadedFile] = [ @@ -1850,14 +1855,6 @@ function ($exists, $file) use ($hash) { } } - // 5.1 Items to remove phase - if (!empty($rowExistingImages)) { - $galleryItemsToRemove[] = \array_diff( - \array_keys($rowExistingImages), - $uploadedFiles - ); - } - // 6. Attributes phase $rowStore = (self::SCOPE_STORE == $rowScope) ? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) @@ -1968,8 +1965,6 @@ function ($exists, $file) use ($hash) { $tierPrices )->_saveMediaGallery( $mediaGallery - )->_removeOldMediaGalleryItems( - $galleryItemsToRemove )->_saveProductAttributes( $attributes )->updateMediaGalleryVisibility( @@ -1988,29 +1983,31 @@ function ($exists, $file) use ($hash) { } //phpcs:enable Generic.Metrics.NestingLevel + // phpcs:enable + /** * Generate hashes for existing images for comparison with newly uploaded images. * * @param array $images + * @return void */ - public function addImageHashes(&$images) + private function addImageHashes(array &$images): void { $productMediaPath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) - ->getAbsolutePath('/catalog/product'); + ->getAbsolutePath(DS . 'catalog' . DS . 'product'); foreach ($images as $storeId => $skus) { foreach ($skus as $sku => $files) { foreach ($files as $path => $file) { - if (file_exists($productMediaPath . $file['value'])) { - $images[$storeId][$sku][$path]['hash'] = hash_file('sha256', $productMediaPath . $file['value']); + if ($this->fileDriver->isExists($productMediaPath . $file['value'])) { + $fileName = $productMediaPath . $file['value']; + $images[$storeId][$sku][$path]['hash'] = hash_file('sha256', $fileName); } } } } } - // phpcs:enable - /** * Clears entries from Image Set and Row Data marked as no_selection * @@ -2172,17 +2169,14 @@ protected function _saveProductTierPrices(array $tierPriceData) * * @return string */ - protected function getImportDir() + private function getImportDir(): string { $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; - if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) { - $tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; - } else { - $tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import'); - } - return $tmpPath; + return empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]) + ? $dirAddon . DS . $this->_mediaDirectory->getRelativePath('import') + : $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; } /** @@ -2286,28 +2280,6 @@ protected function _saveMediaGallery(array $mediaGalleryData) return $this; } - /** - * Remove old media gallery items. - * - * @param array $itemsToRemove - * @return $this - */ - protected function _removeOldMediaGalleryItems(array $itemsToRemove) - { - if (empty($itemsToRemove)) { - return $this; - } - - $itemsToRemove = array_merge(...$itemsToRemove); - if (empty($itemsToRemove)) { - return $this; - } - - $this->mediaProcessor->removeOldMediaItems($itemsToRemove); - - return $this; - } - /** * Save product websites. * diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index a608d792ff6fb..d4694b72ba64f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -104,7 +104,6 @@ public function __construct( * * @param array $mediaGalleryData * @return void - * @throws \Exception */ public function saveMediaGallery(array $mediaGalleryData) { @@ -271,7 +270,6 @@ private function prepareMediaGalleryValueData( * * @param array $labels * @return void - * @throws \Exception */ public function updateMediaGalleryLabels(array $labels) { @@ -283,7 +281,6 @@ public function updateMediaGalleryLabels(array $labels) * * @param array $images * @return void - * @throws \Exception */ public function updateMediaGalleryVisibility(array $images) { @@ -296,7 +293,6 @@ public function updateMediaGalleryVisibility(array $images) * @param array $data * @param string $field * @return void - * @throws \Exception */ private function updateMediaGalleryField(array $data, $field) { @@ -341,7 +337,6 @@ private function updateMediaGalleryField(array $data, $field) * * @param array $bunch * @return array - * @throws \Exception */ public function getExistingImages(array $bunch) { @@ -451,25 +446,10 @@ private function getLastMediaPositionPerProduct(array $productIds): array return $result; } - /** - * Remove old media gallery items. - * - * @param array $oldMediaValues - * @return void - */ - public function removeOldMediaItems(array $oldMediaValues) - { - $this->connection->delete( - $this->mediaGalleryTableName, - $this->connection->quoteInto('value IN (?)', $oldMediaValues) - ); - } - /** * Get product entity link field. * * @return string - * @throws \Exception */ private function getProductEntityLinkField() { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image_2.jpg b/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image_2.jpg deleted file mode 100644 index 2c21e0238ede73faf93b6ae9641a2f649f537f55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12137 zcmd^lby!qg+wX><a|n@=97<4W0qK<P6s4P?6_7@1lx`6zDd`rZ!=W3Lp}RyHX*q-O zJ<s!g=X%dMe|+aU-*xuPwXb{jy6@i|Yp>s0d+oWJxS9vR3bOLD0165U@DTX{uBHGf z0ECKq{Y4&V$S*n;IyxE}Iu0f#1{NL;9v&_ZE-pR+F&LkKhyWKCOa>+*A-!?q1|H!} zaxzkKV$vI=*B~e$qzxK6Haa>sDLyVf>HqO@)dmn@p#UfV2!#khB|-rap<HzU)W}c) zR1os?9|tA~3k@9u1r^y!b)5&Y@o%jdsOXqzD4?rJ01t$;1cSf;fHE0<ZH=k9dMsz@ z=y$J1$eLzubm2b`$@h9JGRqRzId&J$@DK}Xb8yY$VFY)_ABFvomtzj5=h^%A;(+^; zMxWRe%<(NZ|6k1ngi9MSDuySwL>|lo0AZ=uhu0+|k_T6SBj-;AQEyeZaSi?6NZOTi zf7&PKjy%UZgD81<-hiNA>4HI4Xy$f~@8z{Bro<8gzIv}=@P!j_>r0`LSzf@=n_G1I zAHR(`SpM`58aY2AGw>j|0uV(=18Q$EeXXOSDVawnr_rFhdRq7QsxB{{8I}QNoopc? z@W&aIpF)fY=ca8QAZ{1q=`xCS0l=O?0r}zhn*Q2%0J9ijz2!C}GFaFG00im1JCRCr zp0^x!fd^u8k*$iD-)bsbP?*v_Bn={bP5@VAN_@{_NoF0G{gKXT2-OwoQ|jk^MWZnX z3xd(0wjp#V_{r;|{UR#fPQ3?&&Aw{K8si15zD8e900G*lyf<g3SAdZ(;oxAyz24!$ z%Z3&LmX-5vE9hE^L20_^!g(5XW#%?hiPpvPPoQEEwabgs@@Ld}V_Uo}gy|$Rd|I(C zOO_6&7=~r!<%9Z}LYEGaG{JQV=ZC=l<mJG}hff{b*R!^n9R3DDW#-jUY2km1Fv@~5 zx%og8zhzP-0PeEl|0(kMhrx5s!`Vxb#57RTcRTU+$meg-r^Nr~0DF`lS-sO`FP0AR zv)8Gu?&{&`trdD-0hK6cy(?{%spme~>;D!YX?@E~GYL`Ne!mZ6#EpO_i35Yfb-c+B z(L`P`h&jDA=ER4;?DW5cAW2&Kewt1@XnXIZ>1P?V=O4(?-W%ON<2ZD>@xO;Kk6+%n zs9(`y(kY@6_$>ohgc@DSlbUlWEl=Xv5Nkc`QG%EtAZ01U`&Cs7V`6rX17XZ`_JF)& z<pgB!=#gk&M(#YQnNe?zIw*&tHY<lGu=alFaTU(pMV%GZ0dX)QGD2u{9Hr)dgCf5k zwr?byOVAgXqMBuT!+}BVI<|gy5~v(*(YhbgP9V7Hb`y&Fsy3lc^B@|H-|U@A)sZ+f zVu!dt5!L8jdAN#^KR^5!X4#7pN_`}RTjT%#agvPbwXxQBe-{$1S<Z|+wHf$TE6}e( z0{ZLox2d`oe$^`A(Oml%R{9puCp%V#gSViZ1N+~#yf-`nXSvt%duaT=<;b23;Dqc8 z{JJL^JKwchXZwFY7aRgWWP-iCI!~3io+y<QU)~Bgb-DQ6x4%0c#p<1Zz9VA@hijbm zor3fAqSGJ7NQOZ9cW#jF$V2h)85gW~7o$UWzuBm4IEt}!)~+g4>vtaWKXp*rFPc5J zj+W<?_4f{ayx(nGh&Cx7j;GVged&>1Rrw@^*>~ypQxi#{3A+wiu8(mCJ^B3s5zJD3 zXwF(%;atw|W<ZRVJ8``z`y-XFC~f{=(wf0sht`Lf*MCtx@RY{SO~)?dhY{2*^}P1* z<fo+{a;||OClWM_>&fQNY=fLCF#r@)FdYvOFCV8kF&A>u!9up8prf9%-oB}Xuis}P zOKg~{X(FOkk?fZ*!G;rT74$wJq^Knk%eIAxX`v5;iDCG3+)yTtUf#GT$CJ76wPG<7 zTwirRRR{%0c>99tG`k*y4Q&wczSk<a$7E$KlQCQ#`Rb*jl9^M{D0#MDasodWy~g8K za+vCCAFa=?m^2ESEHpfFvnjNSnQHa9H;YGloU$V=?lx#7_2u+)*;f>c(f46!F<Avm z%4qi~lwsY57c-@!1dH<;)49EptQ1Qoz1>oZW0(9CQol(6a8hJc>bTvc#~cf1!+xn~ z-;>G$NmP6{pB1aG#%GBL>I-{Mu1v%<WcWk%h9-L7qEgt}CwW^yb+c#;Sf*wW@~Ev$ zP$K7fvGzIs1g2}F_&U2vh6`N*?I8yN4dAble#EZg*9S`jPV^{PiCPj;7?NPUu#T^= z!DO@N6QVwL3MKTnuqvSBD&F$?lSJ0M_GVeZ1#O$R?M)@K>m_-J)45wsuqqNr3g47B z+S7_ww~jyM%j(uDsKqN#X|*l;SQJ0coy7YotEXxjs{0G_MZrLhL>!J5<Ws={Qls=? z98PXQMvnytf8iett2qtLMW)AX<0elHo^}eile{;nejM@D`1GiRpiuNIF~Lx}a1|BV zs?x;p)b$yQM!Ri2)#uiOuZFjDe2zFVwL`{V{EJO8sG!}p6*g8jSpq*Q^FI56sh#el zgfebYZQQY`_g8F#O}W5k*<_jN(|^RH2|U$<>TwArQfW`j))1x}%x_liy!RI>;x8h9 ztB7B)<@gA5u-NIT{_$ALEgdQyJe8b~@n`pok%g}nDmvdZCMd)&#P}2FYNEO8{eKC- zBIh+QaQmBSk6L!L&F$KMwyLGZTc}Bgyf@$R&r}nz-KtXoC7~ex@)eXspUu4{fClaj zJ^HHvC&&zm3}v*|D@KC~EXcWp(!&s(s%T{cq*W@0DkJG6avpd(WHbz~VS}$HHCC;+ z7L+9<jbm{4**_>0%B$mzD0?zxQR)=a*J?8xrF~>03l6d1YnjK=Q$z(q(Bv!?k~4%P z-yN~$kZEC?6j|^SO|EJ!2tq2<{2Y-3mY)bYU{SEJK_K+sLly-UghmITV}OZ>8Mx_r zcqP=GU0R1ZxfaA@G8rWU%6Cajehy*eAQwY9N~zFaD9NUQz^!F+8BKZ9m9gqO44JP0 zukKN%*D0T*PG^oR*dp&MkcB7rF$m1~G<3ld7^dm;ZlngQeNZ6_N>>tVES>5ImjXK^ zbI|5n@ZCMQS?+NXCyT|6OM)-#4*&L&5cLyC_13-@Z^<B;>E6L%{(bH4638_mj6T9! z_QLzjF;_YX9zx;^skeAUxtM7+<o_rG0yk4Pbl?odzXFJ5(54a>C5)xdyyWq)Oz$r$ zSyg>$X(I6zWza{PCR@z3mOq4N=}j#XQ2j8cw+uLX-T$?b8s%O1Yh`Pm5lUq^i(|Cq zBduG)_qz|b>iYy0+xzYFv=Ix(_XvrtFEKN1&GJG{()z<hMuhO;s>~}00?y&iQDw#= zJ6P~LW*%(Ch|e(#4DmQ*WF)EY6ZhaPaU18~`OZ&b1}yMBmaN+}lV5neGwu$*Jiq00 z(G_|-xIv<do=d#dB>T*iZLfMr7?LF?{e=`4bI6Q`V~sLd@ifjdtm@8KAWLNh-fjq^ z(*cWPl82o(?UH-e_-0?ft<;4>90lTLvK)4;2dr26apGns%P4n$d0tp^axR8-L2`*x zRSUnL`rQ@goThvP%jjeYCu2}^x_0>YLT_Btd#lRR+3O!hs7%ulS+T}-sO?yY%XT?_ zsi)6yf~vi2C`8jq6hYono$h^!kL56%@+Ubn5V|b2(U0hHF?w3=P+j)}&0?lGYsouT zqlfksT<Ijbpm4@c_m0NHU~z-@G+j?h+VQCo_~R4o6$r7^q-Q3q5y1;s=pRv)EctH0 zm$01X_8@A?bq)|Rw5Vh|X5-i{=J^jda3xi9`H10jV5hkQdR$&S4i=_KavYG2$3xVf zjlY4?i8=?Q&ejiKat_0la;qpd@R^tN5Ae<VZZxJ0d1bp%g{LWht)Lylr#5DG3XItb z-rV9G?vI{%90*#YT-C7=)|xvSHTTaD|A>NeZ3e~`UvQJu9OZpoV03C-Q(=Y0_D^pm z5nr`d)DkOKDZ7)|QOD)^-tQN^Zo`qQxLH{Ct8iF^sdn$4j|=KrRDffyF^PeMMBRU* zDCym<&lAKOXOMooJC!cmi~e~%9<QtUeO+V!Q~1aG#hURf1AK7;QvViW>Ax?;EM1Ce zK)*>4^hbuM^UwNXA4HfBfJc;Vy$g}O7A-6z-CBn+3QS#gz&#)5f)+VaL?t$$5B(yJ zXa&B=9u>F*ox+h_Wy4%8svRewwXuoU>#2UgGCH!oOM>l#O5L5ZhtV2GDNY|SH`f=* zpDg>YRZYPo_Ib$BGP<*<!1`3#d&&B2%+Vl^RW;R)*(oH}PIKkZ?fSPB_WNh1EXVg~ z`T7qGcF+j7o`}+Cr;MHYP_h?HACK_8U?$nOGj<K~*gi>W^xgO4%&p-+QEqqdNLFHd z7coG;P`~<Q{9OLxB1M48l;%N`dBtX}$dR)3B|F9VGXs#khesUSC(PEfYnGW+VdWwm z`^iL`_0x<Q4Z@caN)W0~x42EjU``K4uYe}=lpw7Sqg0aBqzp1m&Sj7vengl2>3+mq z;m!>vRldcA<A{`fsYNB0_*?U$Bz}$AKRhJTP#5o?&#KJbJ^Ye@WRX(j#K1;`T(zP7 zT+97l7hX>c=yY5Fn1~)ZGw?VMbBf1EO8lG)P_O5L)3h*XK*0b5W2&D?S%<J|xV<~M zCH)6@zL<S<%=-@M-cze%9CiIg9UFCtae>g+e)oLm!I}BzL`22q)dRFJ7rKL#378A@ zKD89Q=Er9-?M}6>Mg0!xPYy00%1#zm*&P%5;Z#ov2_UpNL|h7j2Ga|jWE+e61vG{q z1XCwW84PPq-%M*wy|Uh;Tcj~h*sm`?oqOp(?P^3s6e(^0>;@O!vcxiZXdpCV+chi@ zdb#Cv!4tifs!d3~EZXiI!tf?+qZwYEQ{M3|3>G6?^kY8Ooi~R`PI{5%(WS^~P&ULm z<sl9wwX~vNt{-Q1{H+@gAr4%G+-xge$5;l2)be6z(q8uad9I=9&IbkkYP)yE8bc^T z9jL7Xb{h$%mSn=E#cSb9&YG&Vt>c*By>E>@#Sxk;sR}P_9@uCq-&tx42=jEITy=Hb z?@%1t;qjst8bJuN-%xzwtZ*m)TMm!t?%sB?r%O`bb~}DeVki#!U7-*;;(>c_%ck-@ zoasiEY}}lROc@NaUHNKr^=CyzG<kLiY1Tu6paJ=f>LjG^5{d_?ogLDcQkmQN^cFsl z4KGS`2!CQ<aW~UaVswm9UQcw7iqh&kZ}0p_Vj{Jt+#YGS$iUSVQN*JhVp~2y!`0<e z!r7te3`#t_4To8IoE#|)HVAuV@>6s+)u;)GO%uzH4ZXn|Hxq~4lBJ`4)Fte<YFnQ5 zS18IMt|~zz*BVNtnfe9)#ON0yQLyLHj^J(zwk)J%db_kV{xjvE2`k=pWwx?@9p0X^ z8!1KBe~&VCG?O<MnawEsy3T(aSIpEJFJM$QlJ-0)Zp`r$$ZRcqaFFRK>KSMms!b_m z(LM5VE^i;&=5*8R=BM~aVW?l5bPj}(hXz33j@O-#MrUSrNPjr9IzUDIT@4!M>3Dd& zN*o=K<!h5CX>;9wxw=E8dB{p~4nrFR>|J56pfT;P$bK+VhL5Z-=ygcfJzTYORuca7 zaK+s@4E8dq=X2ZNZ#T^Guor&@P0DyV-qKp)MCc3TVDfDRA_y3mG7fl7gCQBag}m8C z>#0(`RjXbH09pj*lxN5sC5I^kI^3NqT&kt^`<h9J9yA~ea}Y^0_rXY-LB+ttL__Ya z{v;YC&CodmXka3)a$*t*bqyC+dPYu3&DLQg=ZHg00$!NKW&VDK#1KQ>t4e>|X)4Pc zjI~X^pu+5irR&>?Q=^cgRQkb|fbo0J=ZyGp`Amnbd!iMM!8p_E1_O{uvdNcZoNO5m z4Puk&&(Sj_Yy^nT(eHmvKOEgroHZIQubO9UOt^fYR$4X0xZB)PTygo>Y#WV5h-~vq z*_WHi7ak@b`**6F`$TPuOj@c|&hDqRO3rAkr7rD9rQD=(sx957Te193IkPH8dRN=g zr-YXCG)$m3xO#^a|MRxRfEe3J&Qg`f%wkpQ#H4ekyikn~@A)TJ*0X0t^Q>gDbwBv* z6LNHIjgCla-0GHnsHjyi*khlDpV@xTdscjtzoy8Wd#MmV1Il}6EF{oJGwLK$1EvuP zX#-Cxe0D+&=-Imh6ny$o!O=l@Kv1R)u6zgO&D&y!`74s4#A@YXTJ-Wvx5<gbXWVS^ z#>uaM6iiAzXH4r|T&VlbHm!B;n{68hl~%C+w%(EsLSt3^2&&(4B6N@OgDIRL*sRGc z;sFcU8v^ScDtwT`jIQ7LxzfXeObdLrM#gurZ(Oz$_Qm>Yf_inwA3Wt=8Wzszdb!7s z>YQ6(UIBQD1;rLnrbhaluy5e%VTuCWP@4>w4_<e6J)=H>>}#mRC+I(m&P%#x@W&~e z5uoR>DmwC0OH5cB7CbrdrZG8Cj54XL=zPw?+xA7;GM~6A!mPtr#!Jd`i}cR;UC#kA z*3(SqHQZOT)-Z!Qwf|Ic$(;8>AN((1#{sjje$H$EXbJ>Jx^?AMOOmlq?ayqEtBI>! zK1koP6HRG+KXZExS!!d$4EHQor8{o2wgJS|jS|C}FY6Vb;>2g=P|G7PxcIZ(W>af< zDOf5bQ*C&oYO+pzM|w5cU_E>sl?kewx?%;*Pbf@iOw_4^>y)TH7#7SW`^d;A4tu1% zCMb!FQp#YB!r)(xaF}dY@a<A9Y*#>yBq=6#Sv*=;3Dr+r<5?35<M~RQk!ONK6`SCa zuzocLopH|rwJap~=l>^Ms2e~lmgF5}3<M%af0f{`Y*4?d+%L3IUe8k`nPgyhJlWY} z0dXm&J1lSMohyg^GsA=wj{Ckx=~Ze1Yx}gbMQEY^YKxLX&I<X4`Bcpdy5(7NN7wcJ z5Ae_<@o(PKBBZJPASfL@2rUm@aFCiQU2K?H>>A#zQaIoed}pq}n-E@HQLP+T>>UMv z8y8YNz<tR*`Yn{GcBo#n@q4Xs`)U%kF}}`@Pl*`kX{K$S7yk1!vpw{7N=){ovM-p) z7izriCRGl!1zNK2uK;z?^Nrcu@81Pd@q9+^I@-N#QDeoo`%%#N!M9amgfuRU_wE=0 z>`*)OBojJD_L&Q(ihl&>vlDj4v*sgO8PP*5Nl6V((Dy@Hhsz#Xr%Y$suEISQA};aj z;~TzwrWVR%RJGsq&!eJ{G|7sjNfZop<TLu8eKq9!A9^l{%yKXtr@FIAKuqhd`0(DJ zy)`lV&#D@Zwe4*Ca;$5GqpaMR96ubDF1d&smX2OXy|)J0Q-B4$rOR116SEi3edB{T zsoMc|ktjS`*_?c_j7Ks2)H%<b{BO8@Q;kbwCoY|dGKrVW-i&KrVT)Fz@n@bAanwEB z?|83q@DAfCeE#E)K6K7l5j4eJ{|>;DSt%l;d*p<Cqp0T=kf>KVa6f4D`|d#36JK9E z2?i`lrg-%1u7i5I6FR;o3?K|^SNiDP49=lnYp?y%z*3KA1_k%!juJY3!cFPYu%s+y z?3px98QKT4k;dP)+}-6fm5I&ZOA!JiMe7ubnc(I&BZ&wl(rOkqnjVbo3d4DpPEarQ zbcQvC+8Hmtgb6!6toAv_@^(;=1s|uzP>61q@!j3@VQY<|O<w9M7403(b@c>#@GUqG zopz2`;4>?$Xt^J*S-V`s7gnS6Zp&hU`ib<;MgtUIrJ^rbhWi+ILg-ORDwDxd6+79n zD0SRccT`<c&#U<Er+|eQA2)FIy<q9W+m8jj$j_I;0ROFv!7vq*<a$&rv`9cX(fc7~ zGp;+k#Mn-Jqy)!v;0+!GOFgVd9GwI6X9YP*hT8KHiQW=Iw7B3FI%f<$JO@*fLnG1q zWU`yYH=~p?xBwdYIA&{Zw<ZjFn~8b(_x$imOB9>wW#u?ta>_XnAdTk1;Hh03guaG| zpn118_Q)Kj$3A=oU}{W}AJp!r@xC$Buc4c&+NRDPVn=tOUEk!^h8CP2kxk^Wy;F#m z=gPVrpPd?ittDx$&4$I1`Yx7^*kk;s4uXzeFyxt;d=1|l)3jBHYB##hWNErOGzBEt zwsGhBGUvxv#KJ?r5GTRv&<LGL@c)fpStoYA<l-`nR?Asak~nwD3y|mH3L<Ac$yin@ zA;Gv^_7Sh$?^<o7ehUKX&`fl)iE-pvXM%nA1zV54Xh0*&6+p;41k8w&l0Dysl$^xu z!+95hTUe|$@IojSp5Ssa4V90-u=hhxtGmKzpuJX^8oo!q6LdtQ{)yRpk2e8>*yeTr z`1@A*+!lghmGrg_PAp8YGaVMIR4d*aaVNt1H+KGr3l2$Qt)y6II)8Zqpl6Z}-!C{o z%KdqqSkE%`Lc^}P#R}~e5VP)@8;kS6&uoZ)-tZo^*q3$kPVq5tR}6K`w#NK$DtTFS zxYE)P6I0UsORR++Zw%5<@gbDXuQH5O4#Zt<0ULrRKCdxJL@-5Y5D&sw1XYXukks(= zT`zFG$^ysALt^ELF|N-)zp7tN>;A}+Q_$ew@=N#>w65x>4kLEWVoc4B%e6iuv}QTH zW-8=brh*njVkV~Sk+Q&){V60?`VDjr68CvZ33=rqGqip=nzTzpzUqxUL(U>3+P)!{ z3oir{l4=i$h8D$r?+b2TPWmYa^7jSTp#74=R{Mg_wSLL3Ou~cv<A2N0q9o{ll^pY} z=+$rh<ygT#G8ZIPJoVT0HyhVw`6d6V8&VF1#{ZHdFYB5e4S&h8Ry9-TziSvP5sC2p zBg=zsUl$F6q;wV{B$b1(kjvh`D4i=40MpS+m^hcKBN?4je0cXKqhD{ef-ew5L0{;5 zk3@<w*7T`q9enAnUps`T;(Mm^CzZV5Yd>tA`riK@LQ$MS2!i`|)QT#Sf**3<^EOY8 z;RrIC`F41*Vd;PkLpwSrh<2=AD8~OLQ1#hvHWkd(4W*AhUb19*hla3eS8=_6srPN@ zzCO)2v={TkU%w?zPSD4kSXmblVw4&@@3oWtrao<cc1bEO14WP8PrU;4w^TZNXhabu z+Y|=upze3GO%=04)}qQrYpEZ~U?eyidFi=kb=HN=*qtX^16|R9zBUY>v;7`NIhE1x zu&j?Z?m6XBlp5LNO5;|1No-KGTPSy|TFHB4pl(wXTCCia9HeR^X4!b6s6-ZX&-Yfk z`h|r?3hm}3>J$a_$6bs57htp(bZyZAd|h{lS+?E0eEBLcAmc9=nCD|TRi8;>Zn`}X z<ttx=#K3~Nu=-r7WT;$OLRF}1E44zR&YA;3-&W_(ePj`ir%yfoI$w}F*BH2rhR;-2 z1<T`wVou%M?Hy0)ckzjKucCS@zfwveyx1?+{=WT(aAMe*r=aMd$xF6jqCPs-(B#TR za0O(ytw?)<`K(IvUYu^DfGAX~8}-%daC<3XxAl#eZy<ei6;&iznb95Us$496W+6Ei z7|$eiSxNm3*a;T|t^j<5KBMN~=YaAMZrhNr3|+m7Lm@TcDer2#+iqko?MNefhBQSr z(|4!K{2xYi4YUwf<Kk*G%0x@y#U8JfV*KnC_Z7h(l@o7Y#tZvYKAzHToO|MKnWHME z)af#;>q`=~B=Q<T3|8Wj!W$WE&wC=<`aC%Jy|LYsU3une#<MN2-~e3DLz~&-FV}`1 z*ig}auh~)gZ22D#MBfEg?VuwawtQ}!!;2=uClk5?MD0znAQTul$Ms(ab_HHiM4DNf z`xjVqjh^oL*vuj~y>kMJy_#OnL|a)CH#Kgb9n@bW0ttn0=M3Bfe>vXCb$`xWkyz^0 z)Ue$%L2P8WeKy)a%5g^#WN_@m)4;dm{`u`fiTmHtW+}hae^uH2@}TLjfRuLd?PiYh zJna4n9a4P_$2YK3{Ge5`ZLEy1)P3|pI}+JumU0b=JfCS|tA~W!c)Ng%vuX$5^%_~N zteOJ}cQ~+%Ox3_Wr0yD-RnH(10|{s4VfO?H*Zw<D4&64!=rB6&aCvVz^nbOXJHLU~ z1_btSSsdW0jZGO2(B`s)J>qxQj$S@zaD4hN>h2ab>l7t~b)Hb_h3<xCX+o|G6mnu& zY-@VyJ)XmJQ7V8~=Wp&0>G$=rSevZmJ}h>T#p&Ez&2|5i?iHZ&dX_BnR-6ju)5ax9 zD*gt(uiktESAf#!t4;(2QAK>S$Mw>pjvWilr4a=~2iAT{cmRJyQiXc6&2>fj^#ls4 zZvG9IDz&JfrIX%gI&P~6hY$J1w4Fc9L4_tq*ow`?a@T6jBFr`@N*C&_#-@C#zdBCf z_{(!EO1vbbNab4|v}|jORq7oMXRafA8AK>}X49MYDfK;bUFp4VEBBXO4bxqBCvjUU zJbeUDBC=9hmtD?y7N7OuLAa@D?H^wOV%O_l<SyiYTK9tKkn3K0E)(ZgBuftOa*8kf zWXbDINbqI75_Z}ghT#<4$`$^o!t%34`4aP>q^a45_Ok&3f2Fdv#m#xI-Og+c%O{lP z3??CJBB@bN$UQdkIuGxZO1oJ2Q0NEHanssS#-SQ7`)*PQe^<qDA3b&Ip|u!Kbhk-= z+)(2etMZL&r#Z1R|B-Kc+d~w^d)ve`TNBx|4bpLyN@qI@iTpNEJ`G&h0YOYC=8YqA z<H=yjs41G88CE~sO?#{(pHDrECMRK`>t5wksv)lbGFl)=(o_`G5<@}zksQXB<*@-J ze+F3>yaF1VxxxM<9QFP}m0L_c$uNdEEYA4X7`?l7!h{@n5L8K$g?w%f?j~K#xkLyt z5hFHOnm(=^Q`V<rrydBBNWQo!;9NJaUQRF~WkM{!{d$YVauC(hYH!d~)@%o_vH|8G zN@Q{?J8~gTz_on-Go*+3r7AJcMvz`Lm4kUVHTm&e=VoIoGHHH%*>j4HI&n39Qw$%j zkc4Lp<pe-%K-_ovbY0^yRBG-}#8YFn%;ii)vv|?LY)Ap<&Q=`Fb_ZCk7DL*0d_kjR z52rQoNoA*Mc~CTJqR})&WJ-rk%>Z_&MC@PTyTG_PSw<HOPQ(`K6mcvBqKco~xV_7o z0Z)j=y8;Hb^kRmok7}Z_d`Nof*3*O@$-?84sjq<9A$19ZhTGcn>LL!A5F;gXZDt&n zVR48Sed1=U%=a@^cHLlw1{{{pZ@AgPXDq|Q7~O?3=`!fHyYF=aq|ZjJqKhQ?NY<0l zjfsx32D1XIJ+um2vV3f+cvZ7Ps`L-lbV_j&JV;fG`9olbYM)<;zQjSMQi=*Y${iXO zeMvcNHT>JZXw@Vo%dnII$KQNPNuROW{oS3lb-E0`b>(`-uk6wdp!g)~nZ;y2q&EuN za-N`*d{xfO@*(B>6^a5c8fl-Y%44p;zqu0+l@vD|pj#QmN^swcnRY>7J?uJu`ZvR8 zY?NINz8tXGeJTQFF;o-e?SY!TJ%7I6CXRYG`a6P8N*MH#&s=;wja%u6X{&$d`{)Ge z9|x4>@zp(nXqfu2fiG*W^Od+oW1!j0L?F)1z~m6cX=af*ok!EmK96CBiY<@~^Tt4{ z@k4t`%>4khaq6R{;y`wK3cr~7k}Q{VOVoe@j}0HcjQrcU9L6+7^)!Uhof(plCzMRg z5+UJI!`<_qCjJ?;UtY>dah0EN5B%VvinQ;3u#wiD`sO3b>z&=pE8u#i1V;W{1Qi4I zucZ<S5xqLVg<L1?f;s5|#LHtcTYqr?Qc(<iEFoEXVKd`D{>g^wQm=uoVJ*ZI=gGZB zLHY7DhKC_HPb0eTU({l|tbMHFbV?A59yXfKRq;sNAc!>j5|5*yYo9zCn=><bmoV4k z)g4hUnA|?2o*G$m!;)eA%f;unC4L;loOVAjG(@Q9zGy<d`&#&3(UNk?57xZHM1!6} z;|?je0`zv~9=}|o7+LN9!<Rwox0>U99=_`8Au8UCtO5-fHV~pRg=zWnkE@oAOqT z-3-<xiY?JYWP4nqlwo#|0)^sS{0ngs72S<SQ^X1`c%c}|n^T5@Xp&)aOBL8^W!C)C z9TVg+N#}R1oD&JVU=OKd#*SGB&^fez>}0RJ%r0o5v=y~6K2=1Z8j0R`rdWfDCL`>3 zbD*w}bl(ez=JT-<au_AC(I5~F4LB?9se~IwOm}`_S8s`};H;H>G%{j#PY)d?%s=CX z^U>@9Na{hIy9L4|_dV|PxNS3q9_!8o=WTt4fU}Q2Ttw#7-%&&1995@V5!AMWYJ{SO z>6Us9CQQ$k_Sgy}4<HJWjA1g!Q7q?jD-qz3->B)%ANw-QYjty&#-=-KKJA%-_(r1Z zn;S;43ge2QUp=|%!a6Y2UL3tJ#c$+7#|K@&H~8DTbu^-8_aW20D<UJ@ZG&>D{Uju^ zfKL4>d!5Cp6+JphU=<}1ci)DPBy^8ZG12r?&1KO9jtN0nP0PYPkdHiSga-2XN`!ZN z!s;oGvo=ZyMB-!lwWxi*ED4D_hG)}dx#@(;nqo!xa_1;eYuXtp;VoQ*k-qQ3g9L^L zUQ%D;?Zr@p6f9|fEbVyV2}-OenCSQfkL?z<yz54W-Hx$~iT_l)a&KS^5#0BAQNSNl zkoHU<BGSW;AsB~;1Q)G}OZmMI*KLh63x#e(c?Xk)Ol(Hi7lj$$-*<tDQ&lE=TVdGr zN`#iekRB^P(D}$Jt!B$MODC{fqJsIRq9nMXpzbYPPmF<y&P7gh2^&St*qDO4vY0iw ze@L$vaa&i_suk|QW|_Cn2eX0sD~sdv(ejpgeziaNJhNIGm=W?Z3dXM98a}P@jc~$- zb97;J=34@LUS;nE)_Uxqgd?5}zEf0m{}!@`32FvKpmQMM5HoX_`V!$TiM8S#(gt0F zFN3P?HCiQUgTW(A1dIwvqaa9v@Y5R4uMWEz3a#!SI)!b$WG3I6ZpU603L9Ieh6%|3 z^pcil{>nu$lqjSiC%j4bck|)nQN&ta{jmFQP?;H#wC7%pf2{wjnx9+Tr?*{iH^7$< zvt$!}?j>^5?Yrm3Hqzb9u9{1{L~fN(mN~4*+qUb~MjM8GNX39&!u&(Oazv=(Es?ZW zAH9SRkDuS&*xsY+gIqo#z<!KLDv5GzBEM|Il8F~*{#`cm7N2FMJ>n^av3_Wxc>15T z!1>YRI=rykmY<UDjdGBO`@eZ{eB%Do_r+96fEp_+gG<GV9MrpwK}##%CBmhot91Es zR$oscPH~4bt%hXmLQ+__oG>IoJV>cyAC^jwwI0cu&@Jy!CwMMsQc<DS`h>#c?I$G$ zD^`V%g)5MJw1Q;}93ycx!+g)TAs)k)7TTY5Hk)7N4tu9q6PV<X5QI;#)4MM6$2N1m z&VR7zXZ*QLmuL}(I%N$6D=0k0B|(4ECkuOFM^NWl`euDv6ZeOCv--$@>T4R-lKIPx zu*XDC5Xe`~7#m6?cWEEckrMX4Vtxp1O2><Q_p%u0Oeir?Uf#`QT|DLoCXX9gYB`ud PY$<|U+}aCC>xlmWy16?N 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 7368d755036de..4c4f2138abf35 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -34,6 +34,7 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use Magento\TestFramework\Indexer\TestCase; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; use Psr\Log\LoggerInterface; @@ -50,8 +51,10 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * phpcs:disable Generic.PHP.NoSilencedErrors, Generic.Metrics.NestingLevel, Magento2.Functions.StaticFunction */ -class ProductTest extends \Magento\TestFramework\Indexer\TestCase +class ProductTest extends TestCase { + private const LONG_FILE_NAME_IMAGE = 'magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg'; + /** * @var \Magento\CatalogImportExport\Model\Import\Product */ @@ -602,7 +605,6 @@ private function createImportModel($pathToFile, $behavior = \Magento\ImportExpor /** * @param string $productSku * @return array ['optionId' => ['optionValueId' => 'optionValueTitle', ...], ...] - * @throws \Magento\Framework\Exception\NoSuchEntityException */ private function getCustomOptionValues($productSku) { @@ -1030,13 +1032,12 @@ function (\Magento\Framework\DataObject $item) { ) ); - $this->importDataForMediaTest('import_media_additional_images.csv'); + $this->importDataForMediaTest('import_media_additional_long_name_image.csv'); $product->cleanModelCache(); $product = $this->getProductBySku('simple_new'); $items = array_values($product->getMediaGalleryImages()->getItems()); - $images[] = ['file' => '/m/a/magento_additional_image_three.jpg', 'label' => '']; - $images[] = ['file' => '/m/a/magento_additional_image_four.jpg', 'label' => '']; - $this->assertCount(7, $items); + $images[] = ['file' => '/m/a/' . self::LONG_FILE_NAME_IMAGE, 'label' => '']; + $this->assertCount(6, $items); $this->assertEquals( $images, array_map( @@ -1049,33 +1050,20 @@ function (\Magento\Framework\DataObject $item) { } /** - * Test that product import with images works properly + * Test import twice and check that image will not be duplicate * * @magentoDataFixture mediaImportImageFixture - * @magentoAppIsolation enabled - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return void */ - public function testSaveMediaImageDuplicateImages() + public function testSaveMediaImageDuplicateImages(): void { - // Will check that existing product update works - // New unique images as per MD5 should be added, images not mentioned in the import should be removed - $this->importDataForMediaTest('import_media_update_images.csv'); - - $product = $this->getProductBySku('simple_new'); - - $gallery = $product->getMediaGalleryImages(); - $this->assertEquals('/m/a/magento_image.jpg', $product->getData('image')); - - // small_image should be skipped from update as it is a duplicate (md5 is the same) - $this->assertEquals('/m/a/magento_small_image.jpg', $product->getData('small_image')); - $this->assertEquals('/m/a/magento_thumbnail.jpg', $product->getData('thumbnail')); - $this->assertEquals('/m/a/magento_image.jpg', $product->getData('swatch_image')); + $this->importDataForMediaTest('import_media.csv'); + $imagesCount = count($this->getProductBySku('simple_new')->getMediaGalleryImages()->getItems()); - $gallery = $product->getMediaGalleryImages(); - $this->assertInstanceOf(\Magento\Framework\Data\Collection::class, $gallery); + // import the same file again + $this->importDataForMediaTest('import_media.csv'); - $items = $gallery->getItems(); - $this->assertCount(4, $items); + $this->assertCount($imagesCount, $this->getProductBySku('simple_new')->getMediaGalleryImages()->getItems()); } /** @@ -1120,6 +1108,10 @@ public static function mediaImportImageFixture() 'source' => __DIR__ . '/../../../../Magento/Catalog/_files/magento_thumbnail.jpg', 'dest' => $dirPath . '/magento_thumbnail.jpg', ], + [ + 'source' => __DIR__ . '/../../../../Magento/Catalog/_files/' . self::LONG_FILE_NAME_IMAGE, + 'dest' => $dirPath . '/' . self::LONG_FILE_NAME_IMAGE, + ], [ 'source' => __DIR__ . '/_files/magento_additional_image_one.jpg', 'dest' => $dirPath . '/magento_additional_image_one.jpg', diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_long_name_image.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_long_name_image.csv new file mode 100644 index 0000000000000..2d2a192ed6c7c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_additional_long_name_image.csv @@ -0,0 +1,2 @@ +sku,additional_images +simple_new,magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv deleted file mode 100644 index 56dd5b4e977bf..0000000000000 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_media_update_images.csv +++ /dev/null @@ -1,2 +0,0 @@ -sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label1,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,crosssell_skus,upsell_skus,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,associated_skus -simple_new,,Default,simple,,base,New Product,,,,1,Taxable Goods,"Catalog, Search",10,,,,new-product,New Product,New Product,New Product ,magento_image_2.jpg,Image Label,magento_small_image_2.jpg,Small Image Label,magento_thumbnail.jpg,Thumbnail Label,magento_image.jpg,Image Label,10/20/15 07:05,10/20/15 07:05,,,Block after Info Column,,,,,,,,,,,,,"has_options=1,quantity_and_stock_status=In Stock,required_options=1",100,0,1,0,0,1,1,1,10000,1,1,1,1,1,0,1,1,0,0,0,1,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php index d426a1521e5b6..0ee59aedd8979 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_with_filesystem_images.php @@ -29,10 +29,6 @@ 'source' => __DIR__ . '/../../../../../Magento/Catalog/_files/magento_image.jpg', 'dest' => $dirPath . '/magento_image.jpg', ], - [ - 'source' => __DIR__ . '/../../../../../Magento/Catalog/_files/magento_image_2.jpg', - 'dest' => $dirPath . '/magento_image_2.jpg', - ], [ 'source' => __DIR__ . '/../../../../../Magento/Catalog/_files/magento_small_image.jpg', 'dest' => $dirPath . '/magento_small_image.jpg', From 740ce6b4019ac25a7676e0ce57977a31f12ab792 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Fri, 14 Aug 2020 16:15:53 +0300 Subject: [PATCH 0477/1013] minor change --- .../GraphQl/Test/Unit/Query/EnumLookupTest.php | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php index 2d60518806000..1a6ab82d25b0b 100644 --- a/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php +++ b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php @@ -114,9 +114,7 @@ protected function setUp(): void ) ->getMock(); - $this->enumDataMapperMock = $this->getMockBuilder(DataMapperInterface::class) - ->setConstructorArgs($this->map) - ->getMock(); + $this->enumDataMapperMock = $this->getMockForAbstractClass(DataMapperInterface::class); $this->configDataMock = $this->getMockBuilder(DataInterface::class) ->getMock(); @@ -125,15 +123,7 @@ protected function setUp(): void $this->queryFieldsMock = $this->getMockBuilder(QueryFields::class) ->getMock(); - $this->typeConfigMock = $this->getMockBuilder(ConfigInterface::class) - ->setConstructorArgs( - [ - $this->configDataMock, - $this->configElementFactoryMock, - $this->queryFieldsMock, - ] - ) - ->getMock(); + $this->typeConfigMock = $this->getMockForAbstractClass(ConfigInterface::class); $this->enumLookup = $this->objectManager->getObject( EnumLookup::class, From e113e37d4cd122ac6bf2b76ab51a1c661945b2e8 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Mon, 14 Sep 2020 12:29:24 +0300 Subject: [PATCH 0478/1013] minor change --- .../GraphQl/Test/Unit/Query/EnumLookupTest.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php index 1a6ab82d25b0b..7e5d6ab2d6565 100644 --- a/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php +++ b/lib/internal/Magento/Framework/GraphQl/Test/Unit/Query/EnumLookupTest.php @@ -81,9 +81,6 @@ class EnumLookupTest extends TestCase */ private $values = []; - /** - * @inheritDoc - */ protected function setUp(): void { $this->objectManager = new ObjectManager($this); @@ -114,16 +111,11 @@ protected function setUp(): void ) ->getMock(); - $this->enumDataMapperMock = $this->getMockForAbstractClass(DataMapperInterface::class); - - $this->configDataMock = $this->getMockBuilder(DataInterface::class) - ->getMock(); - $this->configElementFactoryMock = $this->getMockBuilder(ConfigElementFactoryInterface::class) - ->getMock(); - $this->queryFieldsMock = $this->getMockBuilder(QueryFields::class) - ->getMock(); - - $this->typeConfigMock = $this->getMockForAbstractClass(ConfigInterface::class); + $this->enumDataMapperMock = $this->createMock(DataMapperInterface::class); + $this->configDataMock = $this->createMock(DataInterface::class); + $this->configElementFactoryMock = $this->createMock(ConfigElementFactoryInterface::class); + $this->queryFieldsMock = $this->createMock(QueryFields::class); + $this->typeConfigMock = $this->createMock(ConfigInterface::class); $this->enumLookup = $this->objectManager->getObject( EnumLookup::class, From 814cf2bd43fda76c2741d67c3209da5aa29b4c49 Mon Sep 17 00:00:00 2001 From: Alex Taranovskyi <a.taranovskyi@atwix.com> Date: Mon, 14 Sep 2020 15:57:56 +0300 Subject: [PATCH 0479/1013] Revert exception --- .../Magento/Framework/GraphQl/Query/EnumLookup.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php index 4afeac9238623..bbdcf70c9fb16 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php @@ -7,9 +7,11 @@ namespace Magento\Framework\GraphQl\Query; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\GraphQl\Config\Element\Enum; use Magento\Framework\GraphQl\ConfigInterface; use Magento\Framework\GraphQl\Schema\Type\Enum\DataMapperInterface; +use Magento\Framework\Phrase; /** * Processor that looks up definition data of an enum to lookup and convert data as it's specified in the schema. @@ -27,8 +29,6 @@ class EnumLookup private $enumDataMapper; /** - * EnumLookup constructor. - * * @param ConfigInterface $typeConfig * @param DataMapperInterface $enumDataMapper */ @@ -44,11 +44,19 @@ public function __construct(ConfigInterface $typeConfig, DataMapperInterface $en * @param string $enumName * @param string $fieldValue * @return string + * @throws RuntimeException */ public function getEnumValueFromField(string $enumName, string $fieldValue) : string { /** @var Enum $enumObject */ $enumObject = $this->typeConfig->getConfigElement($enumName); + + if (!($enumObject instanceof Enum)) { + throw new RuntimeException( + new Phrase('Enum type "%1" not defined', [$enumName]) + ); + } + $mappedValues = $this->enumDataMapper->getMappedEnums($enumName); foreach ($enumObject->getValues() as $enumItem) { From d35dd1ca716a4dbba5c37a6c4a784a0d868d1109 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Fri, 11 Sep 2020 17:46:58 -0500 Subject: [PATCH 0480/1013] MC-37351: Cart contents lost after switching to different store with different domain - Add ability to extend store switch request data --- .../RedirectDataPostprocessor.php | 91 ++++++++++++++ .../RedirectDataPreprocessor.php | 68 ++++++++++ app/code/Magento/Checkout/etc/frontend/di.xml | 14 +++ .../RedirectDataPostprocessor.php | 75 +++++++++++ .../RedirectDataPreprocessor.php | 70 +++++++++++ app/code/Magento/Customer/etc/frontend/di.xml | 14 +++ .../Quote/Plugin/UpdateQuoteItemStore.php | 72 ----------- app/code/Magento/Quote/etc/frontend/di.xml | 3 - .../Store/Controller/Store/Redirect.php | 52 +++++--- .../Store/Model/StoreSwitcher/Context.php | 68 ++++++++++ .../Model/StoreSwitcher/ContextInterface.php | 37 ++++++ .../Model/StoreSwitcher/HashGenerator.php | 5 +- .../Model/StoreSwitcher/HashProcessor.php | 118 +++++++++--------- .../Model/StoreSwitcher/RedirectData.php | 66 ++++++++++ .../RedirectDataCacheSerializer.php | 99 +++++++++++++++ .../StoreSwitcher/RedirectDataGenerator.php | 82 ++++++++++++ .../StoreSwitcher/RedirectDataInterface.php | 35 ++++++ .../RedirectDataPostprocessorComposite.php | 37 ++++++ .../RedirectDataPostprocessorInterface.php | 22 ++++ .../RedirectDataPreprocessorComposite.php | 39 ++++++ .../RedirectDataPreprocessorInterface.php | 23 ++++ .../RedirectDataSerializerInterface.php | 30 +++++ .../StoreSwitcher/RedirectDataValidator.php | 55 ++++++++ .../Unit/Controller/Store/RedirectTest.php | 20 ++- app/code/Magento/Store/etc/di.xml | 5 + 25 files changed, 1042 insertions(+), 158 deletions(-) create mode 100644 app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPostprocessor.php create mode 100644 app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php create mode 100644 app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php create mode 100644 app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php delete mode 100644 app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/Context.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectData.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataInterface.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorComposite.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorInterface.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorComposite.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorInterface.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataSerializerInterface.php create mode 100644 app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php diff --git a/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPostprocessor.php b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPostprocessor.php new file mode 100644 index 0000000000000..04f3d9aa37722 --- /dev/null +++ b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPostprocessor.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Psr\Log\LoggerInterface; + +/** + * Process checkout data redirected from origin store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RedirectDataPostprocessor implements RedirectDataPostprocessorInterface +{ + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param CartRepositoryInterface $quoteRepository + * @param CustomerSession $customerSession + * @param CheckoutSession $checkoutSession + * @param LoggerInterface $logger + */ + public function __construct( + CartRepositoryInterface $quoteRepository, + CustomerSession $customerSession, + CheckoutSession $checkoutSession, + LoggerInterface $logger + ) { + $this->quoteRepository = $quoteRepository; + $this->customerSession = $customerSession; + $this->checkoutSession = $checkoutSession; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): void + { + if (!empty($data['quote_id']) + && $this->checkoutSession->getQuoteId() === null + && !$this->customerSession->isLoggedIn() + ) { + try { + $quote = $this->quoteRepository->get((int) $data['quote_id']); + if ($quote + && $quote->getIsActive() + && in_array($context->getTargetStore()->getId(), $quote->getSharedStoreIds()) + ) { + $this->checkoutSession->setQuoteId($quote->getId()); + } + } catch (\Throwable $e) { + $this->logger->error($e); + } + } + $quote = $this->checkoutSession->getQuote(); + if ($quote->getIsActive()) { + // Update quote items so that product names are updated for current store view + $quote->setStoreId($context->getTargetStore()->getId()); + $quote->getItemsCollection(false); + $this->quoteRepository->save($quote); + } + } +} diff --git a/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php new file mode 100644 index 0000000000000..45e1719e34546 --- /dev/null +++ b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; + +/** + * Collect checkout data to be redirected to target store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface +{ + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + + /** + * @param CartRepositoryInterface $quoteRepository + * @param CustomerSession $customerSession + * @param CheckoutSession $checkoutSession + */ + public function __construct( + CartRepositoryInterface $quoteRepository, + CustomerSession $customerSession, + CheckoutSession $checkoutSession + ) { + $this->quoteRepository = $quoteRepository; + $this->customerSession = $customerSession; + $this->checkoutSession = $checkoutSession; + } + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): array + { + if ($this->checkoutSession->getQuoteId() && !$this->customerSession->isLoggedIn()) { + $quote = $this->checkoutSession->getQuote(); + if ($quote + && $quote->getIsActive() + && in_array($context->getTargetStore()->getId(), $quote->getSharedStoreIds()) + ) { + $data['quote_id'] = (int) $quote->getId(); + } + } + return $data; + } +} diff --git a/app/code/Magento/Checkout/etc/frontend/di.xml b/app/code/Magento/Checkout/etc/frontend/di.xml index 8f35fe9f37abf..f28e2a5d91ba3 100644 --- a/app/code/Magento/Checkout/etc/frontend/di.xml +++ b/app/code/Magento/Checkout/etc/frontend/di.xml @@ -99,4 +99,18 @@ <type name="Magento\Quote\Model\Quote"> <plugin name="clear_addresses_after_product_delete" type="Magento\Checkout\Plugin\Model\Quote\ResetQuoteAddresses"/> </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="checkout_session" xsi:type="object">Magento\Checkout\Model\StoreSwitcher\RedirectDataPreprocessor</item> + </argument> + </arguments> + </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="checkout_session" xsi:type="object">Magento\Checkout\Model\StoreSwitcher\RedirectDataPostprocessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php new file mode 100644 index 0000000000000..ce532091b3649 --- /dev/null +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\StoreSwitcher; + +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Psr\Log\LoggerInterface; + +/** + * Process customer data redirected from origin store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class RedirectDataPostprocessor implements RedirectDataPostprocessorInterface +{ + /** + * @var Session + */ + private $session; + /** + * @var LoggerInterface + */ + private $logger; + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @param CustomerRegistry $customerRegistry + * @param Session $session + * @param LoggerInterface $logger + */ + public function __construct( + CustomerRegistry $customerRegistry, + Session $session, + LoggerInterface $logger + ) { + $this->customerRegistry = $customerRegistry; + $this->session = $session; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): void + { + if (!empty($data['customer_id'])) { + try { + $customer = $this->customerRegistry->retrieve($data['customer_id']); + if (!$this->session->isLoggedIn() + && in_array($context->getTargetStore()->getId(), $customer->getSharedStoreIds()) + ) { + $this->session->setCustomerDataAsLoggedIn($customer->getDataModel()); + } + } catch (NoSuchEntityException $e) { + $this->logger->error($e); + throw new LocalizedException(__('The requested customer does not exist.'), $e); + } catch (LocalizedException $e) { + $this->logger->error($e); + throw new LocalizedException(__('There was an error retrieving the customer record.'), $e); + } + } + } +} diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php new file mode 100644 index 0000000000000..678e1eb0ca79b --- /dev/null +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\StoreSwitcher; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Collect customer data to be redirected to target store + */ +class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface +{ + /** + * @var UserContextInterface + */ + private $userContext; + /** + * @var LoggerInterface + */ + private $logger; + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @param CustomerRegistry $customerRegistry + * @param UserContextInterface $userContext + * @param LoggerInterface $logger + */ + public function __construct( + CustomerRegistry $customerRegistry, + UserContextInterface $userContext, + LoggerInterface $logger + ) { + $this->customerRegistry = $customerRegistry; + $this->userContext = $userContext; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function process(ContextInterface $context, array $data): array + { + if ($this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER + && $this->userContext->getUserId() + ) { + try { + $customer = $this->customerRegistry->retrieve($this->userContext->getUserId()); + if (in_array($context->getTargetStore()->getId(), $customer->getSharedStoreIds())) { + $data['customer_id'] = (int) $customer->getId(); + } + } catch (Throwable $e) { + $this->logger->error($e); + } + } + + return $data; + } +} diff --git a/app/code/Magento/Customer/etc/frontend/di.xml b/app/code/Magento/Customer/etc/frontend/di.xml index 2a6e36a1ea3d7..31f3e11522e12 100644 --- a/app/code/Magento/Customer/etc/frontend/di.xml +++ b/app/code/Magento/Customer/etc/frontend/di.xml @@ -113,4 +113,18 @@ </argument> </arguments> </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="customer_session" xsi:type="object">Magento\Customer\Model\StoreSwitcher\RedirectDataPreprocessor</item> + </argument> + </arguments> + </type> + <type name="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorComposite"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="customer_session" xsi:type="object">Magento\Customer\Model\StoreSwitcher\RedirectDataPostprocessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php b/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php deleted file mode 100644 index 19a7e03264d8a..0000000000000 --- a/app/code/Magento/Quote/Plugin/UpdateQuoteItemStore.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Quote\Plugin; - -use Magento\Checkout\Model\Session; -use Magento\Quote\Model\QuoteRepository; -use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\StoreSwitcherInterface; - -/** - * Updates quote items store id. - * - * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) - */ -class UpdateQuoteItemStore -{ - /** - * @var QuoteRepository - */ - private $quoteRepository; - - /** - * @var Session - */ - private $checkoutSession; - - /** - * @param QuoteRepository $quoteRepository - * @param Session $checkoutSession - */ - public function __construct( - QuoteRepository $quoteRepository, - Session $checkoutSession - ) { - $this->quoteRepository = $quoteRepository; - $this->checkoutSession = $checkoutSession; - } - - /** - * Update store id in active quote after store view switching. - * - * @param StoreSwitcherInterface $subject - * @param string $result - * @param StoreInterface $fromStore store where we came from - * @param StoreInterface $targetStore store where to go to - * @param string $redirectUrl original url requested for redirect after switching - * @return string url to be redirected after switching - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterSwitch( - StoreSwitcherInterface $subject, - $result, - StoreInterface $fromStore, - StoreInterface $targetStore, - string $redirectUrl - ): string { - $quote = $this->checkoutSession->getQuote(); - if ($quote->getIsActive()) { - $quote->setStoreId( - $targetStore->getId() - ); - $quote->getItemsCollection(false); - $this->quoteRepository->save($quote); - } - return $result; - } -} diff --git a/app/code/Magento/Quote/etc/frontend/di.xml b/app/code/Magento/Quote/etc/frontend/di.xml index ecad94fbbc249..125afb96f20fd 100644 --- a/app/code/Magento/Quote/etc/frontend/di.xml +++ b/app/code/Magento/Quote/etc/frontend/di.xml @@ -12,9 +12,6 @@ <argument name="checkoutSession" xsi:type="object">Magento\Checkout\Model\Session\Proxy</argument> </arguments> </type> - <type name="Magento\Store\Model\StoreSwitcherInterface"> - <plugin name="update_quote_item_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteItemStore"/> - </type> <type name="Magento\Store\Api\StoreCookieManagerInterface"> <plugin name="update_quote_store_after_switch_store_view" type="Magento\Quote\Plugin\UpdateQuoteStore"/> </type> diff --git a/app/code/Magento/Store/Controller/Store/Redirect.php b/app/code/Magento/Store/Controller/Store/Redirect.php index 45924b5b0d28a..ac2c05840153f 100644 --- a/app/code/Magento/Store/Controller/Store/Redirect.php +++ b/app/code/Magento/Store/Controller/Store/Redirect.php @@ -21,10 +21,14 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; use Magento\Store\Model\StoreSwitcher\HashGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; /** * Builds correct url to target store (group) and performs redirect. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionInterface { @@ -47,6 +51,14 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @var StoreManagerInterface */ private $storeManager; + /** + * @var RedirectDataGenerator|null + */ + private $redirectDataGenerator; + /** + * @var ContextInterfaceFactory|null + */ + private $contextFactory; /** * @param Context $context @@ -55,7 +67,9 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @param Generic $session * @param SidResolverInterface $sidResolver * @param HashGenerator $hashGenerator - * @param StoreManagerInterface $storeManager + * @param StoreManagerInterface|null $storeManager + * @param RedirectDataGenerator|null $redirectDataGenerator + * @param ContextInterfaceFactory|null $contextFactory * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -65,13 +79,19 @@ public function __construct( Generic $session, SidResolverInterface $sidResolver, HashGenerator $hashGenerator, - StoreManagerInterface $storeManager = null + StoreManagerInterface $storeManager = null, + ?RedirectDataGenerator $redirectDataGenerator = null, + ?ContextInterfaceFactory $contextFactory = null ) { parent::__construct($context); $this->storeRepository = $storeRepository; $this->storeResolver = $storeResolver; $this->hashGenerator = $hashGenerator; $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + $this->redirectDataGenerator = $redirectDataGenerator + ?: ObjectManager::getInstance()->get(RedirectDataGenerator::class); + $this->contextFactory = $contextFactory + ?: ObjectManager::getInstance()->get(ContextInterfaceFactory::class); } /** @@ -85,7 +105,6 @@ public function execute() $currentStore = $this->storeRepository->getById($this->storeResolver->getCurrentStoreId()); $targetStoreCode = $this->_request->getParam(StoreResolver::PARAM_NAME); $fromStoreCode = $this->_request->getParam('___from_store'); - $error = null; if ($targetStoreCode === null) { return $this->_redirect($currentStore->getBaseUrl()); @@ -97,30 +116,33 @@ public function execute() /** @var Store $targetStore */ $targetStore = $this->storeRepository->get($targetStoreCode); $this->storeManager->setCurrentStore($targetStore); - } catch (NoSuchEntityException $e) { - $error = __("Requested store is not found ({$fromStoreCode})"); - } - - if ($error !== null) { - $this->messageManager->addErrorMessage($error); - $this->_redirect->redirect($this->_response, $currentStore->getBaseUrl()); - } else { $encodedUrl = $this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED); + $redirectData = $this->redirectDataGenerator->generate( + $this->contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => $this->_redirect->getRedirectUrl() + ] + ) + ); $query = [ '___from_store' => $fromStore->getCode(), StoreResolverInterface::PARAM_NAME => $targetStoreCode, ActionInterface::PARAM_NAME_URL_ENCODED => $encodedUrl, + 'data' => $redirectData->getData(), + 'time_stamp' => $redirectData->getTimestamp(), + 'signature' => $redirectData->getSignature(), ]; - - $customerHash = $this->hashGenerator->generateHash($fromStore); - $query = array_merge($query, $customerHash); - $arguments = [ '_nosid' => true, '_query' => $query ]; $this->_redirect->redirect($this->_response, 'stores/store/switch', $arguments); + } catch (NoSuchEntityException $e) { + $this->messageManager->addErrorMessage(__("Requested store is not found ({$fromStoreCode})")); + $this->_redirect->redirect($this->_response, $currentStore->getBaseUrl()); } return null; diff --git a/app/code/Magento/Store/Model/StoreSwitcher/Context.php b/app/code/Magento/Store/Model/StoreSwitcher/Context.php new file mode 100644 index 0000000000000..c67dc3d67b01a --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/Context.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Api\Data\StoreInterface; + +/** + * Store switcher context + */ +class Context implements ContextInterface +{ + /** + * @var StoreInterface + */ + private $fromStore; + /** + * @var StoreInterface + */ + private $targetStore; + /** + * @var string + */ + private $redirectUrl; + + /** + * @param StoreInterface $fromStore + * @param StoreInterface $targetStore + * @param string $redirectUrl + */ + public function __construct( + StoreInterface $fromStore, + StoreInterface $targetStore, + string $redirectUrl + ) { + $this->fromStore = $fromStore; + $this->targetStore = $targetStore; + $this->redirectUrl = $redirectUrl; + } + + /** + * @inheritDoc + */ + public function getFromStore(): StoreInterface + { + return $this->fromStore; + } + + /** + * @inheritDoc + */ + public function getTargetStore(): StoreInterface + { + return $this->targetStore; + } + + /** + * @inheritDoc + */ + public function getRedirectUrl(): string + { + return $this->redirectUrl; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php new file mode 100644 index 0000000000000..a18c7cc9ccc27 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Store\Api\Data\StoreInterface; + +/** + * Store switcher context interface + */ +interface ContextInterface +{ + /** + * Store to switch from + * + * @return StoreInterface + */ + public function getFromStore(): StoreInterface; + + /** + * Store to switch to + * + * @return StoreInterface + */ + public function getTargetStore(): StoreInterface; + + /** + * The URL to redirect after switching store + * + * @return string + */ + public function getRedirectUrl(): string; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php index d1858939434b7..3c2320df2ed1a 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashGenerator.php @@ -8,7 +8,6 @@ namespace Magento\Store\Model\StoreSwitcher; use Magento\Authorization\Model\UserContextInterface; -use Magento\Framework\App\ActionInterface; use Magento\Framework\App\DeploymentConfig as DeploymentConfig; use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\Url\Helper\Data as UrlHelper; @@ -17,6 +16,10 @@ /** * Generate one time token and build redirect url + * + * @deplacated No longer used + * @see RedirectDataGenerator + * @see RedirectDataValidator */ class HashGenerator { diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php index 909fe9f6683f8..d957e89a4435e 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php @@ -7,71 +7,74 @@ namespace Magento\Store\Model\StoreSwitcher; -use Magento\Authorization\Model\UserContextInterface; -use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Model\ResourceModel\CustomerRepository; -use Magento\Customer\Model\Session as CustomerSession; -use Magento\Framework\App\DeploymentConfig as DeploymentConfig; use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\Url\Helper\Data as UrlHelper; use Magento\Store\Api\Data\StoreInterface; -use Magento\Store\Model\StoreSwitcher\HashGenerator\HashData; use Magento\Store\Model\StoreSwitcherInterface; /** * Process one time token and build redirect url * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class HashProcessor implements StoreSwitcherInterface { - /** - * @var HashGenerator - */ - private $hashGenerator; - /** * @var RequestInterface */ private $request; - + /** + * @var RedirectDataPostprocessorInterface + */ + private $postprocessor; + /** + * @var RedirectDataSerializerInterface + */ + private $dataSerializer; /** * @var ManagerInterface */ private $messageManager; - /** - * @var customerSession + * @var RedirectDataInterfaceFactory */ - private $customerSession; - + private $dataFactory; + /** + * @var ContextInterfaceFactory + */ + private $contextFactory; /** - * @var CustomerRepositoryInterface + * @var RedirectDataValidator */ - private $customerRepository; + private $dataValidator; /** - * @param HashGenerator $hashGenerator * @param RequestInterface $request + * @param RedirectDataPostprocessorInterface $postprocessor + * @param RedirectDataSerializerInterface $dataSerializer * @param ManagerInterface $messageManager - * @param CustomerRepository $customerRepository - * @param CustomerSession $customerSession + * @param ContextInterfaceFactory $contextFactory + * @param RedirectDataInterfaceFactory $dataFactory + * @param RedirectDataValidator $dataValidator */ public function __construct( - HashGenerator $hashGenerator, RequestInterface $request, + RedirectDataPostprocessorInterface $postprocessor, + RedirectDataSerializerInterface $dataSerializer, ManagerInterface $messageManager, - CustomerRepository $customerRepository, - CustomerSession $customerSession + ContextInterfaceFactory $contextFactory, + RedirectDataInterfaceFactory $dataFactory, + RedirectDataValidator $dataValidator ) { - $this->hashGenerator = $hashGenerator; $this->request = $request; + $this->postprocessor = $postprocessor; + $this->dataSerializer = $dataSerializer; $this->messageManager = $messageManager; - $this->customerSession = $customerSession; - $this->customerRepository = $customerRepository; + $this->contextFactory = $contextFactory; + $this->dataFactory = $dataFactory; + $this->dataValidator = $dataValidator; } /** @@ -85,41 +88,34 @@ public function __construct( */ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string { - $customerId = $this->request->getParam('customer_id'); - - if ($customerId) { - $fromStoreCode = (string)$this->request->getParam('___from_store'); - $timeStamp = (string)$this->request->getParam('time_stamp'); - $signature = (string)$this->request->getParam('signature'); + $timestamp = (int) $this->request->getParam('time_stamp'); + $signature = (string) $this->request->getParam('signature'); + $data = (string) $this->request->getParam('data'); + $context = $this->contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => $redirectUrl + ] + ); + $redirectDataObject = $this->dataFactory->create( + [ + 'signature' => $signature, + 'timestamp' => $timestamp, + 'data' => $data + ] + ); - $error = null; - - $data = new HashData( - [ - "customer_id" => $customerId, - "time_stamp" => $timeStamp, - "___from_store" => $fromStoreCode - ] - ); - - if ($redirectUrl && $this->hashGenerator->validateHash($signature, $data)) { - try { - $customer = $this->customerRepository->getById($customerId); - if (!$this->customerSession->isLoggedIn()) { - $this->customerSession->setCustomerDataAsLoggedIn($customer); - } - } catch (NoSuchEntityException $e) { - $error = __('The requested customer does not exist.'); - } catch (LocalizedException $e) { - $error = __('There was an error retrieving the customer record.'); - } + try { + if ($redirectUrl && $this->dataValidator->validate($context, $redirectDataObject)) { + $this->postprocessor->process($context, $this->dataSerializer->unserialize($data)); } else { - $error = __('The requested store cannot be found. Please check the request and try again.'); - } - - if ($error !== null) { - $this->messageManager->addErrorMessage($error); + throw new LocalizedException( + __('The requested store cannot be found. Please check the request and try again.') + ); } + } catch (LocalizedException $exception) { + $this->messageManager->addErrorMessage($exception->getMessage()); } return $redirectUrl; diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectData.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectData.php new file mode 100644 index 0000000000000..58185ea3d712a --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectData.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data + */ +class RedirectData implements RedirectDataInterface +{ + /** + * @var string + */ + private $signature; + /** + * @var string + */ + private $data; + /** + * @var int + */ + private $timestamp; + + /** + * @param string $signature + * @param string $data + * @param int $timestamp + */ + public function __construct( + string $signature, + string $data, + int $timestamp + ) { + $this->signature = $signature; + $this->data = $data; + $this->timestamp = $timestamp; + } + + /** + * @inheritDoc + */ + public function getSignature(): string + { + return $this->signature; + } + + /** + * @inheritDoc + */ + public function getData(): string + { + return $this->data; + } + + /** + * @inheritDoc + */ + public function getTimestamp(): int + { + return $this->timestamp; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php new file mode 100644 index 0000000000000..4469e39335104 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Math\Random; +use Magento\Framework\Serialize\Serializer\Json; +use Psr\Log\LoggerInterface; + +/** + * Store switcher redirect data cache serializer + */ +class RedirectDataCacheSerializer implements RedirectDataSerializerInterface +{ + private const CACHE_KEY_PREFIX = 'store_switch_'; + private const CACHE_LIFE_TIME = 10; + private const CACHE_ID_LENGTH = 32; + + /** + * @var CacheInterface + */ + private $cache; + /** + * @var Json + */ + private $json; + /** + * @var Random + */ + private $random; + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Json $json + * @param Random $random + * @param CacheInterface $cache + * @param LoggerInterface $logger + */ + public function __construct( + Json $json, + Random $random, + CacheInterface $cache, + LoggerInterface $logger + ) { + $this->cache = $cache; + $this->json = $json; + $this->random = $random; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function serialize(array $data): string + { + $token = ''; + try { + if ($data) { + $token = $this->random->getRandomString(self::CACHE_ID_LENGTH); + $cacheKey = self::CACHE_KEY_PREFIX . $token; + $this->cache->save($this->json->serialize($data), $cacheKey, [], self::CACHE_LIFE_TIME); + } + } catch (\Throwable $exception) { + $this->logger->error($exception); + $token = ''; + } + + return $token; + } + + /** + * @inheritDoc + */ + public function unserialize(string $data): array + { + $result = []; + try { + if (strlen($data) === self::CACHE_ID_LENGTH) { + $cacheKey = self::CACHE_KEY_PREFIX . $data; + $json = $this->cache->load($cacheKey); + $result = $this->json->unserialize($json); + $this->cache->remove($cacheKey); + } + } catch (\Throwable $exception) { + $this->logger->error($exception); + $result = []; + } + + return $result; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php new file mode 100644 index 0000000000000..99f51713adcdf --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; + +/** + * Store switcher redirect data collector + */ +class RedirectDataGenerator +{ + /** + * @var RedirectDataPreprocessorInterface + */ + private $preprocessor; + /** + * @var RedirectDataSerializerInterface + */ + private $serializer; + /** + * @var RedirectDataInterfaceFactory + */ + private $dataFactory; + /** + * @var Encryptor + */ + private $encryptor; + + /** + * @param Encryptor $encryptor + * @param RedirectDataPreprocessorInterface $preprocessor + * @param RedirectDataSerializerInterface $serializer + * @param RedirectDataInterfaceFactory $dataFactory + */ + public function __construct( + Encryptor $encryptor, + RedirectDataPreprocessorInterface $preprocessor, + RedirectDataSerializerInterface $serializer, + RedirectDataInterfaceFactory $dataFactory + ) { + $this->preprocessor = $preprocessor; + $this->serializer = $serializer; + $this->dataFactory = $dataFactory; + $this->encryptor = $encryptor; + } + + /** + * Collect data to be redirected to the target store + * + * @param ContextInterface $context + * @return RedirectDataInterface + */ + public function generate(ContextInterface $context): RedirectDataInterface + { + $data = $this->preprocessor->process($context, []); + $dataStr = $this->serializer->serialize($data); + $timestamp = time(); + $token = implode( + ',', + [ + $dataStr, + $timestamp, + $context->getFromStore()->getCode(), + $context->getTargetStore()->getCode(), + ] + ); + $signature = $this->encryptor->hash($token, Encryptor::HASH_VERSION_SHA256); + + return $this->dataFactory->create( + [ + 'data' => $dataStr, + 'timestamp' => $timestamp, + 'signature' => $signature + ] + ); + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataInterface.php new file mode 100644 index 0000000000000..f7fc066634b63 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data interface + */ +interface RedirectDataInterface +{ + /** + * Redirect data signature + * + * @return string + */ + public function getSignature(): string; + + /** + * Data to redirect from store to store + * + * @return string + */ + public function getData(): string; + + /** + * Expire date of the redirect data + * + * @return int + */ + public function getTimestamp(): int; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorComposite.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorComposite.php new file mode 100644 index 0000000000000..579ab80f31897 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorComposite.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data post-processors collection + */ +class RedirectDataPostprocessorComposite implements RedirectDataPostprocessorInterface +{ + /** + * @var RedirectDataPostprocessorInterface[] + */ + private $processors; + + /** + * @param RedirectDataPostprocessorInterface[] $processors + */ + public function __construct(array $processors = []) + { + $this->processors = $processors; + } + + /** + * @inheritdoc + */ + public function process(ContextInterface $context, array $data): void + { + foreach ($this->processors as $processor) { + $processor->process($context, $data); + } + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorInterface.php new file mode 100644 index 0000000000000..de117915e23da --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPostprocessorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data post-processor interface + */ +interface RedirectDataPostprocessorInterface +{ + /** + * Process data redirected from origin source + * + * @param ContextInterface $context + * @param array $data + */ + public function process(ContextInterface $context, array $data): void; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorComposite.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorComposite.php new file mode 100644 index 0000000000000..4b93df1cdc677 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorComposite.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data pre-processors collection + */ +class RedirectDataPreprocessorComposite implements RedirectDataPreprocessorInterface +{ + /** + * @var RedirectDataPreprocessorInterface[] + */ + private $processors; + + /** + * @param RedirectDataPreprocessorInterface[] $processors + */ + public function __construct(array $processors = []) + { + $this->processors = $processors; + } + + /** + * @inheritdoc + */ + public function process(ContextInterface $context, array $data): array + { + foreach ($this->processors as $processor) { + $data = $processor->process($context, $data); + } + + return $data; + } +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorInterface.php new file mode 100644 index 0000000000000..d28a7dd776ab7 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataPreprocessorInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data pre-processor interface + */ +interface RedirectDataPreprocessorInterface +{ + /** + * Collect data to be redirected to target store + * + * @param ContextInterface $context + * @param array $data + * @return array + */ + public function process(ContextInterface $context, array $data): array; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataSerializerInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataSerializerInterface.php new file mode 100644 index 0000000000000..0f7cde4d94ccd --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataSerializerInterface.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +/** + * Store switcher redirect data serializer interface + */ +interface RedirectDataSerializerInterface +{ + /** + * Serialize provided data and return the serialized data + * + * @param array $data + * @return string + */ + public function serialize(array $data): string; + + /** + * Unserialize provided data and return the unserialized data + * + * @param string $data + * @return array + */ + public function unserialize(string $data): array; +} diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php new file mode 100644 index 0000000000000..40ba8d1fcb1d9 --- /dev/null +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; + +/** + * Store switcher redirect data validator + */ +class RedirectDataValidator +{ + const TIMEOUT = 5; + /** + * @var Encryptor + */ + private $encryptor; + + /** + * @param Encryptor $encryptor + */ + public function __construct( + Encryptor $encryptor + ) { + $this->encryptor = $encryptor; + } + + /** + * Validate data redirected from origin store + * + * @param ContextInterface $context + * @param RedirectDataInterface $redirectData + * @return bool + */ + public function validate(ContextInterface $context, RedirectDataInterface $redirectData) + { + $timeStamp = $redirectData->getTimestamp(); + $signature = $redirectData->getSignature(); + $value = implode( + ',', + [ + $redirectData->getData(), + $timeStamp, + $context->getFromStore()->getCode(), + $context->getTargetStore()->getCode() + ] + ); + return time() - $timeStamp <= self::TIMEOUT + && $this->encryptor->validateHash($value, $signature); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php b/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php index 91fff641338db..7d873ee6c1d8e 100755 --- a/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php +++ b/app/code/Magento/Store/Test/Unit/Controller/Store/RedirectTest.php @@ -22,7 +22,10 @@ use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; use Magento\Store\Model\StoreSwitcher\HashGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -163,6 +166,11 @@ protected function setUp(): void ->method('getCurrentStoreId') ->willReturnSelf(); + $redirectDataGenerator = $this->createMock(RedirectDataGenerator::class); + $contextFactory = $this->createMock(ContextInterfaceFactory::class); + $contextFactory->method('create') + ->willReturn($this->createMock(ContextInterface::class)); + $objectManager = new ObjectManagerHelper($this); $context = $objectManager->getObject( Context::class, @@ -182,6 +190,8 @@ protected function setUp(): void 'sidResolver' => $this->sidResolverMock, 'hashGenerator' => $this->hashGeneratorMock, 'context' => $context, + 'redirectDataGenerator' => $redirectDataGenerator, + 'contextFactory' => $contextFactory, ] ); } @@ -220,11 +230,6 @@ public function testRedirect(string $defaultStoreViewCode, string $storeCode): v ->expects($this->once()) ->method('getCode') ->willReturn($defaultStoreViewCode); - $this->hashGeneratorMock - ->expects($this->once()) - ->method('generateHash') - ->with($this->fromStoreMock) - ->willReturn([]); $this->storeManagerMock ->expects($this->once()) ->method('setCurrentStore') @@ -239,7 +244,10 @@ public function testRedirect(string $defaultStoreViewCode, string $storeCode): v '_query' => [ 'uenc' => $defaultStoreViewCode, '___from_store' => $defaultStoreViewCode, - '___store' => $storeCode + '___store' => $storeCode, + 'data' => '', + 'time_stamp' => 0, + 'signature' => '', ] ] ); diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 2da9e91e1fddd..ccfec562ba103 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -26,6 +26,11 @@ <preference for="Magento\Framework\App\ScopeTreeProviderInterface" type="Magento\Store\Model\ScopeTreeProvider"/> <preference for="Magento\Framework\App\ScopeValidatorInterface" type="Magento\Store\Model\ScopeValidator"/> <preference for="Magento\Store\Model\StoreSwitcherInterface" type="Magento\Store\Model\StoreSwitcher" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface" type="Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorComposite" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface" type="Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorComposite" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface" type="Magento\Store\Model\StoreSwitcher\RedirectDataCacheSerializer" /> + <preference for="Magento\Store\Model\StoreSwitcher\ContextInterface" type="Magento\Store\Model\StoreSwitcher\Context" /> + <preference for="Magento\Store\Model\StoreSwitcher\RedirectDataInterface" type="Magento\Store\Model\StoreSwitcher\RedirectData" /> <type name="Magento\Framework\App\Http\Context"> <arguments> <argument name="default" xsi:type="array"> From a44b2000b7a590ecb4dc30190a5595c4a6106e3f Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 14 Sep 2020 17:52:48 +0300 Subject: [PATCH 0481/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Indexer/Price/ConfigurableTest.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 ba3d5e46b98fb..28cbf80703d51 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 @@ -159,6 +159,40 @@ public function testReindexWithCorrectPriority() $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } + /** + * Test get product minimal price if all children is out of stock + * + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testReindexIfAllChildrenIsOutOfStock(): void + { + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct1 = $this->productRepository->getById(10, false, null, true); + $stockItem = $childProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $childProduct2 = $this->productRepository->getById(20, false, null, true); + $stockItem = $childProduct2->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct1 = $this->productRepository->getById(1, false, null, true); + $stockItem = $configurableProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + } + /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php From 104685ac17b5ad2246d29d3a973c9615f89c8c5b Mon Sep 17 00:00:00 2001 From: Stanislav Idolov <sidolov@adobe.com> Date: Mon, 14 Sep 2020 11:05:09 -0500 Subject: [PATCH 0482/1013] Review fixes --- .../Magento/CatalogSearch/Model/Search/Category.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Search/Category.php b/app/code/Magento/CatalogSearch/Model/Search/Category.php index 13e15dbf0c0f0..cc13ca2d4625f 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/Category.php +++ b/app/code/Magento/CatalogSearch/Model/Search/Category.php @@ -30,11 +30,6 @@ class Category extends DataObject */ private $categoryRepository; - /** - * @var SearchCriteriaBuilder - */ - private $searchCriteriaBuilder; - /** * @var FilterBuilder */ @@ -53,7 +48,6 @@ class Category extends DataObject /** * @param Data $adminhtmlData * @param CategoryListInterface $categoryRepository - * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory * @param FilterBuilder $filterBuilder * @param StringUtils $string @@ -61,14 +55,12 @@ class Category extends DataObject public function __construct( Data $adminhtmlData, CategoryListInterface $categoryRepository, - SearchCriteriaBuilder $searchCriteriaBuilder, SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory, FilterBuilder $filterBuilder, StringUtils $string ) { $this->adminhtmlData = $adminhtmlData; $this->categoryRepository = $categoryRepository; - $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; $this->filterBuilder = $filterBuilder; $this->string = $string; @@ -107,7 +99,7 @@ public function load() foreach ($searchResults->getItems() as $category) { $description = $category->getDescription() ? strip_tags($category->getDescription()) : ''; $result[] = [ - 'id' => 'category/1/' . $category->getId(), + 'id' => sprintf('category/1/%d', $category->getId()), 'type' => __('Category'), 'name' => $category->getName(), 'description' => $this->string->substr($description, 0, 30), From f6c1774918eddfcd4ecb116091b3c0d50a366feb Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 14 Sep 2020 13:19:49 -0500 Subject: [PATCH 0483/1013] MC-37615: Graphql urlresolver returning Null when using SEO friendly URLs with parameters --- .../Model/Resolver/EntityUrl.php | 2 +- .../GraphQl/CmsUrlRewrite/UrlResolverTest.php | 69 ++++++++++--------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index e6b03755bea47..2c87017f185f8 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -64,7 +64,7 @@ public function resolve( $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $result = null; - $url = $args['url']; + $url = parse_url($args['url'], PHP_URL_PATH); if (substr($url, 0, 1) === '/' && $url !== '/') { $url = ltrim($url, '/'); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php index e059960074fbf..0e4cde897f9c7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php @@ -45,18 +45,7 @@ public function testCMSPageUrlResolver() $targetPath = $urlPathGenerator->getCanonicalUrlPath($page); $expectedEntityType = CmsPageUrlRewriteGenerator::ENTITY_TYPE; - $query - = <<<QUERY -{ - urlResolver(url:"{$requestPath}") - { - id - relative_url - type - redirectCode - } -} -QUERY; + $query = $this->createQuery($requestPath); $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); @@ -64,18 +53,7 @@ public function testCMSPageUrlResolver() $this->assertEquals(0, $response['urlResolver']['redirectCode']); // querying by non seo friendly url path should return seo friendly relative url - $query - = <<<QUERY -{ - urlResolver(url:"{$targetPath}") - { - id - relative_url - type - redirectCode - } -} -QUERY; + $query = $this->createQuery($targetPath); $response = $this->graphQlQuery($query); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); @@ -83,6 +61,23 @@ public function testCMSPageUrlResolver() $this->assertEquals(0, $response['urlResolver']['redirectCode']); } + /** + * @magentoApiDataFixture Magento/Cms/_files/pages.php + */ + public function testResolveCMSPageWithQueryParameters() + { + $page = $this->objectManager->create(\Magento\Cms\Model\Page::class); + $page->load('page100'); + $cmsPageId = $page->getId(); + $requestPath = $page->getIdentifier(); + + $query = $this->createQuery($requestPath . '?key=value'); + $response = $this->graphQlQuery($query); + $this->assertNotEmpty($response['urlResolver']); + $this->assertEquals($cmsPageId, $response['urlResolver']['id']); + $this->assertEquals($requestPath, $response['urlResolver']['relative_url']); + } + /** * Test resolution of '/' path to home page */ @@ -98,10 +93,24 @@ public function testResolveSlash() $page = $this->objectManager->get(\Magento\Cms\Model\Page::class); $page->load($homePageIdentifier); $homePageId = $page->getId(); - $query - = <<<QUERY + $query = $this->createQuery('/'); + $response = $this->graphQlQuery($query); + $this->assertArrayHasKey('urlResolver', $response); + $this->assertEquals($homePageId, $response['urlResolver']['id']); + $this->assertEquals($homePageIdentifier, $response['urlResolver']['relative_url']); + $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); + $this->assertEquals(0, $response['urlResolver']['redirectCode']); + } + + /** + * @param string $path + * @return string + */ + private function createQuery(string $path): string + { + return <<<QUERY { - urlResolver(url:"/") + urlResolver(url:"{$path}") { id relative_url @@ -110,11 +119,5 @@ public function testResolveSlash() } } QUERY; - $response = $this->graphQlQuery($query); - $this->assertArrayHasKey('urlResolver', $response); - $this->assertEquals($homePageId, $response['urlResolver']['id']); - $this->assertEquals($homePageIdentifier, $response['urlResolver']['relative_url']); - $this->assertEquals('CMS_PAGE', $response['urlResolver']['type']); - $this->assertEquals(0, $response['urlResolver']['redirectCode']); } } From 76fd92b5fe37579abab230f03ee101832494acef Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 14 Sep 2020 14:01:51 -0500 Subject: [PATCH 0484/1013] MC-37615: Graphql urlresolver returning Null when using SEO friendly URLs with parameters --- app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 2c87017f185f8..114d35326f707 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -64,6 +64,7 @@ public function resolve( $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $result = null; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $url = parse_url($args['url'], PHP_URL_PATH); if (substr($url, 0, 1) === '/' && $url !== '/') { $url = ltrim($url, '/'); From 939056fca8886f0f8abf60c4b5f6568ce5b190d9 Mon Sep 17 00:00:00 2001 From: Lena Orobei <oorobei@adobe.com> Date: Mon, 14 Sep 2020 16:14:25 -0500 Subject: [PATCH 0485/1013] Update file-uploader.js - static code issues fixes --- .../Magento/Ui/view/base/web/js/form/element/file-uploader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js index 6099c61bd3afc..e7dc245d47d6f 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/file-uploader.js @@ -333,7 +333,8 @@ define([ * @param {Object} data - File data that will be uploaded. */ onFilesChoosed: function (event, data) { - // no option exists in file uploader for restricting upload chains to single files; this enforces that policy + // no option exists in file uploader for restricting upload chains to single files + // this enforces that policy if (!this.isMultipleFiles) { data.files.splice(1); } From fbd38132f05137c61f67d6454fa8ad3236c602cc Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 14 Sep 2020 16:46:58 -0500 Subject: [PATCH 0486/1013] MC-37351: Cart contents lost after switching to different store with different domain - Add customer ID in ContextInterface - Move error handling from cache serializer --- .../RedirectDataPostprocessor.php | 2 +- .../Store/Controller/Store/Redirect.php | 17 ++++++- .../Store/Model/StoreSwitcher/Context.php | 17 ++++++- .../Model/StoreSwitcher/ContextInterface.php | 7 +++ .../Model/StoreSwitcher/HashProcessor.php | 29 +++++++++++- .../RedirectDataCacheSerializer.php | 46 ++++++------------- .../StoreSwitcher/RedirectDataGenerator.php | 17 ++++++- 7 files changed, 96 insertions(+), 39 deletions(-) diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php index ce532091b3649..c7c6bb8694393 100644 --- a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php @@ -65,7 +65,7 @@ public function process(ContextInterface $context, array $data): void } } catch (NoSuchEntityException $e) { $this->logger->error($e); - throw new LocalizedException(__('The requested customer does not exist.'), $e); + throw new LocalizedException(__('Failed to sign into the customer account.'), $e); } catch (LocalizedException $e) { $this->logger->error($e); throw new LocalizedException(__('There was an error retrieving the customer record.'), $e); diff --git a/app/code/Magento/Store/Controller/Store/Redirect.php b/app/code/Magento/Store/Controller/Store/Redirect.php index ac2c05840153f..72da0b00a3d29 100644 --- a/app/code/Magento/Store/Controller/Store/Redirect.php +++ b/app/code/Magento/Store/Controller/Store/Redirect.php @@ -7,6 +7,7 @@ namespace Magento\Store\Controller\Store; +use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Framework\App\Action\HttpGetActionInterface; @@ -59,6 +60,10 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @var ContextInterfaceFactory|null */ private $contextFactory; + /** + * @var UserContextInterface|null + */ + private $userContext; /** * @param Context $context @@ -70,7 +75,9 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @param StoreManagerInterface|null $storeManager * @param RedirectDataGenerator|null $redirectDataGenerator * @param ContextInterfaceFactory|null $contextFactory + * @param UserContextInterface|null $userContext * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, @@ -81,7 +88,8 @@ public function __construct( HashGenerator $hashGenerator, StoreManagerInterface $storeManager = null, ?RedirectDataGenerator $redirectDataGenerator = null, - ?ContextInterfaceFactory $contextFactory = null + ?ContextInterfaceFactory $contextFactory = null, + ?UserContextInterface $userContext = null ) { parent::__construct($context); $this->storeRepository = $storeRepository; @@ -92,6 +100,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(RedirectDataGenerator::class); $this->contextFactory = $contextFactory ?: ObjectManager::getInstance()->get(ContextInterfaceFactory::class); + $this->userContext = $userContext + ?: ObjectManager::getInstance()->get(UserContextInterface::class); } /** @@ -122,7 +132,10 @@ public function execute() [ 'fromStore' => $fromStore, 'targetStore' => $targetStore, - 'redirectUrl' => $this->_redirect->getRedirectUrl() + 'redirectUrl' => $this->_redirect->getRedirectUrl(), + 'customerId' => $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER ? + (int) $this->userContext->getUserId() + : null ] ) ); diff --git a/app/code/Magento/Store/Model/StoreSwitcher/Context.php b/app/code/Magento/Store/Model/StoreSwitcher/Context.php index c67dc3d67b01a..26c1807ea0e69 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/Context.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/Context.php @@ -26,20 +26,27 @@ class Context implements ContextInterface * @var string */ private $redirectUrl; + /** + * @var int|null + */ + private $customerId; /** * @param StoreInterface $fromStore * @param StoreInterface $targetStore * @param string $redirectUrl + * @param int|null $customerId */ public function __construct( StoreInterface $fromStore, StoreInterface $targetStore, - string $redirectUrl + string $redirectUrl, + ?int $customerId = null ) { $this->fromStore = $fromStore; $this->targetStore = $targetStore; $this->redirectUrl = $redirectUrl; + $this->customerId = $customerId; } /** @@ -65,4 +72,12 @@ public function getRedirectUrl(): string { return $this->redirectUrl; } + + /** + * @inheritDoc + */ + public function getCustomerId(): ?int + { + return $this->customerId; + } } diff --git a/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php index a18c7cc9ccc27..7c4b8dca5f723 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php @@ -34,4 +34,11 @@ public function getTargetStore(): StoreInterface; * @return string */ public function getRedirectUrl(): string; + + /** + * The logged in customer ID + * + * @return int + */ + public function getCustomerId(): ?int; } diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php index d957e89a4435e..5e4e598a41fe2 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php @@ -7,11 +7,13 @@ namespace Magento\Store\Model\StoreSwitcher; +use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Message\ManagerInterface; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreSwitcherInterface; +use Psr\Log\LoggerInterface; /** * Process one time token and build redirect url @@ -49,6 +51,14 @@ class HashProcessor implements StoreSwitcherInterface * @var RedirectDataValidator */ private $dataValidator; + /** + * @var LoggerInterface + */ + private $logger; + /** + * @var UserContextInterface + */ + private $userContext; /** * @param RequestInterface $request @@ -58,6 +68,8 @@ class HashProcessor implements StoreSwitcherInterface * @param ContextInterfaceFactory $contextFactory * @param RedirectDataInterfaceFactory $dataFactory * @param RedirectDataValidator $dataValidator + * @param LoggerInterface $logger + * @param UserContextInterface $userContext */ public function __construct( RequestInterface $request, @@ -66,7 +78,9 @@ public function __construct( ManagerInterface $messageManager, ContextInterfaceFactory $contextFactory, RedirectDataInterfaceFactory $dataFactory, - RedirectDataValidator $dataValidator + RedirectDataValidator $dataValidator, + LoggerInterface $logger, + UserContextInterface $userContext ) { $this->request = $request; $this->postprocessor = $postprocessor; @@ -75,6 +89,8 @@ public function __construct( $this->contextFactory = $contextFactory; $this->dataFactory = $dataFactory; $this->dataValidator = $dataValidator; + $this->logger = $logger; + $this->userContext = $userContext; } /** @@ -91,11 +107,15 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s $timestamp = (int) $this->request->getParam('time_stamp'); $signature = (string) $this->request->getParam('signature'); $data = (string) $this->request->getParam('data'); + $customerId = $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER ? + (int) $this->userContext->getUserId() + : null; $context = $this->contextFactory->create( [ 'fromStore' => $fromStore, 'targetStore' => $targetStore, - 'redirectUrl' => $redirectUrl + 'redirectUrl' => $redirectUrl, + 'customerId' => $customerId ] ); $redirectDataObject = $this->dataFactory->create( @@ -116,6 +136,11 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s } } catch (LocalizedException $exception) { $this->messageManager->addErrorMessage($exception->getMessage()); + } catch (\Throwable $exception) { + $this->logger->error($exception); + $this->messageManager->addErrorMessage( + __('Something went wrong while switching to the store.') + ); } return $redirectUrl; diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php index 4469e39335104..d32cb815d5513 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php @@ -7,10 +7,10 @@ namespace Magento\Store\Model\StoreSwitcher; +use http\Exception\InvalidArgumentException; use Magento\Framework\App\CacheInterface; use Magento\Framework\Math\Random; use Magento\Framework\Serialize\Serializer\Json; -use Psr\Log\LoggerInterface; /** * Store switcher redirect data cache serializer @@ -33,27 +33,20 @@ class RedirectDataCacheSerializer implements RedirectDataSerializerInterface * @var Random */ private $random; - /** - * @var LoggerInterface - */ - private $logger; /** * @param Json $json * @param Random $random * @param CacheInterface $cache - * @param LoggerInterface $logger */ public function __construct( Json $json, Random $random, - CacheInterface $cache, - LoggerInterface $logger + CacheInterface $cache ) { $this->cache = $cache; $this->json = $json; $this->random = $random; - $this->logger = $logger; } /** @@ -61,17 +54,9 @@ public function __construct( */ public function serialize(array $data): string { - $token = ''; - try { - if ($data) { - $token = $this->random->getRandomString(self::CACHE_ID_LENGTH); - $cacheKey = self::CACHE_KEY_PREFIX . $token; - $this->cache->save($this->json->serialize($data), $cacheKey, [], self::CACHE_LIFE_TIME); - } - } catch (\Throwable $exception) { - $this->logger->error($exception); - $token = ''; - } + $token = $this->random->getRandomString(self::CACHE_ID_LENGTH); + $cacheKey = self::CACHE_KEY_PREFIX . $token; + $this->cache->save($this->json->serialize($data), $cacheKey, [], self::CACHE_LIFE_TIME); return $token; } @@ -81,18 +66,17 @@ public function serialize(array $data): string */ public function unserialize(string $data): array { - $result = []; - try { - if (strlen($data) === self::CACHE_ID_LENGTH) { - $cacheKey = self::CACHE_KEY_PREFIX . $data; - $json = $this->cache->load($cacheKey); - $result = $this->json->unserialize($json); - $this->cache->remove($cacheKey); - } - } catch (\Throwable $exception) { - $this->logger->error($exception); - $result = []; + if (strlen($data) !== self::CACHE_ID_LENGTH) { + throw new InvalidArgumentException("Invalid cache key '$data' supplied."); + } + + $cacheKey = self::CACHE_KEY_PREFIX . $data; + $json = $this->cache->load($cacheKey); + if (!$json) { + throw new InvalidArgumentException('Couldn\'t retrieve data from cache.'); } + $result = $this->json->unserialize($json); + $this->cache->remove($cacheKey); return $result; } diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php index 99f51713adcdf..a68e0bb7b795d 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php @@ -8,6 +8,7 @@ namespace Magento\Store\Model\StoreSwitcher; use Magento\Framework\Encryption\Encryptor; +use Psr\Log\LoggerInterface; /** * Store switcher redirect data collector @@ -30,23 +31,30 @@ class RedirectDataGenerator * @var Encryptor */ private $encryptor; + /** + * @var LoggerInterface + */ + private $logger; /** * @param Encryptor $encryptor * @param RedirectDataPreprocessorInterface $preprocessor * @param RedirectDataSerializerInterface $serializer * @param RedirectDataInterfaceFactory $dataFactory + * @param LoggerInterface $logger */ public function __construct( Encryptor $encryptor, RedirectDataPreprocessorInterface $preprocessor, RedirectDataSerializerInterface $serializer, - RedirectDataInterfaceFactory $dataFactory + RedirectDataInterfaceFactory $dataFactory, + LoggerInterface $logger ) { $this->preprocessor = $preprocessor; $this->serializer = $serializer; $this->dataFactory = $dataFactory; $this->encryptor = $encryptor; + $this->logger = $logger; } /** @@ -58,7 +66,12 @@ public function __construct( public function generate(ContextInterface $context): RedirectDataInterface { $data = $this->preprocessor->process($context, []); - $dataStr = $this->serializer->serialize($data); + try { + $dataStr = $this->serializer->serialize($data); + } catch (\Throwable $exception) { + $this->logger->error($exception); + $dataStr = ''; + } $timestamp = time(); $token = implode( ',', From f820b9b66ef66e26da5074b0e800da580e6f9352 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 14 Sep 2020 19:09:43 -0500 Subject: [PATCH 0487/1013] MC-37351: Cart contents lost after switching to different store with different domain - Fix static test --- .../Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php index d32cb815d5513..59cafd096781e 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php @@ -7,7 +7,7 @@ namespace Magento\Store\Model\StoreSwitcher; -use http\Exception\InvalidArgumentException; +use InvalidArgumentException; use Magento\Framework\App\CacheInterface; use Magento\Framework\Math\Random; use Magento\Framework\Serialize\Serializer\Json; From 2bebab275761696cbec83690dfb553aedf7d494e Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 14 Sep 2020 20:08:06 -0500 Subject: [PATCH 0488/1013] MC-37615: Graphql urlresolver returning Null when using SEO friendly URLs with parameters --- .../Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php | 6 +++++- .../Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php index 114d35326f707..6430f71765fe4 100644 --- a/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php +++ b/app/code/Magento/UrlRewriteGraphQl/Model/Resolver/EntityUrl.php @@ -65,7 +65,8 @@ public function resolve( $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $result = null; // phpcs:ignore Magento2.Functions.DiscouragedFunction - $url = parse_url($args['url'], PHP_URL_PATH); + $urlParts = parse_url($args['url']); + $url = $urlParts['path'] ?? $args['url']; if (substr($url, 0, 1) === '/' && $url !== '/') { $url = ltrim($url, '/'); } @@ -82,6 +83,9 @@ public function resolve( 'redirectCode' => $this->redirectType, 'type' => $this->sanitizeType($finalUrlRewrite->getEntityType()) ]; + if (!empty($urlParts['query'])) { + $resultArray['relative_url'] .= '?' . $urlParts['query']; + } if (empty($resultArray['id'])) { throw new GraphQlNoSuchEntityException( diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php index 0e4cde897f9c7..ce74a432dcba3 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CmsUrlRewrite/UrlResolverTest.php @@ -70,8 +70,9 @@ public function testResolveCMSPageWithQueryParameters() $page->load('page100'); $cmsPageId = $page->getId(); $requestPath = $page->getIdentifier(); + $requestPath .= '?key=value'; - $query = $this->createQuery($requestPath . '?key=value'); + $query = $this->createQuery($requestPath); $response = $this->graphQlQuery($query); $this->assertNotEmpty($response['urlResolver']); $this->assertEquals($cmsPageId, $response['urlResolver']['id']); From 893910efd99c804f03af07ee489f18222e28c0e5 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 15 Sep 2020 11:36:24 +0300 Subject: [PATCH 0489/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- .../Magento/Catalog/Block/Product/ListProduct/SortingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php index 52e2047917e8e..b88edc656176c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php @@ -439,7 +439,7 @@ public function productListWithOutOfStockSortOrderDataProvider(): array 'default_order_price_desc' => [ 'sort' => 'price', 'direction' => Collection::SORT_ORDER_DESC, - 'expectation' => ['simple3', 'simple2', 'simple1', 'configurable'], + 'expectation' => ['configurable', 'simple3', 'simple2', 'simple1'], ], ]; } From b97c6c3c4b63e6dc14f996dfcbf34d24051a6bd0 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Tue, 15 Sep 2020 16:25:09 +0300 Subject: [PATCH 0490/1013] add style for input --- .../web/css/source/module/checkout/_payments.less | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less index 494483ff60dda..8fbe67abe2960 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/checkout/_payments.less @@ -49,6 +49,10 @@ form { &.form-purchase-order { margin-bottom: 15px; + + .input-text { + width: 40%; + } } } } @@ -119,7 +123,7 @@ margin: 0 0 @indent__base; .primary { - .action-update { + .action-update { margin-bottom: 20px; margin-right: 0; } @@ -133,7 +137,7 @@ .lib-css(line-height, @checkout-billing-address-details__line-height); .lib-css(padding, @checkout-billing-address-details__padding); } - + input[type="checkbox"] { vertical-align: top; } From 0442e1b47952e6c59b0f691943acf48f8f8b2269 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 15 Sep 2020 09:29:37 -0500 Subject: [PATCH 0491/1013] MC-36785: Unable to set YouTube API key by CLI --- app/code/Magento/ProductVideo/etc/config.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/ProductVideo/etc/config.xml b/app/code/Magento/ProductVideo/etc/config.xml index 8afefa14a3651..55b7def6fb8ef 100644 --- a/app/code/Magento/ProductVideo/etc/config.xml +++ b/app/code/Magento/ProductVideo/etc/config.xml @@ -12,6 +12,7 @@ <play_if_base>0</play_if_base> <show_related>0</show_related> <video_auto_restart>0</video_auto_restart> + <youtube_api_key/> </product_video> </catalog> </default> From cfd0808fab93dd07882831ef2bf632b639e62d0e Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Tue, 15 Sep 2020 18:11:36 +0300 Subject: [PATCH 0492/1013] magento/security-package#274: [B2B only] Save button is not displayed on Edit Account Information page. --- .../Customer/view/frontend/templates/form/edit.phtml | 6 +++++- .../Customer/view/frontend/templates/form/register.phtml | 4 ++++ .../luma/Magento_Customer/web/css/source/_module.less | 9 +++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) 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 9821cff73a3dd..b64ad58c17afc 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml @@ -105,7 +105,11 @@ use Magento\Customer\Block\Widget\Name; </div> </div> </fieldset> - <?= $block->getChildHtml('form_additional_info') ?> + + <fieldset class="fieldset additional_info"> + <?= $block->getChildHtml('form_additional_info') ?> + </fieldset> + <div class="actions-toolbar"> <div class="primary"> <button type="submit" class="action save primary" title="<?= $block->escapeHtmlAttr(__('Save')) ?>"> 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 99040706e50ac..5e58f94683ec1 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml @@ -259,8 +259,12 @@ $formData = $block->getFormData(); autocomplete="off"> </div> </div> + </fieldset> + + <fieldset class="fieldset additional_info"> <?= $block->getChildHtml('form_additional_info') ?> </fieldset> + <div class="actions-toolbar"> <div class="primary"> <button type="submit" diff --git a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less index a0a36f55574fe..28ab32d13c88b 100644 --- a/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Customer/web/css/source/_module.less @@ -83,6 +83,11 @@ .fieldset.password { display: none; } + fieldset { + &.additional_info { + clear: both; + } + } } .form-create-account { @@ -349,9 +354,9 @@ } } - .additional-addresses { + .additional-addresses { table > thead > tr > th { - white-space: nowrap; + white-space: nowrap; } } } From b70851de42e1430dc09f0ac88b4ecc8f82abc4fd Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 18:38:31 +0200 Subject: [PATCH 0493/1013] ADD: formatting rules for json and yaml --- .editorconfig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.editorconfig b/.editorconfig index 37cfad78c270b..877666f2aba34 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,9 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false + +[*.{yml,yaml,json}] +indent_size = 2 + +[composer.{json,lock}] +indent_size = 4 From f7f5dce0d1415661441e9c8f5ccf0b24bea30f28 Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 18:39:06 +0200 Subject: [PATCH 0494/1013] IMP: formatting for grunt config files --- dev/tools/grunt/configs/autoprefixer.json | 23 +++++++-------- dev/tools/grunt/configs/concat.json | 18 ++++++------ dev/tools/grunt/configs/cssmin.json | 28 +++++++++--------- dev/tools/grunt/configs/eslint.json | 30 +++++++++---------- dev/tools/grunt/configs/jscs.json | 26 ++++++++-------- dev/tools/grunt/configs/mage-minify.json | 36 +++++++++++------------ dev/tools/grunt/configs/styledocco.json | 22 +++++++------- 7 files changed, 89 insertions(+), 94 deletions(-) diff --git a/dev/tools/grunt/configs/autoprefixer.json b/dev/tools/grunt/configs/autoprefixer.json index 28cd9c8a1255f..ea6301c2fd7ae 100644 --- a/dev/tools/grunt/configs/autoprefixer.json +++ b/dev/tools/grunt/configs/autoprefixer.json @@ -1,14 +1,11 @@ { - "options": { - "browsers": [ - "last 2 versions", - "ie 11" - ] - }, - "setup": { - "src": "<%= path.css.setup %>/setup.css" - }, - "updater": { - "src": "<%= path.css.updater %>/updater.css" - } -} \ No newline at end of file + "options": { + "browsers": ["last 2 versions", "ie 11"] + }, + "setup": { + "src": "<%= path.css.setup %>/setup.css" + }, + "updater": { + "src": "<%= path.css.updater %>/updater.css" + } +} diff --git a/dev/tools/grunt/configs/concat.json b/dev/tools/grunt/configs/concat.json index b02024c38559f..de3e5a73cc266 100644 --- a/dev/tools/grunt/configs/concat.json +++ b/dev/tools/grunt/configs/concat.json @@ -1,10 +1,10 @@ { - "options": { - "stripBanners": true, - "banner": "/**\n * Copyright © <%= grunt.template.today(\"yyyy\") %> Magento. All rights reserved.\n * See COPYING.txt for license details.\n */\n" - }, - "setup": { - "src": "<%= path.css.setup %>/setup.css", - "dest": "<%= path.css.setup %>/setup.css" - } -} \ No newline at end of file + "options": { + "stripBanners": true, + "banner": "/**\n * Copyright © <%= grunt.template.today(\"yyyy\") %> Magento. All rights reserved.\n * See COPYING.txt for license details.\n */\n" + }, + "setup": { + "src": "<%= path.css.setup %>/setup.css", + "dest": "<%= path.css.setup %>/setup.css" + } +} diff --git a/dev/tools/grunt/configs/cssmin.json b/dev/tools/grunt/configs/cssmin.json index 032657cc3ed81..7b56ec43eccdd 100644 --- a/dev/tools/grunt/configs/cssmin.json +++ b/dev/tools/grunt/configs/cssmin.json @@ -1,16 +1,16 @@ { - "options": { - "report": "gzip", - "keepSpecialComments": 0 - }, - "setup": { - "files": { - "<%= path.css.setup %>/setup.css": "<%= path.css.setup %>/setup.css" - } - }, - "updater": { - "files": { - "<%= path.css.updater %>/updater.css": "<%= path.css.updater %>/updater.css" - } + "options": { + "report": "gzip", + "keepSpecialComments": 0 + }, + "setup": { + "files": { + "<%= path.css.setup %>/setup.css": "<%= path.css.setup %>/setup.css" } -} \ No newline at end of file + }, + "updater": { + "files": { + "<%= path.css.updater %>/updater.css": "<%= path.css.updater %>/updater.css" + } + } +} diff --git a/dev/tools/grunt/configs/eslint.json b/dev/tools/grunt/configs/eslint.json index 78719ce9548f6..ec0d821a1c0d8 100644 --- a/dev/tools/grunt/configs/eslint.json +++ b/dev/tools/grunt/configs/eslint.json @@ -1,18 +1,18 @@ { - "file": { - "options": { - "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", - "reset": true, - "useEslintrc": false - } - }, - "test": { - "options": { - "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", - "reset": true, - "outputFile": "dev/tests/static/eslint-error-report.xml", - "format": "junit", - "quiet": true - } + "file": { + "options": { + "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", + "reset": true, + "useEslintrc": false } + }, + "test": { + "options": { + "configFile": "dev/tests/static/testsuite/Magento/Test/Js/_files/eslint/.eslintrc", + "reset": true, + "outputFile": "dev/tests/static/eslint-error-report.xml", + "format": "junit", + "quiet": true + } + } } diff --git a/dev/tools/grunt/configs/jscs.json b/dev/tools/grunt/configs/jscs.json index dd9254abbdc68..e4002f035f824 100644 --- a/dev/tools/grunt/configs/jscs.json +++ b/dev/tools/grunt/configs/jscs.json @@ -1,16 +1,16 @@ { - "file": { - "options": { - "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc" - }, - "src": "" + "file": { + "options": { + "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc" }, - "test": { - "options": { - "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc", - "reporterOutput": "dev/tests/static/jscs-error-report.xml", - "reporter": "junit" - }, - "src": "" - } + "src": "" + }, + "test": { + "options": { + "config": "dev/tests/static/testsuite/Magento/Test/Js/_files/jscs/.jscsrc", + "reporterOutput": "dev/tests/static/jscs-error-report.xml", + "reporter": "junit" + }, + "src": "" + } } diff --git a/dev/tools/grunt/configs/mage-minify.json b/dev/tools/grunt/configs/mage-minify.json index 1ddfa3910a3a8..c6ecfc5579701 100644 --- a/dev/tools/grunt/configs/mage-minify.json +++ b/dev/tools/grunt/configs/mage-minify.json @@ -1,21 +1,21 @@ { - "legacy": { - "options": { - "compressor": "yui-js" - }, - "files": { - "<%= path.uglify.legacy %>": [ - "lib/web/prototype/prototype.js", - "lib/web/prototype/window.js", - "lib/web/scriptaculous/builder.js", - "lib/web/scriptaculous/effects.js", - "lib/web/lib/ccard.js", - "lib/web/prototype/validation.js", - "lib/web/varien/js.js", - "lib/web/mage/adminhtml/varienLoader.js", - "lib/web/mage/adminhtml/tools.js", - "dev/tools/grunt/assets/legacy-build/shim.js" - ] - } + "legacy": { + "options": { + "compressor": "yui-js" + }, + "files": { + "<%= path.uglify.legacy %>": [ + "lib/web/prototype/prototype.js", + "lib/web/prototype/window.js", + "lib/web/scriptaculous/builder.js", + "lib/web/scriptaculous/effects.js", + "lib/web/lib/ccard.js", + "lib/web/prototype/validation.js", + "lib/web/varien/js.js", + "lib/web/mage/adminhtml/varienLoader.js", + "lib/web/mage/adminhtml/tools.js", + "dev/tools/grunt/assets/legacy-build/shim.js" + ] } + } } diff --git a/dev/tools/grunt/configs/styledocco.json b/dev/tools/grunt/configs/styledocco.json index 8a75d43ac35e4..1527b26114b97 100644 --- a/dev/tools/grunt/configs/styledocco.json +++ b/dev/tools/grunt/configs/styledocco.json @@ -1,14 +1,12 @@ { - "documentation": { - "options": { - "name": "Magento UI Library", - "verbose": true, - "include": [ - "<%= path.doc %>/docs.css" - ] - }, - "files": { - "<%= path.doc %>": "<%= path.doc %>/source" - } + "documentation": { + "options": { + "name": "Magento UI Library", + "verbose": true, + "include": ["<%= path.doc %>/docs.css"] + }, + "files": { + "<%= path.doc %>": "<%= path.doc %>/source" } -} \ No newline at end of file + } +} From 9ef8d9ddb9dbdc185489f0fe3475aade28da167a Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 18:39:31 +0200 Subject: [PATCH 0495/1013] IMP: formatting for dev test config file --- dev/tests/acceptance/staticRuleset.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dev/tests/acceptance/staticRuleset.json b/dev/tests/acceptance/staticRuleset.json index 82cc9dfe74152..91521d5f16bcc 100644 --- a/dev/tests/acceptance/staticRuleset.json +++ b/dev/tests/acceptance/staticRuleset.json @@ -1,8 +1,8 @@ { - "tests": [ - "actionGroupArguments", - "deprecatedEntityUsage", - "annotations", - "pauseActionUsage" - ] + "tests": [ + "actionGroupArguments", + "deprecatedEntityUsage", + "annotations", + "pauseActionUsage" + ] } From 93d79b8d638568ac5a529b833cbb043333d3be0d Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 18:39:53 +0200 Subject: [PATCH 0496/1013] IMP: formatting for auth.json --- auth.json.sample | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auth.json.sample b/auth.json.sample index be1c70cfe1e18..bd4b61d607328 100644 --- a/auth.json.sample +++ b/auth.json.sample @@ -1,8 +1,8 @@ { - "http-basic": { - "repo.magento.com": { - "username": "<public-key>", - "password": "<private-key>" - } + "http-basic": { + "repo.magento.com": { + "username": "<public-key>", + "password": "<private-key>" } + } } From 0533f2e35f4779d8b6fc14ed316c3c0e483698cb Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 18:40:21 +0200 Subject: [PATCH 0497/1013] IMP: formatting for grunt-config --- grunt-config.json.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grunt-config.json.sample b/grunt-config.json.sample index 7ef28a856f925..5704e5f012e17 100644 --- a/grunt-config.json.sample +++ b/grunt-config.json.sample @@ -1,3 +1,3 @@ { - "themes": "dev/tools/grunt/configs/local-themes" + "themes": "dev/tools/grunt/configs/local-themes" } From 5b476475ad8eb2a99db7543f2881978b9b7fbe53 Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 18:40:39 +0200 Subject: [PATCH 0498/1013] IMP: formatting for package.json --- package.json.sample | 80 ++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/package.json.sample b/package.json.sample index 93fe72afbd24a..1d07bcff26d8d 100644 --- a/package.json.sample +++ b/package.json.sample @@ -1,42 +1,42 @@ { - "name": "magento2", - "author": "Magento Commerce Inc.", - "description": "Magento2 node modules dependencies for local development", - "license": "(OSL-3.0 OR AFL-3.0)", - "repository": { - "type": "git", - "url": "https://github.com/magento/magento2.git" - }, - "homepage": "http://magento.com/", - "devDependencies": { - "glob": "~7.1.1", - "grunt": "~1.0.1", - "grunt-autoprefixer": "~3.0.4", - "grunt-banner": "~0.6.0", - "grunt-continue": "~0.1.0", - "grunt-contrib-clean": "~1.1.0", - "grunt-contrib-connect": "~1.0.2", - "grunt-contrib-cssmin": "~2.2.1", - "grunt-contrib-imagemin": "~2.0.1", - "grunt-contrib-jasmine": "~1.2.0", - "grunt-contrib-less": "~1.4.1", - "grunt-contrib-watch": "~1.0.0", - "grunt-eslint": "~20.1.0", - "grunt-exec": "~3.0.0", - "grunt-jscs": "~3.0.1", - "grunt-replace": "~1.0.1", - "grunt-styledocco": "~0.3.0", - "grunt-template-jasmine-requirejs": "~0.2.3", - "grunt-text-replace": "~0.4.0", - "imagemin-svgo": "~5.2.1", - "load-grunt-config": "~0.19.2", - "morgan": "~1.9.0", - "node-minify": "~2.3.1", - "path": "~0.12.7", - "serve-static": "~1.13.1", - "squirejs": "~0.2.1", - "strip-json-comments": "~2.0.1", - "time-grunt": "~1.4.0", - "underscore": "~1.8.0" - } + "name": "magento2", + "author": "Magento Commerce Inc.", + "description": "Magento2 node modules dependencies for local development", + "license": "(OSL-3.0 OR AFL-3.0)", + "repository": { + "type": "git", + "url": "https://github.com/magento/magento2.git" + }, + "homepage": "http://magento.com/", + "devDependencies": { + "glob": "~7.1.1", + "grunt": "~1.0.1", + "grunt-autoprefixer": "~3.0.4", + "grunt-banner": "~0.6.0", + "grunt-continue": "~0.1.0", + "grunt-contrib-clean": "~1.1.0", + "grunt-contrib-connect": "~1.0.2", + "grunt-contrib-cssmin": "~2.2.1", + "grunt-contrib-imagemin": "~2.0.1", + "grunt-contrib-jasmine": "~1.2.0", + "grunt-contrib-less": "~1.4.1", + "grunt-contrib-watch": "~1.0.0", + "grunt-eslint": "~20.1.0", + "grunt-exec": "~3.0.0", + "grunt-jscs": "~3.0.1", + "grunt-replace": "~1.0.1", + "grunt-styledocco": "~0.3.0", + "grunt-template-jasmine-requirejs": "~0.2.3", + "grunt-text-replace": "~0.4.0", + "imagemin-svgo": "~5.2.1", + "load-grunt-config": "~0.19.2", + "morgan": "~1.9.0", + "node-minify": "~2.3.1", + "path": "~0.12.7", + "serve-static": "~1.13.1", + "squirejs": "~0.2.1", + "strip-json-comments": "~2.0.1", + "time-grunt": "~1.4.0", + "underscore": "~1.8.0" + } } From f529c597660e5c9c62f5a178b6bee24e9e9072b8 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 15 Sep 2020 14:32:48 -0500 Subject: [PATCH 0499/1013] MC-37351: Cart contents lost after switching to different store with different domain - Add unit tests --- .../RedirectDataPreprocessor.php | 9 - .../RedirectDataPostprocessorTest.php | 167 ++++++++++++++++++ .../RedirectDataPreprocessorTest.php | 126 +++++++++++++ .../RedirectDataPostprocessor.php | 2 +- .../RedirectDataPostprocessorTest.php | 136 ++++++++++++++ .../RedirectDataPreprocessorTest.php | 121 +++++++++++++ .../Store/Controller/Store/Redirect.php | 8 +- .../Model/StoreSwitcher/HashProcessor.php | 5 +- .../StoreSwitcher/RedirectDataGenerator.php | 10 +- .../StoreSwitcher/RedirectDataValidator.php | 2 +- .../Model/StoreSwitcher/HashProcessorTest.php | 166 +++++++++++++++++ .../RedirectDataCacheSerializerTest.php | 96 ++++++++++ .../RedirectDataGeneratorTest.php | 130 ++++++++++++++ .../RedirectDataValidatorTest.php | 144 +++++++++++++++ 14 files changed, 1101 insertions(+), 21 deletions(-) create mode 100644 app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php create mode 100644 app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php create mode 100644 app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php create mode 100644 app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataGeneratorTest.php create mode 100644 app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataValidatorTest.php diff --git a/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php index 45e1719e34546..6047bb8bcad46 100644 --- a/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php +++ b/app/code/Magento/Checkout/Model/StoreSwitcher/RedirectDataPreprocessor.php @@ -9,7 +9,6 @@ use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Customer\Model\Session as CustomerSession; -use Magento\Quote\Api\CartRepositoryInterface; use Magento\Store\Model\StoreSwitcher\ContextInterface; use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; @@ -20,11 +19,6 @@ */ class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface { - /** - * @var CartRepositoryInterface - */ - private $quoteRepository; - /** * @var CustomerSession */ @@ -36,16 +30,13 @@ class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface private $checkoutSession; /** - * @param CartRepositoryInterface $quoteRepository * @param CustomerSession $customerSession * @param CheckoutSession $checkoutSession */ public function __construct( - CartRepositoryInterface $quoteRepository, CustomerSession $customerSession, CheckoutSession $checkoutSession ) { - $this->quoteRepository = $quoteRepository; $this->customerSession = $customerSession; $this->checkoutSession = $checkoutSession; } diff --git a/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php new file mode 100644 index 0000000000000..f8e17ca8acdec --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php @@ -0,0 +1,167 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Checkout\Model\StoreSwitcher\RedirectDataPostprocessor; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataPostprocessorTest extends TestCase +{ + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @var CustomerSession + */ + private $customerSession; + + /** + * @var CheckoutSession + */ + private $checkoutSession; + /** + * @var ContextInterface|MockObject + */ + private $context; + /** + * @var RedirectDataPostprocessor + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->quoteRepository = $this->createMock(CartRepositoryInterface::class); + $this->customerSession = $this->createMock(CustomerSession::class); + $this->checkoutSession = $this->createMock(CheckoutSession::class); + $logger = $this->createMock(LoggerInterface::class); + $this->model = new RedirectDataPostprocessor( + $this->quoteRepository, + $this->customerSession, + $this->checkoutSession, + $logger + ); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + } + + /** + * @dataProvider processDataProvider + * @param array $mock + * @param array $data + * @param bool $isQuoteSet + */ + public function testProcess(array $mock, array $data, bool $isQuoteSet): void + { + $this->customerSession->method('isLoggedIn') + ->willReturn($mock['isLoggedIn']); + $this->checkoutSession->method('getQuoteId') + ->willReturn($mock['getQuoteId']); + $this->checkoutSession->method('getQuote') + ->willReturnCallback( + function () use ($mock) { + return $this->createQuoteMock($mock); + } + ); + $this->quoteRepository->method('get') + ->willReturnCallback( + function ($id) use ($mock) { + return $this->createQuoteMock(array_merge($mock, ['getQuoteId' => $id])); + } + ); + $this->checkoutSession->expects($isQuoteSet ? $this->once() : $this->never()) + ->method('setQuoteId') + ->with($data['quote_id'] ?? null); + + $this->model->process($this->context, $data); + } + + /** + * @return array + */ + public function processDataProvider(): array + { + return [ + [ + ['isLoggedIn' => false, 'getQuoteId' => 4], + ['quote_id' => 2], + false + ], + [ + ['isLoggedIn' => true, 'getQuoteId' => null], + ['quote_id' => 2], + false + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null], + ['quote_id' => 1], + false + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null, 'getIsActive' => false], + ['quote_id' => 2], + false + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null], + ['quote_id' => 2], + true + ], + ]; + } + + /** + * @param array $mock + * @return Quote + */ + private function createQuoteMock(array $mock): Quote + { + return $this->createConfiguredMock( + Quote::class, + [ + 'getIsActive' => $mock['getIsActive'] ?? true, + 'getId' => $mock['getQuoteId'], + 'getSharedStoreIds' => !($mock['getQuoteId'] % 2) ? [1, 2] : [2], + ] + ); + } +} diff --git a/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php new file mode 100644 index 0000000000000..d5c4691d36a14 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Test\Unit\Model\StoreSwitcher; + +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Checkout\Model\StoreSwitcher\RedirectDataPreprocessor; +use Magento\Customer\Model\Session as CustomerSession; +use Magento\Quote\Model\Quote; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class RedirectDataPreprocessorTest extends TestCase +{ + /** + * @var CustomerSession + */ + private $customerSession; + /** + * @var CheckoutSession + */ + private $checkoutSession; + /** + * @var RedirectDataPreprocessor + */ + private $model; + /** + * @var ContextInterface|MockObject + */ + private $context; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerSession = $this->createMock(CustomerSession::class); + $this->checkoutSession = $this->createMock(CheckoutSession::class); + $this->model = new RedirectDataPreprocessor( + $this->customerSession, + $this->checkoutSession + ); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + } + + /** + * @dataProvider processDataProvider + * @param array $mock + * @param array $data + */ + public function testProcess(array $mock, array $data): void + { + $this->customerSession->method('isLoggedIn') + ->willReturn($mock['isLoggedIn']); + $this->checkoutSession->method('getQuoteId') + ->willReturn($mock['getQuoteId']); + $this->checkoutSession->method('getQuote') + ->willReturnCallback( + function () use ($mock) { + return $this->createConfiguredMock( + Quote::class, + [ + 'getIsActive' => $mock['getIsActive'] ?? true, + 'getId' => $mock['getQuoteId'], + 'getSharedStoreIds' => !($mock['getQuoteId'] % 2) ? [1, 2] : [2], + ] + ); + } + ); + $this->assertEquals($data, $this->model->process($this->context, [])); + } + + /** + * @return array + */ + public function processDataProvider(): array + { + return [ + [ + ['isLoggedIn' => true, 'getQuoteId' => 1], + [] + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => null], + [] + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => 1], + [] + ], + [ + ['isLoggedIn' => false, 'getQuoteId' => 2], + ['quote_id' => 2] + ], + ]; + } +} diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php index c7c6bb8694393..f0528d97b233c 100644 --- a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php @@ -68,7 +68,7 @@ public function process(ContextInterface $context, array $data): void throw new LocalizedException(__('Failed to sign into the customer account.'), $e); } catch (LocalizedException $e) { $this->logger->error($e); - throw new LocalizedException(__('There was an error retrieving the customer record.'), $e); + throw new LocalizedException(__('Failed to sign into the customer account.'), $e); } } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php new file mode 100644 index 0000000000000..d42e081935a96 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\StoreSwitcher; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\StoreSwitcher\RedirectDataPostprocessor; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataPostprocessorTest extends TestCase +{ + /** + * @var Session + */ + private $session; + /** + * @var RedirectDataPostprocessor + */ + private $model; + /** + * @var ContextInterface|MockObject + */ + private $context; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $customerRegistry = $this->createMock(CustomerRegistry::class); + $this->session = $this->createMock(Session::class); + $logger = $this->createMock(LoggerInterface::class); + $this->model = new RedirectDataPostprocessor( + $customerRegistry, + $this->session, + $logger + ); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + + $customerRegistry->method('retrieve') + ->willReturnCallback( + function ($id) { + switch ($id) { + case 1: + throw new NoSuchEntityException(__('Customer does not exist')); + case 2: + throw new LocalizedException(__('Something went wrong')); + default: + $customer = $this->createMock(Customer::class); + $customer->method('getSharedStoreIds') + ->willReturn(!($id % 2) ? [1, 2] : [2]); + $customer->method('getDataModel') + ->willReturn( + $this->createConfiguredMock( + CustomerInterface::class, + [ + 'getId' => $id + ] + ) + ); + return $customer; + } + } + ); + } + + public function testProcessShouldLoginCustomerIfCustomerIsRegisteredInTargetStore(): void + { + $data = ['customer_id' => 4]; + $this->session->expects($this->once()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } + + public function testProcessShouldNotLoginCustomerIfNotRegisteredInTargetStore(): void + { + $data = ['customer_id' => 3]; + $this->session->expects($this->never()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } + + public function testProcessShouldThrowExceptionIfCustomerDoesNotExist(): void + { + $this->expectErrorMessage('Failed to sign into the customer account.'); + $data = ['customer_id' => 1]; + $this->session->expects($this->never()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } + + public function testProcessShouldThrowExceptionIfAnErrorOccur(): void + { + $this->expectErrorMessage('Failed to sign into the customer account.'); + $data = ['customer_id' => 2]; + $this->session->expects($this->never()) + ->method('setCustomerDataAsLoggedIn'); + $this->model->process($this->context, $data); + } +} diff --git a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php new file mode 100644 index 0000000000000..edfc236c9f690 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\StoreSwitcher; + +use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\StoreSwitcher\RedirectDataPreprocessor; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataPreprocessorTest extends TestCase +{ + /** + * @var RedirectDataPreprocessor + */ + private $model; + /** + * @var ContextInterface|MockObject + */ + private $context; + /** + * @var UserContextInterface|MockObject + */ + private $userContext; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $customerRegistry = $this->createMock(CustomerRegistry::class); + $logger = $this->createMock(LoggerInterface::class); + $this->userContext = $this->createMock(UserContextInterface::class); + $this->model = new RedirectDataPreprocessor( + $customerRegistry, + $this->userContext, + $logger + ); + + $this->userContext->method('getUserType') + ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); + + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + + $customerRegistry->method('retrieve') + ->willReturnCallback( + function ($id) { + switch ($id) { + case 1: + throw new NoSuchEntityException(__('Customer does not exist')); + case 2: + throw new LocalizedException(__('Something went wrong')); + default: + $customer = $this->createMock(Customer::class); + $customer->method('getSharedStoreIds') + ->willReturn(!($id % 2) ? [1, 2] : [2]); + $customer->method('getId') + ->willReturn($id); + return $customer; + } + } + ); + } + + /** + * @dataProvider processDataProvider + * @param int|null $customerId + * @param array $data + */ + public function testProcess(?int $customerId, array $data): void + { + $this->userContext->method('getUserId') + ->willReturn($customerId); + $this->assertEquals($data, $this->model->process($this->context, [])); + } + + /** + * @return array + */ + public function processDataProvider(): array + { + return [ + [1, []], + [2, []], + [3, []], + [4, ['customer_id' => 4]] + ]; + } +} diff --git a/app/code/Magento/Store/Controller/Store/Redirect.php b/app/code/Magento/Store/Controller/Store/Redirect.php index 72da0b00a3d29..c0967eeea942d 100644 --- a/app/code/Magento/Store/Controller/Store/Redirect.php +++ b/app/code/Magento/Store/Controller/Store/Redirect.php @@ -127,15 +127,17 @@ public function execute() $targetStore = $this->storeRepository->get($targetStoreCode); $this->storeManager->setCurrentStore($targetStore); $encodedUrl = $this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED); + $customerId = $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER + && $this->userContext->getUserId() + ? (int) $this->userContext->getUserId() + : null; $redirectData = $this->redirectDataGenerator->generate( $this->contextFactory->create( [ 'fromStore' => $fromStore, 'targetStore' => $targetStore, 'redirectUrl' => $this->_redirect->getRedirectUrl(), - 'customerId' => $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER ? - (int) $this->userContext->getUserId() - : null + 'customerId' => $customerId ] ) ); diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php index 5e4e598a41fe2..edf6f4536c292 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php @@ -107,8 +107,9 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s $timestamp = (int) $this->request->getParam('time_stamp'); $signature = (string) $this->request->getParam('signature'); $data = (string) $this->request->getParam('data'); - $customerId = $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER ? - (int) $this->userContext->getUserId() + $customerId = $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER + && $this->userContext->getUserId() + ? (int) $this->userContext->getUserId() : null; $context = $this->contextFactory->create( [ diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php index a68e0bb7b795d..3ff0375a0c348 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataGenerator.php @@ -22,7 +22,7 @@ class RedirectDataGenerator /** * @var RedirectDataSerializerInterface */ - private $serializer; + private $dataSerializer; /** * @var RedirectDataInterfaceFactory */ @@ -39,19 +39,19 @@ class RedirectDataGenerator /** * @param Encryptor $encryptor * @param RedirectDataPreprocessorInterface $preprocessor - * @param RedirectDataSerializerInterface $serializer + * @param RedirectDataSerializerInterface $dataSerializer * @param RedirectDataInterfaceFactory $dataFactory * @param LoggerInterface $logger */ public function __construct( Encryptor $encryptor, RedirectDataPreprocessorInterface $preprocessor, - RedirectDataSerializerInterface $serializer, + RedirectDataSerializerInterface $dataSerializer, RedirectDataInterfaceFactory $dataFactory, LoggerInterface $logger ) { $this->preprocessor = $preprocessor; - $this->serializer = $serializer; + $this->dataSerializer = $dataSerializer; $this->dataFactory = $dataFactory; $this->encryptor = $encryptor; $this->logger = $logger; @@ -67,7 +67,7 @@ public function generate(ContextInterface $context): RedirectDataInterface { $data = $this->preprocessor->process($context, []); try { - $dataStr = $this->serializer->serialize($data); + $dataStr = $this->dataSerializer->serialize($data); } catch (\Throwable $exception) { $this->logger->error($exception); $dataStr = ''; diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php index 40ba8d1fcb1d9..9200e80cae05c 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataValidator.php @@ -14,7 +14,7 @@ */ class RedirectDataValidator { - const TIMEOUT = 5; + private const TIMEOUT = 5; /** * @var Encryptor */ diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php new file mode 100644 index 0000000000000..c82d418d20bd9 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php @@ -0,0 +1,166 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use InvalidArgumentException; +use Magento\Authorization\Model\UserContextInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Message\ManagerInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\HashProcessor; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataValidator; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class HashProcessorTest extends TestCase +{ + /** + * @var RequestInterface|MockObject + */ + private $request; + /** + * @var RedirectDataPostprocessorInterface|MockObject + */ + private $postprocessor; + /** + * @var RedirectDataSerializerInterface|MockObject + */ + private $dataSerializer; + /** + * @var ManagerInterface|MockObject + */ + private $messageManager; + /** + * @var RedirectDataValidator|MockObject + */ + private $dataValidator; + /** + * @var StoreInterface|MockObject + */ + private $store1; + /** + * @var StoreInterface|MockObject + */ + private $store2; + /** + * @var HashProcessor + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->request = $this->createMock(RequestInterface::class); + $this->postprocessor = $this->createMock(RedirectDataPostprocessorInterface::class); + $this->dataSerializer = $this->createMock(RedirectDataSerializerInterface::class); + $this->messageManager = $this->createMock(ManagerInterface::class); + $contextFactory = $this->createMock(ContextInterfaceFactory::class); + $dataFactory = $this->createMock(RedirectDataInterfaceFactory::class); + $this->dataValidator = $this->createMock(RedirectDataValidator::class); + $logger = $this->createMock(LoggerInterface::class); + $userContext = $this->createMock(UserContextInterface::class); + $this->store1 = $this->createMock(StoreInterface::class); + $this->store2 = $this->createMock(StoreInterface::class); + $this->model = new HashProcessor( + $this->request, + $this->postprocessor, + $this->dataSerializer, + $this->messageManager, + $contextFactory, + $dataFactory, + $this->dataValidator, + $logger, + $userContext + ); + + $contextFactory->method('create') + ->willReturn($this->createMock(ContextInterface::class)); + $dataFactory->method('create') + ->willReturnCallback( + function (array $data) { + return $this->createConfiguredMock( + RedirectDataInterface::class, + [ + 'getTimestamp' => $data['timestamp'], + 'getData' => $data['data'], + 'getSignature' => $data['signature'], + ] + ); + } + ); + } + + public function testShouldProcessIfDataValidationPassed(): void + { + $redirectUrl = '/category-1/category-1.1.html'; + $this->request->method('getParam') + ->willReturnMap( + [ + ['time_stamp', null, time() - 1], + ['data', null, '{"customer_id":1}'], + ['signature', null, 'randomstring'], + ] + ); + $this->dataValidator->method('validate') + ->willReturn(true); + $this->dataSerializer->method('unserialize') + ->with('{"customer_id":1}') + ->willReturnCallback( + function ($arg) { + return json_decode($arg, true); + } + ); + $this->postprocessor->expects($this->once()) + ->method('process') + ->with($this->isInstanceOf(ContextInterface::class), ['customer_id' => 1]); + $this->assertEquals($redirectUrl, $this->model->switch($this->store1, $this->store2, $redirectUrl)); + } + + public function testShouldNotProcessIfDataValidationFailed(): void + { + $redirectUrl = '/category-1/category-1.1.html'; + $this->dataValidator->method('validate') + ->willReturn(false); + $this->postprocessor->expects($this->never()) + ->method('process'); + $this->messageManager->expects($this->once()) + ->method('addErrorMessage') + ->with('The requested store cannot be found. Please check the request and try again.'); + + $this->assertEquals($redirectUrl, $this->model->switch($this->store1, $this->store2, $redirectUrl)); + } + + public function testShouldNotProcessIfDataUnserializationFailed(): void + { + $redirectUrl = '/category-1/category-1.1.html'; + $this->dataValidator->method('validate') + ->willReturn(true); + $this->dataSerializer->method('unserialize') + ->willThrowException(new InvalidArgumentException('Invalid token supplied')); + $this->postprocessor->expects($this->never()) + ->method('process'); + $this->messageManager->expects($this->once()) + ->method('addErrorMessage') + ->with('Something went wrong while switching to the store.'); + + $this->assertEquals($redirectUrl, $this->model->switch($this->store1, $this->store2, $redirectUrl)); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php new file mode 100644 index 0000000000000..68284a8163c26 --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use InvalidArgumentException; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Math\Random; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Model\StoreSwitcher\RedirectDataCacheSerializer; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use RuntimeException; + +class RedirectDataCacheSerializerTest extends TestCase +{ + private const RANDOM_STRING = '7ddf32e17a6ac5ce04a8ecbf782ca509'; + /** + * @var CacheInterface|MockObject + */ + private $cache; + /** + * @var RedirectDataCacheSerializer + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->cache = $this->createMock(CacheInterface::class); + $random = $this->createMock(Random::class); + $this->model = new RedirectDataCacheSerializer( + new Json(), + $random, + $this->cache + ); + $random->method('getRandomString')->willReturn(self::RANDOM_STRING); + } + + public function testSerialize(): void + { + $this->cache->expects($this->once()) + ->method('save') + ->with( + '{"customer_id":1}', + 'store_switch_' . self::RANDOM_STRING, + [], + 10 + ); + $this->assertEquals(self::RANDOM_STRING, $this->model->serialize(['customer_id' => 1])); + } + + public function testSerializeShouldThrowExceptionIfCannotSaveCache(): void + { + $exception = new RuntimeException('Failed to connect to cache server'); + $this->expectExceptionObject($exception); + $this->cache->expects($this->once()) + ->method('save') + ->willThrowException($exception); + $this->assertEquals(self::RANDOM_STRING, $this->model->serialize(['customer_id' => 1])); + } + + public function testUnserialize(): void + { + $this->cache->expects($this->once()) + ->method('load') + ->with('store_switch_' . self::RANDOM_STRING) + ->willReturn('{"customer_id":1}'); + $this->assertEquals(['customer_id' => 1], $this->model->unserialize(self::RANDOM_STRING)); + } + + public function testUnserializeShouldThrowExceptionIfCacheHasExpired(): void + { + $this->expectExceptionObject(new InvalidArgumentException('Couldn\'t retrieve data from cache.')); + $this->cache->expects($this->once()) + ->method('load') + ->with('store_switch_' . self::RANDOM_STRING) + ->willReturn(null); + $this->assertEquals(['customer_id' => 1], $this->model->unserialize(self::RANDOM_STRING)); + } + + public function testUnserializeShouldThrowExceptionIfCacheKeyIsInvalid(): void + { + $this->expectExceptionObject(new InvalidArgumentException('Invalid cache key \'abc\' supplied.')); + $this->cache->expects($this->never()) + ->method('load'); + $this->assertEquals(['customer_id' => 1], $this->model->unserialize('abc')); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataGeneratorTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataGeneratorTest.php new file mode 100644 index 0000000000000..67270f5f70dce --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataGeneratorTest.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class RedirectDataGeneratorTest extends TestCase +{ + /** + * @var RedirectDataPreprocessorInterface|MockObject + */ + private $preprocessor; + /** + * @var RedirectDataSerializerInterface|MockObject + */ + private $dataSerializer; + /** + * @var ContextInterface|MockObject + */ + private $context; + /** + * @var RedirectDataGenerator + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->preprocessor = $this->createMock(RedirectDataPreprocessorInterface::class); + $this->dataSerializer = $this->createMock(RedirectDataSerializerInterface::class); + $dataFactory = $this->createMock(RedirectDataInterfaceFactory::class); + $encryptor = $this->createMock(Encryptor::class); + $logger = $this->createMock(LoggerInterface::class); + $this->model = new RedirectDataGenerator( + $encryptor, + $this->preprocessor, + $this->dataSerializer, + $dataFactory, + $logger + ); + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + $encryptor->method('hash') + ->willReturnCallback( + function (string $arg1) { + // phpcs:ignore Magento2.Security.InsecureFunction + return md5($arg1); + } + ); + $dataFactory->method('create') + ->willReturnCallback( + function (array $data) { + return $this->createConfiguredMock( + RedirectDataInterface::class, + [ + 'getTimestamp' => $data['timestamp'], + 'getData' => $data['data'], + 'getSignature' => $data['signature'], + ] + ); + } + ); + } + + public function testGenerate(): void + { + $this->preprocessor->method('process') + ->willReturn(['customer_id' => 1]); + $this->dataSerializer->method('serialize') + ->willReturnCallback('json_encode'); + $redirectData = $this->model->generate($this->context); + $time = time(); + $this->assertEqualsWithDelta($time, $redirectData->getTimestamp(), 1); + $time = $redirectData->getTimestamp(); + $this->assertEquals('{"customer_id":1}', $redirectData->getData()); + // phpcs:ignore Magento2.Security.InsecureFunction + $this->assertEquals(md5("{\"customer_id\":1},{$time},fr,en"), $redirectData->getSignature()); + } + + public function testShouldGenerateEmptyDataIfDataSerializationFailed(): void + { + $this->dataSerializer->method('serialize') + ->willThrowException(new \InvalidArgumentException('Failed to connect to cache server')); + + $redirectData = $this->model->generate($this->context); + $time = time(); + $this->assertEqualsWithDelta($time, $redirectData->getTimestamp(), 1); + $time = $redirectData->getTimestamp(); + $this->assertEquals('', $redirectData->getData()); + // phpcs:ignore Magento2.Security.InsecureFunction + $this->assertEquals(md5(",{$time},fr,en"), $redirectData->getSignature()); + } +} diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataValidatorTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataValidatorTest.php new file mode 100644 index 0000000000000..9960fad2071be --- /dev/null +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataValidatorTest.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Store\Test\Unit\Model\StoreSwitcher; + +use Magento\Framework\Encryption\Encryptor; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataValidator; +use PHPUnit\Framework\TestCase; + +class RedirectDataValidatorTest extends TestCase +{ + /** + * @var RedirectDataValidator + */ + private $model; + /** + * @var ContextInterface + */ + private $context; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $encryptor = $this->createMock(Encryptor::class); + $this->model = new RedirectDataValidator( + $encryptor + ); + $store1 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'en', + 'getId' => 1, + ] + ); + $store2 = $this->createConfiguredMock( + StoreInterface::class, + [ + 'getCode' => 'fr', + 'getId' => 2, + ] + ); + $this->context = $this->createConfiguredMock( + ContextInterface::class, + [ + 'getFromStore' => $store2, + 'getTargetStore' => $store1, + ] + ); + $encryptor->method('validateHash') + ->willReturnCallback( + function (string $value, string $hash) { + // phpcs:ignore Magento2.Security.InsecureFunction + return md5($value) === $hash; + } + ); + } + + /** + * @param array $params + * @param bool $result + * @dataProvider validationDataProvider + */ + public function testValidation(array $params, bool $result): void + { + $originalData = '{"customer_id":1}'; + $timestamp = time() - $params['elapsedTime']; + $fromStoreCode = $params['fromStoreCode'] ?? $this->context->getFromStore()->getCode(); + $targetStoreCode = $params['targetStoreCode'] ?? $this->context->getTargetStore()->getCode(); + // phpcs:ignore Magento2.Security.InsecureFunction + $signature = md5("{$originalData},{$timestamp},{$fromStoreCode},{$targetStoreCode}"); + $redirectData = $this->createConfiguredMock( + RedirectDataInterface::class, + [ + 'getTimestamp' => $params['timestamp'] ?? $timestamp, + 'getData' => $params['data'] ?? $originalData, + 'getSignature' => $params['signature'] ?? $signature, + ] + ); + $this->assertEquals($result, $this->model->validate($this->context, $redirectData)); + } + + /** + * @return array + */ + public function validationDataProvider(): array + { + return [ + [ + [ + 'elapsedTime' => 1, + ], + true + ], + [ + [ + 'elapsedTime' => 6, + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'data' => '{"customer_id":2}' + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'fromStoreCode' => 'es' + + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'targetStoreCode' => 'de' + + ], + false + ], + [ + [ + 'elapsedTime' => 1, + 'signature' => 'abcd1efgh2ijkl3mnop4qrst5uvwx6yz' + + ], + false + ] + ]; + } +} From c2f29b40dbef2ed1cc43136945e4ed94b15991f3 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Tue, 15 Sep 2020 14:40:41 -0500 Subject: [PATCH 0500/1013] MC-35855: CSP cannot be configured from a theme --- .../Csp/Model/Collector/CspWhitelistXml/FileResolver.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php index bfbc88219ca94..acc0dd1600db1 100644 --- a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/FileResolver.php @@ -87,7 +87,9 @@ public function get($filename, $scope) } $theme = $theme->getParentTheme(); } - $configs = $this->iteratorFactory->create(['paths' => $files, 'existingIterator' => $configs]); + $configs = $this->iteratorFactory->create( + ['paths' => array_reverse($files), 'existingIterator' => $configs] + ); } return $configs; From 9d8a772c4f0efeb15f4fac12e294123f27e9f81b Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 22:50:25 +0200 Subject: [PATCH 0501/1013] IMP: formatting by keeping auth.json indent, same as composer --- .editorconfig | 3 +++ auth.json.sample | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.editorconfig b/.editorconfig index 877666f2aba34..0452085768830 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,3 +16,6 @@ indent_size = 2 [composer.{json,lock}] indent_size = 4 + +[auth.json] +indent_size = 4 diff --git a/auth.json.sample b/auth.json.sample index bd4b61d607328..be1c70cfe1e18 100644 --- a/auth.json.sample +++ b/auth.json.sample @@ -1,8 +1,8 @@ { - "http-basic": { - "repo.magento.com": { - "username": "<public-key>", - "password": "<private-key>" + "http-basic": { + "repo.magento.com": { + "username": "<public-key>", + "password": "<private-key>" + } } - } } From d15f34dc6fdda1653e854b36f39f9549f8a70f72 Mon Sep 17 00:00:00 2001 From: GrimLink <sean.grimlink@gmail.com> Date: Tue, 15 Sep 2020 19:11:00 +0200 Subject: [PATCH 0502/1013] Fixes #30064 - wrong input type for date datatime is deprecated! --- lib/web/css/source/lib/_forms.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/css/source/lib/_forms.less b/lib/web/css/source/lib/_forms.less index a22ec5f52db6d..9e16962c45584 100644 --- a/lib/web/css/source/lib/_forms.less +++ b/lib/web/css/source/lib/_forms.less @@ -271,7 +271,7 @@ input[type="tel"], input[type="search"], input[type="number"], - input[type="datetime"], + input[type*="date"], input[type="email"] { .lib-form-element-input(@_type: input-text); } From 663d23ab887918dbdd54614cc531f35f3e70c245 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 15 Sep 2020 17:43:20 -0500 Subject: [PATCH 0503/1013] MC-37351: Cart contents lost after switching to different store with different domain - Add integration test --- .../Store/Controller/Store/RedirectTest.php | 67 ++++++++++- .../Controller/Store/SwitchActionTest.php | 113 +++++++++++++++++- 2 files changed, 175 insertions(+), 5 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php index ba61592903b4d..d13b84ab7ef49 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php @@ -7,9 +7,12 @@ namespace Magento\Store\Controller\Store; +use Magento\Framework\App\CacheInterface; use Magento\Framework\Session\SidResolverInterface; use Magento\Store\Model\StoreResolver; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; use Magento\TestFramework\TestCase\AbstractController; +use PHPUnit\Framework\MockObject\MockObject; /** * Test Redirect controller. @@ -18,6 +21,68 @@ */ class RedirectTest extends AbstractController { + /** + * @var RedirectDataPreprocessorInterface + */ + private $preprocessor; + /** + * @var MockObject + */ + private $preprocessorMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); + $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); + $this->_objectManager->addSharedInstance($this->preprocessorMock, get_class($this->preprocessor)); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + if ($this->preprocessor) { + $this->_objectManager->addSharedInstance($this->preprocessor, get_class($this->preprocessor)); + } + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture web/url/use_store 0 + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_link_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_link_url http://second_store.test/ + */ + public function testRedirectToSecondStoreOnAnotherUrl(): void + { + $this->preprocessorMock->method('process') + ->willReturn(['key1' => 'value1', 'key2' => 1]); + $this->getRequest()->setParam(StoreResolver::PARAM_NAME, 'fixture_second_store'); + $this->getRequest()->setParam('___from_store', 'default'); + $this->dispatch('/stores/store/redirect'); + $header = $this->getResponse()->getHeader('Location'); + $this->assertNotEmpty($header); + $result = $header->getFieldValue(); + $this->assertStringStartsWith('http://second_store.test/', $result); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $urlParts = parse_url($result); + $this->assertStringEndsWith('stores/store/switch/', $urlParts['path']); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + parse_str($urlParts['query'], $params); + $this->assertTrue(!empty($params['time_stamp'])); + $this->assertTrue(!empty($params['signature'])); + $this->assertTrue(!empty($params['data'])); + $cache = $this->_objectManager->get(CacheInterface::class); + $this->assertEquals('{"key1":"value1","key2":1}', $cache->load('store_switch_' . $params['data'])); + } + /** * Check that there's no SID in redirect URL. * @@ -35,6 +100,6 @@ public function testNoSid(): void $result = (string)$this->getResponse()->getHeader('location'); $this->assertNotEmpty($result); - $this->assertStringNotContainsString(SidResolverInterface::SESSION_ID_QUERY_PARAM .'=', $result); + $this->assertStringNotContainsString(SidResolverInterface::SESSION_ID_QUERY_PARAM . '=', $result); } } diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php index bc8ca2ba07a80..b10e7d604d900 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php @@ -5,11 +5,116 @@ */ namespace Magento\Store\Controller\Store; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\Encryption\UrlCoder; +use Magento\Store\Api\StoreResolverInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterface; +use Magento\Store\Model\StoreSwitcher\ContextInterfaceFactory; +use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; +use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\TestFramework\TestCase\AbstractController; +use PHPUnit\Framework\MockObject\MockObject; + /** * Test for store switch controller. */ -class SwitchActionTest extends \Magento\TestFramework\TestCase\AbstractController +class SwitchActionTest extends AbstractController { + /** + * @var RedirectDataPreprocessorInterface + */ + private $preprocessor; + /** + * @var MockObject + */ + private $preprocessorMock; + /** + * @var RedirectDataPostprocessorInterface + */ + private $postprocessor; + /** + * @var MockObject + */ + private $postprocessorMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); + $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); + $this->_objectManager->addSharedInstance($this->preprocessorMock, get_class($this->preprocessor)); + + $this->postprocessor = $this->_objectManager->get(RedirectDataPostprocessorInterface::class); + $this->postprocessorMock = $this->createMock(RedirectDataPostprocessorInterface::class); + $this->_objectManager->addSharedInstance($this->postprocessorMock, get_class($this->postprocessor)); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + if ($this->preprocessor) { + $this->_objectManager->addSharedInstance($this->preprocessor, get_class($this->preprocessor)); + } + if ($this->postprocessor) { + $this->_objectManager->addSharedInstance($this->postprocessor, get_class($this->postprocessor)); + } + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture web/url/use_store 0 + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/unsecure/base_link_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_url http://second_store.test/ + * @magentoConfigFixture fixture_second_store_store web/secure/base_link_url http://second_store.test/ + */ + public function testSwitch() + { + $data = ['key1' => 'value1', 'key2' => 1]; + $this->preprocessorMock->method('process') + ->willReturn($data); + $this->postprocessorMock->expects($this->once()) + ->method('process') + ->with($this->isInstanceOf(ContextInterface::class), $data); + + $redirectDataGenerator = $this->_objectManager->get(RedirectDataGenerator::class); + $contextFactory = $this->_objectManager->get(ContextInterfaceFactory::class); + $storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $urlEncoder = $this->_objectManager->get(UrlCoder::class); + $fromStore = $storeManager->getStore('fixture_second_store'); + $targetStore = $storeManager->getStore('default'); + $redirectData = $redirectDataGenerator->generate( + $contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => '/', + ] + ) + ); + $this->getRequest()->setParams( + [ + '___from_store' => $fromStore->getCode(), + StoreResolverInterface::PARAM_NAME => $targetStore->getCode(), + ActionInterface::PARAM_NAME_URL_ENCODED => $urlEncoder->encode('/'), + 'data' => $redirectData->getData(), + 'time_stamp' => $redirectData->getTimestamp(), + 'signature' => $redirectData->getSignature(), + ] + ); + $this->dispatch('stores/store/switch'); + $this->assertRedirect($this->equalTo('http://localhost/index.php/')); + } + /** * Ensure that proper default store code is calculated. * @@ -41,10 +146,10 @@ public function testExecuteWithCustomDefaultStore() * @param string $from * @param string $to */ - protected function changeStoreCode($from, $to) + private function changeStoreCode($from, $to) { - /** @var \Magento\Store\Model\Store $store */ - $store = $this->_objectManager->create(\Magento\Store\Model\Store::class); + /** @var Store $store */ + $store = $this->_objectManager->create(Store::class); $store->load($from, 'code'); $store->setCode($to); $store->save(); From e7e156cb426737e0bc50a8032853407641cb0b7e Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 15 Sep 2020 20:22:24 -0500 Subject: [PATCH 0504/1013] MC-36785: Unable to set YouTube API key by CLI --- .../App/Config/Source/ModularConfigSource.php | 68 ++++++++++++++++--- app/code/Magento/ProductVideo/etc/config.xml | 1 - 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php b/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php index 01cea0a8ee4e7..c047fb61e828c 100644 --- a/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php @@ -5,9 +5,11 @@ */ namespace Magento\Config\App\Config\Source; +use Magento\Config\Model\Config\Structure\Reader as ConfigStructureReader; +use Magento\Framework\App\Area; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\DataObject; -use Magento\Framework\App\Config\Initial\Reader; +use Magento\Framework\App\Config\Initial\Reader as InitialConfigReader; /** * Class for retrieving initial configuration from modules @@ -18,16 +20,25 @@ class ModularConfigSource implements ConfigSourceInterface { /** - * @var Reader + * @var InitialConfigReader */ - private $reader; + private $initialConfigReader; /** - * @param Reader $reader + * @var ConfigStructureReader */ - public function __construct(Reader $reader) - { - $this->reader = $reader; + private $configStructureReader; + + /** + * @param InitialConfigReader $initialConfigReader + * @param ConfigStructureReader $configStructureReader + */ + public function __construct( + InitialConfigReader $initialConfigReader, + ConfigStructureReader $configStructureReader + ) { + $this->initialConfigReader = $initialConfigReader; + $this->configStructureReader = $configStructureReader; } /** @@ -39,10 +50,51 @@ public function __construct(Reader $reader) */ public function get($path = '') { - $data = new DataObject($this->reader->read()); + $initialConfig = $this->initialConfigReader->read(); + $configStructure = $this->configStructureReader->read(Area::AREA_ADMINHTML); + $sections = $configStructure['config']['system']['sections'] ?? []; + $defaultConfig = $initialConfig['data']['default'] ?? []; + $initialConfig['data']['default'] = $this->merge($defaultConfig, $sections); + + $data = new DataObject($initialConfig); if ($path !== '') { $path = '/' . $path; } return $data->getData('data' . $path) ?: []; } + + private function addPathKey(array $data, string $path): array + { + if (strpos($path, '/') !== false) { + list ($key, $subPath) = explode('/', $path, 2); + $data[$key] = $this->addPathKey($data[$key] ?? [], $subPath); + } else { + $data += [$path => null]; + } + + return $data; + } + + /** + * Merge initial config with config structure + * + * @param array $config + * @param array $sections + * @return array + */ + private function merge(array $config, array $sections): array + { + foreach ($sections as $key => $section) { + if (isset($section['children'])) { + $config[$section['id']] = $this->merge( + $config[$section['id']] ?? [], + $section['children'] + ); + } elseif ($section['_elementType'] === 'field') { + $config += [$section['id'] => null]; + } + } + + return $config; + } } diff --git a/app/code/Magento/ProductVideo/etc/config.xml b/app/code/Magento/ProductVideo/etc/config.xml index 55b7def6fb8ef..8afefa14a3651 100644 --- a/app/code/Magento/ProductVideo/etc/config.xml +++ b/app/code/Magento/ProductVideo/etc/config.xml @@ -12,7 +12,6 @@ <play_if_base>0</play_if_base> <show_related>0</show_related> <video_auto_restart>0</video_auto_restart> - <youtube_api_key/> </product_video> </catalog> </default> From f7fbeb94f6f3e033d7165f7425d8b4a9c66c6ed4 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Tue, 15 Sep 2020 21:08:37 -0500 Subject: [PATCH 0505/1013] MC-37351: Cart contents lost after switching to different store with different domain - Fix static tests --- .../Store/Controller/Store/RedirectTest.php | 30 +++++++-- .../Controller/Store/SwitchActionTest.php | 61 +++++++++++++++++-- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php index d13b84ab7ef49..5a1348d9da49b 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/RedirectTest.php @@ -7,16 +7,18 @@ namespace Magento\Store\Controller\Store; -use Magento\Framework\App\CacheInterface; +use Magento\Framework\Interception\InterceptorInterface; use Magento\Framework\Session\SidResolverInterface; use Magento\Store\Model\StoreResolver; use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\Store\Model\StoreSwitcher\RedirectDataSerializerInterface; use Magento\TestFramework\TestCase\AbstractController; use PHPUnit\Framework\MockObject\MockObject; /** * Test Redirect controller. * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea frontend */ class RedirectTest extends AbstractController @@ -38,7 +40,7 @@ protected function setUp(): void parent::setUp(); $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); - $this->_objectManager->addSharedInstance($this->preprocessorMock, get_class($this->preprocessor)); + $this->_objectManager->addSharedInstance($this->preprocessorMock, $this->getClassName($this->preprocessor)); } /** @@ -47,7 +49,7 @@ protected function setUp(): void protected function tearDown(): void { if ($this->preprocessor) { - $this->_objectManager->addSharedInstance($this->preprocessor, get_class($this->preprocessor)); + $this->_objectManager->addSharedInstance($this->preprocessor, $this->getClassName($this->preprocessor)); } parent::tearDown(); } @@ -62,8 +64,9 @@ protected function tearDown(): void */ public function testRedirectToSecondStoreOnAnotherUrl(): void { + $data = ['key1' => 'value1', 'key2' => 1]; $this->preprocessorMock->method('process') - ->willReturn(['key1' => 'value1', 'key2' => 1]); + ->willReturn($data); $this->getRequest()->setParam(StoreResolver::PARAM_NAME, 'fixture_second_store'); $this->getRequest()->setParam('___from_store', 'default'); $this->dispatch('/stores/store/redirect'); @@ -79,8 +82,23 @@ public function testRedirectToSecondStoreOnAnotherUrl(): void $this->assertTrue(!empty($params['time_stamp'])); $this->assertTrue(!empty($params['signature'])); $this->assertTrue(!empty($params['data'])); - $cache = $this->_objectManager->get(CacheInterface::class); - $this->assertEquals('{"key1":"value1","key2":1}', $cache->load('store_switch_' . $params['data'])); + $serializer = $this->_objectManager->get(RedirectDataSerializerInterface::class); + $this->assertEquals($data, $serializer->unserialize($params['data'])); + } + + /** + * Return class name of the given object + * + * @param mixed $instance + */ + private function getClassName($instance): string + { + if ($instance instanceof InterceptorInterface) { + $actionClass = get_parent_class($instance); + } else { + $actionClass = get_class($instance); + } + return $actionClass; } /** diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php index b10e7d604d900..503f9cb8ce7f8 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php @@ -5,8 +5,10 @@ */ namespace Magento\Store\Controller\Store; +use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\ActionInterface; use Magento\Framework\Encryption\UrlCoder; +use Magento\Framework\Interception\InterceptorInterface; use Magento\Store\Api\StoreResolverInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -20,6 +22,9 @@ /** * Test for store switch controller. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @magentoAppArea frontend */ class SwitchActionTest extends AbstractController { @@ -39,6 +44,14 @@ class SwitchActionTest extends AbstractController * @var MockObject */ private $postprocessorMock; + /** + * @var UserContextInterface + */ + private $userContext; + /** + * @var MockObject + */ + private $userContextMock; /** * @inheritDoc @@ -48,11 +61,15 @@ protected function setUp(): void parent::setUp(); $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); - $this->_objectManager->addSharedInstance($this->preprocessorMock, get_class($this->preprocessor)); + $this->_objectManager->addSharedInstance($this->preprocessorMock, $this->getClassName($this->preprocessor)); $this->postprocessor = $this->_objectManager->get(RedirectDataPostprocessorInterface::class); $this->postprocessorMock = $this->createMock(RedirectDataPostprocessorInterface::class); - $this->_objectManager->addSharedInstance($this->postprocessorMock, get_class($this->postprocessor)); + $this->_objectManager->addSharedInstance($this->postprocessorMock, $this->getClassName($this->postprocessor)); + + $this->userContext = $this->_objectManager->get(UserContextInterface::class); + $this->userContextMock = $this->createMock(UserContextInterface::class); + $this->_objectManager->addSharedInstance($this->userContextMock, $this->getClassName($this->userContext)); } /** @@ -61,15 +78,19 @@ protected function setUp(): void protected function tearDown(): void { if ($this->preprocessor) { - $this->_objectManager->addSharedInstance($this->preprocessor, get_class($this->preprocessor)); + $this->_objectManager->addSharedInstance($this->preprocessor, $this->getClassName($this->preprocessor)); } if ($this->postprocessor) { - $this->_objectManager->addSharedInstance($this->postprocessor, get_class($this->postprocessor)); + $this->_objectManager->addSharedInstance($this->postprocessor, $this->getClassName($this->postprocessor)); + } + if ($this->userContext) { + $this->_objectManager->addSharedInstance($this->userContext, $this->getClassName($this->userContext)); } parent::tearDown(); } /** + * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Store/_files/second_store.php * @magentoConfigFixture web/url/use_store 0 * @magentoConfigFixture fixture_second_store_store web/unsecure/base_url http://second_store.test/ @@ -82,10 +103,23 @@ public function testSwitch() $data = ['key1' => 'value1', 'key2' => 1]; $this->preprocessorMock->method('process') ->willReturn($data); + $this->userContextMock->method('getUserType') + ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); + $this->userContextMock->method('getUserId') + ->willReturn(1); $this->postprocessorMock->expects($this->once()) ->method('process') - ->with($this->isInstanceOf(ContextInterface::class), $data); - + ->with( + $this->callback( + function (ContextInterface $context) { + return $context->getFromStore()->getCode() === 'fixture_second_store' + && $context->getTargetStore()->getCode() === 'default' + && $context->getRedirectUrl() === 'http://localhost/index.php/' + && $context->getCustomerId() === 1; + } + ), + $data + ); $redirectDataGenerator = $this->_objectManager->get(RedirectDataGenerator::class); $contextFactory = $this->_objectManager->get(ContextInterfaceFactory::class); $storeManager = $this->_objectManager->get(StoreManagerInterface::class); @@ -115,6 +149,21 @@ public function testSwitch() $this->assertRedirect($this->equalTo('http://localhost/index.php/')); } + /** + * Return class name of the given object + * + * @param mixed $instance + */ + private function getClassName($instance): string + { + if ($instance instanceof InterceptorInterface) { + $actionClass = get_parent_class($instance); + } else { + $actionClass = get_class($instance); + } + return $actionClass; + } + /** * Ensure that proper default store code is calculated. * From ec3e6da7b188cc5c88179d9aa89f698fafd345d2 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Tue, 15 Sep 2020 16:22:51 +0300 Subject: [PATCH 0506/1013] add mftf test --- ...ntCategoryUrlRewriteDifferentStoreTest.xml | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml new file mode 100644 index 0000000000000..a79651a39cc37 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.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="StorefrontCategoryUrlRewriteDifferentStoreTest"> + <annotations> + <stories value="Url rewrites"/> + <title value="Verify url category for different store view."/> + <description value="Verify url category for different store view, after change ukr_key category for one of them store view."/> + <features value="CatalogUrlRewrite"/> + <severity value="MAJOR"/> + </annotations> + <before> + <magentoCLI command="config:set catalog/seo/product_use_categories 1" stepKey="setEnableUseCategoriesPath"/> + <createData entity="SubCategory" stepKey="rootCategory"/> + <createData entity="SimpleSubCategoryDifferentUrlStore" stepKey="subCategory"> + <requiredEntity createDataKey="rootCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="subCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="CreateStoreViewActionGroup" stepKey="createCustomStoreViewFr"> + <argument name="storeView" value="customStoreFR"/> + </actionGroup> + + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBefore"> + <argument name="tags" value=""/> + </actionGroup> + </before> + <after> + <magentoCLI command="config:set catalog/seo/product_use_categories 0" stepKey="setEnableUseCategoriesPath"/> + <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> + <deleteData stepKey="deleteSubCategory" createDataKey="subCategory"/> + <deleteData stepKey="deleteRootCategory" createDataKey="rootCategory"/> + + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreView"> + <argument name="customStore" value="customStoreFR"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="navigateToCreatedSubCategory"> + <argument name="Category" value="$$subCategory$$"/> + </actionGroup> + <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="AdminSwitchCustomStoreViewForSubCategory"> + <argument name="storeView" value="customStoreFR.name"/> + </actionGroup> + <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyForSubCategoryCustomStore"> + <argument name="value" value="{{SimpleSubCategoryDifferentUrlStore.url_key_custom_store}}"/> + </actionGroup> + + <actionGroup ref="StorefrontGoToSubCategoryPageActionGroup" stepKey="goToCategoryC"> + <argument name="categoryName" value="$$rootCategory.name$$"/> + <argument name="subCategoryName" value="$$subCategory.name$$"/> + </actionGroup> + + <click selector="{{StorefrontCategoryProductSection.ProductInfoByName($$createProduct.name$$)}}" stepKey="navigateToCreateProduct"/> + + <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchStore"> + <argument name="storeView" value="customStoreFR" /> + </actionGroup> + + <grabFromCurrentUrl stepKey="grabUrl"/> + <assertStringContainsString stepKey="assertUrl"> + <expectedResult type="string">{{SimpleSubCategoryDifferentUrlStore.url_key_custom_store}}</expectedResult> + <actualResult type="string">{$grabUrl}</actualResult> + </assertStringContainsString> + </test> +</tests> From 27f2d9d6dbc8b256d590a5557e0537c6b642fd3a Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Wed, 16 Sep 2020 11:18:50 +0300 Subject: [PATCH 0507/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Catalog/Model/Product/Option/Value.php | 21 ++++- .../CalculateCustomOptionCatalogRule.php | 94 +++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 313513a9151dc..6eeaa44cda706 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -10,6 +10,8 @@ use Magento\Catalog\Model\Product\Option; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Pricing\Price\BasePrice; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use Magento\Framework\App\ObjectManager; use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; use Magento\Catalog\Pricing\Price\RegularPrice; @@ -69,6 +71,11 @@ 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 @@ -77,6 +84,7 @@ 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, @@ -85,11 +93,14 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - CustomOptionPriceCalculator $customOptionPriceCalculator = null + CustomOptionPriceCalculator $customOptionPriceCalculator = null, + CalculateCustomOptionCatalogRule $CalculateCustomOptionCatalogRule = null ) { $this->_valueCollectionFactory = $valueCollectionFactory; $this->customOptionPriceCalculator = $customOptionPriceCalculator - ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); + ?? ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); + $this->calculateCustomOptionCatalogRule = $CalculateCustomOptionCatalogRule + ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); parent::__construct( $context, @@ -253,7 +264,11 @@ public function saveValues() public function getPrice($flag = false) { if ($flag) { - return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE); + return $this->calculateCustomOptionCatalogRule->execute( + $this->getProduct(), + (float)$this->getData(self::KEY_PRICE), + $this->getPriceType() === self::TYPE_PERCENT + ); } return $this->_getData(self::KEY_PRICE); } diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php new file mode 100644 index 0000000000000..eb3ae760cbfed --- /dev/null +++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php @@ -0,0 +1,94 @@ +<?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\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 + ); + + // Apply catalog price rules to product options only if catalog price rules are applied to product. + if ($catalogRulePrice < $regularPrice) { + $optionPrice = $this->getOptionPriceWithoutPriceRule($optionPriceValue, $isPercent, $regularPrice); + $totalCatalogRulePrice = $this->priceModifier->modifyPrice( + $regularPrice + $optionPrice, + $product + ); + $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; + } else { + $finalOptionPrice = $this->getOptionPriceWithoutPriceRule( + $optionPriceValue, + $isPercent, + $regularPrice + ); + } + + return $this->priceCurrency->convertAndRound($finalOptionPrice); + } + + + /** + * 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; + } +} From b29ca8e60d280a0a94a4cad0cc53cf83429c76db Mon Sep 17 00:00:00 2001 From: Viktor Sevch <viktor.sevch@transoftgroup.com> Date: Wed, 16 Sep 2020 17:39:37 +0300 Subject: [PATCH 0508/1013] MC-23536: CatalogProductListWidgetOrderTest is flaky and fails randomly --- .../CatalogProductListWidgetOrderTest.xml | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml index fd87d58e47125..3b51428739117 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml @@ -18,9 +18,6 @@ <testCaseId value="MC-13794"/> <group value="CatalogWidget"/> <group value="WYSIWYGDisabled"/> - <skip> - <issueId value="MC-13923"/> - </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> @@ -40,49 +37,56 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> </before> + <after> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> <!--Open created cms page--> <comment userInput="Open created cms page" stepKey="commentOpenCreatedCmsPage"/> <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage1"> - <argument name="CMSPage" value="$$createPreReqPage$$"/> + <argument name="CMSPage" value="$createPreReqPage$"/> </actionGroup> <!--Add widget to cms page--> <comment userInput="Add widget to cms page" stepKey="commentAddWidgetToCmsPage"/> + <waitForElementVisible selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="waitInsertWidgetIconVisible"/> <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> <waitForPageLoad stepKey="waitForPageLoad1" /> + <waitForElementVisible selector="{{WidgetSection.WidgetType}}" stepKey="waitForWidgetTypeSelectorVisible"/> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear1" /> + <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParamBtnVisible"/> <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Category" stepKey="selectCategoryCondition" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear2" /> + <waitForElementVisible selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParamVisible"/> <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> - <click selector="{{WidgetSection.PreCreateCategory('$$simplecategory.name$$')}}" stepKey="selectCategory" /> + <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="waitForCategoryVisible" /> + <click selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="selectCategory" /> <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> <waitForPageLoad stepKey="waitForPageLoad2" /> <!--Save cms page and go to Storefront--> <comment userInput="Save cms page and go to Storefront" stepKey="commentSaveCmsPageAndGoToStorefront"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForExpandButtonMenuVisible"/> <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> <see userInput="You saved the page." stepKey="seeSuccessMessage"/> - <amOnPage url="$$createPreReqPage.identifier$$" stepKey="amOnPageTestPage"/> + <amOnPage url="$createPreReqPage.identifier$" stepKey="amOnPageTestPage"/> <waitForPageLoad stepKey="waitForPageLoad3" /> <!--Check order of products: recently added first--> <comment userInput="Check order of products: recently added first" stepKey="commentCheckOrderOfProductsRecentlyAddedFirst"/> - <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$$createThirdProduct.name$$')}}" stepKey="seeElementByName1"/> - <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$$createSecondProduct.name$$')}}" stepKey="seeElementByName2"/> - <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$$createFirstProduct.name$$')}}" stepKey="seeElementByName3"/> - <after> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> - <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> - <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> - <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> - <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> - <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> + <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="waitForThirdProductVisible"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="seeElementByName1"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="seeElementByName2"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="seeElementByName3"/> </test> </tests> From d1c39819eb17900e4965d7ebf2c14dbff52d1ff7 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 16 Sep 2020 14:24:47 -0500 Subject: [PATCH 0509/1013] MC-36785: Unable to set YouTube API key by CLI --- .../App/Config/Source/ModularConfigSource.php | 22 +++++------------ .../Config/Source/ModularConfigSourceTest.php | 24 ++++++++++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php b/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php index c047fb61e828c..17ea535951bd6 100644 --- a/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php @@ -8,6 +8,7 @@ use Magento\Config\Model\Config\Structure\Reader as ConfigStructureReader; use Magento\Framework\App\Area; use Magento\Framework\App\Config\ConfigSourceInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; use Magento\Framework\App\Config\Initial\Reader as InitialConfigReader; @@ -31,14 +32,15 @@ class ModularConfigSource implements ConfigSourceInterface /** * @param InitialConfigReader $initialConfigReader - * @param ConfigStructureReader $configStructureReader + * @param ConfigStructureReader|null $configStructureReader */ public function __construct( InitialConfigReader $initialConfigReader, - ConfigStructureReader $configStructureReader + ?ConfigStructureReader $configStructureReader = null ) { $this->initialConfigReader = $initialConfigReader; - $this->configStructureReader = $configStructureReader; + $this->configStructureReader = $configStructureReader + ?? ObjectManager::getInstance()->get(ConfigStructureReader::class); } /** @@ -63,18 +65,6 @@ public function get($path = '') return $data->getData('data' . $path) ?: []; } - private function addPathKey(array $data, string $path): array - { - if (strpos($path, '/') !== false) { - list ($key, $subPath) = explode('/', $path, 2); - $data[$key] = $this->addPathKey($data[$key] ?? [], $subPath); - } else { - $data += [$path => null]; - } - - return $data; - } - /** * Merge initial config with config structure * @@ -84,7 +74,7 @@ private function addPathKey(array $data, string $path): array */ private function merge(array $config, array $sections): array { - foreach ($sections as $key => $section) { + foreach ($sections as $section) { if (isset($section['children'])) { $config[$section['id']] = $this->merge( $config[$section['id']] ?? [], diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php index d9ff44c2e05d0..6cfce1959d0b1 100644 --- a/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php +++ b/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php @@ -8,7 +8,8 @@ namespace Magento\Config\Test\Unit\App\Config\Source; use Magento\Config\App\Config\Source\ModularConfigSource; -use Magento\Framework\App\Config\Initial\Reader; +use Magento\Config\Model\Config\Structure\Reader as ConfigStructureReader; +use Magento\Framework\App\Config\Initial\Reader as InitialConfigReader; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -18,9 +19,14 @@ class ModularConfigSourceTest extends TestCase { /** - * @var Reader|MockObject + * @var InitialConfigReader|MockObject */ - private $reader; + private $initialConfigReader; + + /** + * @var ConfigStructureReader|MockObject + */ + private $configStructureReader; /** * @var ModularConfigSource @@ -29,15 +35,17 @@ class ModularConfigSourceTest extends TestCase protected function setUp(): void { - $this->reader = $this->getMockBuilder(Reader::class) - ->disableOriginalConstructor() - ->getMock(); - $this->source = new ModularConfigSource($this->reader); + $this->initialConfigReader = $this->createMock(InitialConfigReader::class); + $this->configStructureReader = $this->createMock(ConfigStructureReader::class); + $this->source = new ModularConfigSource( + $this->initialConfigReader, + $this->configStructureReader + ); } public function testGet() { - $this->reader->expects($this->once()) + $this->initialConfigReader->expects($this->once()) ->method('read') ->willReturn(['data' => ['path' => 'value']]); $this->assertEquals('value', $this->source->get('path')); From 10bcde3d6a9f664670e2e944af0b447203e71c8c Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Wed, 16 Sep 2020 14:53:38 -0500 Subject: [PATCH 0510/1013] MC-34385: Filter fields allowing HTML --- .../Magento/Cms/Model/Wysiwyg/Validator.php | 36 +++++++++++++++---- .../Test/Unit/Model/Wysiwyg/ValidatorTest.php | 13 +++++-- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Validator.php b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php index eb17a0f3127ea..39360e6350967 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Validator.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Validator.php @@ -10,9 +10,11 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\Message\MessageInterface; use Magento\Framework\Validation\ValidationException; use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; use Psr\Log\LoggerInterface; +use Magento\Framework\Message\Factory as MessageFactory; /** * Processes backend validator results. @@ -41,22 +43,30 @@ class Validator implements WYSIWYGValidatorInterface */ private $logger; + /** + * @var MessageFactory + */ + private $messageFactory; + /** * @param WYSIWYGValidatorInterface $validator * @param ManagerInterface $messages * @param ScopeConfigInterface $config * @param LoggerInterface $logger + * @param MessageFactory $messageFactory */ public function __construct( WYSIWYGValidatorInterface $validator, ManagerInterface $messages, ScopeConfigInterface $config, - LoggerInterface $logger + LoggerInterface $logger, + MessageFactory $messageFactory ) { $this->validator = $validator; $this->messages = $messages; $this->config = $config; $this->logger = $logger; + $this->messageFactory = $messageFactory; } /** @@ -71,18 +81,30 @@ public function validate(string $content): void if ($throwException) { throw $exception; } else { - $this->messages->addWarningMessage( - __( - 'Temporarily allowed to save HTML value that contains restricted elements. %1', - $exception->getMessage() - ) + $this->messages->addUniqueMessages( + [ + $this->messageFactory->create( + MessageInterface::TYPE_WARNING, + (string)__( + 'Temporarily allowed to save HTML value that contains restricted elements. %1', + $exception->getMessage() + ) + ) + ] ); } } catch (\Throwable $exception) { if ($throwException) { throw $exception; } else { - $this->messages->addWarningMessage(__('Invalid HTML provided')->render()); + $this->messages->addUniqueMessages( + [ + $this->messageFactory->create( + MessageInterface::TYPE_WARNING, + (string)__('Invalid HTML provided') + ) + ] + ); $this->logger->error($exception); } } diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php index b14ad81aa2c1a..8e2fa44a24545 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ValidatorTest.php @@ -11,10 +11,12 @@ use Magento\Cms\Model\Wysiwyg\Validator; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Message\ManagerInterface; +use Magento\Framework\Message\MessageInterface; use Magento\Framework\Validation\ValidationException; use Magento\Framework\Validator\HTML\WYSIWYGValidatorInterface; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use Magento\Framework\Message\Factory as MessageFactory; class ValidatorTest extends TestCase { @@ -45,6 +47,13 @@ public function testValidate(bool $isFlagSet, ?\Throwable $thrown, bool $excepti { $actuallyWarned = false; + $messageFactoryMock = $this->createMock(MessageFactory::class); + $messageFactoryMock->method('create') + ->willReturnCallback( + function () { + return $this->getMockForAbstractClass(MessageInterface::class); + } + ); $configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $configMock->method('isSetFlag') ->with(Validator::CONFIG_PATH_THROW_EXCEPTION) @@ -56,7 +65,7 @@ public function testValidate(bool $isFlagSet, ?\Throwable $thrown, bool $excepti } $messagesMock = $this->getMockForAbstractClass(ManagerInterface::class); - $messagesMock->method('addWarningMessage') + $messagesMock->method('addUniqueMessages') ->willReturnCallback( function () use (&$actuallyWarned): void { $actuallyWarned = true; @@ -65,7 +74,7 @@ function () use (&$actuallyWarned): void { $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $validator = new Validator($backendMock, $messagesMock, $configMock, $loggerMock); + $validator = new Validator($backendMock, $messagesMock, $configMock, $loggerMock, $messageFactoryMock); try { $validator->validate('content'); $actuallyThrown = false; From e8f7b175543c698959ae6e645b4277c7768135c6 Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Wed, 16 Sep 2020 23:21:19 +0300 Subject: [PATCH 0511/1013] add AdminOpenCustomersGridActionGroup --- .../Test/CheckTierPricingOfProductsTest.xml | 3 +-- .../AdminOpenCustomersGridActionGroup.xml | 19 +++++++++++++++++++ ...aultBillingShippingCustomerAddressTest.xml | 2 +- ...hangeCustomerGenderInCustomersGridTest.xml | 3 +-- .../Mftf/Test/AdminCreateCustomerTest.xml | 3 +-- ...stomerOnStorefrontSignupNewsletterTest.xml | 3 +-- ...DeleteCustomerAddressesFromTheGridTest.xml | 2 +- ...AddressesFromTheGridViaMassActionsTest.xml | 2 +- ...eleteDefaultBillingCustomerAddressTest.xml | 2 +- ...aultBillingShippingCustomerAddressTest.xml | 2 +- ...dminExactMatchSearchInCustomerGridTest.xml | 4 ++-- ...dminSearchCustomerAddressByKeywordTest.xml | 2 +- ...inSetCustomerDefaultBillingAddressTest.xml | 2 +- ...nSetCustomerDefaultShippingAddressTest.xml | 2 +- ...ustomerInfoFromDefaultToNonDefaultTest.xml | 8 ++------ ...tomerAddressStateContainValuesOnceTest.xml | 4 +--- ...CountriesRestrictionApplyOnBackendTest.xml | 3 +-- .../Test/SearchByEmailInCustomerGridTest.xml | 4 ++-- .../CreateOrderFromEditCustomerPageTest.xml | 2 +- ...rableProductsInComparedOnOrderPageTest.xml | 4 +--- ...impleProductsInComparedOnOrderPageTest.xml | 4 +--- .../AdminNavigateWhileUserExpiredTest.xml | 2 +- 22 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomersGridActionGroup.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index 55d697e35deba..8436c94acc685 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -127,8 +127,7 @@ <waitForPageLoad stepKey="waitForCustomersPage"/> <see userInput="You saved the customer." stepKey="CustomerIsSaved"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForPageLoad1" /> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomers"/> <click selector="{{AdminCustomerFiltersSection.clearAll}}" stepKey="ClearFilters"/> <waitForPageLoad stepKey="waitForFiltersClear"/> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomersGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomersGridActionGroup.xml new file mode 100644 index 0000000000000..c1dedfefda309 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminOpenCustomersGridActionGroup.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="AdminOpenCustomersGridActionGroup"> + <annotations> + <description>Open the Admin Customers grid page.</description> + </annotations> + + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml index b061b6a256471..2220f69700265 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminAddNewDefaultBillingShippingCustomerAddressTest.xml @@ -30,7 +30,7 @@ Step2. On *Customers* page choose customer from preconditions and open it to edit Step3. Open *Addresses* tab on edit customer page and press *Add New Address* button <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml index 423954a7d9bf7..d1934a82bea0e 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeCustomerGenderInCustomersGridTest.xml @@ -26,8 +26,7 @@ <after> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <!-- Reset customer grid filter --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersGridPage"/> - <waitForPageLoad stepKey="waitForCustomersGrid"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="goToCustomersGridPage"/> <actionGroup ref="AdminResetFilterInCustomerGrid" stepKey="resetFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index cb003ed837294..d12a89f01cb96 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -29,8 +29,7 @@ </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForLoad1"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomers"/> <click selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}" stepKey="clickCreateCustomer"/> <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/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml index 683b275ca1ed6..44eab9d0c19ae 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerOnStorefrontSignupNewsletterTest.xml @@ -35,8 +35,7 @@ </actionGroup> <!--Assert verify created new customer in grid--> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> - <waitForPageLoad stepKey="waitForNavigateToCustomersPageLoad"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomers"/> <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="clickFilterButton"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="clickApplyFilter"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml index 615a6ebcf24cc..62dcd6fc4d894 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml @@ -34,7 +34,7 @@ Step2. On *Customers* page choose customer from preconditions and open it to edit Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml index 57446a1ee0c72..c6e72901b062c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml @@ -34,7 +34,7 @@ Step2. On *Customers* page choose customer from preconditions and open it to edit Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml index f08ea83a70da6..52c8029b8f778 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml @@ -33,7 +33,7 @@ Step1. Login to admin and go to Customers > All Customers. Step2. On *Customers* page choose customer from preconditions and open it to edit <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml index 6e44fe96b0d7b..72bda91445256 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminEditDefaultBillingShippingCustomerAddressTest.xml @@ -30,7 +30,7 @@ Step2. On *Customers* page choose customer from preconditions and open it to edit Step3. Open *Addresses* tab on edit customer page and press *Add New Address* button <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml index ea4b3645d371f..14d569ed9101d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminExactMatchSearchInCustomerGridTest.xml @@ -28,12 +28,12 @@ <after> <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="AdminResetFilterInCustomerAddressGrid" stepKey="clearCustomerGridFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Step 1: Go to Customers > All Customers--> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <!--Step 2: On Customers grid page search customer by keyword with quotes--> <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchCustomer"> <argument name="keyword" value="$$createSecondCustomer.firstname$$"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml index b13a06b9ef858..bac1c665cbe78 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml @@ -34,7 +34,7 @@ Step2. On *Customers* page choose customer from preconditions and open it to edit Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml index 5ce96a8dcab3c..65dcf572f19fb 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultBillingAddressTest.xml @@ -30,7 +30,7 @@ Step2. On *Customers* page choose customer from preconditions and open it to edit Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml index a9832c86562f1..bf76e29b185ba 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSetCustomerDefaultShippingAddressTest.xml @@ -30,7 +30,7 @@ Step2. On *Customers* page choose customer from preconditions and open it to edit Step3. On edit customer page open *Addresses* tab and find a grid with the additional addresses <!- --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="Simple_US_Customer_Multiple_Addresses_No_Default_Address"/> </actionGroup> 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 8af07bc2c2d53..fb6793b1751a6 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml @@ -27,9 +27,7 @@ </before> <after> <deleteData stepKey="deleteCustomer" createDataKey="customer"/> - <!-- Reset customer grid filter --> - <amOnPage stepKey="goToCustomersGridPage" url="{{AdminCustomerPage.url}}"/> - <waitForPageLoad stepKey="waitForCustomersGrid"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="goToCustomersGridPage"/> <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -47,9 +45,7 @@ <argument name="customerAddress" value="CustomerAddressSimple"/> </actionGroup> <actionGroup stepKey="saveAndCheckSuccessMessage" ref="AdminSaveCustomerAndAssertSuccessMessage"/> - <!-- Assert Customer in Customer grid --> - <amOnPage stepKey="goToCustomersGridPage" url="{{AdminCustomerPage.url}}"/> - <waitForPageLoad stepKey="waitForCustomersGrid"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="goToCustomersGridPage"/> <actionGroup stepKey="resetFilter" ref="AdminResetFilterInCustomerGrid"/> <actionGroup stepKey="filterByEamil" ref="AdminFilterCustomerGridByEmail"> <argument name="email" value="updated$$customer.email$$"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml index 2aa85f8c966a9..785ee1a02abe1 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCustomerAddressStateContainValuesOnceTest.xml @@ -31,9 +31,7 @@ <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - - <!-- Go to Customers > All Customers.--> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <!--Select created customer, Click Edit mode--> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPageWithAddresses"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml index 781d721fd5132..6a157c6312530 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -116,8 +116,7 @@ <waitForPageLoad stepKey="waitForCustomersPage"/> <!--Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount--> <comment userInput="Go to Customers grid and check that filter countries amount is the same as initial allowed countries amount" stepKey="compareCountriesAmount"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="goToCustomersGrid"/> - <waitForPageLoad stepKey="waitForCustomersGrid"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="goToCustomersGrid"/> <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openFiltersSectionOnCustomersGrid"/> <executeJS function="var len = document.querySelectorAll('{{AdminCustomerFiltersSection.countryOptions}}').length; return len-1;" stepKey="countriesAmount2"/> <assertEquals stepKey="assertCountryAmounts"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/SearchByEmailInCustomerGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/SearchByEmailInCustomerGridTest.xml index d4351c8bcdc84..b2a78686cdad5 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/SearchByEmailInCustomerGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/SearchByEmailInCustomerGridTest.xml @@ -26,12 +26,12 @@ <after> <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="AdminResetFilterInCustomerAddressGrid" stepKey="clearCustomerGridFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Step 1: Go to Customers > All Customers--> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <!--Step 2: On Customers grid page search customer by keyword--> <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchCustomer"> <argument name="keyword" value="$$createSecondCustomer.email$$"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index ca705405809bd..77b119dd583de 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -91,7 +91,7 @@ <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigurableProduct"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerIndexPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomerIndexPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearCustomerGridFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml index c3058ca6ede87..bace51cea17d5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveConfigurableProductsInComparedOnOrderPageTest.xml @@ -132,9 +132,7 @@ <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!-- Open Customers -> All Customers --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> - <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml index 176fb05bc74b3..00e401941036e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveSimpleProductsInComparedOnOrderPageTest.xml @@ -70,9 +70,7 @@ <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!-- Open Customers -> All Customers --> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomersGridPage"/> - <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomersGridPage"/> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="openEditCustomerPage"> <argument name="customer" value="$$createCustomer$$"/> </actionGroup> diff --git a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml index c1a951afd87ec..dc88ad9d2cbf1 100644 --- a/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml +++ b/app/code/Magento/Security/Test/Mftf/Test/AdminNavigateWhileUserExpiredTest.xml @@ -46,7 +46,7 @@ </actionGroup> <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> <wait time="120" stepKey="waitForUserToExpire"/> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomers"/> <!-- Confirm that user is logged out --> <seeInCurrentUrl url="{{AdminLoginPage.url}}" stepKey="seeAdminLoginUrl"/> From 902fcdd03159d65c51f52576c828468285d58611 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 17 Sep 2020 10:36:31 +0300 Subject: [PATCH 0512/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Unit/Model/Product/Option/ValueTest.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) 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 e46884d1637da..28cb53bd3cb2f 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,12 +12,12 @@ 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\CustomOptionPriceCalculator; - use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\PriceInfoInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use PHPUnit\Framework\MockObject\MockObject; /** * Test for \Magento\Catalog\Model\Product\Option\Value class. @@ -30,17 +30,18 @@ class ValueTest extends TestCase private $model; /** - * @var CustomOptionPriceCalculator + * @var CalculateCustomOptionCatalogRule|MockObject */ - private $customOptionPriceCalculatorMock; + private $CalculateCustomOptionCatalogRule; protected function setUp(): void { $mockedResource = $this->getMockedResource(); $mockedCollectionFactory = $this->getMockedValueCollectionFactory(); - $this->customOptionPriceCalculatorMock = $this->createMock( - CustomOptionPriceCalculator::class + + $this->CalculateCustomOptionCatalogRule = $this->createMock( + CalculateCustomOptionCatalogRule::class ); $helper = new ObjectManager($this); @@ -49,7 +50,7 @@ protected function setUp(): void [ 'resource' => $mockedResource, 'valueCollectionFactory' => $mockedCollectionFactory, - 'customOptionPriceCalculator' => $this->customOptionPriceCalculatorMock, + 'CalculateCustomOptionCatalogRule' => $this->CalculateCustomOptionCatalogRule ] ); $this->model->setOption($this->getMockedOption()); @@ -77,8 +78,8 @@ public function testGetPrice() $this->assertEquals($price, $this->model->getPrice(false)); $percentPrice = 100.0; - $this->customOptionPriceCalculatorMock->expects($this->atLeastOnce()) - ->method('getOptionPriceByPriceCode') + $this->CalculateCustomOptionCatalogRule->expects($this->atLeastOnce()) + ->method('execute') ->willReturn($percentPrice); $this->assertEquals($percentPrice, $this->model->getPrice(true)); } From 3db593beb44cdd8823f280e628c42d1c41d25cec Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 17 Sep 2020 11:55:31 +0300 Subject: [PATCH 0513/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Price/CalculateCustomOptionCatalogRule.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php index eb3ae760cbfed..5e5e1b08cf721 100644 --- a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php +++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php @@ -54,19 +54,12 @@ public function execute( $regularPrice = (float)$product->getPriceInfo() ->getPrice(RegularPrice::PRICE_CODE) ->getValue(); - $catalogRulePrice = $this->priceModifier->modifyPrice( - $regularPrice, - $product - ); - + $catalogRulePrice = $product->getPriceInfo()->getPrice('final_price')->getValue(); // Apply catalog price rules to product options only if catalog price rules are applied to product. if ($catalogRulePrice < $regularPrice) { $optionPrice = $this->getOptionPriceWithoutPriceRule($optionPriceValue, $isPercent, $regularPrice); - $totalCatalogRulePrice = $this->priceModifier->modifyPrice( - $regularPrice + $optionPrice, - $product - ); - $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; + $discount = $catalogRulePrice / $regularPrice; + $finalOptionPrice = $optionPrice*$discount; } else { $finalOptionPrice = $this->getOptionPriceWithoutPriceRule( $optionPriceValue, @@ -78,7 +71,6 @@ public function execute( return $this->priceCurrency->convertAndRound($finalOptionPrice); } - /** * Calculate option price without catalog price rule discount. * From 075ddb4b76367df4e123ab71e2d1fb37e821160e Mon Sep 17 00:00:00 2001 From: Ihor Sviziev <svizev.igor@gmail.com> Date: Mon, 14 Sep 2020 09:59:35 +0300 Subject: [PATCH 0514/1013] Fix URL generation for new store Emulate adminhtml area where url rewrites are created --- .../Command/App/ConfigImportCommand.php | 79 ++++++++++++++++--- .../Command/App/ConfigImportCommandTest.php | 76 +++++++++++++++++- app/code/Magento/Deploy/etc/di.xml | 6 ++ 3 files changed, 148 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php index 8a75ad0def222..eb87a9c12125b 100644 --- a/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php +++ b/app/code/Magento/Deploy/Console/Command/App/ConfigImportCommand.php @@ -3,14 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Deploy\Console\Command\App; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; +use Magento\Deploy\Console\Command\App\ConfigImport\Processor; +use Magento\Framework\App\Area; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Magento\Framework\Console\Cli; -use Magento\Deploy\Console\Command\App\ConfigImport\Processor; /** * Runs the process of importing configuration data from shared source to appropriate application sources @@ -21,9 +28,6 @@ */ class ConfigImportCommand extends Command { - /** - * Command name. - */ const COMMAND_NAME = 'app:config:import'; /** @@ -33,12 +37,40 @@ class ConfigImportCommand extends Command */ private $processor; + /** + * @var EmulatedAdminhtmlAreaProcessor + */ + private $adminhtmlAreaProcessor; + + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var AreaList + */ + private $areaList; + /** * @param Processor $processor the configuration importer + * @param DeploymentConfig|null $deploymentConfig + * @param EmulatedAdminhtmlAreaProcessor|null $adminhtmlAreaProcessor + * @param AreaList|null $areaList */ - public function __construct(Processor $processor) - { + public function __construct( + Processor $processor, + DeploymentConfig $deploymentConfig = null, + EmulatedAdminhtmlAreaProcessor $adminhtmlAreaProcessor = null, + AreaList $areaList = null + ) { $this->processor = $processor; + $this->deploymentConfig = $deploymentConfig + ?? ObjectManager::getInstance()->get(DeploymentConfig::class); + $this->adminhtmlAreaProcessor = $adminhtmlAreaProcessor + ?? ObjectManager::getInstance()->get(EmulatedAdminhtmlAreaProcessor::class); + $this->areaList = $areaList + ?? ObjectManager::getInstance()->get(AreaList::class); parent::__construct(); } @@ -55,12 +87,26 @@ protected function configure() } /** - * Imports data from deployment configuration files to the DB. {@inheritdoc} + * Imports data from deployment configuration files to the DB. + * {@inheritdoc} + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output) { try { - $this->processor->execute($input, $output); + if ($this->canEmulateAdminhtmlArea()) { + // Emulate adminhtml area in order to execute all needed plugins declared only for this area + // For instance URL rewrite generation during creating store view + $this->adminhtmlAreaProcessor->process(function () use ($input, $output) { + $this->processor->execute($input, $output); + }); + } else { + $this->processor->execute($input, $output); + } } catch (RuntimeException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); @@ -69,4 +115,19 @@ protected function execute(InputInterface $input, OutputInterface $output) return Cli::RETURN_SUCCESS; } + + /** + * Detects if we can emulate adminhtml area + * + * This area could be not available for instance during setup:install + * + * @return bool + * @throws RuntimeException + * @throws FileSystemException + */ + private function canEmulateAdminhtmlArea(): bool + { + return $this->deploymentConfig->isAvailable() + && in_array(Area::AREA_ADMINHTML, $this->areaList->getCodes()); + } } diff --git a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php index da790a19f480a..32bdd63ef4638 100644 --- a/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php +++ b/app/code/Magento/Deploy/Test/Unit/Console/Command/App/ConfigImportCommandTest.php @@ -7,8 +7,11 @@ namespace Magento\Deploy\Test\Unit\Console\Command\App; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; use Magento\Deploy\Console\Command\App\ConfigImport\Processor; use Magento\Deploy\Console\Command\App\ConfigImportCommand; +use Magento\Framework\App\AreaList; +use Magento\Framework\App\DeploymentConfig; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\RuntimeException; use PHPUnit\Framework\MockObject\MockObject; @@ -27,16 +30,37 @@ class ConfigImportCommandTest extends TestCase */ private $commandTester; + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfigMock; + + /** + * @var EmulatedAdminhtmlAreaProcessor|MockObject + */ + private $adminhtmlAreaProcessorMock; + + /** + * @var AreaList|MockObject + */ + private $areaListMock; + /** * @return void */ protected function setUp(): void { - $this->processorMock = $this->getMockBuilder(Processor::class) - ->disableOriginalConstructor() - ->getMock(); + $this->processorMock = $this->createMock(Processor::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->adminhtmlAreaProcessorMock = $this->createMock(EmulatedAdminhtmlAreaProcessor::class); + $this->areaListMock = $this->createMock(AreaList::class); - $configImportCommand = new ConfigImportCommand($this->processorMock); + $configImportCommand = new ConfigImportCommand( + $this->processorMock, + $this->deploymentConfigMock, + $this->adminhtmlAreaProcessorMock, + $this->areaListMock + ); $this->commandTester = new CommandTester($configImportCommand); } @@ -46,6 +70,13 @@ protected function setUp(): void */ public function testExecute() { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->once()) + ->method('process')->willReturnCallback(function (callable $callback, array $params = []) { + return $callback(...$params); + }); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn(['adminhtml']); + $this->processorMock->expects($this->once()) ->method('execute'); @@ -57,6 +88,13 @@ public function testExecute() */ public function testExecuteWithException() { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->once()) + ->method('process')->willReturnCallback(function (callable $callback, array $params = []) { + return $callback(...$params); + }); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn(['adminhtml']); + $this->processorMock->expects($this->once()) ->method('execute') ->willThrowException(new RuntimeException(__('Some error'))); @@ -64,4 +102,34 @@ public function testExecuteWithException() $this->assertSame(Cli::RETURN_FAILURE, $this->commandTester->execute([])); $this->assertStringContainsString('Some error', $this->commandTester->getDisplay()); } + + /** + * @return void + */ + public function testExecuteWithDeploymentConfigNotAvailable() + { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(false); + $this->adminhtmlAreaProcessorMock->expects($this->never())->method('process'); + $this->areaListMock->expects($this->never())->method('getCodes'); + + $this->processorMock->expects($this->once()) + ->method('execute'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute([])); + } + + /** + * @return void + */ + public function testExecuteWithMissingAdminhtmlLocale() + { + $this->deploymentConfigMock->expects($this->once())->method('isAvailable')->willReturn(true); + $this->adminhtmlAreaProcessorMock->expects($this->never())->method('process'); + $this->areaListMock->expects($this->once())->method('getCodes')->willReturn([]); + + $this->processorMock->expects($this->once()) + ->method('execute'); + + $this->assertSame(Cli::RETURN_SUCCESS, $this->commandTester->execute([])); + } } diff --git a/app/code/Magento/Deploy/etc/di.xml b/app/code/Magento/Deploy/etc/di.xml index 0c32baebf12df..d40ed3144e7e6 100644 --- a/app/code/Magento/Deploy/etc/di.xml +++ b/app/code/Magento/Deploy/etc/di.xml @@ -35,6 +35,12 @@ </argument> </arguments> </type> + <type name="Magento\Deploy\Console\Command\App\ConfigImportCommand"> + <arguments> + <argument name="adminhtmlAreaProcessor" xsi:type="object">Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor\Proxy</argument> + <argument name="areaList" xsi:type="object">Magento\Framework\App\AreaList\Proxy</argument> + </arguments> + </type> <type name="Magento\Deploy\Model\Filesystem"> <arguments> <argument name="shell" xsi:type="object">Magento\Framework\App\Shell</argument> From f85ba48186c4cc54a8ed599d212a1c948b18c5fc Mon Sep 17 00:00:00 2001 From: Viktor Sevch <viktor.sevch@transoftgroup.com> Date: Thu, 17 Sep 2020 12:38:18 +0300 Subject: [PATCH 0515/1013] MC-23536: CatalogProductListWidgetOrderTest is flaky and fails randomly --- .../Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml index 3b51428739117..f73939948c8ec 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml @@ -86,7 +86,9 @@ <comment userInput="Check order of products: recently added first" stepKey="commentCheckOrderOfProductsRecentlyAddedFirst"/> <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="waitForThirdProductVisible"/> <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="seeElementByName1"/> + <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="waitForSecondProductVisible"/> <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="seeElementByName2"/> + <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="waitForFirstProductVisible"/> <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="seeElementByName3"/> </test> </tests> From 9a28b746c36caa53d8c16e4603cffab55d789926 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 17 Sep 2020 16:21:38 +0300 Subject: [PATCH 0516/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Product/View/Options/AbstractOptions.php | 26 +++- .../Magento/Catalog/Model/Product/Option.php | 125 ++++++------------ .../Model/Product/Option/Type/DefaultType.php | 19 ++- .../Model/Product/Option/Type/Select.php | 30 +++-- .../Test/Mftf/Data/ProductOptionValueData.xml | 6 + .../Unit/Model/Product/Option/ValueTest.php | 1 - ...RuleForSimpleProductAndFixedMethodTest.xml | 7 +- ...SimpleProductWithSelectFixedMethodTest.xml | 110 +++++++++++++++ 8 files changed, 226 insertions(+), 98 deletions(-) create mode 100644 app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml 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 1dcbf60db15c3..310158ed99948 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -12,7 +12,10 @@ 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. @@ -47,20 +50,29 @@ 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 = [] + array $data = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->pricingHelper = $pricingHelper; $this->_catalogHelper = $catalogData; + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule + ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); parent::__construct($context, $data); } @@ -112,7 +124,6 @@ public function getOption() * Retrieve formatted price * * @return string - * @since 102.0.6 */ public function getFormattedPrice() { @@ -132,7 +143,7 @@ public function getFormattedPrice() * * @return string * - * @deprecated 102.0.6 + * @deprecated * @see getFormattedPrice() */ public function getFormatedPrice() @@ -162,6 +173,15 @@ 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/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index e83982b8ce672..ca5251de13e69 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product; @@ -16,8 +17,10 @@ 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\BasePrice; +use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractExtensibleModel; @@ -123,6 +126,11 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ private $customOptionValuesFactory; + /** + * @var CalculateCustomOptionCatalogRule + */ + private $calculateCustomOptionCatalogRule; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -138,6 +146,7 @@ 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( @@ -154,14 +163,17 @@ public function __construct( array $data = [], ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null, array $optionGroups = [], - array $optionTypesToGroups = [] + array $optionTypesToGroups = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; $this->string = $string; $this->validatorPool = $validatorPool; $this->customOptionValuesFactory = $customOptionValuesFactory ?: - \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? + ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); $this->optionGroups = $optionGroups ?: [ self::OPTION_GROUP_DATE => Date::class, self::OPTION_GROUP_FILE => File::class, @@ -193,10 +205,7 @@ public function __construct( } /** - * Get resource instance - * - * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - * @deprecated 102.0.0 because resource models should be used directly + * @inheritdoc */ protected function _getResource() { @@ -246,7 +255,7 @@ public function getValueById($valueId) * * @param string $type * @return bool - * @since 102.0.0 + * @since 101.1.0 */ public function hasValues($type = null) { @@ -462,10 +471,12 @@ public function afterSave() */ public function getPrice($flag = false) { - 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; + if ($flag) { + return $this->calculateCustomOptionCatalogRule->execute( + $this->getProduct(), + (float)$this->getData(self::KEY_PRICE), + $this->getPriceType() === Value::TYPE_PERCENT + ); } return $this->_getData(self::KEY_PRICE); } @@ -559,9 +570,7 @@ public function getSearchableData($productId, $storeId) } /** - * Clearing object's data - * - * @return $this + * @inheritdoc */ protected function _clearData() { @@ -571,9 +580,7 @@ protected function _clearData() } /** - * Clearing cyclic references - * - * @return $this + * @inheritdoc */ protected function _clearReferences() { @@ -594,9 +601,7 @@ protected function _getValidationRulesBeforeSave() } /** - * Get product SKU - * - * @return string + * @inheritdoc */ public function getProductSku() { @@ -608,9 +613,7 @@ public function getProductSku() } /** - * Get option id - * - * @return int|null + * @inheritdoc * @codeCoverageIgnoreStart */ public function getOptionId() @@ -619,9 +622,7 @@ public function getOptionId() } /** - * Get option title - * - * @return string + * @inheritdoc */ public function getTitle() { @@ -629,9 +630,7 @@ public function getTitle() } /** - * Get option type - * - * @return string + * @inheritdoc */ public function getType() { @@ -639,9 +638,7 @@ public function getType() } /** - * Get sort order - * - * @return int + * @inheritdoc */ public function getSortOrder() { @@ -649,10 +646,7 @@ public function getSortOrder() } /** - * Get is require - * - * @return bool - * @SuppressWarnings(PHPMD.BooleanGetMethodName) + * @inheritdoc */ public function getIsRequire() { @@ -660,9 +654,7 @@ public function getIsRequire() } /** - * Get price type - * - * @return string|null + * @inheritdoc */ public function getPriceType() { @@ -670,9 +662,7 @@ public function getPriceType() } /** - * Get Sku - * - * @return string|null + * @inheritdoc */ public function getSku() { @@ -720,10 +710,7 @@ public function getImageSizeY() } /** - * Set product SKU - * - * @param string $productSku - * @return $this + * @inheritdoc */ public function setProductSku($productSku) { @@ -731,10 +718,7 @@ public function setProductSku($productSku) } /** - * Set option id - * - * @param int $optionId - * @return $this + * @inheritdoc */ public function setOptionId($optionId) { @@ -742,10 +726,7 @@ public function setOptionId($optionId) } /** - * Set option title - * - * @param string $title - * @return $this + * @inheritdoc */ public function setTitle($title) { @@ -753,10 +734,7 @@ public function setTitle($title) } /** - * Set option type - * - * @param string $type - * @return $this + * @inheritdoc */ public function setType($type) { @@ -764,10 +742,7 @@ public function setType($type) } /** - * Set sort order - * - * @param int $sortOrder - * @return $this + * @inheritdoc */ public function setSortOrder($sortOrder) { @@ -775,10 +750,7 @@ public function setSortOrder($sortOrder) } /** - * Set is require - * - * @param bool $isRequired - * @return $this + * @inheritdoc */ public function setIsRequire($isRequired) { @@ -786,10 +758,7 @@ public function setIsRequire($isRequired) } /** - * Set price - * - * @param float $price - * @return $this + * @inheritdoc */ public function setPrice($price) { @@ -797,10 +766,7 @@ public function setPrice($price) } /** - * Set price type - * - * @param string $priceType - * @return $this + * @inheritdoc */ public function setPriceType($priceType) { @@ -808,10 +774,7 @@ public function setPriceType($priceType) } /** - * Set Sku - * - * @param string $sku - * @return $this + * @inheritdoc */ public function setSku($sku) { @@ -952,7 +915,7 @@ public function setExtensionAttributes( private function getOptionRepository() { if (null === $this->optionRepository) { - $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance() + $this->optionRepository = ObjectManager::getInstance() ->get(\Magento\Catalog\Model\Product\Option\Repository::class); } return $this->optionRepository; @@ -966,7 +929,7 @@ private function getOptionRepository() private function getMetadataPool() { if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() + $this->metadataPool = 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 16fdd4cdeeb1c..a6ab77a090b4e 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -13,6 +13,8 @@ use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; 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; /** * Catalog product option default type @@ -60,21 +62,30 @@ 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 = [] + array $data = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->_checkoutSession = $checkoutSession; parent::__construct($data); $this->_scopeConfig = $scopeConfig; + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance() + ->get(CalculateCustomOptionCatalogRule::class); } /** @@ -341,7 +352,11 @@ public function getOptionPrice($optionValue, $basePrice) { $option = $this->getOption(); - return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); + return $this->calculateCustomOptionCatalogRule->execute( + $option->getProduct(), + (float)$option->getPrice(), + $option->getPriceType() === Value::TYPE_PERCENT + ); } /** 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 d2766b1bbb054..17f0fb3b25f99 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -6,6 +6,9 @@ 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; /** @@ -37,6 +40,11 @@ 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 @@ -44,6 +52,7 @@ 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, @@ -51,7 +60,8 @@ public function __construct( \Magento\Framework\Stdlib\StringUtils $string, \Magento\Framework\Escaper $escaper, array $data = [], - array $singleSelectionTypes = [] + array $singleSelectionTypes = [], + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->string = $string; $this->_escaper = $escaper; @@ -61,6 +71,8 @@ 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); } /** @@ -248,10 +260,10 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $result += $this->_getChargeableOptionPrice( - $_result->getPrice(), - $_result->getPriceType() == 'percent', - $basePrice + $result += $this->calculateCustomOptionCatalogRule->execute( + $option->getProduct(), + (float)$_result->getPrice(), + $_result->getPriceType() === Value::TYPE_PERCENT ); } else { if ($this->getListener()) { @@ -263,10 +275,10 @@ public function getOptionPrice($optionValue, $basePrice) } elseif ($this->_isSingleSelection()) { $_result = $option->getValueById($optionValue); if ($_result) { - $result = $this->_getChargeableOptionPrice( - $_result->getPrice(), - $_result->getPriceType() == 'percent', - $basePrice + $result = $this->calculateCustomOptionCatalogRule->execute( + $option->getProduct(), + (float)$_result->getPrice(), + $_result->getPriceType() === Value::TYPE_PERCENT ); } else { if ($this->getListener()) { diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml index e738994357366..04c24ff7e36dd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml @@ -26,6 +26,12 @@ <data key="price">99.99</data> <data key="price_type">fixed</data> </entity> + <entity name="ProductOptionValueRadioButtonsWithDiscountedPrice" type="product_option_value"> + <data key="title">OptionValueRadioButtons1</data> + <data key="sort_order">1</data> + <data key="price">78.33</data> + <data key="price_type">fixed</data> + </entity> <entity name="ProductOptionValueRadioButtons2" type="product_option_value"> <data key="title">OptionValueRadioButtons2</data> <data key="sort_order">2</data> 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 28cb53bd3cb2f..d604e41018c07 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 @@ -39,7 +39,6 @@ protected function setUp(): void $mockedResource = $this->getMockedResource(); $mockedCollectionFactory = $this->getMockedValueCollectionFactory(); - $this->CalculateCustomOptionCatalogRule = $this->createMock( CalculateCustomOptionCatalogRule::class ); diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml index 8103e6b115950..43f8decb874cb 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductAndFixedMethodTest"> + <test name="ApplyCatalogRuleForSimpleProductAndFixedMethodTest" deprecated="Use ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> - <title value="Admin should be able to apply the catalog price rule for simple product with custom options"/> + <title value="DEPRECATED. Admin should be able to apply the catalog price rule for simple product with custom options"/> <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> <severity value="CRITICAL"/> <testCaseId value="MC-14771"/> <group value="CatalogRule"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead</issueId> + </skip> </annotations> <before> <!-- Login as Admin --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml new file mode 100644 index 0000000000000..8b96feee6dbc6 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -0,0 +1,110 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Apply catalog price rule"/> + <title value="Admin should be able to apply the catalog price rule for simple product with custom options"/> + <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14771"/> + <group value="CatalogRule"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create Simple Product --> + <createData entity="_defaultProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + + <!-- Update all products to have custom options --> + <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateProductWithOptions1"/> + <magentoCron stepKey="runCronIndex" groups="index"/> + </before> + <after> + <!-- Delete products and category --> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete the catalog price rule --> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{CatalogRuleByFixed.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!-- 1. Begin creating a new catalog price rule --> + <actionGroup ref="NewCatalogPriceRuleByUIWithConditionIsCategoryActionGroup" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> + <argument name ="categoryId" value="$createCategory.id$"/> + <argument name ="catalogRule" value="CatalogRuleByFixed"/> + </actionGroup> + + <!-- Select not logged in customer group --> + <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> + + <!-- Save and apply the new catalog price rule --> + <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + <actionGroup ref="CreateCatalogRuleStagingUpdateWithItsStartActionGroup" stepKey="fillOutActionGroup"> + <argument name="stagingUpdate" value="_defaultCatalogRule"/> + </actionGroup> + + <!-- Navigate to category on store front --> + <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> + + <!-- Check product 1 name on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> + <argument name="productInfo" value="$createProduct1.name$"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Check product 1 price on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> + <argument name="productInfo" value="$44.48"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Check product 1 regular price on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> + <argument name="productInfo" value="$56.78"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Navigate to product 1 on store front --> + <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> + + <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> + <actionGroup ref="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> + <argument name="customOption" value="ProductOptionRadioButton2"/> + <argument name="customOptionValue" value="ProductOptionValueRadioButtonsWithDiscountedPrice"/> + <argument name="productPrice" value="$156.77"/> + <argument name="productFinalPrice" value="$122.81"/> + </actionGroup> + + <!-- Add product 1 to cart --> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct1ToCart"> + <argument name="productQty" value="1"/> + </actionGroup> + + <!-- Assert sub total on mini shopping cart --> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$122.81"/> + </actionGroup> + </test> +</tests> From b2139152bcdb50a3c4972c9c84f5af8b57b463e5 Mon Sep 17 00:00:00 2001 From: "taras.gamanov" <engcom-vendorworker-hotel@adobe.com> Date: Thu, 17 Sep 2020 18:05:52 +0300 Subject: [PATCH 0517/1013] Integration test has been added --- .../Adminhtml/Order/AddToPackageTest.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php new file mode 100644 index 0000000000000..0455181d42b00 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Shipping\Block\Adminhtml\Order; + +use Magento\Backend\Block\Template; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\Data\ShipmentTrackInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Class verifies packaging popup. + * + * @magentoAppArea adminhtml + */ +class AddToPackageTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + /** + * @var OrderInterfaceFactory|mixed + */ + private $orderFactory; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + } + + /** + * Test that Packaging popup renders + * + * @magentoDataFixture Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php + */ + public function testGetCommentsHtml() + { + /** @var Template $block */ + $block = $this->objectManager->get(Packaging::class); + + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + + /** @var ShipmentTrackInterface $track */ + $shipment = $order->getShipmentsCollection()->getFirstItem(); + + $this->registry->register('current_shipment', $shipment); + + $block->setTemplate('Magento_Shipping::order/packaging/popup.phtml'); + $html = $block->toHtml(); + $expectedNeedle = "packaging.setItemQtyCallback(function(itemId){ + var item = $$('[name=\"shipment[items]['+itemId+']\"]')[0], + itemTitle = $('order_item_' + itemId + '_title'); + if (!itemTitle && !item) { + return 0; + } + if (item && !isNaN(item.value)) { + return item.value; + } + });"; + $this->assertStringContainsString($expectedNeedle, $html); + } +} From fbb40c25f05d6fdfea18087772e381805506b861 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Thu, 17 Sep 2020 10:39:53 -0500 Subject: [PATCH 0518/1013] MC-34385: Filter fields allowing HTML --- app/code/Magento/Cms/Model/BlockRepository.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Cms/Model/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index c26e2d809d996..ef57f8ca7b849 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -217,6 +217,7 @@ public function deleteById($blockId) */ private function getCollectionProcessor() { + //phpcs:disable Magento2.PHP.LiteralNamespaces if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( 'Magento\Cms\Model\Api\SearchCriteria\BlockCollectionProcessor' From 99366e70b910ea2f63730cd4d749a3a867f99d3a Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Thu, 17 Sep 2020 12:40:19 -0500 Subject: [PATCH 0519/1013] MC-37351: Cart contents lost after switching to different store with different domain - Remove customer_id from context interface --- .../RedirectDataPostprocessor.php | 4 ++-- .../RedirectDataPreprocessor.php | 20 ++++++++-------- .../RedirectDataPostprocessorTest.php | 4 ++-- .../RedirectDataPreprocessorTest.php | 16 ++++++------- .../Store/Controller/Store/Redirect.php | 18 ++------------ .../Store/Model/StoreSwitcher/Context.php | 17 +------------ .../Model/StoreSwitcher/ContextInterface.php | 7 ------ .../Model/StoreSwitcher/HashProcessor.php | 19 +++------------ .../RedirectDataCacheSerializer.php | 17 +++++++++++-- .../Model/StoreSwitcher/HashProcessorTest.php | 7 ++---- .../RedirectDataCacheSerializerTest.php | 5 +++- .../Controller/Store/SwitchActionTest.php | 24 +------------------ 12 files changed, 50 insertions(+), 108 deletions(-) diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php index f0528d97b233c..9fb3d6f432c2f 100644 --- a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPostprocessor.php @@ -65,10 +65,10 @@ public function process(ContextInterface $context, array $data): void } } catch (NoSuchEntityException $e) { $this->logger->error($e); - throw new LocalizedException(__('Failed to sign into the customer account.'), $e); + throw new LocalizedException(__('Something went wrong.'), $e); } catch (LocalizedException $e) { $this->logger->error($e); - throw new LocalizedException(__('Failed to sign into the customer account.'), $e); + throw new LocalizedException(__('Something went wrong.'), $e); } } } diff --git a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php index 678e1eb0ca79b..94f7619678df0 100644 --- a/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php +++ b/app/code/Magento/Customer/Model/StoreSwitcher/RedirectDataPreprocessor.php @@ -7,8 +7,8 @@ namespace Magento\Customer\Model\StoreSwitcher; -use Magento\Authorization\Model\UserContextInterface; use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; use Magento\Store\Model\StoreSwitcher\ContextInterface; use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; use Psr\Log\LoggerInterface; @@ -16,13 +16,15 @@ /** * Collect customer data to be redirected to target store + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface { /** - * @var UserContextInterface + * @var Session */ - private $userContext; + private $session; /** * @var LoggerInterface */ @@ -34,16 +36,16 @@ class RedirectDataPreprocessor implements RedirectDataPreprocessorInterface /** * @param CustomerRegistry $customerRegistry - * @param UserContextInterface $userContext + * @param Session $session * @param LoggerInterface $logger */ public function __construct( CustomerRegistry $customerRegistry, - UserContextInterface $userContext, + Session $session, LoggerInterface $logger ) { $this->customerRegistry = $customerRegistry; - $this->userContext = $userContext; + $this->session = $session; $this->logger = $logger; } @@ -52,11 +54,9 @@ public function __construct( */ public function process(ContextInterface $context, array $data): array { - if ($this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER - && $this->userContext->getUserId() - ) { + if ($this->session->isLoggedIn()) { try { - $customer = $this->customerRegistry->retrieve($this->userContext->getUserId()); + $customer = $this->customerRegistry->retrieve($this->session->getCustomerId()); if (in_array($context->getTargetStore()->getId(), $customer->getSharedStoreIds())) { $data['customer_id'] = (int) $customer->getId(); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php index d42e081935a96..0be0212652058 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPostprocessorTest.php @@ -118,7 +118,7 @@ public function testProcessShouldNotLoginCustomerIfNotRegisteredInTargetStore(): public function testProcessShouldThrowExceptionIfCustomerDoesNotExist(): void { - $this->expectErrorMessage('Failed to sign into the customer account.'); + $this->expectErrorMessage('Something went wrong.'); $data = ['customer_id' => 1]; $this->session->expects($this->never()) ->method('setCustomerDataAsLoggedIn'); @@ -127,7 +127,7 @@ public function testProcessShouldThrowExceptionIfCustomerDoesNotExist(): void public function testProcessShouldThrowExceptionIfAnErrorOccur(): void { - $this->expectErrorMessage('Failed to sign into the customer account.'); + $this->expectErrorMessage('Something went wrong.'); $data = ['customer_id' => 2]; $this->session->expects($this->never()) ->method('setCustomerDataAsLoggedIn'); diff --git a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php index edfc236c9f690..3d0c9c2e0a630 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/StoreSwitcher/RedirectDataPreprocessorTest.php @@ -10,6 +10,7 @@ use Magento\Authorization\Model\UserContextInterface; use Magento\Customer\Model\Customer; use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Session; use Magento\Customer\Model\StoreSwitcher\RedirectDataPreprocessor; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; @@ -26,13 +27,13 @@ class RedirectDataPreprocessorTest extends TestCase */ private $model; /** - * @var ContextInterface|MockObject + * @var Session|MockObject */ private $context; /** * @var UserContextInterface|MockObject */ - private $userContext; + private $session; /** * @inheritDoc @@ -42,16 +43,13 @@ protected function setUp(): void parent::setUp(); $customerRegistry = $this->createMock(CustomerRegistry::class); $logger = $this->createMock(LoggerInterface::class); - $this->userContext = $this->createMock(UserContextInterface::class); + $this->session = $this->createMock(Session::class); $this->model = new RedirectDataPreprocessor( $customerRegistry, - $this->userContext, + $this->session, $logger ); - $this->userContext->method('getUserType') - ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); - $store1 = $this->createConfiguredMock( StoreInterface::class, [ @@ -101,7 +99,9 @@ function ($id) { */ public function testProcess(?int $customerId, array $data): void { - $this->userContext->method('getUserId') + $this->session->method('isLoggedIn') + ->willReturn(true); + $this->session->method('getCustomerId') ->willReturn($customerId); $this->assertEquals($data, $this->model->process($this->context, [])); } diff --git a/app/code/Magento/Store/Controller/Store/Redirect.php b/app/code/Magento/Store/Controller/Store/Redirect.php index c0967eeea942d..c20e3b31e09b1 100644 --- a/app/code/Magento/Store/Controller/Store/Redirect.php +++ b/app/code/Magento/Store/Controller/Store/Redirect.php @@ -7,7 +7,6 @@ namespace Magento\Store\Controller\Store; -use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context; use Magento\Framework\App\Action\HttpGetActionInterface; @@ -60,10 +59,6 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @var ContextInterfaceFactory|null */ private $contextFactory; - /** - * @var UserContextInterface|null - */ - private $userContext; /** * @param Context $context @@ -75,7 +70,6 @@ class Redirect extends Action implements HttpGetActionInterface, HttpPostActionI * @param StoreManagerInterface|null $storeManager * @param RedirectDataGenerator|null $redirectDataGenerator * @param ContextInterfaceFactory|null $contextFactory - * @param UserContextInterface|null $userContext * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -88,8 +82,7 @@ public function __construct( HashGenerator $hashGenerator, StoreManagerInterface $storeManager = null, ?RedirectDataGenerator $redirectDataGenerator = null, - ?ContextInterfaceFactory $contextFactory = null, - ?UserContextInterface $userContext = null + ?ContextInterfaceFactory $contextFactory = null ) { parent::__construct($context); $this->storeRepository = $storeRepository; @@ -100,8 +93,6 @@ public function __construct( ?: ObjectManager::getInstance()->get(RedirectDataGenerator::class); $this->contextFactory = $contextFactory ?: ObjectManager::getInstance()->get(ContextInterfaceFactory::class); - $this->userContext = $userContext - ?: ObjectManager::getInstance()->get(UserContextInterface::class); } /** @@ -127,17 +118,12 @@ public function execute() $targetStore = $this->storeRepository->get($targetStoreCode); $this->storeManager->setCurrentStore($targetStore); $encodedUrl = $this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED); - $customerId = $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER - && $this->userContext->getUserId() - ? (int) $this->userContext->getUserId() - : null; $redirectData = $this->redirectDataGenerator->generate( $this->contextFactory->create( [ 'fromStore' => $fromStore, 'targetStore' => $targetStore, - 'redirectUrl' => $this->_redirect->getRedirectUrl(), - 'customerId' => $customerId + 'redirectUrl' => $this->_redirect->getRedirectUrl() ] ) ); diff --git a/app/code/Magento/Store/Model/StoreSwitcher/Context.php b/app/code/Magento/Store/Model/StoreSwitcher/Context.php index 26c1807ea0e69..c67dc3d67b01a 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/Context.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/Context.php @@ -26,27 +26,20 @@ class Context implements ContextInterface * @var string */ private $redirectUrl; - /** - * @var int|null - */ - private $customerId; /** * @param StoreInterface $fromStore * @param StoreInterface $targetStore * @param string $redirectUrl - * @param int|null $customerId */ public function __construct( StoreInterface $fromStore, StoreInterface $targetStore, - string $redirectUrl, - ?int $customerId = null + string $redirectUrl ) { $this->fromStore = $fromStore; $this->targetStore = $targetStore; $this->redirectUrl = $redirectUrl; - $this->customerId = $customerId; } /** @@ -72,12 +65,4 @@ public function getRedirectUrl(): string { return $this->redirectUrl; } - - /** - * @inheritDoc - */ - public function getCustomerId(): ?int - { - return $this->customerId; - } } diff --git a/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php index 7c4b8dca5f723..a18c7cc9ccc27 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/ContextInterface.php @@ -34,11 +34,4 @@ public function getTargetStore(): StoreInterface; * @return string */ public function getRedirectUrl(): string; - - /** - * The logged in customer ID - * - * @return int - */ - public function getCustomerId(): ?int; } diff --git a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php index edf6f4536c292..45e93a5af06de 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/HashProcessor.php @@ -7,7 +7,6 @@ namespace Magento\Store\Model\StoreSwitcher; -use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Message\ManagerInterface; @@ -55,10 +54,6 @@ class HashProcessor implements StoreSwitcherInterface * @var LoggerInterface */ private $logger; - /** - * @var UserContextInterface - */ - private $userContext; /** * @param RequestInterface $request @@ -69,7 +64,6 @@ class HashProcessor implements StoreSwitcherInterface * @param RedirectDataInterfaceFactory $dataFactory * @param RedirectDataValidator $dataValidator * @param LoggerInterface $logger - * @param UserContextInterface $userContext */ public function __construct( RequestInterface $request, @@ -79,8 +73,7 @@ public function __construct( ContextInterfaceFactory $contextFactory, RedirectDataInterfaceFactory $dataFactory, RedirectDataValidator $dataValidator, - LoggerInterface $logger, - UserContextInterface $userContext + LoggerInterface $logger ) { $this->request = $request; $this->postprocessor = $postprocessor; @@ -90,7 +83,6 @@ public function __construct( $this->dataFactory = $dataFactory; $this->dataValidator = $dataValidator; $this->logger = $logger; - $this->userContext = $userContext; } /** @@ -107,16 +99,11 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s $timestamp = (int) $this->request->getParam('time_stamp'); $signature = (string) $this->request->getParam('signature'); $data = (string) $this->request->getParam('data'); - $customerId = $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER - && $this->userContext->getUserId() - ? (int) $this->userContext->getUserId() - : null; $context = $this->contextFactory->create( [ 'fromStore' => $fromStore, 'targetStore' => $targetStore, - 'redirectUrl' => $redirectUrl, - 'customerId' => $customerId + 'redirectUrl' => $redirectUrl ] ); $redirectDataObject = $this->dataFactory->create( @@ -140,7 +127,7 @@ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, s } catch (\Throwable $exception) { $this->logger->error($exception); $this->messageManager->addErrorMessage( - __('Something went wrong while switching to the store.') + __('Something went wrong.') ); } diff --git a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php index 59cafd096781e..5360d403d1388 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/RedirectDataCacheSerializer.php @@ -11,6 +11,8 @@ use Magento\Framework\App\CacheInterface; use Magento\Framework\Math\Random; use Magento\Framework\Serialize\Serializer\Json; +use Psr\Log\LoggerInterface; +use Throwable; /** * Store switcher redirect data cache serializer @@ -33,20 +35,27 @@ class RedirectDataCacheSerializer implements RedirectDataSerializerInterface * @var Random */ private $random; + /** + * @var LoggerInterface + */ + private $logger; /** * @param Json $json * @param Random $random * @param CacheInterface $cache + * @param LoggerInterface $logger */ public function __construct( Json $json, Random $random, - CacheInterface $cache + CacheInterface $cache, + LoggerInterface $logger ) { $this->cache = $cache; $this->json = $json; $this->random = $random; + $this->logger = $logger; } /** @@ -76,7 +85,11 @@ public function unserialize(string $data): array throw new InvalidArgumentException('Couldn\'t retrieve data from cache.'); } $result = $this->json->unserialize($json); - $this->cache->remove($cacheKey); + try { + $this->cache->remove($cacheKey); + } catch (Throwable $exception) { + $this->logger->error($exception); + } return $result; } diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php index c82d418d20bd9..89dc1d1c99ebd 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/HashProcessorTest.php @@ -8,7 +8,6 @@ namespace Magento\Store\Test\Unit\Model\StoreSwitcher; use InvalidArgumentException; -use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\RequestInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Store\Api\Data\StoreInterface; @@ -76,7 +75,6 @@ protected function setUp(): void $dataFactory = $this->createMock(RedirectDataInterfaceFactory::class); $this->dataValidator = $this->createMock(RedirectDataValidator::class); $logger = $this->createMock(LoggerInterface::class); - $userContext = $this->createMock(UserContextInterface::class); $this->store1 = $this->createMock(StoreInterface::class); $this->store2 = $this->createMock(StoreInterface::class); $this->model = new HashProcessor( @@ -87,8 +85,7 @@ protected function setUp(): void $contextFactory, $dataFactory, $this->dataValidator, - $logger, - $userContext + $logger ); $contextFactory->method('create') @@ -159,7 +156,7 @@ public function testShouldNotProcessIfDataUnserializationFailed(): void ->method('process'); $this->messageManager->expects($this->once()) ->method('addErrorMessage') - ->with('Something went wrong while switching to the store.'); + ->with('Something went wrong.'); $this->assertEquals($redirectUrl, $this->model->switch($this->store1, $this->store2, $redirectUrl)); } diff --git a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php index 68284a8163c26..c21d785b268a9 100644 --- a/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/StoreSwitcher/RedirectDataCacheSerializerTest.php @@ -14,6 +14,7 @@ use Magento\Store\Model\StoreSwitcher\RedirectDataCacheSerializer; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use RuntimeException; class RedirectDataCacheSerializerTest extends TestCase @@ -36,10 +37,12 @@ protected function setUp(): void parent::setUp(); $this->cache = $this->createMock(CacheInterface::class); $random = $this->createMock(Random::class); + $logger = $this->createMock(LoggerInterface::class); $this->model = new RedirectDataCacheSerializer( new Json(), $random, - $this->cache + $this->cache, + $logger ); $random->method('getRandomString')->willReturn(self::RANDOM_STRING); } diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php index 503f9cb8ce7f8..e4d78de54d308 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php @@ -5,7 +5,6 @@ */ namespace Magento\Store\Controller\Store; -use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\App\ActionInterface; use Magento\Framework\Encryption\UrlCoder; use Magento\Framework\Interception\InterceptorInterface; @@ -44,14 +43,6 @@ class SwitchActionTest extends AbstractController * @var MockObject */ private $postprocessorMock; - /** - * @var UserContextInterface - */ - private $userContext; - /** - * @var MockObject - */ - private $userContextMock; /** * @inheritDoc @@ -66,10 +57,6 @@ protected function setUp(): void $this->postprocessor = $this->_objectManager->get(RedirectDataPostprocessorInterface::class); $this->postprocessorMock = $this->createMock(RedirectDataPostprocessorInterface::class); $this->_objectManager->addSharedInstance($this->postprocessorMock, $this->getClassName($this->postprocessor)); - - $this->userContext = $this->_objectManager->get(UserContextInterface::class); - $this->userContextMock = $this->createMock(UserContextInterface::class); - $this->_objectManager->addSharedInstance($this->userContextMock, $this->getClassName($this->userContext)); } /** @@ -83,14 +70,10 @@ protected function tearDown(): void if ($this->postprocessor) { $this->_objectManager->addSharedInstance($this->postprocessor, $this->getClassName($this->postprocessor)); } - if ($this->userContext) { - $this->_objectManager->addSharedInstance($this->userContext, $this->getClassName($this->userContext)); - } parent::tearDown(); } /** - * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Store/_files/second_store.php * @magentoConfigFixture web/url/use_store 0 * @magentoConfigFixture fixture_second_store_store web/unsecure/base_url http://second_store.test/ @@ -103,10 +86,6 @@ public function testSwitch() $data = ['key1' => 'value1', 'key2' => 1]; $this->preprocessorMock->method('process') ->willReturn($data); - $this->userContextMock->method('getUserType') - ->willReturn(UserContextInterface::USER_TYPE_CUSTOMER); - $this->userContextMock->method('getUserId') - ->willReturn(1); $this->postprocessorMock->expects($this->once()) ->method('process') ->with( @@ -114,8 +93,7 @@ public function testSwitch() function (ContextInterface $context) { return $context->getFromStore()->getCode() === 'fixture_second_store' && $context->getTargetStore()->getCode() === 'default' - && $context->getRedirectUrl() === 'http://localhost/index.php/' - && $context->getCustomerId() === 1; + && $context->getRedirectUrl() === 'http://localhost/index.php/'; } ), $data From 38dbccf14cd629f857f19f53c05828c9d146a348 Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Thu, 17 Sep 2020 22:32:47 +0100 Subject: [PATCH 0520/1013] Fix out of scope linter complaints --- .../Payment/Model/PaymentMethodList.php | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Payment/Model/PaymentMethodList.php b/app/code/Magento/Payment/Model/PaymentMethodList.php index 007555fe10c1f..b27d02bbdff4b 100644 --- a/app/code/Magento/Payment/Model/PaymentMethodList.php +++ b/app/code/Magento/Payment/Model/PaymentMethodList.php @@ -6,37 +6,37 @@ namespace Magento\Payment\Model; use Magento\Payment\Api\Data\PaymentMethodInterface; +use Magento\Payment\Api\Data\PaymentMethodInterfaceFactory; +use Magento\Payment\Api\PaymentMethodListInterface; +use Magento\Payment\Helper\Data; use UnexpectedValueException; -/** - * Payment method list class. - */ -class PaymentMethodList implements \Magento\Payment\Api\PaymentMethodListInterface +class PaymentMethodList implements PaymentMethodListInterface { /** - * @var \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory + * @var PaymentMethodInterfaceFactory */ private $methodFactory; /** - * @var \Magento\Payment\Helper\Data + * @var Data */ private $helper; /** - * @param \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory $methodFactory - * @param \Magento\Payment\Helper\Data $helper + * @param PaymentMethodInterfaceFactory $methodFactory + * @param Data $helper */ public function __construct( - \Magento\Payment\Api\Data\PaymentMethodInterfaceFactory $methodFactory, - \Magento\Payment\Helper\Data $helper + PaymentMethodInterfaceFactory $methodFactory, + Data $helper ) { $this->methodFactory = $methodFactory; $this->helper = $helper; } /** - * {@inheritdoc} + * @inheritDoc */ public function getList($storeId) { @@ -56,7 +56,7 @@ function ($code) { return $method && !($method instanceof \Magento\Payment\Model\Method\Substitution); }); - @uasort( + uasort( $methodsInstances, function (MethodInterface $a, MethodInterface $b) use ($storeId) { return (int)$a->getConfigData('sort_order', $storeId) - (int)$b->getConfigData('sort_order', $storeId); @@ -80,7 +80,7 @@ function (MethodInterface $methodInstance) use ($storeId) { } /** - * {@inheritdoc} + * @inheritDoc */ public function getActiveList($storeId) { From 9945bcd2ec217a33450f3f197c9e018b5fc83c52 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Fri, 18 Sep 2020 12:28:14 +0300 Subject: [PATCH 0521/1013] MC-23908: Tax estimation fails on CI --- ...ontCartShippingMethodSelectActionGroup.xml | 24 ++ .../Section/CheckoutCartSummarySection.xml | 1 + ...tCheckoutUsingFreeShippingAndTaxesTest.xml | 6 +- ...tCheckoutUsingFreeShippingAndTaxesTest.xml | 216 ++++++++++++++++++ .../Tax/Test/Mftf/Data/TaxRuleData.xml | 13 ++ 5 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml create mode 100644 app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml new file mode 100644 index 0000000000000..d9f8c17a81545 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.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"> + <!-- Select Shipping Method on Cart --> + <actionGroup name="StorefrontCartShippingMethodSelectActionGroup"> + <annotations> + <description>Select a shipping method in the Estimate Shipping and Tax block on the Storefront Shopping Cart page.</description> + </annotations> + <arguments> + <argument name="carrierCode" defaultValue="flatrate" type="string"/> + <argument name="methodCode" defaultValue="flatrate" type="string"/> + </arguments> + + <conditionalClick selector="{{CheckoutCartSummarySection.shippingMethodElementId(carrierCode, methodCode)}}" dependentSelector="{{CheckoutCartSummarySection.shippingMethodChecked(carrierCode, methodCode)}}" visible="false" stepKey="selectShippingMethod"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index de71fc3f8ad0e..4ec45e7b26759 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -35,6 +35,7 @@ <element name="methodName" type="text" selector="#co-shipping-method-form label"/> <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> + <element name="shippingMethodChecked" type="radio" parameterized="true" selector="#s_method_{{carrierCode}}_{{methodCode}}:checked"/> <element name="estimateShippingAndTaxForm" type="block" selector="#shipping-zip-form"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index 5a0610f5c5b0a..e5897501cc067 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -7,16 +7,16 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest"> + <test name="StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest" deprecated="Use StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest"> <annotations> <stories value="Checkout"/> - <title value="Verify guest checkout using free shipping and tax variations"/> + <title value="DEPRECATED. Verify guest checkout using free shipping and tax variations"/> <description value="Verify guest checkout using free shipping and tax variations"/> <severity value="CRITICAL"/> <testCaseId value="MC-14709"/> <group value="mtf_migrated"/> <skip> - <issueId value="MC-18802"/> + <issueId value="DEPRECATED">Use StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest</issueId> </skip> </annotations> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml new file mode 100644 index 0000000000000..24460738e1c20 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -0,0 +1,216 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout"/> + <title value="Verify guest checkout using free shipping and tax variations"/> + <description value="Verify guest checkout using free shipping and tax variations"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14709"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> + <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> + <createData entity="MinimumOrderAmount100" stepKey="minimumOrderAmount100"/> + <createData entity="taxRate_US_NY_8_1" stepKey="createTaxRateUSNY"/> + <createData entity="DefaultTaxRuleWithCustomTaxRate" stepKey="createTaxRuleUSNY"> + <requiredEntity createDataKey="createTaxRateUSNY" /> + </createData> + + <!--Create Simple Product --> + <createData entity="defaultSimpleProduct" stepKey="simpleProduct"> + <field key="price">10.00</field> + </createData> + + <!-- Create the configurable product with product Attribute options--> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <createData entity="AddToDefaultSet" stepKey="addToDefaultSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <field key="price">10.00</field> + </createData> + + <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + + <!-- Create Bundle Product --> + <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <field key="price">200.00</field> + </createData> + <!--Create Bundle product with multi select option--> + <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> + <requiredEntity createDataKey="createBundleProduct"/> + <field key="required">True</field> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> + <requiredEntity createDataKey="createBundleProduct"/> + <requiredEntity createDataKey="createBundleOption1_1"/> + <requiredEntity createDataKey="simpleProduct2"/> + </createData> + <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"/> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigProduct1"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createTaxRuleUSNY" stepKey="deleteTaxRuleUSNY"/> + <deleteData createDataKey="createTaxRateUSNY" stepKey="deleteTaxRateUSNY"/> + <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> + <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <!--Open Product page in StoreFront and assert product and price range --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + + <!--Add product to the cart --> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="addProductToTheCart"> + <argument name="productQty" value="1"/> + </actionGroup> + + <!-- Add Configurable Product to the cart --> + <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> + <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> + <argument name="productAttribute" value="$$createConfigProductAttribute.default_value$$"/> + <argument name="productOption" value="$$getConfigAttributeOption1.label$$"/> + <argument name="qty" value="1"/> + </actionGroup> + + <!--Open Product page in StoreFront --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openBundleProduct"> + <argument name="product" value="$$createBundleProduct$$"/> + </actionGroup> + + <!-- Click on customize And Add To Cart Button --> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddtoCartButton"/> + + <!-- Select Two Products, enter the quantity and add product to the cart --> + <selectOption selector="{{StorefrontBundledSection.multiSelectOption}}" userInput="$$simpleProduct1.name$$ +$100.00" stepKey="selectOption"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + + <!--Open View and edit --> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="clickMiniCart"/> + + <!-- Fill the Estimate Shipping and Tax section --> + <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"> + <argument name="address" value="US_Address_NY_Default_Shipping"/> + </actionGroup> + + <!-- Select Free Shipping Method on Cart --> + <actionGroup ref="StorefrontCartShippingMethodSelectActionGroup" stepKey="selectFreeShippingShippingMethod"> + <argument name="carrierCode" value="freeshipping"/> + <argument name="methodCode" value="freeshipping"/> + </actionGroup> + <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.72" stepKey="seeTaxAmount"/> + <reloadPage stepKey="reloadThePage"/> + <waitForPageLoad stepKey="waitForPageToReload"/> + <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.72" stepKey="seeTaxAmountAfterLoadPage"/> + + <!-- Proceed to checkout --> + <scrollTo selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="scrollToProceedToCheckout" /> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> + <waitForPageLoad stepKey="waitForPageToLoad"/> + + <!-- Fill Guest form --> + <actionGroup ref="FillGuestCheckoutShippingAddressFormActionGroup" stepKey="fillTheSignInForm"> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="customerAddress" value="US_Address_NY_Default_Shipping"/> + </actionGroup> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> + + <!-- Place order and Assert success message --> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> + + <!-- Assert empty Mini Cart --> + <seeElement selector="{{StorefrontMinicartSection.emptyMiniCart}}" stepKey="assertEmptyCart" /> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> + + <!-- Open Order Index Page --> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> + + <!-- Filter Order using orderId and assert order--> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$orderId"/> + </actionGroup> + <click selector="{{AdminOrdersGridSection.viewLink('$orderId')}}" stepKey="clickOnViewLink"/> + <waitForPageLoad stepKey="waitForOrderPageToLoad"/> + + <!-- Assert order buttons --> + <actionGroup ref="AdminAssertOrderAvailableButtonsActionGroup" stepKey="assertOrderButtons"/> + + <!-- Assert Grand Total --> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$129.72" stepKey="seeGrandTotal"/> + <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeOrderStatus"/> + + <!-- Ship the order and assert the status --> + <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="shipTheOrder"/> + + <!-- Assert customer order address --> + <actionGroup ref="AssertOrderAddressInformationActionGroup" stepKey="assertCustomerInformation"> + <argument name="customer" value=""/> + <argument name="shippingAddress" value="US_Address_NY_Default_Shipping"/> + <argument name="billingAddress" value="US_Address_NY_Default_Shipping"/> + <argument name="customerGroup" value=""/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml b/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml index fde43cd10e3ea..fd0cb31fd8655 100644 --- a/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml +++ b/app/code/Magento/Tax/Test/Mftf/Data/TaxRuleData.xml @@ -123,4 +123,17 @@ <entity name="TaxRuleZeroRate" type="taxRule"> <data key="name" unique="suffix">TaxNameZeroRate</data> </entity> + <entity name="DefaultTaxRuleWithCustomTaxRate" type="taxRule"> + <data key="code" unique="suffix">TaxRule</data> + <data key="position">0</data> + <data key="priority">0</data> + <array key="customer_tax_class_ids"> + <item>3</item> + </array> + <array key="product_tax_class_ids"> + <item>2</item> + </array> + <var key="tax_rate_ids" entityType="taxRate" entityKey="id"/> + <data key="calculate_subtotal">false</data> + </entity> </entities> From 6dec2298f328080361fe178459e7b087f5ebfa1e Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 18 Sep 2020 12:37:12 +0300 Subject: [PATCH 0522/1013] MC-37086: Create automated test for "Check that CSV can be exported" --- .../MysqlMq/DeleteTopicRelatedMessages.php | 42 +++++++ .../Adminhtml/Export/ExportTest.php | 103 ++++++++++++++++++ .../Model/Export/ConsumerTest.php | 100 +++++++++++++++++ .../ImportExport/_files/export_queue_data.php | 34 ++++++ .../_files/export_queue_data_rollback.php | 17 +++ 5 files changed, 296 insertions(+) create mode 100644 dev/tests/integration/framework/Magento/TestFramework/MysqlMq/DeleteTopicRelatedMessages.php create mode 100644 dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php create mode 100644 dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php create mode 100644 dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php create mode 100644 dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data_rollback.php diff --git a/dev/tests/integration/framework/Magento/TestFramework/MysqlMq/DeleteTopicRelatedMessages.php b/dev/tests/integration/framework/Magento/TestFramework/MysqlMq/DeleteTopicRelatedMessages.php new file mode 100644 index 0000000000000..3b14cda3b0150 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/MysqlMq/DeleteTopicRelatedMessages.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\MysqlMq; + +use Magento\MysqlMq\Model\QueueManagement; +use Magento\MysqlMq\Model\ResourceModel\Message; + +/** + * Delete messages from queue by topic + */ +class DeleteTopicRelatedMessages +{ + /** @var Message */ + private $queueMessageResource; + + /** + * @param Message $queueMessageResource + */ + public function __construct( + Message $queueMessageResource + ) { + $this->queueMessageResource = $queueMessageResource; + } + + /** + * Delete messages from queue + * + * @param string $topic + * @return void + */ + public function execute(string $topic): void + { + $connection = $this->queueMessageResource->getConnection(); + $condition = $connection->quoteInto(QueueManagement::MESSAGE_TOPIC . '= ?', $topic); + $connection->delete($this->queueMessageResource->getMainTable(), $condition); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php new file mode 100644 index 0000000000000..7d892df50ea29 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Controller\Adminhtml\Export; + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\MysqlMq\Model\QueueManagement; +use Magento\MysqlMq\Model\ResourceModel\Message; +use Magento\TestFramework\MysqlMq\DeleteTopicRelatedMessages; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for export controller + * + * @see \Magento\ImportExport\Controller\Adminhtml\Export\Export + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class ExportTest extends AbstractBackendController +{ + private const TOPIC_NAME = 'import_export.export'; + + /** @var QueueManagement */ + private $queueManagement; + + /** @var Message */ + private $queueMessageResource; + + /** @var SerializerInterface */ + private $json; + + /** @var DeleteTopicRelatedMessages */ + private $deleteTopicRelatedMessages; + + /** + * @inheridoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->queueManagement = $this->_objectManager->get(QueueManagement::class); + $this->queueMessageResource = $this->_objectManager->get(Message::class); + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->deleteTopicRelatedMessages = $this->_objectManager->get(DeleteTopicRelatedMessages::class); + $this->deleteTopicRelatedMessages->execute(self::TOPIC_NAME); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->deleteTopicRelatedMessages->execute(self::TOPIC_NAME); + + parent::tearDown(); + } + + /** + * @magentoConfigFixture default_store admin/security/use_form_key 1 + * + * @return void + */ + public function testExecute(): void + { + $expectedSessionMessage = (string)__('Message is added to queue, wait to get your file soon.' + . ' Make sure your cron job is running to export the file'); + $fileFormat = 'csv'; + $filter = ['price' => [0,1000]]; + $this->getRequest()->setParams( + [ + 'entity' => ProductAttributeInterface::ENTITY_TYPE_CODE, + 'file_format' => $fileFormat, + ] + ); + $this->getRequest()->setMethod(Http::METHOD_POST) + ->setPostValue([ + 'export_filter' => [ + $filter, + ], + ]); + $this->dispatch('backend/admin/export/export'); + $this->assertSessionMessages($this->containsEqual($expectedSessionMessage)); + $this->assertRedirect($this->stringContains('/export/index/key/')); + $messages = $this->queueManagement->readMessages('export'); + $this->assertCount(1, $messages); + $message = reset($messages); + $this->assertEquals(self::TOPIC_NAME, $message[QueueManagement::MESSAGE_TOPIC]); + $body = $this->json->unserialize($message[QueueManagement::MESSAGE_BODY]); + $this->assertStringContainsString(ProductAttributeInterface::ENTITY_TYPE_CODE, $body['file_name']); + $this->assertEquals($fileFormat, $body['file_format']); + $actualFilter = $this->json->unserialize($body['export_filter']); + $this->assertCount(1, $actualFilter); + $this->assertEquals($filter, reset($actualFilter)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php new file mode 100644 index 0000000000000..1109a9d60e613 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Export; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Csv; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Write; +use Magento\Framework\MessageQueue\MessageEncoder; +use Magento\Framework\ObjectManagerInterface; +use Magento\MysqlMq\Model\Driver\Queue; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for export consumer + * + * @see \Magento\ImportExport\Model\Export\Consumer + * + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ +class ConsumerTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var MessageEncoder */ + private $messageEncoder; + + /** @var Consumer */ + private $consumer; + + /** @var Queue */ + private $queue; + + /** @var Csv */ + private $csv; + + /** @var Write */ + private $directory; + + /** @var string */ + private $filePath; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->queue = $this->objectManager->create(Queue::class, ['queueName' => 'export']); + $this->messageEncoder = $this->objectManager->get(MessageEncoder::class); + $this->consumer = $this->objectManager->get(Consumer::class); + $this->directory = $this->objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->csv = $this->objectManager->get(Csv::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->filePath && $this->directory->isExist($this->filePath)) { + $this->directory->delete($this->filePath); + } + + parent::tearDown(); + } + + /** + * @magentoConfigFixture default_store admin/security/use_form_key 1 + * + * @magentoDataFixture Magento/ImportExport/_files/export_queue_data.php + * @magentoDataFixture Magento/Catalog/_files/product_virtual.php + * + * @return void + */ + public function testProcess(): void + { + $envelope = $this->queue->dequeue(); + $decodedMessage = $this->messageEncoder->decode('import_export.export', $envelope->getBody()); + $this->consumer->process($decodedMessage); + $this->filePath = 'export/' . $decodedMessage->getFileName(); + $this->assertTrue($this->directory->isExist($this->filePath)); + $data = $this->csv->getData($this->directory->getAbsolutePath($this->filePath)); + $this->assertCount(2, $data); + $skuPosition = array_search(ProductInterface::SKU, array_keys($data)); + $this->assertNotFalse($skuPosition); + $this->assertEquals('simple2', $data[1][$skuPosition]); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php new file mode 100644 index 0000000000000..a3fce3e2a218d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.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\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\MessageQueue\PublisherInterface; +use Magento\ImportExport\Model\Export\Entity\ExportInfoFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ExportInfoFactory $exportInfoFactory */ +$exportInfoFactory = $objectManager->get(ExportInfoFactory::class); +/** @var PublisherInterface $messagePublisher */ +$messagePublisher = $objectManager->get(PublisherInterface::class); +$params = [ + 'file_format' => 'csv', + 'entity' => ProductAttributeInterface::ENTITY_TYPE_CODE, + 'export_filter' => [ProductInterface::SKU => 'simple2'], + 'skip_attr' => [], +]; +$dataObject = $exportInfoFactory->create( + $params['file_format'], + $params['entity'], + $params['export_filter'], + $params['skip_attr'] +); +$messagePublisher->publish('import_export.export', $dataObject); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data_rollback.php b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data_rollback.php new file mode 100644 index 0000000000000..f1d2ba67aa035 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data_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\TestFramework\MysqlMq\DeleteTopicRelatedMessages; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DeleteTopicRelatedMessages $deleteTopicRelatedMessages */ +$deleteTopicRelatedMessages = $objectManager->get(DeleteTopicRelatedMessages::class); +$deleteTopicRelatedMessages->execute('import_export.export'); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); From d0e1712311067e1941d75e231bdfa40b30cdfbd9 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Fri, 18 Sep 2020 14:38:32 +0300 Subject: [PATCH 0523/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Magento/Catalog/Model/Product/Option.php | 98 ++++++++++++++----- .../CalculateCustomOptionCatalogRule.php | 40 +++++++- ...SimpleProductWithSelectFixedMethodTest.xml | 13 +-- 3 files changed, 115 insertions(+), 36 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index ca5251de13e69..15c40af09d7b9 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; @@ -205,7 +204,10 @@ public function __construct( } /** - * @inheritdoc + * Get resource instance + * + * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb + * @deprecated 102.0.0 because resource models should be used directly */ protected function _getResource() { @@ -255,7 +257,7 @@ public function getValueById($valueId) * * @param string $type * @return bool - * @since 101.1.0 + * @since 102.0.0 */ public function hasValues($type = null) { @@ -570,7 +572,9 @@ public function getSearchableData($productId, $storeId) } /** - * @inheritdoc + * Clearing object's data + * + * @return $this */ protected function _clearData() { @@ -580,7 +584,9 @@ protected function _clearData() } /** - * @inheritdoc + * Clearing cyclic references + * + * @return $this */ protected function _clearReferences() { @@ -601,7 +607,9 @@ protected function _getValidationRulesBeforeSave() } /** - * @inheritdoc + * Get product SKU + * + * @return string */ public function getProductSku() { @@ -613,7 +621,9 @@ public function getProductSku() } /** - * @inheritdoc + * Get option id + * + * @return int|null * @codeCoverageIgnoreStart */ public function getOptionId() @@ -622,7 +632,9 @@ public function getOptionId() } /** - * @inheritdoc + * Get option title + * + * @return string */ public function getTitle() { @@ -630,7 +642,9 @@ public function getTitle() } /** - * @inheritdoc + * Get option type + * + * @return string */ public function getType() { @@ -638,7 +652,9 @@ public function getType() } /** - * @inheritdoc + * Get sort order + * + * @return int */ public function getSortOrder() { @@ -646,7 +662,10 @@ public function getSortOrder() } /** - * @inheritdoc + * Get is require + * + * @return bool + * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ public function getIsRequire() { @@ -654,7 +673,9 @@ public function getIsRequire() } /** - * @inheritdoc + * Get price type + * + * @return string|null */ public function getPriceType() { @@ -662,7 +683,9 @@ public function getPriceType() } /** - * @inheritdoc + * Get Sku + * + * @return string|null */ public function getSku() { @@ -710,7 +733,10 @@ public function getImageSizeY() } /** - * @inheritdoc + * Set product SKU + * + * @param string $productSku + * @return $this */ public function setProductSku($productSku) { @@ -718,7 +744,10 @@ public function setProductSku($productSku) } /** - * @inheritdoc + * Set option id + * + * @param int $optionId + * @return $this */ public function setOptionId($optionId) { @@ -726,7 +755,10 @@ public function setOptionId($optionId) } /** - * @inheritdoc + * Set option title + * + * @param string $title + * @return $this */ public function setTitle($title) { @@ -734,7 +766,10 @@ public function setTitle($title) } /** - * @inheritdoc + * Set option type + * + * @param string $type + * @return $this */ public function setType($type) { @@ -742,7 +777,10 @@ public function setType($type) } /** - * @inheritdoc + * Set sort order + * + * @param int $sortOrder + * @return $this */ public function setSortOrder($sortOrder) { @@ -750,7 +788,10 @@ public function setSortOrder($sortOrder) } /** - * @inheritdoc + * Set is require + * + * @param bool $isRequired + * @return $this */ public function setIsRequire($isRequired) { @@ -758,7 +799,10 @@ public function setIsRequire($isRequired) } /** - * @inheritdoc + * Set price + * + * @param float $price + * @return $this */ public function setPrice($price) { @@ -766,7 +810,10 @@ public function setPrice($price) } /** - * @inheritdoc + * Set price type + * + * @param string $priceType + * @return $this */ public function setPriceType($priceType) { @@ -774,7 +821,10 @@ public function setPriceType($priceType) } /** - * @inheritdoc + * Set Sku + * + * @param string $sku + * @return $this */ public function setSku($sku) { @@ -915,7 +965,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; @@ -929,7 +979,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/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php index 5e5e1b08cf721..cf7c12b4560e0 100644 --- a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php +++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php @@ -9,6 +9,8 @@ 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; /** @@ -54,23 +56,53 @@ public function execute( $regularPrice = (float)$product->getPriceInfo() ->getPrice(RegularPrice::PRICE_CODE) ->getValue(); - $catalogRulePrice = $product->getPriceInfo()->getPrice('final_price')->getValue(); + $catalogRulePrice = $this->priceModifier->modifyPrice( + $regularPrice, + $product + ); // Apply catalog price rules to product options only if catalog price rules are applied to product. if ($catalogRulePrice < $regularPrice) { $optionPrice = $this->getOptionPriceWithoutPriceRule($optionPriceValue, $isPercent, $regularPrice); - $discount = $catalogRulePrice / $regularPrice; - $finalOptionPrice = $optionPrice*$discount; + $totalCatalogRulePrice = $this->priceModifier->modifyPrice( + $regularPrice + $optionPrice, + $product + ); + $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; } else { $finalOptionPrice = $this->getOptionPriceWithoutPriceRule( $optionPriceValue, $isPercent, - $regularPrice + $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. * diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index 8b96feee6dbc6..ff144c1686e5d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -15,7 +15,7 @@ <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> <severity value="CRITICAL"/> <testCaseId value="MC-14771"/> - <group value="CatalogRule"/> + <group value="catalogRule"/> <group value="mtf_migrated"/> </annotations> <before> @@ -33,7 +33,7 @@ <!-- Update all products to have custom options --> <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateProductWithOptions1"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCron groups="index" stepKey="runCronIndex"/> </before> <after> <!-- Delete products and category --> @@ -42,7 +42,7 @@ <!-- Delete the catalog price rule --> <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> - <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{CatalogRuleByFixed.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> </actionGroup> @@ -60,13 +60,10 @@ <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> <!-- Save and apply the new catalog price rule --> - <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <actionGroup ref="CreateCatalogRuleStagingUpdateWithItsStartActionGroup" stepKey="fillOutActionGroup"> - <argument name="stagingUpdate" value="_defaultCatalogRule"/> - </actionGroup> + <actionGroup ref="AdminEnableCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <!-- Navigate to category on store front --> - <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> + <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToStorefrontCategoryPage"/> <!-- Check product 1 name on store front category page --> <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> From dafc76411ba7f7a1834df229d674bc026ddb7dbd Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 18 Sep 2020 15:49:08 +0300 Subject: [PATCH 0524/1013] MC-37086: Create automated test for "Check that CSV can be exported" --- .../Adminhtml/Export/ExportTest.php | 20 ++++++++----------- .../Model/Export/ConsumerTest.php | 6 +++--- .../ImportExport/_files/export_queue_data.php | 14 ++++--------- 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php index 7d892df50ea29..834d6a4f06b65 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/ExportTest.php @@ -73,19 +73,15 @@ public function testExecute(): void $expectedSessionMessage = (string)__('Message is added to queue, wait to get your file soon.' . ' Make sure your cron job is running to export the file'); $fileFormat = 'csv'; - $filter = ['price' => [0,1000]]; - $this->getRequest()->setParams( - [ - 'entity' => ProductAttributeInterface::ENTITY_TYPE_CODE, - 'file_format' => $fileFormat, - ] - ); + $filter = ['price' => [0, 1000]]; $this->getRequest()->setMethod(Http::METHOD_POST) - ->setPostValue([ - 'export_filter' => [ - $filter, - ], - ]); + ->setPostValue(['export_filter' => [$filter]]) + ->setParams( + [ + 'entity' => ProductAttributeInterface::ENTITY_TYPE_CODE, + 'file_format' => $fileFormat, + ] + ); $this->dispatch('backend/admin/export/export'); $this->assertSessionMessages($this->containsEqual($expectedSessionMessage)); $this->assertRedirect($this->stringContains('/export/index/key/')); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php index 1109a9d60e613..a016ba1d962b9 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/ConsumerTest.php @@ -41,7 +41,7 @@ class ConsumerTest extends TestCase private $queue; /** @var Csv */ - private $csv; + private $csvReader; /** @var Write */ private $directory; @@ -61,7 +61,7 @@ protected function setUp(): void $this->messageEncoder = $this->objectManager->get(MessageEncoder::class); $this->consumer = $this->objectManager->get(Consumer::class); $this->directory = $this->objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::VAR_DIR); - $this->csv = $this->objectManager->get(Csv::class); + $this->csvReader = $this->objectManager->get(Csv::class); } /** @@ -91,7 +91,7 @@ public function testProcess(): void $this->consumer->process($decodedMessage); $this->filePath = 'export/' . $decodedMessage->getFileName(); $this->assertTrue($this->directory->isExist($this->filePath)); - $data = $this->csv->getData($this->directory->getAbsolutePath($this->filePath)); + $data = $this->csvReader->getData($this->directory->getAbsolutePath($this->filePath)); $this->assertCount(2, $data); $skuPosition = array_search(ProductInterface::SKU, array_keys($data)); $this->assertNotFalse($skuPosition); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php index a3fce3e2a218d..1fc71ffbf3975 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/_files/export_queue_data.php @@ -19,16 +19,10 @@ $exportInfoFactory = $objectManager->get(ExportInfoFactory::class); /** @var PublisherInterface $messagePublisher */ $messagePublisher = $objectManager->get(PublisherInterface::class); -$params = [ - 'file_format' => 'csv', - 'entity' => ProductAttributeInterface::ENTITY_TYPE_CODE, - 'export_filter' => [ProductInterface::SKU => 'simple2'], - 'skip_attr' => [], -]; $dataObject = $exportInfoFactory->create( - $params['file_format'], - $params['entity'], - $params['export_filter'], - $params['skip_attr'] + 'csv', + ProductAttributeInterface::ENTITY_TYPE_CODE, + [ProductInterface::SKU => 'simple2'], + [] ); $messagePublisher->publish('import_export.export', $dataObject); From febbb20f94446ce97c7ba33e6e13feb8735a47b1 Mon Sep 17 00:00:00 2001 From: Viktor Sevch <viktor.sevch@transoftgroup.com> Date: Fri, 18 Sep 2020 16:58:46 +0300 Subject: [PATCH 0525/1013] MC-23536: CatalogProductListWidgetOrderTest is flaky and fails randomly --- app/code/Magento/CatalogWidget/Block/Product/ProductsList.php | 2 +- .../CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 9934cc9ad106a..fa81dab4ef7a1 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -339,7 +339,7 @@ public function createCollection() $collection = $this->_addProductAttributesAndPrices($collection) ->addStoreFilter() - ->addAttributeToSort('created_at', 'desc') + ->addAttributeToSort('entity_id', 'desc') ->setPageSize($this->getPageSize()) ->setCurPage($this->getRequest()->getParam($this->getData('page_var_name'), 1)); diff --git a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php index 3feb44ee23acf..87a76ab801a1f 100644 --- a/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php +++ b/app/code/Magento/CatalogWidget/Test/Unit/Block/Product/ProductsListTest.php @@ -314,7 +314,7 @@ public function testCreateCollection($pagerEnable, $productsCount, $productsPerP $collection->expects($this->once())->method('addAttributeToSelect')->willReturnSelf(); $collection->expects($this->once())->method('addUrlRewrite')->willReturnSelf(); $collection->expects($this->once())->method('addStoreFilter')->willReturnSelf(); - $collection->expects($this->once())->method('addAttributeToSort')->with('created_at', 'desc')->willReturnSelf(); + $collection->expects($this->once())->method('addAttributeToSort')->with('entity_id', 'desc')->willReturnSelf(); $collection->expects($this->once())->method('setPageSize')->with($expectedPageSize)->willReturnSelf(); $collection->expects($this->once())->method('setCurPage')->willReturnSelf(); $collection->expects($this->once())->method('distinct')->willReturnSelf(); From 16962dd07efcdb682b1dbbc8eb9be0c6458f003a Mon Sep 17 00:00:00 2001 From: Timon de Groot <timon@mooore.nl> Date: Fri, 18 Sep 2020 16:27:32 +0200 Subject: [PATCH 0526/1013] Fix BIC changes --- .../Framework/Image/Adapter/ImageMagick.php | 12 +----- .../Test/Unit/Adapter/ImageMagickTest.php | 41 ++----------------- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index a66ba7a8bfd35..568636e840356 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -589,7 +589,7 @@ private function addSingleWatermark($positionX, int $positionY, \Imagick $waterm * * @return int */ - public function getColorspace(): int + private function getColorspace(): int { if ($this->colorspace === -1) { $this->colorspace = $this->_imageHandler->getImageColorspace(); @@ -599,16 +599,6 @@ public function getColorspace(): int return $this->colorspace; } - /** - * Get the original image colorspace. - * - * @return int - */ - public function getOriginalColorspace(): int - { - return $this->originalColorspace; - } - /** * Convert colorspace to SRGB if current colorspace * is COLORSPACE_CMYK or COLORSPACE_UNDEFINED. 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 d109b023d5fdf..b0a7dc31f305b 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -73,6 +73,9 @@ public function testWatermark($imagePath, $expectedMessage) $this->imageMagic->watermark($imagePath); } + /** + * @return array + */ public function watermarkDataProvider(): array { return [ @@ -85,44 +88,6 @@ public function watermarkDataProvider(): array ]; } - /** - * @param string $imagePath - * @throws \Magento\Framework\Exception\LocalizedException - * @dataProvider cmykDataProvider - */ - public function testCmyk(string $imagePath) - { - $this->imageMagic->open($imagePath); - $this->assertEquals(\Imagick::COLORSPACE_CMYK, $this->imageMagic->getOriginalColorspace()); - $this->assertEquals(\Imagick::COLORSPACE_SRGB, $this->imageMagic->getColorspace()); - } - - public function cmykDataProvider(): array - { - return [ - [__DIR__ . '/_files/cmyk_image.jpg'] - ]; - } - - /** - * @param string $imagePath - * @throws \Magento\Framework\Exception\LocalizedException - * @dataProvider srgbDataProvider - */ - public function testSrgb(string $imagePath) - { - $this->imageMagic->open($imagePath); - $this->assertEquals(\Imagick::COLORSPACE_SRGB, $this->imageMagic->getOriginalColorspace()); - $this->assertEquals(\Imagick::COLORSPACE_SRGB, $this->imageMagic->getColorspace()); - } - - public function srgbDataProvider(): array - { - return [ - [__DIR__ . '/_files/srgb_image.jpg'] - ]; - } - public function testSaveWithException() { $this->expectException('Exception'); From a4bfb729e7d1932b8492544b071ca3bd2e0920c6 Mon Sep 17 00:00:00 2001 From: Timon de Groot <timon@mooore.nl> Date: Fri, 18 Sep 2020 16:28:19 +0200 Subject: [PATCH 0527/1013] Remove unused private member --- .../Magento/Framework/Image/Adapter/ImageMagick.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 568636e840356..5810b62304dc9 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -47,12 +47,6 @@ class ImageMagick extends AbstractAdapter * @var int */ private $colorspace = -1; - /** - * Original colorspace of the image - * - * @var int - */ - private $originalColorspace = -1; /** * Set/get background color. Check Imagick::COLOR_* constants From 82565e8df05d9fcc0a735183a60b3c6a11218e41 Mon Sep 17 00:00:00 2001 From: Timon de Groot <timon@mooore.nl> Date: Fri, 18 Sep 2020 16:29:00 +0200 Subject: [PATCH 0528/1013] Remove a space --- .../Framework/Image/Test/Unit/Adapter/ImageMagickTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b0a7dc31f305b..80e6b8484b68c 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -74,7 +74,7 @@ public function testWatermark($imagePath, $expectedMessage) } /** - * @return array + * @return array */ public function watermarkDataProvider(): array { From 9efd600982a6ee715bb885397d4effea7378abf2 Mon Sep 17 00:00:00 2001 From: Timon de Groot <timon@mooore.nl> Date: Fri, 18 Sep 2020 16:29:43 +0200 Subject: [PATCH 0529/1013] Remove unused test images --- .../Test/Unit/Adapter/_files/cmyk_image.jpg | Bin 362387 -> 0 bytes .../Test/Unit/Adapter/_files/srgb_image.jpg | Bin 1692 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/cmyk_image.jpg delete mode 100644 lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/srgb_image.jpg diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/cmyk_image.jpg b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/cmyk_image.jpg deleted file mode 100644 index 27f7230d053ba470380a3b325c23c28ebbe23007..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362387 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 r4eYQE%W} zsn3kJ!G36(10){BUwL#A9e$+2?)VSF_nkp(BF?iWQ4BrRi{;0pCW7pm4m7%v%(+w9 zOInbW`)+S5aYbJuDYBEL>1devU3+xH^2A5Sx<gyp$?C1n>NLfq?}X^Kcsrvmry)$7 z5wXAHTjq2>JMIZaPX?gD?wc0P{``0@h1=qAG%m>X_&9_&xFB{u5wCC*Xv}RD^9z@> zt9jmQ+THBj11@7vY1^GwZvC42lS;PPYD2@%qC{XjROyoa?~CDdgjuVPuDPHHl|DC` zKINtJx8`w*A}bT+E$6sLQjx@UqL6Ia-|bEF+3W<oeYcr4xHm5Q5+8n(?HIiF)gDwH z%P`zoo#c0&J}Y({mS0gZo5t7ep2YU?X`CYxuX{8;=XFuDOcDH!XP_Vq;-$QpYL<F! z{{L9}%C@N5wrx_nQ@R_K76gV)=?3WrX;5H<A*8!Y>Fyp<L}EyhZjkQo0mf%t@B0_- z&x?%>*1&ll$DYEW#vTVsDcXRo30OST^%BYja12ey{~;|eADkFbIbP9nU=^bR2A37( zYTjZ|sF+U<B&emQZ<Rp3)*YY*V&Ct0ry}Lu)|KtS10|;WAH4TMKrwaT%kU23d9A*h z9%DMA*U!0%^XnD0;ay^jOkA5S0EhO@u|?G6*LKd@;2XmncL{(tvOHUo3=k{kKLnWX zhk6upqO|$JkVs;MPMilCVzUt-WV#dB`==46=1Qa$Kp7&Ptr}CoFvvhW1TvG>BMd+Z zSEn`j*`IxM%CzeNbzPHg6|ud3+eJNLWn~dRUvh8UqBmd&yIl`^ew7CgegfJu!T!Sv z;}OgKfaKfO%uLsU*OsK5*Rs!CqlHQ1E6~XzQE>74bU8eY{x5LT{f_88Ea`F<cuBKv z&R^LH_@i)^m)X2n!g)!N^t-u}!uovWG;1NlK=#}Ljw5cd9I_8m5V_X)eQS;V#X!yJ zv&1%F#6scYRLB)&y~b0fye@(mfa2IwV9jF={nHbr#VOJ>z*ju|34u6Esh0Gm_xz<c zglGivADv9Am)p7hN&~r>-NjLizfp=3*8hZN=m>y$h@J%hBd1HEOu$O^NDE&1u8ZKf z=F_V99v*ziCP0=TZUW>kclcOr(*T-i=t&RZ+_=R+qb@8_h(ouX&#CP)#^3d=<a3$s zh4TK->Er!8-#m>-$6l;0M72L)I|6SV_lvk6Iee{X<WFq~RZrlVY20+C;nqjRB~4Kn ze+&-3^l&SyKZTEfZg)*3tz>*enF;g*Wk{Q@SvrG9wZdj^*hNc|2Yd-YJ5$bHI=rUB zk^;R6e6f}KNW^%&x}FEnxrx$#gyfImO+CF~bZQ0W%W=FBxJ+G~cW^Zl)gk&gV5M1X z3Gw};KWcnyaC<GTG{+s4NFoL4n~K5$j8a-~h>R<s&R*jRHn20!!Ty@5xMf0iQO~=L zFWK5NeK64yq#Ha{yZ*svVx5o*+x``nj#WRtZh(Bo6+i+LxcWjQTLrU`Hh~p5vi}#K z@OR8!?6>U}`N=DWf%QNtdl4_5Sb@!pY=FGs9-37ltt`;#yL^#!jR%`rJE@*ZJyMV> zpaLEW3)#k~$E5V8;tWD+dSI&_AF{najOFr)b#>f9(ei#qm9i!-R9-n#=b7*Bt%g=^ zc#3Iwlg(AJ@&T!$?u|oc4Uu9!p?0$V(hbI<7#IgbnR&PLTc?#j_)_&VCCD5;1eRq2 z!JZ1G%3L<HjD{_Ic0{Ny_<Fu=8wo(Qd}QJITyUkoh+%KoA<&3;a^tB9!>;xs9BF0g z7ZusMRBXXsa-OsdsfHN<7s8F2p&fd${wKr`ND)e}?&1vgr3&)bkmz{rks<+JirnV7 zn8_DfGD2^T7X&h_yU))0rwc2p?c2WNdg=jeOLMorf~8m%4u_|byh~t;v)4cyXnO7b z+RM$BFy<|TM;SmwQps?!%+#=Eub3pdOo-erGgxc;Uk%f6tDrssFoYv+^J~(fvAS*Y z6u9BVzyrWCo2)+7d(EQv>lBqm^aJ{n6-kToSmnofCG-|DkX9fMWbOG_fRRk7P8x68 zwHEp!>ly&MVvWLU-zZPJlOhkZ^(DZzA9W6HedyM)T-KriS5brOM^#Ez_Y!>;5k^-- zGReYMqCgmjaV8aJgP9P3BNhe3KfG^XrXeSLu&)DSx9U4(+<&)4yDBCwQH&Q#MdwF> z9ZFWNZ%lNkGOIE`^@hd$6a6xr+tNpHGD(~LF$grwx{pmq_ws-Kx*VW14*8LtX7ifL z(Ah@zMz8zqxbp2J^yLXlM&DFPi*`%dDCu)IQtN8&?&TrV<r^s5w>KI<=gL^wuP0%h zchAqqPo1-3L!7r}Tw>|`+?cfdMvI)ijDZV1%L+6EmGw_1EA~IuVUYIaL7UkP3{A*1 z5duXl=i|fYJN0?VNGayuscb#R9@kIs3XNUam1fG5qd(8+YN;VqC+n$WpV)Q_m0h%+ zaGVu25`F=xvpe>qb3=zhC~CabJ|C!JQ+h7&)Z9@$zfu2JmkXSw_UV;)4SKydNA!1} zQcv$b<4V%MhSJjGx557C9OKfl5v(XRXgZghOQ$8vJ1i6?lc?e>YxOjL+F7DpIbQd^ zZm6+ho$}E%*_W*H!AQHchpq*%Gz|6WX9t_-Dci-apjmLjN>1>t-uv?k*_?=(O)6;* z3eoF{w-yXNJ)+!6u2dDK^~-9+3B9{V#;ue9Ew^;X+}?)o>bSE^jRn#P0*r&bQZ|PE z6ox9c*r8Li4sP}t<Ty>7zJTTtS)gSy*_yG)_lA(y{iJe^J&yUgr*XCaFtM$dmd1!_ zl)g{B3@o$5#;@D_^}r}y$nC_Ds1%@LAr8d+#Uo}d1Dt&YdWHFNra&(1)a=J3jn}n* z`wOUkdL05r+0O<04@^eb<v+{JZc}Qj+$e*>iQgr%>Ug@Y;DG?2L*0ok1skqIog#Vq z0id}o%%32{vhf@R4`u%Z&A}dJ&)rU(MmI&`O4Ba)*Z9jhivT@HHbrIusz#iTM4dfD z%C+zm;E*$4{=pQ_7do}mrOA1{1!Vj*0Ev*<%tB#Z24JD;C7px13-F_DiK*hXqP~c# zww(2|nJIY1sVs}`t##skVVh2|nZ^Yiu1G)gxULRk8VYOH1k@4M+VOVTAQ{HAA&6Ey zKb21Dx+#6a@SVP?5!-jMXsYP9fM{HAf+EA&vJ?}yZ4A)F8Ty#N_Y*VZH~g(AMmq)Q z4NNDl8@fUpr=)-JdmLq7_iAkpRXYYLPRt))0hugI+9jhEta?hb2fRP)<1|+iiZFT} z2^{R3=Wu+p9>qL4&@5<ufD@q0!EgXKBQ6%*g|1O3NLs|=$9H`wP_#w=Cz@pQoG$P9 zdQ$;+zzKZxpi>*!17BG&IXW@v-cad|n9m}4>lHWZ+9g+8RgM{^4q}%jat;y9ZvQCx zZGE+dQsMDK{|pw3j07stDgt(OFrMv`;J)G_qaRt?109_%kCvHjw)`k=+4k4V-4}Wf zu7l)$_x)aW{@C&9PlBG^VxRUWGlqhz5$8{VGFeWsT7iqQPp>L1KB~<<(R-YA=Qw@v zWNee0_dGqg#ZIK^v3QX;QT^lb+f|dNNY94?o~T-w7f7DhL@IWyxcVElk@lN1HD_3b zm?)sO6@fz%;E3zHBt2%Xl1#rG*@MPLBq@>h1`!(Y-g5ffMdP1}qkVqu%J&XSMQD?4 zx+wCFJLb^Zz~$%Z(*L#7XXbyiaQz3Su2LeCq7jtagEnPEZs6_`&2jJ`I;x0!MU4=( z>b$Ezke`4bXXGDJI-5iTJto{zFbu&SKfWCoai~)Lbh(0XtIblohnwtc{P-HMz7Y;& zUvVZujO?}^A+LUT2mV9)?BaD7zROx`9Yu0UmjBe0D*;}-ON=7dsLe;FHy@&(Ug<Wj zOz98u|K)uGP;K4U$wSPxcox3~56j?zN^N%?-vw*MCiUilf@seJ+H}L%#7^Gd8R`H5 zEJ9O3erG;fIaEm$DGG?`x;$Q*C|iv!wJ7gPLwcF!pisgWFS`7tH<eYAIWGZdy<JCp zpO2#9Qh<)27~TfGA-_#_$4#o(sCw#V?eJ2W2f%p>_v@ZhF6AP5Lo@rbNJnk`5ge59 zL#Dm;{CJV*jZEF@AcDjRDTzUCD4A>biB<y90ImmSpHfeS)YON(aTo*mO*4v+3rcv; z_I|ChPa?45X(A3vIO;K%_x0zw{Oa2=o*{OJr$bcj%*9jclH>qN-HORj>Bv^WK`t_l z)0!CIeA6-z_MF<E!t+bfKarr!1kies-UXV1mwH?oONz=X^!Yko0yMxX>x;ww0IFY4 zL?>S^6kLX<SNsbM@}f58F<6Y(bJiC#78j8-VLIb9AwJ3u$wri%^ISio60V<}kI~!E zW0Sj-Tw{4$@(z#XzW@`ozrrc!02&zyPGvSv)aLp(A63yC6udE2#PUL2XD+6&>tT9+ zSXqS2p6$2#nGlu%-7KLZQ&SZ4P+0wYwiKDQSWw^0G<jNxj3P2Zy`s}zPj_Nv`4yGH zwD@XX%XYjehoR^y(a4-<aq1sRlYP<!kJIlSZz2X{H5l-y3Y1Rqg1Des6OXljAq{uJ zJflM09r1LOK%cg>^YXk&QMUcFV%5(WwRQPlRgHvd&&Dwg?Rz((Pmjtre0C%?Qpl^b zK#Hpq6qNI*%EZI>opM5<HKSCDEWxO|Q8_-fWD<8+wQi&+{=*7RMH}j}yS|*&Zu9=? ztJHd98-RVVDK{aQ-g3)VP4Sw5SLJypNnuIfcbF1Yed)MsJ#VUcYTjO1C15(Z?xEi# zXwkS2{2d#|kqQhjaMT;POwu@@F#%~qXuxgqoZ15FUH_5q#H{6?KfNZ>gC8`}|J-^G zzKxLJ5qyH*b&W+_^aVoZ5nUGmb?k3t#tk?gB7)b*fuhGuF6sc{pGg)xe*bZ?Duroi zvDceADv7{|x&6Uwus7kz44FMXfITKrKB4tV;pTt`DpLZ$z;_ZCUi*^#WV+5R!Q$tE z`RfN~mS*Z05o$2ck}v__5^K{|+mlOOH5MfcLi*r|*>Q<=r>|Z5b>`Z_NL>>Mw69L1 zf4pt|GUvM+O*8e=hJL%L$ab^Lk6c~ZVauxQ2?AR$Hf0rzPceM2f9F?mtgo@#P7m~U zIhwcFlm(O0Yq2btNm3|r3~nbD!Jj3H@KmEPCCzsZwmY?WYM%dV;r?P*k}*&MQ6=n0 zelCF<=DHy6&fK^|rB)qZN7a~203_}PCn$|)N45NQ+0@INn-8zi$nWZXuv1`VG50|v z0IbJRkeY105jV{8f&QqsjqZs|5qXwMhj1?NclTMwk}_Ljm^3a~ToBtWB6MG|`(~q^ z%d!i8*WS39g>s1c<Z_MZGI@r8X}o5w+Z}hU2rPloVAbOd<p`(650z4@F}zTok3`w? zwm8r72|XHcPmcH4@iCu^WI4+K4fL3SoEhdjzZqpg?GxKFFCK+2?gCcMdg`h#=^u1v zgmPp9De@*7soQ4nBw&J)=)Qlprk{Qk(Y_Gv?&3yha3R@xm%cSn3pC%A5Z;~<)JlG( zZMX{QO_yW%FAYqWbASH0+~*-{(lVBxG0M9sXe8-;sK8g#4|TDn2E7Itt;6c>#pqXl zGp)QLwXoQWmjdk^dcQa3Sd3dNQeZ#u)Ft)>D#`^@^)z@miB%AsWkgNoVkqMUKE$t< zbmHAk4ilwm{9S5$6)FbAhEG(LgD;D+b}%pMn{80=296E(*Ul=s{n-Kr`pEF&yH8~{ z=oF`Z9bF@#kPki?61n-ar0Bp(&4ECCihBSb!f+eFMC<p1Uk=_;FmSSXA%N1MvNH^X z05BB%ki}K%sP0v(F7QP~;5ZX}975;tH7Hi=AWJ-y0Iy^8rBW^0|6PLzMjRkemz}($ zIbjCWk*WXn9X>~Dw1)C(S#tG-mq(=qp;I$!sm4<v1^B20^q?lr54IWVMJ&N2_-UG= z-PxaL!OLQQOUlZ$M>-bRnk+qkN1j6RJCg$*;?vl=xikExc9>oJfGQld=rnU3L%a_` z=s#LPQWDLyG3B*ler{S@JiyeZ*(+Q6-7ZS+3rhd8!ORMx3Zc<5h4|()8XsfRc7ZkL z0VGepNuHtSNvU>Z6w|D1*bc-(d;Fsu_A(E8d06BV&B8%K7Z?CuPLrJCKt-P`OB@>m z)D0$D!gME7SnY9{nX~&CLqJ%RI?a@DvAhZLb0UQPPqO5XQ|8Z9q1MF_o&wnEy+><* zZ7#+*V-q})UddO3Fb)9+MXS{}s`L|#oxYMe1E~)(RlAP!-K7jwf^>?BezfwN8U@at zNT_*bJ$`k|lPj%2#?55O+g9Ko;aGd|F<ETrU~zl;HP0VI_pPhSdC5f3GPpX!j2hi; zT+$R-G{Nqi%9`pAJ`E6F4<3I~Ps`dcB0z)K5T<jxx6*5!jDnj3jKie%UvOJ8HHcax zfDNhQ6WeMYdb-*`^wgN0fL`Hh8h*V-L2Z8UGe#4pOaE8X{&rf8m4Y9=5X;Bb%UCo1 z!ot+LF&zo;@~d~E2JN2wtJyXcWi_RbeuOGW7nOyTQOrrW$w5(NhkJ`^r%SdXpQQqO z!sP?kGw|a-<0a$8xIvi+s~am`f{p=kM!nL2sZ@&E44kJcfyHTRwVjRuGADB9*+WZ$ z7kZf6(EGPh_kqSGaILbsswRYq_A}XsX}wbpZiP}OgM#4{b;rzfQ-<?m0^f`_*ZEGx zws&?jlloITFAWe**vYe7eIw0B3<WzmypKz-tal-#ywAnaJ5AC=$liY}b}|I(CTdv! zA!X*dePSbc7@7Iwy2SO3heY>;hONI~NGZ?Oys2R!TDM^IPa;l@<lib1fx15<cn@^_ z-%dEM9!RE#*bmk2q42r8w=P#NB5VA6@g158%_Nhg{vlEP1{`rM8B(tIP2ksLEd4|4 zzqZWtP3RfO8`0^_2fBZu2ElHS{NRDS*Km3#O!Nr3H=RgQZ})AI73{18_pPu)Dpi8- z+bM1{CbVGUudq3Z@m?RG)iWnjA4+7qG~MfLly6DAHB2$yEBnaZ>=_cFytj|uvrJ@p zoP8UP+>pj(Sdmt7h^#a;>$`NvgLgx1vUoBTl`4kKLFSgHQk}|qLRqJ}U`zZORCNH` z?Mp$dFQA)UX31&eycVIK8}aQ9zWs1Kl8+jF&DpAlEoo|Zn^i}(s<$rZA;Rl+jC$R3 z4}soJy?ZH>67B8dpu7dE1KU?TS5V5n$)Yt)B~>lw_PA$HaQZT#*jgqI#0qJtq;Lu( zFqa{%t0y5Cc^~M;YJxa((|yi-^(tbkLrUjmTuLDQ0Fixr6oi?7dH6H?zsnh3X&2`+ z07`vd$d6L<Yd!e7P2}l0pjl6uy-fE>2skIakBE_?q1i;GcOsqvtV-;UvI&6fG)+!P zkOtsS7b*)zyAQV8Bww%&M2H`;hd}7n1MVS`X~a^6-vWSjs0Xd*bJU7q-AjI94tob_ zyJz@GyJpMfR0yV6S^uc^>w9uU$Xv_~bC=?f3fY4NT~4I_-%j@$f(3c*2cNji5Gl}^ z`Sg+&$`B3(3HMj#Fl0Tci`V3{1-^Snt^qUKNWOG++>Jw(d~+I}g~8h2hP)NaOfF-7 zhs{289|K{^h4-G;f`{kHu8b2|z%XQXTQQ+MxT>dz9nS||ClXi8lQZI>eNV<<a^uH! zfxGeyq5(5QeZNpg_H0U}aX4?UD3<Nb3TMp!(5y8R!Moye0wo=y#a5V7(g;kC^k2FX z#(|EE_ZHM%5odVFr!DzRzJ;AuOM+2#M9Y-*b3+B9D+e!&>6xU3J{ol25M#C97G<}F zt+qNp5_U(Cq|wEa{&_mM+J}sqT(5L?e_ZKLb_8>`+0`JA*$~{HIKA}|!Y|fGRF*xr zVXVb1KRS&!LvwGIuKKSpPfRDi{Tx<@*HLda`gr5w4Y$i`00rVdoZW;~oyVY^-Z5!j z=67Lk=h-*cC-tuE-aBhXD+vQWmNdoi3_UA|v(M;)F9c9XGBBXa?m8pr;sAZhe8<g? z{}HS8zpGSQDPPS<_H8oEmA+1LyEzT$_`5&z0-1i}V8J2zg_jiab2Gs7--X?)M!u&Q zkPI%+)Rmsk?1&+9)-c)0l@kw;0t48=-4w*Nx$k6;RpBR3$e$Va&F$!E3{uH}FT(-H zcgStvt5dRG|MpJHN31e<gxRg5VVuqSAz)Upl460>-wJKa2_ks|Q`lCtL1N}M)=Q)} zOn3Aq0U*g09!v?-y+6%1XFyW-LHU}zRPj(?{%eeQWr`^zX!CE%sqZ8jUM1OWz+*>; z5)tpH4-7L)C+<au{Up~toeFgt5fg=Xof9;`H$OO7s%(%(CehN|x<)GP55B1fM3Akv zRErdQdCSwE1;V29@1`SwQ7_teB^o<gOw8d)@P3sJ(<D9J2=h`DUNPsl{VdUy;mSgO zQDTsvcKrTu9`Aw<WDl^t46|Ai;*%=zoNLZ1He4!+{m!w5)U;+}*(eDl9Oc}38FGC1 zQ;th&iU29#m96O*%OR)AIrZV0O6nC*+>v)netVP{qc5m4<v7k_wE{qkJCCFhG}zqA zdzu{>Go6GKp>7jUax%j2q91*v-U1`BCl_C2Di-?taZ+`nlBkc=+5u#dKo!a3N)_o) z;)p|TPg9kR4)DPZnAHwX59qv-q>R=X%HAX<!=1pq37`&<>VdEIeSx_)F2NW?DWBTB zZK1kCYK~^fCaoF6(UtC4KVpKd-H|X7ZX%lSx;eP`SKYcDIQ6niq4uX-4tly20zeyH zypUF)(P9w&W<cL^cz$vi?X}>egQ5{ei%EbF^lQ5ex<)DR8MEusCa<-vwLf??nD83m z;R!)Lx`;V7Xr49<;3>Ff#})tOXE3rweGY`cR)t-R?`?{sDvRm#!hxD4ax`Q{aTCon z6<}}eidv|(=&WZq<RVQBoD;mfo{%!Ltj~QH9?$VR5mpnn$y=+(;;3DqOHgt;xciej zoPK^Ow@`(Xs1BFkDAdV$1*0`D|F6dhPjvK{?kqQ~P}Pl&i&ICv=}~DS;;{!5Nu{Ec z*>yTkRL=&N#mCS0rO9)g6LCK-_m0hLD&VczLc$Vq@;P&ejm6Q>(cJgpK(=(7p^iCM zdI~-%wf)eLf4ML1dVG;D{vR+r{uqZ!Vuzm93+KH@$^oLa%v}Ut6zK1ta%K_7Bm!N5 zY9y%K#s?ME@{!_s1w9~hmkg950|R)GX3K=xU0BHKOJ?o@$_7OGlOdhkN$y+PkM#^e z0KfJ_y8wu>Ej0Rwct@;+7=|07LjHewe9GKr(iTgwE-iS?3)kr)D^y|RMf<6WfJu1c zAv2KZQLcc5q@hB$r$d}i2oN<(5Xxvm{Q410`dPPuI=6{tmn^N|lez~n#%{DW0C;>v zf6qWUhC5D`MOhb1nIylsxPwRI1l?}XeA@%<t6*@VZw<Z8Mt;amM03kCCB~Rvisqt{ zAB@e_kHq~F+c>4I8@W-tLI6(}WiOlN6{bbO<HvEbKInmxbL>S<Fl$+}t2qhLgPIK{ zf-eOR!;Oe$^?nl&?HmD^QRvB0z1z@j&iI_9uHRAm+l}zot%H+VzZi-JbrN}T30ImF zWa2#Ob)e;!0ZlAZD;?rhD((j^4VWHU+E^9CkB}k2uUZ@FofbrEcSQ5;Anr#=Bq$zK z*8K!iNE(_<MkoUB2=gx=7H-0Jl3^2I&VdxP^hn;pYFdW9Dgce`sgD>(xB{5tlQ65F zSQOk{3I6EKe*k1Mm(5TmRY`!?*ustn-;U<%)MUQVlZNM<yY`$sPhw)Qmoaw~-3bA^ zH5FD#R7kL@*g~^<B|<OugK(l$ea}t99hBjqfL21_D83j;8fbg{*4VQ7S<isgH21r* zKI%SAQu*?wM2VFuiF(pAbpt;4x4%Hovp83ax03Zm@)R$Bx~As7Ig_bc?FV+c^(*6x z^8GSTH+-iSe;_pDk8U+L^cXjZ<;rErH{Exvw2&3ch_q1Lijz3piYzmwwD00LYyofT z8EY65<9BhwC<t1}JuDaJbR(vhlaN6Gy3r}cy^f(Quy^6UONzO2&WJ^Z&22yOPRPmm zKC2L=-tQ(TgqIsm<q9&&{}KR7GLwcR$By?~>)R!Ae>B4$2o#R|tIgs*qzpD!)X_`k z=>pWE4y2Vw=^v8sU3LU(Oo1|UrRG>oNie85z2(Q(WaOuSfQn#;Sj<y#vFbU2ReC}v z+_%^lM;YyDq8v^;A=%k)^psTGJEU#JGkS^>A+V)(-Hv`j-O#+nX&ifT6n!6NC~%r; zcA;Qy+L^P|22<kr>$)Suy@qcKE8kx1OHwBH>Affd1hZvPrZ+scsk|-jQWi*0N!QG9 z@FYrj=xC|UCn+D0*&T1gFAdY7_J(bfoV>EuStTLn;zGDw4ZxMo6RB5zIQ-lLue)h$ zcrssZyQX!!`<6*|w6<-HE-cyM$W{{H7|KX+{`6Z|wpibKyCI2KT~Lpu)kg&B4b9_< z`p9qWPc*vI%+F@syfL1sXh`3vLV>VN5#R@pNE@K>PMv)-!5fHOGcL)^@$-!$s>(*y zs(i|8^yaVPjfOS7x8|R4b!wALT&cP$-7#u4MoT>3tty<7ryy(p@P5f7sXKhMuHmti zEx;bGP3-V1Ja*}zE|F>;``1e7A$`)BGjV65XmQMYZ!?=DLa`9dhyOy&(@Nr*eVEpR z0wbQcmIgGVNg@wI#p4q3#O|PP<v2RVl7jruf*(Zw8Uso~uwZYknEcz+7BXePQPPzl zu6%9#Ny{^TSqahg?B)jRj^j19uT*qnp4TP>M&u^lfc{Tf)`wIY^KksYZ;vxUsa!Ao z{oi&`FyyXvpCo%<8KI9{*7epuQ3EOYhsFGacpmDph1NtfY6L{{-G%(VQ6=B4<y_cQ zn=UL`f%H<7M-5i@qRo9h%l2ZA*>C#5zzc+#;J1oy;&I8qBef-=EkkW>^>-y`*hG7i zzssSjimgdO6`;Sg;KiJyxQ+N#L66Dz7dm>=(8%IyYu5fR$mJX^`fwEG+d@&ES1f?0 z<-q-gBm~q{4$gkiK5}Yf!(1JmZP18_^95?KPUihibP@-~=yKnX*<deGzL*6QyI6Z& z-+{}se-p7fDxfCR$#?a1Oth{^8~@$?k$JQ>tY?ca>$DwRna^Y-#Nv_HuH=fNswOhY z3QRK#k8b_&{V}CkpeJumc}c^uXEARMmjhF%mw6;&w9W*<3H0op4oQ-oDXSYuG**`Q z)f*`r_*kKxHnG0YPZn|-CKe_d>0=~({*!+x8L^@^(betf;gnGvCw#K0Fms>f{+CXo zrK0AAKa|Hy=r;_g&In4O+d_u=R4@=}^PSH>$5(uQnE7J96wrQAe<%N*Z`!RkcvQCS zLevtCNY~)xeVPxt@FvhcYQ+aSe<0*(_-+uFRx=8&7azbL<!R*Lfudu=>42)aQ0Fcq z#<;X#g^eq^ZX~}dd)qt@_pd>{kT>#*OJeW8(4A$|4E7z3S4p@?Dm39iIUCgeezx8U zQ!N^Ol|)U!V3^L5=V%a>6deR9fCozE<xMngTgW@lI9`XbxGzW;tL*J_qf&X6Z)maT zlqWEB&rmydtmjaB-Q4Dl1w~3DDuE8rg-5<dv2QvF??+$8^%NaVWw@!Z;p>G8e&$E^ z)N~e8-dw4>zBPY`N{?<!vs)<q>5{k_?>ayf4)?v~jT2WD-9n9?E~QMv?Te(vO0^+^ zJ)UVixh<-VZ60h@rccG7=#k7|x4|zB4vwpSoe!fX$k2A2L~~pnLYncsZ5kL%H&JCS zK8uJ0ra&;b?nPOi%(AFlEcXRn_X)Sr-Y3mH`K2j5_DL_hc1>4`r`Kop2{Jf(7ACad z6|qR`*WA?yET7|Cixc$X29qnF=+B`|l%OVB9^kQ$60`gEi=N_^e@IH8yx!Zh{|7$~ zR7VS^GO1UopwB+*n+c)UQ?NWv9t^PPyaYNy6iMYcC2QoBP!B&!iz9$Hb7xj@0|YS5 zv8bFCo2cik!E!vT$6?*NFHqVgKraga`T;(4YOBs;0mG#-nLk-^c*Y32gjbDLhlCIr z#tQ3`g&}3b9~dehM53hx-CifXJR%)2!qoDz{)a?#(yF4&;atU$33$ww4rNJ0;DJk@ zi>LYnRJ!CKwV<-*SZt;9A1pe3)1^Cq@$i)BoA;i&ZRn*D^><S$zh2JEiC_*7vHncu zr)A&Q=*ZrOG?j8H$O6T4>n&X*eb2CCWA5fjqLxO4K6-ODUat@hd3~Ef0y_*fbfVFB ziT^QN;mZqpD<z-2p(OLm>z&RwT}`(4EdU4^yH`4ru3J}_^gC@*H@jM+YLIRP^*N1w zK`zNy1$%C;PD4wF#i3eNk@QdDjGRmFYiblxc+vwMjN0*hJD2m#?l5`8eJF9?NvXzY zfw4;A{O2Z}b#af%ry>9BfZQ))o-|0Hd^(^q#WBF|wb>KwI9(~3M&Xt+@MnLfW;O4N zeb|5wKR)_)cqzNV)(v=<K>Wm|+3WKgo}jSwjZA}G<@}?4>QmG4tsKeIuGl<HmK;~b z))bb;j$B-IU-wNuSncz?NWJJ0C;WdLx%WS-kXr5%)Zhydw6#x{j}}B)Elv!Ie*Iv_ zK;WeJJ^?TJACe=3HbLTy6v9+g7rn~>I|OAr7YQJR!AIA>SKb+vcl5Y9%6LdXnmM+y zymc$yruoH3baLPq6Z-9~zi$^?R^+(cDIoP_z}85yaHliRGv{2pI-B&;Xn(ru>Mvn- z`MvR?5Q|BKyPQZ`RwY3i23Ta%lV}axG3ZM-ddjxl1ERg{6SPd~-=hMI!djdKL#PEr z1iFP_Uh;exI>3$L7Lo-PCpYgjAVg^c3)@I7a`$f#Aat~4jKe}bIlyhL-H1xn0n}lu zn;DaUXI=zfQ5a(%i=6H5ZqK03CgeL=ho)|B_sL)`YxvvRG84hLx&sSW@8u(iLvdOm z7Ztyn|Lbgpr0lpQ;41y{u@tMot-6N2Qr&y$j)LB#rUn69E3EpB-I{WB!22u8DhWBC zxem<tc9?`Y+|-UOWl)j(Gg<)NKO}N!gS5WkWYlcJ4Spo<3(7Pf)Y77dn}HnVmO2D% z$?`@kqoN{!jre+INV)-RKJxQ?-Tdacfi!#DgKJU|KS`A+Q&&iGt@FhuVs-|_$dQxn zdr@PxtC9)ltOc@mVE&^Ci9Ub+XZlO~I!w&p4fBCZSM(nGb&O(KvIhxC2k~pAZc}I( zTX2(AyU4KF5BOCq{t7jw;N>5^HU3|IgZ@M(&4hpAVRN)jepAY~^%RQeTNFCh09y+% zOmED`JKy?yKT+V|6@571EtSglrOE>?SD~3v0Q|>S$ZC#vm04Em?#`_auzYXJ{FQeX zuM%$Loocyd3##rwfIu3)cjFl_*XhqPR(NTY>{lCe#*aoVwGT~#A-3^<Lzkpk=tt)x z_U7{F$D!UfVvpNGyu7$R&F+0hvh9{5+GB|<p4Nw>G7S}U<~&AJHs2CITBAd>?lihv z<_YFkbK2hMrf^x=e7~c6MLe+mfz7|vKbmp}#}e&aB}HyBCr=)Ru`)hnP}B7>X~kHH zlw*XBUGNen?fR+yoe;d&`)d}b0!ns(Zs_g{|Hb)@`Jpk2k%@#(jE-~C8(r15a(37r zU&{_&pn#W!Q^~;Y{@xp8b%QfTN6mct7B2I|fZ~mik~uY`%#|48z-$75UNJwv<;qqx zj0Atuufq;~Km;h>;n`AP^@-%8c|7_H(rs>2{V=B`QM%A9m}S9M$g=4vNy|us+yJ=q zbjyfvm{D^ApA3Y@yLJ5dTJHk|GfEyn5QjdAvFj_CtnX8&qs5?tA>H7a6k`RUQ4xE% z;uhj+_D7}R4nvjW(b^L0&pzC|#Wa(-$<)tMgPZ*^h8Mt{`8YVDh(nr6|3>kWfiCW( zZs*&l(roMYSx6u7*Ix>y1VkE6Oi4gF`oSIt))TiwYZ3ys8pA2{p~Uy8+i^10cqQ3P zEaBb?@V^iZj8*^qHAbS4pEzVRdy1*aytzK}sE7A`rD}5kzZ~&LOlyn14v2j-JXlF} z^2^GEyJ>JRg0Qh<uj!|kOGmv(&u=f+PyXmU??)p{g?NYb3jJ=*+>~3L=Rj@7Mq~QV z!&F$<a(+m{`9dhbju+SSiK(ZP8sVw<4@uhl!)K)}R@(h7;GKA?wclP_`C$*%S3B0) zoy*PNU=^hGURJ(Em7>Z7oZhS@Pg+@cDTJK&;Z6>qJY|Ug(u7#yZ&j+loa_^E+!~iY zlZ^7i_4L}1Ww7QzaM^P6pWUX+2QKF$D*>xdV^a=7B-P_Ie%~5VwA_yva1)ggz9G~J zpL|bh7%xP;r$z5A7>o5m=M%UzxH@5YzPb1%$DX25T!hgS1xS9I`MM9E@^<D4uJSbU z;q*0UfDqOH7G{t*WOEd!u5VLS&Vkl~Pvj^ohh%J6VWAtHo6hV;#en_Q1J=5Bh{|iX zBE+Z_jGIreAHRk=Ix$}w6<sU)M|T=6JD}|-lbrk(#(zklMgN4!QlY@COs@(k@Cn_{ z%ndAeK0)?Kd#*85i4A+D%pZK#TlX)nn%BC`_ue`v;$yP?rO&7MhvarPc-ar>FJ(tK zs{TWg@@_LTL7Uvv1{~feQ@CE~-^G2fDLK$XQU)GC>I5A)%QT42e@MSMA^Z3-aWj;~ zn|YQ;YtLHopO6X{Kaviv@Ch>y83v#~fPerJ<yXDJQJdJ15`-f2@z!*ekNpDrtjRMc zpoI8aR+{}T6faVk;#-^L_%p83f8nOE=5G0lW^qWA;uQ-%l;?Isnu4k25>WTP0koE8 zw~IP5R7S?x3d;0P8+z9jpfGw(%)0`|c46z~QG$mGfUn2#?x6lINv-_S7hwiaV{!}r zHC#e|*h9uw++^Dj8{SAL6ivhlWw<qu(BoO@{vT4ew|i$=ZBprC&^6$Y>F2FcpM0V9 z7=J<@IfG>FBTKF1bFLR^w%HGM+lP|KUh~F2$&TimoiUAap?E7AQ;4FJ{Q<Opf~I8Z ze@<(9Nv7MKY7+D`e~4t9GH0>P^|H$Dw2{Pw_CA4e-m{xL=0GFf+YBO=3r)?6K8K#{ z^ShoHV`ZJbNlvRx$$g1BWbp)7bxoHmD{^E}n&swPI;$MKA<Q=kV<0xUIz~+nQ5=}k z?cyb3AFL5@OdPXuCz0i_g2D6ZnjHF`8Cs*XwxypOT58ld`iWHj%$MxYQBS^nWnqC| zA|r){dW682H04AcQAH+R;6r-AU<g#gc0oYv4?7*_ZEVsF-#dNDekOV6$mNGlPB!Bb zh|!{Y6I9{MK<XhWXb5<lz4=;Jk3aqy5EtkqlKRDu^yuV&mxYD>$}k5>?`O~Tj4tx^ z?N(erj7-i4?{&(F6tVx31Wy;eZZ9(wek{>Lds6a2Xv|8AzhkNgswZZ3yFW_pt4|WS z96X(HKOVT&$sb6e%zP6vHzm4zD<X1RoWn_4EhHIKqAk>xRN{O|(YIrQ6bO}Eeis)? zvT5q*e~vxz^oJjzSAIo{L1Now`zjJU*;7d+P+b=Ya`#zMiWlNWd8N4RDOr>#nY$hP zm2|R`B59bnLjUEf9#gpsSBZ|!upCFjT!la+-;bcIjoYMY;*ayO3N2C?*MI&Y1y{|3 zzL2~l?UMODf<A%t2703{y@?nj$C-;&y|$72GFm=Ss&p7?mtLlg3UoSryq5Yp2hUsv zG#VX^AL^bI{vlN+{X_a=>U)AN?S=znf+TZgr5Gf}nHL`Ri@Fss1Q74#AS60?T}`FW zIkldo5Wa$-jQ0T?GD56Kz;Hj)Y23|Jo=23aWfC!x76PR%M^Bp|nw?^}?oBImwfOGB zR>dhS`A2T3QPt5CqHCapU_QFVXP=mscx$J0z{E(pb?cX~xYJiH5YG1<WU2xIg}eri z9Rg2&6L;%U6(^%O7h1*KGMp?sB%6y1KHM}<5$^PSTZhV|;{&GUoX``&<X&r{+|%tB zHJl182n#wIfx#X(WmgtOKb8-iPm*cZG#u_yw4X@?P$+@+zh}t0$%@fIEUWGz&SqS% z!=fvlkjDW_4)r;NdHJaAq;`fPb>p7pivGa=wFb3@H@lwy=m}?vW9)D1Pqwk9U;Lv9 z3bvPH;z9Gb)2G7hkMH6<{OZw=A3XLChZ(Sx@sxqN?fexqsoN78r<EhqlA+nd^a}tG z!2Y64InS^*wD`B643(F>sSNtGFaUkY!PA+6q`+81k})}%pf^4Ht`a|}6`i-QSC+wb zhYmCn07%*uz@6c+nyUa0;Q^!ugnoBzREt=uF)i`>5|z@dQGd}vWB%+tl2|yeErBrG zbsx$*pVS&|C#0aIjCfzSk`n~o5(IO+gbnwJvn1kPidG1HN{>(-iyaX3D9>3JJQ~?% z6A#c-LCo{TKLm*GEk!hRDLt8^(WjT7A*RGgji0r=eOb{#v+>=>!WI5qJ_hUB>1APD z$u=?{1Xw=RYk$RA0mLT9wayZQyZm#vwy$*OwS0eLq*L|03ZLwyy~z*RMY+T6kk!@x zs+|g)sF^NFE0il?T$Z0nx?XkYaqZgK;vh;UyWg9@0o<Wsxc%UbnGxUBYz^llGJAPb zRy(~r^rEy8X;Pg7aHO62LQfY5=T+r%3ew{X!~IxsFDhm#nXIjfZeC!kojETADBU^R zv)$MRAJ`=&F(OE|32Mc_sdcrjqofymyl=#eC2`C4KG;_asBPj{R2%iEDwTh69&5x| zH;58t@KkKVyU=isWdn&U=<yAd!Ax3P50zQ0W_g;=l7?7bknm^wt|;PEk=pSD{0zJV zhgvo*$u=1?Pq6$l7Hh1Gv%trYD7K%ku6XPL8+3Y|Ni3ZfPV3Lw!W%`kbwwV}c4x+Y z?Q6VyHe}4-3hya>0TgAnO$6`87oPW|)zU^}@nw=<{|)eq9P3jPziCc>Q6b=pU7V_e zKBciZL@)!c=b?LV*I*g#m@F8_!ZWfHi7n)h$+Wj%pZIxaRX9^#BaX130t3Na_I$F9 z6R*2|_jXqgZju#KLK?@^Qcy>7YhCGrFzI2f;HLg)?PIgQSCpI`USl;hMJ2kfte4oY z`<|5AkObm#(`;*4Ki%7a4w-|3G(5jium!86m^FO5bCFMq2Q1`EZgr0@`IBlzN0oZ} z8+5UXrq*;RaC>SZu|;n~C%DPj=HMRAfk=qZg%}rRJ%8MFWAX}p<iNhknuT_yZDnTL zPmZJciErws=Gy6Xu5a;NbqSCa5=N_=9AB((NTz@<1L8iGOV{B?*~bo2l6>$Mur48R z#tDFcPM8s#$Mw68oI7<Dgp0BKc-G2W#%3!BiUS}^dA_os(?Dp#EPAUClSA<yPwew8 z>F$Q1nRZf65Qyon9S-KwpMj73!Sxnew8tMB^8!R0qICj-cD8=nQ=}Y%D}-tb)b0`s z+bMt6mgLWdj@B00jD#wb$!bfV=owvkJ>No`Fawbfruvan46*<O9^FsqD>&v^8noqh z7X$uWg{<Cv#;WUYty3;^okIXaBMf+~xjA#6T}VaZLpD+gL1u^{Cb*${R{w4|O3Vci zH0P-*aO>$())^fw^!Aq7p=Sdq-JxcNejR?AEPnVbfby?XiGKNbY;7^ga>dTHD;Hu8 zo%ZN8{>}rMNBlTF(f@gPh3*(0$b}6YnYh4MJn>0PvF&n@jMd35l$n~d-h1fJlmY)g zq^QT}h;}$JLAR;GxF7F;JHjmU$ipNG;Cd)uJ~}~$)w@qZUgE%*$!vK@2?}-q$>{Rc zNonl9$A_dTKW4^R=^9|H`LQtdN~TN{D!~e+n_PK&e-p~$mRMMZ;EUHRnys;c5rUQ? zZ|URtp@Z|9)rpb;92-5~F(18V^%-ht2J?$h9{{~xiiGgI^DJq!!Fv+f8l`sc!~-AM zYwe#12wh)?LlMU?j*&5Wwut35Ur>Ayo!<9hO%3{HjcqU~R_r+W0m*(;c?rS50W3lh z%EB1FPvaZp_@BMSuE$>8Y+v?a<(NcE<6^nZB2J%bOnv9*{dOUUrHD7QJVGd5NRJ4y zXaL(@WX4Ej2E~B;KwW{ubd%l7jYvkTQ5GEd*(jV><z<WWW<8w-Ss^Egy+<F<?Qv%> zycunSdKiZ-M>)8X5*v#9)bW?@tZ+TV4bgy#U|;J=W(#lW`(U0h@DB-yLe%j6!zIPy zaa4}&R5`PS2yB0Zodvkn{cpl;{&)Rt3+oqGj;agaS7$m|7}F&N7T%fD@@Um;Cx5@3 znb4sk#Bp|VSU|OQt81X=F$r+vI~l3cGa^%5zz2zYzqnHkbk)CI&-IJLBw2Eyz%7mr z(ii5`+EXYA06dS~H;l#SiVX3jx@Ga%lxaFL#ky6j{IBD0O~sV%rZahRT$*<teFqq` z@SIb^bV7Gvb*XW4UcL>daw#_=Bz%(9OEyklD(~rJLhHbPM|S6saPY}KVvuj;ISnYg zNhv*lC!=M^16;YOTAiyxnCAN>hzy|DqXhP3u;{&AK&7r>W``PCd2{BfI|P!l`bjWD z@IrOr3IR*-iMgQ+|BC?xA+l6a0cuVvH!dSG9KUF%%_n9QKpv*;>WBC-%N0h7LAIZ6 zg$)JPH3M=-GMu27M3G32$eE_WsOJRDccLgo`B$R2ifR6LUG+sTj0|o<K>Yy(Th9^A zy$-se**6NwY1aVb9hZ_aO~W_C%~obn?Mj%l1XKqHMAo=2_noh$f_C>?x6xe~{@(PL zi|0HVpMEgUjpzK!F%=zR#zytE%X7bH5edA~Fzm5R$0_>|(9OVr9kww~JP^RV530Mr zI%&ZDrQ!tQ)L6h{N|@e&o+S2!w*mS75iLo4I`a4ajx-IZqt&_VXpZK)MPI9it~a@j zH*dw1X*}hhPxZz4!C&Cb(J!fK#Qxj0&F*eTV-l_SXX*>9%{cRAM*GP5$x@v_$?F;i zC!`uwLW668FBxC;@MB$FAH<L)gUgS%gp6p&Yt;<G2oi7T0<03@x_L*4>%PTJm>%ra ztRwA6ArFZ_uqRG^T+K~mYXJf}e{n`BF|Sd$Si3`s#b7>^cRpV|a!b_z4QAP@^3KjU zk#7b0?KN$e*vvnqhx6kWn3enYLgp;ivSB?J_+g}C7P^FAdJ)-XF#~70_V2N!+MoDu zZ#4p?UA{v*U%V60O0F4jrBUblBL8l3SmO!5G0wSUNS4gcfi7QnT2a%x-9o0pysWJ= zsr!CW)M`i6-n(<UQ@>e9eQC2Q`zB5DZ@}6^Xl9Q0>g{T0|I(ZsbCiV{gSzvtE_A+W z_DiORh7MtfvA$2>W?`uN&9m#%@b=E{>W7zCZQs%ru$@FHXchk&b6Qji@--(qktYg- zcM=^}eee1sO)}cXWN0I=Bu^*6m@H=rOLpWN7Lb1lKMEwcrrX6rTzyHQoowh{V+@hk z(*EmrgKHQt{yep2sL^ynxSjlds@hqDryaIDH_ZQ~>d3(^`+NE40oh*8u$|(?2dd1N zatb=7_+W+}?i-4`!+;i5CeoT!Z6?q8!X=}nc9TPh<8Fvh`%|Dq4fJssY%g*E5?s{X z?67|BAp$C+^O{BrNFg3){_~v8hS9koEh5xi`~t0gC%(NY|EgVcBbg}{U-e!`q9vhA z7p|eLSJk?HO8m-H=`@SV04PE|d9oyGb+vPcGrH~aTJ7s+Fz*Tb@-QmlN?-~g^In6c zYJ<xQ7hC$M$?&bw&EYEIbQl#f6OYj6`A!qXl+0U;%hE#0LZafWW<(GDl~0tU(N>jJ zdJ#!e<zZnwo5$Tl<OTl6URTS?LBYa#IQaMFiT)@1;j_cx84ujl`(v@pu!%&$nNlHB z11h!U1k0L<GsgxP)o|<O@~BrGS3|brBl*hSp7B!$B@o2)38RgRT<t7VIIPuDo|DD1 z1v140PuQ6ge2~wmFkH&9wBCv<dLFL){8e6G^%*GwUp{2{(Crjs_2a`>aoifk@L$Co zQF&4;3#p`=8bW1ell5R{FrZEZ4~2FP3}p-;mus>NZIwL_()fDNeCZ$rSWBOI!2`iv zRa{&W98S8gp&&;ZufB=urY}hoM`a;+?JaR~&Hs?RHeD8svPda7@~ufTEO=q!kIuza zOS3)#3T>w6+bW!yKb(>X0lYwN2ofI3ua|cP34RV}5Lx>%yhE(ohRI@C)rXSp11U%{ zj|cUCfs-Yi{_HfYA!|RK9-Q*|?r~9;W((Nygzdmr%?=yUwK*;)0*Xx7kvs7{jM~#Y zdB(nzj>!C4p^s%3Zm=INrp-)OES;bIq<Jt-Vq0GJl0^4+jyueKRb;}9m{f-DE%^oB zB(S{CQ9s_jiqtBf35dq?H-`eh>h|=CueR50ff~!kCI<Z#q_hnob-U6qH51p}%2Zsb zjFhdhDQrX*Qdg0VYz--s+}(n{m}v?qyIaDv5$SptLaUC<a2Q^MBWQpYh;^$kc`Lqg z&&&!bkqmvPVl_CHG<Q>5f%wc=l|0N%^kdv_A-vnk4s|}CIcvp{Sl)(tk6H}liZWWQ zm2{T)*EHAfDFGY)3N?hpR9HPb;hni2nx3}KkG)~*N~`=hDstmPUv)7|n~JXujM%i| z)E*~-J>XNj5KG#U&{4h`$}n5XE4^5f%K}nCNmI6n$3`@-$l?{IbTZK-*|3!7*M~$2 z6BE@DosTkwN`EWV;^Zbr^jqUNE`4|f4?E%|v4A#d<F{(vLc)yMz((Hut<m}@<7|7x zl_t)`W_^c5Pj7l$@ahHeut?}Ja=Iud@)Y?{wGi?#U7o*&_bU+)@f;%w$#Zcf5vf0) z3sQo#j_jX3C&VefQoNzdL%iR-8;Bz4gQSEZU)e5!>F(oQ;FiTVowzKba7uf-NAWb( zsRz~9ORB`%)5BVXswp1e#~5C*;;K8MZFQ*Ln-NNtU)*SX4zM&T7_0Hzt?xf1zX_ih z;`vXQ0cex;pec<0pRR(J8-F4YY6Hi%h5=?L#l#qgs(rT?2YeL604*TF|2vCE@GHr- zU=BMYtN53zJ6>TwRzuiLEFUm~df}0gIyhx#aR24nENJdfe*Y8}XXTH_uj1xTfCjD# z*bA6N%3C9+ze)zd7pIhaI_nl~ZRM$dXa7U$VXcwyy`Gf6v~H~8-ht`t-U6|(O6&!} zt*_K9lhfX&u{hjeeG3VUMgND?w_Z_Uk<q*PC0n6<jZ;M@RKIWTr&F<B6q)ocfM&ZS zbQ5#{1D^1u<bMdPiI|$moj3T^&==4;02*hfK(cy%ig`hjyTj1VCx?@Ms+tRy3aC+- zxk9LrN@95Wgb@mDEkU3oa2}bcyP3#1WBN9$oUYaqXo`*^o|x-J{zqT-<(_jLaQ<R= zK*Ri4%b0`nf!Y_z08+?s`*RLj!kxDl;wj^)0Mh$#6H>N=VWF))2|aITj+y4h3+@ch zk{{*Z)uIie{|lLye9+dk-cF6`=^`VM8?fQ~rpH4?wfRQwf~R0uJSQNay7h7DF(mzT zUZ1>F4YIU6OcR4h3(~L3MX&Vwb{+m-bce=a?}B|YZyPL+c6BA&KLgqMKm`?6(fCxo zCaBw>tJm8oFW6AUs(YQ2p4>C^lPvm!@~aCxg|Veb^IN*}S?h~g{w1;4$AGTj+(g;P zxK;9<!f41#{vo$ss4y_K9JP;A>!u$^9m*ia7!D$PIy3A(6SZtRmh0#@BRk*6(OCG! zmuY{gUD+W!YRJJtYbD)GVE$g=C{01|#@OpN9hj`<L0f0Ig1*VDsSv*?J$uY%$~}=6 zb9V9MXoGJ>;CZf)Ud!=`B)<&;^_8c~h#LCS-Ui(DOR=B?<}D74-TdFpsKHx#l4T+z zYKypuB?{{c1zeiLxLfW!v-)p(K^~BBW$UUZMY?U7w$DD%pH?n9Yl~wo_k9?iLIT%L z5O>GF=FjBEn@ltW%sFiZF`y5(+t(Uxz^%X#dc(L!sgeR4RH1KrvMx`9ylN68VA5!9 z{Vo-IK%#S6Q50frAuHEp{iU*NdDa^8rF@z|G3gDL>%4zhvhPbiY?NX(ssgu5Ju30> zLy>36)4=Pl0oP>TdDZF)dTe)wP$?oc$3CVA(Tz!2$Y=6ZY8dIthFg_!Ql&1#cbOOs zni(i^Xl~o^CCo=HsBUcPM6C)(2ZCd`k>@?Q-SWjpY`9t3hgboaPH~<CGF`vc!szDw z4dnwqG<{tC$!$@Woy$dGzM-YYk5#crGF|WQJ%xU(jQFv#{Cu@);$*r}v(wgifvKzM zL#&sX9Ttc#eL0|*%e?OUV+(B1vJ$>B00E<!v7Og{cP)*!vAs(4AZ2(;kT2U78UH=C zOif<skE@SEr0;wiK$(Zi@5<9#x$-U~hF0Zzp(12jRlR!83~J&o{~U&gj_zn|4drv8 z6;I=rNAucd&-!@Ttigz9FKklLGktMNvi5wwx4dO3Nb@V5FZVAtq0Q`f?v-{kO0y;s z%g4k=>E@3)k_R2j<32tRdmKDumUz&#F<-S?WtU_1bog8EE4@pBhJgS*p4EutYM(JZ z?Py#x^4#b`D!n(&grnMMGlL~oDRZR$K&AF=A7OHC^h?&C))^C|>eI^P3h{dU?ReWo z#`D<ef61Kl>YDX`oIl2SuTk!mFOEA1LKAC1&0Eple9Myibc3!PK9^OAe3};oX^;j| zx-W_vi>G^Q&5KSQO3*p@xpZ<>Ni^j;rLBX7(wLK~)?lMj$7z2eCf&ujbi5ycKcn?4 z7I&fmJ562DVeIT@d<YAn%L)1VUF;pol%jK@55^y-8toT8w)b>3?E!rO`H8-t9=~}E zdSYj0=lx;y-uBO)4-?bV`mAADBTl64cKDPUiB?j{*XcqfWLqEZ_cCEcsqC;TK%zxD zvN!AhSUU@@sQNct4<!Nuk|NUG-2y{*cPJnwAt@*!jI`vy&?w!VL+4AEAPn6l-95l~ z_WvuKb3OoTF|+ob-`@9gKi3tekhVp2L2qDEC;riD+`lgTq8JP{9IW+P^X>AYGNsXX zhLjmS8Kjj}L3K-e^a6amFuPEv6HeS)_U$bhC$F`DA#`^xipBQD^NkQSue~*fw<Irf zb_JU^>#2nVcxDPJV0kvE{j5&`pS%#0Ay?MIo^E%yOCQ*#iuOf3IeA5g`-o(a!F5xJ z6kWG*==BZ9V&_%hiQ;C!yCr=Ri~lBd{(+QD-A0YGX@ZuZw|)<ne1-7N^;QV6GPjc? z0;@7WB2oJy)hF4Dj6$_YkuLA(amKCEhodN(TZNF}zn*`f|4{40F};2XufUkAG7|#Z zCF)~X*Nzt-W{enskV1MKy#8yqkwvvAN*od_P)GM5$)~;Bd+((MX*0I|73m`0$K6(y zQ~ykraRwE<Q7pfy+#ji<sZ5aM1%&T{YfhJqw0MRaVWHXE_f+4y+@Cj=u^%#*#J$Dv zM`_Qk;&@O@Grg#HWw>Qjo7tR)hv&F@Sn5DlrApxszc0m_^;_5W>KQT`1pahR7e4}~ zTJTDCL%hLde=|0UG9M-74!9bEzf~_2Pib<QyW!f$W!fpu#Iu<eIsDGy@FuHyq^<7s zno`D7Ia-*qalxUgj^ro<*MH0j-bTA_xv}xSP3*kmzRfYQf^$f(N`k=?c~ZR(HJ_?T z3!6mcdglh@E$gq5zR}yLFXA~-<qliF)U}vq$h>V83)o8>6~bE;jljXJ9aSZ3w9Jyi z{OXpPrs{8SDxa~ara%ydZd_=FHj=TTkHJ!9O%H4D&H5TAMv0dvUgo4Tx(E;oG(?7h zmr$$Ufw$|GudM@Pbbp)8Fm^U^6L+#-l|YV`X7vmzS@i9#>0wr8<Ztt;;x?4&^U9WD z=~*5Fq&ohbd=nxS|EA@CD$wlo4@96;76pr$wkti%qkQH}rmF?dbq=CuQv6X7<=Pwl z0Iyzqwu(BpS`(+)+S~JQ;L9C+WP|cH?I!BnK4vUs{Y?jZe^xA><?XH;r7Y)7W8Rr4 z9-{Q~x<!pW8Ppckx@hP`b}(<O#2XATGx<#FBW!BxBsyXjgu@<}qT#@^1+LF(i)Uth z{D`a^LT5}qTf_b3UB6Lmx>v(5O!&~vw`{*v6FrlovdTRpei<=bA+n9bn=9btdZ_PM zs88?z4eS<_P~^0P-oH3FzeMHkAb4+_1Zr(<1`warFr)@kd)oP9a>NM2-VEHZuwaIG z;tzJ}PaTP|JF%wv+z3$f*AD8rs5otxhkrE0qxvJOPASY;GwXHfX1!8<`sx_9-?(%l z+SW&&DgeoG`YsSqh9ZOTPrmU)ow~IP=cg>(WWpYtKSUaxkTGQWDNH@9#x>A`U+h2N zT|n_Ibs9`H;pqdj!|@p~dS%sw(XIdFwYjztJB}WCDQO8rAb5~7Vd*W?)Ul6JYrt++ zLPV@=^+L%&fP<tS8I&%IA`e3&=94gCwR9hF&iK%}ZW&CElOLAH2L75mI@@9Tc6k3W z$8LAo`4s21PnEchQ^|$-P{_AEZ*KwGq&6DIf~Ze{*M+1<<H8!|Z#)ryOmY4^FQ>Sq ziq%s}p2Pu;Sy~@UYVuTA#OEGkP9c7MrX^lx6%I;E%j{A4#7Z6Vw<}(27Xf97r`X4f zjfrC(B4Sk;5N^wjr_7fVx6HQ#%y_Jv4vCW%`Ot=!p&yS2^$14^vSRjrayA<oDx~-% z+PW@HbA3<1L}{o=Oi<8tazl!n9$i;Q5$Zx$EiT3HN!c*&IXq3+sk<8f0L-`iYl?|y zbC?g7A5OdIR_fBf;&;jsV8?l9tDz_1Ux;B-V>W5Ger)$Q@br0veq}4B=v@xFtZ+bo zstnql_Ww4tmu8fC@Rda?#sCq}?lF6Ol*uhet>9Y}>|m3@VZmFGLk_ldRd4+ISv-mh z)7vAV^e1&o<My-)57v}KYla;^q=pb;O%X7;9i3<L@<;Dxl*&TH(tC<~#O`Pb^5Nv+ zL@}*$jcYj<dxKA{fEKO`-BI(peKkr5ouCpl_>wX=s3;jsvBs=><Ge%H7~%Nt`B9lF zw-va$VO8GM70U+C<=vwQWSPH5*&;(5(WY-`K*$T8$8vFVcJb|nQAbj(Nd)@{jj&(j z4QpRqy+bX0u8gtU-dV~cJ*VLaG;oMG-grcsPJZoDvtuCMYJ-#?>NI8=EZsWQ5?Xz8 zdp&Zf3Zjdbk$g}!cJWKssHN^`b28QkB|SK^4Djw@6ml}|gX6he2J3%_C#IgLzWK;M zg4oo^=sIN8T=KjXiI8c;#|Xwg5aA%qR4G0p-Iv9-J>n?TSL|%mv<i@T@unV|yZ7li zkzupP6PNGJAMLu)an_ZMCPj|sGbu%jg>gB0<Ex4aEiuJP&C2nKt;N$fF72}KuSX36 z1!OUHN&!-P7;0_YD60PbpsOL3pkuPhB-J`qb?Lgqm*VAliBy9K50^#Q9J<82LMm82 zL=mnZtbEl}OX6@ew&JSx)0(mc5;3c;VOyCYURv0<N$#^ouWtnRI#0^Vk=sN=l@L~T zB!153Kc*_K8TBGIY_hAE$_`?rI5#IP^w3jQZKTl-^!fW%e{`r`ZJI4zx05y<w2qdD znviuH;yGS`&?1|>ZW<rUqM)zf{BSZa(vwmVd8wbU*CZt0d-Rkn)Q;jT%Kl)=mXg7< zRhVdvqN4A<h`x>-0G=xbY#e_Q#`O){2$>+=x2Sb>&gv>=UJJYN)DM5z-FN#ow;T95 zI!)Ag<=7s-fs-^eC>RC5#Iz;Mejjzim~_*rv}ixS;W<4p%mV+U@f*Z{HY(r$Bnu!| z?~@>Wt4qh#2`lIAZ{6UYB(tth6L`KQh1?rUUM9njKI3gs#ikktq-oEJ()ENlSR8R$ zSQnQnJx?{?PHWOT6vjrg@4zF4a<I?DQCS07h|lzOEg-QoS4t>A_SeeZ7?*e+aNI(? z=38%msP4%eCQB+ris01M*zEeKN*fYT9>4Ijl*Xw~1rj;^_8?k^fXc@!(?;!{;$5I} zZEzj^7dm$t$$y}xruK%Wq#DKQFF23L?Z3bUIm`#z2Rh>3ZK4C?kN0<Mhs1XFb7}>9 z;M{$NWVeIe+3^XWkogugseP^NcEbsbmHlp5m$oJLcP7zyWaVc{ysJ!iQ-rx%y@#z{ zsaxvKc#2Cxt+cj?xrI&DpM~<rt-mD5R5lh7y^*N*#8RdSdG$+WZ`xWq<O%wOcSJHg zrH{~Lc2bBGLQLMrSTTL-AuQFwV;9-Y{^)5c=MuW3Rew3CsJGiYu?cJ+X|Hxy{0+^J z>rL^D0xt%M(rz6wv*9t8bXUvc=a5Cd)^bL<b{u0^s@;C!0M#FWOiY&7o+28N6pPC* zCU3n@9T}Azw|OU$RuvH5?1sqiMcpW=bonzc*3d+B7=XrTS+Oum*xyzH8)N=Bbt^IV z|6B}I;ZNzjr|^%&SPuaJ_DRPHFTJ<Z+!dvM$aqV0%hu&-00(r70%-1P;PIq<u^K?7 z)W)sp^aYxsZX`^y0{qu<61Ea}I%P71I-0HO3UX+BP2(}16bBpM4{=Y6Vylgy-3>tI zfv|h^bG)!23vNd<cZ^us(i<UR6Wny2Hi&lfpgINZ@w<mW`7%85;Zqy=d*kAx9Zhb^ zI40VmGho3dRp+YDlL=Qvq-Ap5F&^kHY(ZUqu}`Q;3L(sX0bJr@%UQ*+<utBXRbu65 zeo?+7E2ium1sVZXdOR|5aqw;Mdhxug=~l=Wczo7l_4cv{3S(!=JjYv?jUejd&F(Qn zrd+upA0iPV$CdxnFCPmy(l@+!mv}x>%xlbU_o;qh;c4s))0P%iDO-dwDBo3=LU$s; z+4i5-)N1GX^KmdB@Ji*2rw*}<BBVCqy3vckKZ6(de%>tPyf3s|N;j352zW+tYEY@t zhsF2l*9GRM8-yQc6p4ku>bY{)&m$VmQwLQ}8*iU$;SeVe_lk514W#TYv))1%7Ey~! zp79X-!g0wSGdP%!eSg+7pbr>f(fk8_YEytB&HISuw#GB~rj%qaioaAzg!60F1Tp-^ zGwUPjMil`H#7C)vH_Vl^RQsZ$Psa8@i!Y*R_3T@UWFReXJ&L#LLN|oZmBuoyTCZdC zwyI;U<6*7!*5dXw@NmU{t>d8@km~+lND=-&xwAKbDJUTIZevFoJP0>^9p|Z!@1v2* zCb0aPxJk^GN+vgo$Il^0#H~_`42O3NNdr6AtDm_zDFw+sm>JU^6r0rFPX47mxLrBT zqGrl%4V4d(Q|B=Vv<?Mme*TGK_Kr*X?-lrKT3TFKlo4Rocj32>Yip`i-V`E9M%){H zf{F67mi5)?Q!m_?SR+k_nQ+$)aHogdi===%X`OoIr#>$<Q<RRjc-<W`|IjGmm%sBr zwnhtw*<Wsp?~vb|DD}0>SEzP>Wi~8Y^oB#tf^z*>pl*9W5gJPgLa&FZtOshINEJ63 z&4o#ksv$m>166mYG2dqmtdtB!N><O}eWxKMnA!G9;X5)EQ{d6bzz35iCK5A%d;*t~ z_X(Q*vYy4G(hQ8OdE4}~68hlaY}uR7C7C7wyyrvyvsbX*P%v~#`x+c_@*g1qx8^{H zXjK>V9+-OFsV=g{Ev6I8Ua_kE16lBXfK<a9Nd<xfUw%VQ6Y1&!P*PVYBej38BE1oj zcsedoML8QK4plt3czU7btPhr!W^e^tF9k7kTJp5kphBLS8ed#l+F4AfofZ{|ZN{cA zg=6|LGQotP-8bp!Qzpx$)4G;giNi7lacwtM2dyGQCldPyy^CpTlJ+}p#GuwBq^m3U zV4c*#&fr{!T9_>c(~H@z_5FG7RsE8XqL89bD`t9=uS0Y3R4EnY_XXarX*kHX*f&l3 zyR6;KyDzl{vwaJybEB%WK#9dgJR9wnt5Ez08eIA!YQ6d3lCX6B2ILzr88M3Ry~<_a zUug^~nAC}(!HCGFsoy*#ZVYE1Hjj0rYnGr9wY78G3jprzL+jd-1y>yFe<0g_RVLU6 zPFCy`Bx|$($`UWF>*~4!VwFg!@LNx%l%m)*tg~lXxLW*3P@CTJeCRvEC-{m{T7fDK zd!o_@oUMd&Aw`r%nji$zL+H*0bVz=+eGo0RFD1&kO0%}Yi!(IAn}1-T&VR2feOR6! zIWKA8&e6|EO9XQ;Q@ZFIRG0pV=HZWa`g3`U^o2{6O{2U!BMQMb`?<Fd!w3x<ns(U! zE0q>H^sszzt<)0l7ek45gZ5oAf}pJJUfl($VX0+(`5I@Xq`s|QFe%IyP_yL6IpmVz zR%kd=4tu@8#_zttBTukM*Ij(pb@IbB&-_4CL@r+zd?c0l#@zH~-r``Eo#zdimyyjy zIlA`op89bE((*Gr6W+Ul;G|a9-@{kFIM{BAfofZHIhvcR6E2Ru<9@FIlwHh1q$7=I z|Ldczo?a{4tqgltC=c&_K9M|s86cr*EsL?>Qnb=kk#DgE(iwoqvI*`kA({gB@e7D` zxB@AP<L`VRwNoO&MH(&r`Wed=hA;Is5)UfN-|L|jGX3)H@SC86--+HN_=))DmkA8P zlo^Ip5)ue1WoQU~0#vBomA{7XTQ8!pD5kgBclM-zhgmE#2_C0*N|g!Tg$9q{T&@GL z@kqJo&Fi5Wb>cC&$^17Pxn_TgP*~o4h#Sxspb04aOgb+7>0rcGpbTm9s{Rn%V&CzL z4Y_!8V`)E@BKzkD1Q?1a`NhM;e&=pfq76Kgp3q92{}rR7f1pd;Lz2YNYQ+9r$8-cl zltyB$(zUhp#Xc${^))F$?$S78xYctWQW~5NV2M5TtBvd{PlWz?(>>BXk62Y!|AJcj zc)@1)qkZyz9udV~heEy_St5CqOM|K2+BEbV+ipp0ep>rY-18vH)lq*3EN?VB)C2%r zw|C&S<vPaHq-R2uCF=Jfq7e64tP^Dv-2XspIF?uWk1LY4q~N$<oEPq^duJ^w8-%E@ z13D!?)Vj)MXYRY2z4c!^$qUw8PVCt61h6cc4zcw7q5-^5h?h%!i&GS{vfW`I)FkIj z3Y)=!Ns{OgK&Q&2b1W@2N_S$($ck~4-A&kZ32ZC*$X`p2L`j_&<@+8`2J3A;v)SM< z0TrxvM|+e3^@qb_SvDt4oWlW%H=ALE54tGV+HHY^smUt)6qVUVIz@%ge3N51=zDWU zl%xU5O=hi563_Lb*aA0}40m8kZsfU#+<5OzhT3VwZ{|~U*qE)Pr%{28B~fKj-|5yt zIkZ;mU>=wdnW1Ufa2?#K=1+;Du0|e-kW;UMJH=JF%67ep37+Lr3J0!X446}=jLci~ zz6K2h3<0R-R2h2D2k-n{9gA%F8-ClkGV10&C^|s+r#)}-9s7NzCyozj*0XZU!}4gI z0-TvVV)cgi5i>7xUYoE{!6%x2EDzHAs?`fu`G``Pz)?kO=1&3n&%f)?VH{DDB9K{W z2N}M>t>Xeo7z3q3(OZmA86!$y7KG<N_SY#Uz&j>xaqLINJUM5#m0OP?lD7M%b5?9b zqy&MIFZZbfB8oRRS&9GU@-G!*3u@Gc;PrDMuN-|TGKp1u+eEhxv>Qjn6v}c^3=HTn z)8|dHbV{5#eqP?)`v2HC8)vkfmzaxWeK_xO+&4@Zo6*x43;$~UpD_J$gNFVPwJ(c+ zVPM|iQbAlzcWp1d2qih+z<JP90s1EY#OCq?i@CEs!faK*`EMD3^@Vqx0~i7YVR80i zVPe&H^w#S2<KQ1HR;vD;Fl1l>N(w0kQK;)mAE_?Wv-RY9jyYTkMb6#0H6^-{CM?ks z2xhQ4R9BJ*cS%bL=~B$ZlbD@R1^8KwZ@;K-_|)krC+T#Or%Z$gl^0$cc!hl9aatOz zCKt=my+C1VO0n8+;+NbtdsA7(L#2Dd#Q1j%@}J)l*3=AoL=h58&CWM3oUbHY8HW;$ zC6l(Q8=Ljf?Nm^If>d|#4M{Eqc?(mDL}&Y1U@<b=MNm**pX85;3hHHW$oHI_2kcZ+ z_B#HNV?l@5k5&VW1z1x;fzTYFGok%$>a|&XLq=$D4wQW(DhzWk@v!)5YprrSNu43m zgxSYndUT|NdkJ0okVy+o*5SE+JieBUSoH+dL|{hfs1aBhfaaVJXdZWi02R+c!Eb0R z+jNBukpcD0vIcn60E?%BHj9UMqV2y4=Ie_#avcJkl8rsTxOR%dzioE~3T#w5FP~#l zMxnFeJF59!ZNg=aom%o?%$Z+V$P@$!)n%;d!ec69oHB+mO-vq4j}Ag}m#^Snpo-DZ zW!?6M3%F#{Hy4WrUOdLk55AZv?D~}y=aLE|;S9qVC%R`=*kicUHw5wisc>`GlybW^ zGi7g_maum5KlWybTZedo*-o!YKbAE<NaV~_<2H$brY!q004$lgni^K+n9f>T<WHL_ zB3C8PoRUky<Vj(eWjQt^u9uAw#*Jq(nafgaSR9Lf@#)XdP_H@3XPc*h@J~dn&X=CS zigI4gXuyQKaOOSu*l{vca(210xu5EHNnMT%#rKzNq%!zq#$i~XtG(d~HAlfo{mtTv zOT)hl%-OFERWZ^DoCIv`aXA1>8y@i>qU%_Cy4f1mE*p|0w%t0U8Y32zh90MKwH(Mp z*d(6uocm^vsVRujFT_x%_{XIVp`68gToHUHke>$?#Em9pptb7#aI<P<c`4D~&qjoX zlKCiXgL!0T*cewa3Rih^b*o*&-cpzR1%4!Qbry^W17fh7i}+zYDV#}x&nTKqMU}&i z5}g1L?fStxVF&G8eHEJk%1^X=nn`G7YuVL}exY{ht~=L!dNJylmO0L}Q-t8-j%6&e zDJ()t->1$F$qK~X#IY3D`L(?nN5&WP{BlR0*5rln%|Lk};?CbAEC7U7HYQtxP<s~# zDN8{0u28Zy@SUY3UD5tMw04}FVR2W+C1!shR4t1tU9T}qPwcfqpJ2cuipBzoO<^_D zLccMGieuIw2+<$IXydqKx*BKFNjYlq_V~abwSIg^)`0y7W#nh73s#j>Wg=F+WM!>D zT=Q~i*?zV4-pPzqb|qxa7#r9{W(Q)H7s2G&fK7Mh*}q%aF3W)h8nCadxkFD8)c*UG z5qQ8x+A1Kcu`hJ!PLP%TGDS-EN*$vB?J3g|TKI&tQ|V5UjJt?G!l*^7693>I5Reio zlY|Q@E;c3t;A9BGc#Ps7PIZPa=={Ree1dyP)Q%x&pqH;Bw^V^S?NTzA2b^2y7Y@^} zdrBD$kb%YGk1j^&bw`G|rbJ&f)o=6|P@j$;hycu!Fa}D+$T^fWt*_<6dH!JXH;#yb zQ{mvyx<`dWEySazsj+1*C#xz>EDWX`OQUk<aPpi<Vj(pSrwcCUJH((0Y^D!B7MBko zRNt1YT6D1Srwhgm>>P4fg_|J;RSCofNk7UaYjsFxtHZD=Cz>*bvc$==X-T6JFi5w@ z_L-u)uS`M;XXj|;kKfX4$xq3@;9@(=r4`10WGrZ0shD|T`XW(LTWB%8Fs;Ih?5hk0 zyh5=8GI)5xd#^FYrK&Ba)cP6|CRtW_U=<28AP2=uch39!`{D?6$7oZQfE)9rT#Uq$ zo^R>oe+TJbU=*LWMv!T9EBK1h`#_VFt}Vn?<z}x3XypZ;!S5v>5{+X?uWHK>hJxgv z+zdU9Pu=Z`-b({2EtZ8(i=68GW@ZZDMc(_?sU8=>;2Vzv8oO7IOkTZls|#F}0S09J zcl$IcwwDTp?CSs|_(jDCneIxLe1O~iEh3J%6`iyyRAoy`p4!>o+wu(6HN95Fy>8NG zC40jLEGKGP$fL6tu#tzH1);Ql@e3QCHr8=dyL_f?jZx*8F>vOnbj;i)c4Am7S<9Y) z6L0R*tLYl5Q-x<*DukxYvbMedF7ZQWO?$A*WZ*lc650c{B5n@{(Rb`=YRsk7ul54) z`vCE0;IB6*iLiLYNP<m*HR<pd8xD=y&#(3&L(-Qzn|o~5u=C2&pCi1&h6TVukACBh z^SHK9rzvEeFm@oVp?pWEYS#cyOF@cRXba}z6pu}HFo-_;n=NV?nMGRIs7U_m<#7}q zQw@43pPB`eI}Uhm&C8v<)OWqCJW8BmNDsHQmdYv`GN@ZLizJY`d-twT;j2J(7InlO zAx(6ZxSM@jOvmqj`{UluRr0|HTPevM!L&sx_o#A`??VP~W^XKhm#*mIA?0^QBzO{0 zvVzKABvirXK)-fYwa!snsG+4|D3&_ejH*$Vc<ZWgDpv**1q_&0Z#hj9Am5e;R4GVC z;AB?>67-T`!_y}(ncB-6L<-htzZ64D_`MPS!F3eYc~I);s8YwR@cg3^M{7F>g)!`G zpun+3^wB>ZTD2Tz6(u?QMGSVKYLLGC!-(ot?6y-SSD7;1>gufdrO_3SEBq4wAZi9O zV2^tLB-1{6Ji%t}mU{53ydg-hf&5D^_DC=fEe|!PbCb8-GkgJ#ny2N4znKn>Ahn?} z&Rh-{+kdSj*R$e4)bI4jqR(uaQc_+~R-IyGlAmzEQIMrty56FcBDXzE;c4R35!rlG zclhhJV~TaD>b88iGE=Bd@}gA&DIl0^S4~D2b?-%4d$XH*BhJ*a*ka?>=!>r|?~JKT zF{(_Nz39gYVM-LtK9JC0vGLv&WP>a7Mn`aGjo2BbbZ+O{<W*Ocm^qE;^c&{k?ulXg z(~6wPpX@m?ahY!Nm$Z@lV%ncysM;HJ0l-|TRP%-80<XemVZ1x#F#^!N0b#0<y2;J> z)W9NJvci8~NI{NVr08sXX1^414vn?bm;CRPJhK`i1k?pH#_1x>l$rmM&?2S~YDZ(5 zqi<op)+%@xAGcrLH6aY2M&tYYVM}e0uh%GAU)<87!o2RIj-~e6B{xr3F(<1iQ{9^b zd&Y8nIu3GT9Q57!ssH5B;#4R~l#Pysx~;yIMtMnC$ZPor&$-6?#MN}-M&iXM4_RZl zUrM>748gBO21mP+(;-mv=(jB*jT_4=tjqopRiZ4@<pp`R$K%kj;)!EF6TOZ%uj30# z!p#~=irmB1{CtJl?fDrNgba1(w)UJiO2Vsf%Oo6g%M0mCo%VOa#d9iRXGatbJR{aY zXf&7E$zN5P0`jJA*fE?GhlEQps)!$oYdU+zmco}O1sq_yazSD$z29vf_v0FQQSaW& zasOH{OshC5%Jw|g=#+Op`Q}TeYN**(V=%c@U#m64Rj(+_{GJ`!ou?4B7+NtVqxvzw zHL)8LFF*zfiCBN2aXF4MZUejNz&BsqKzORRia41*sig=A<&itr+9weo`B;0u`v+=& z!1}C{Z!i>t!O4yYc`&A$5A?Z_Tw1$2e7BWjH%YmKkzX5R?<7lS&!NXhvyE&h94NmC zY`Vd<=dYJH<mefHdw?W(mAJS5k_$}pn~Wtr4SkV)TT_BN!C8&_^HiOvUYT&^@fCs> zbqlc)Im(ZFojqtzTY}Cbzta94@?#S<T%CXRxBGZX@N)0eHhs!a4u|ZJw`OPvgNX6W zQoFJWXU0W3&keGF%uZKAcklH!SzgIZXq+?XfAMZ(-LJQ=$ZN^5k(PVd<TSHO#;TQ$ zQ=_U2+^Nke)wtO}4PV?e&X=T*8lkbM?&Ky1J+_RMd}wc%+4t^P@`={%9RgJGp1P8v zu{K}M{csFyJh6bKQtejI;A|$BK=5`72*{}h;(IskV{(&C;`Oz%q$7})OHY!tMuvb9 zySXS^UkoFdcW`p)5_qRP+wc2=$kzonaPwpHnGQq{H<TIwjRl76U*EO@IqFUBCg8CX zQk!`bSeKTtTU;LQvoL<D_^vvpDn(9JQc}0Z=5BXoSjk8Rh#a*?M+?^Pz7i$KaPAxk zfXC-L3d&v-NatV(Ho|qS>poXffCzE|^N|mQIO`I8;cy#0MIbPH;ndP@Y)cXrYw})A zK+uscn(S$ivcgwJc5A`zN_S2-(mN<b)^cgEOsnUelMuzk`<Kt@zRRu$qhUhiz|w0? z1zle=3&xYVzS1oJXX})LN`%F+<c-%aMQbJ%ruX01VlPI2CY{?;HR7fW`s*yit`9U; zoJLYyD4fI+>UZUwz^Nv=n9nd@Vo)>8g5E9m?qPN0HYv>%t@(rNBxXCq+4uYjD@0NA zDdI5kt%s4yVp!z>31-RpFo*GEe-0U|4*53W2Z*N#eEBAG*X>h9BXNnV1a7XC>?3RA z_n0f;n?s-GdEZOe?Spx06kxY&Of;)Cdd{LvT=AUV%h<xfAb!63y_Q^Ai-cS=*P0CZ z_%JvK;`in+J8R#zvqy6Lj{}cuLsF7*B{G=P>nxbnK{uJSi8k%rn#hSYzcvL!NU!CY zUc>!VSGJ{|QL4v@$gF>XfynL4f+!_fBg0%814gl}Spz#SBcm4u=oD{408;#MBmhfr zSCu5-MqM9=)73IY7(0<$iy2p00FZb?U`6(H1u!Pw{%>CJe`UV<_XnV7{l8jw|CbNX z4yaK3uUtYHHN!mv+Lqp;9IkudU*ZIDv~~3Udwprjd|AyH!LL`cl?5qok9z1hhL|?H zy*hdJLk%b#xS8pX9#=~IHNO3I`Srx|ldomcRx|&}+pz7}{24|DcyVs^NT&blZ@i-E zvF<X1niK|Q3C*64W&}41Z0Xy<6&3spboZ%&rg_V9X%6qCofq3xCC)}obZJ*R)#4V% znt6NLg-1;C$*1FPYyWgLfbXIN18Q+Q`rmPP6ZmlH>2OI&gu_v*@f}E$kf7rj74$*^ zFCK)60&L$};K3C}{nuQ*q<dLk3C6Gi%vqHC6hZ}!ld+V_Q%X7f5#zY94LVqrk^}OE zWao`uV9E)e1F8@ebB&=;VxFd^b$9KnT8xzfd8VjQHK*#!J-)-mfe!!0;7pA2zAhz+ z{uWB*>!#i8>yE1DRDYViEIG;4swshRxb&q-2nPp+!2u|+9OjBfm2kDY1SBhA)Qo~E zg#B|JZ8n9lELpLqFRlMT70SR=(LXj=rLQ#iYQH}v#|RFrdkS}*d?f7=iki1H!=tvl zM#ubu`4bhGwVD)RM#w;TB)o6y{`zy2H!x`b;5PmbRJ^+ob+A&#?-Mqpi9?pWW1+Xc zLe^ze{#_XlCB60AYFV+rFTO{oIQHAngY;_XtcXqTXAPUj4v<uFJHuewe*VwLtY>yF zTGgU<9r~yaTV&)t#dr6Qz(?Px(lZxSB?eY0jl78!UEPK_A(fpG`xEu!Ffj$AR8PCD zN@?Za9b_wj^{B%N=Zumd)a9r68~ZG^pB2@wvUCCqM!#}_drm!2*b7mIcU~XH*=h`m zeSTNxHrEGWPs`rYeoiClxT?b)IOi+Q#z2f7kN5U>x)g!FIaKAj%^zq=dsQ2>Jg%_Q z4_|feW-~q{s76kp6{lkH*Vhr_e@B}M41K6RzT}#6(|{Xl!Q-exYf`!n%skHus~~m1 z9{wr|NGyxhMXJ?2OWx>_;+*@#+bB1%yL6b@%7}VP|FMV-z|w&k4`*z?@h{Ck{XAbP zaooESJtyc~@)cK<dJ=A-7k=+mUBOzUn(mdQTHDni&~6usQ=~FOJ=SG3Q<@HNP>$&y zm|LQ3n`(3!Haa1H_nMM7Tk2Ob(AQsXi;nr~({3E})xndz#g?<asCsLhPZbJt<-B_q z#u&LG%Rx}cG@r$N3^yM)SUxOLM0jQE;K~9d6ACoc+6u4B%G>y;K;G<1iS(8!<nmEU zsqLG-95SIT?S}iSKX47NtE&rRStehq=%HFur6i7B6;D%BJ$V;N8p5qbPt)|WgB(M* zH?p)p;<@USxreNAkmcuYj?A%zwaKL0aS?&@vRrT3&(k$xfB6QkhDp>Cl?{9>@Vc0U zHYM;ne#3vWOot?<;6*@v4%z!U8+0lRu)nMypf3k#W!p|tzYO4nIDkLuv4stkB}~VO zv}q(=IDIuT_IOzvH^E6%;q>(G@|ZT>A=$RrS28X{?-gd=5ZWw$#49AB8y8^#WgNBm zI-FB?vOYLVR$tqk7&9_YpULI`BM`17lu)$(?jZulZZKnz1GXHDBMqjc!8#83<}RJ} zXV^Vqh{4Jm!c)E>I$Dg35~==OB$BNcNi1sa-aUf-<Lna|1pWkmbxTy9!W5IX`9VjR z(*%~S1E#V7glS)+43qxIJ|gGCCejYf*Qs!rj4PG$zlP<nD|7NPf1ep5e|-2F=j3OS zSC)L;nJH_t9bH=^(XjN?1-HOJ#x<>rmh5xuB$HihE9Up&nmQ5tNB?BM+D8x;pj?hS z*=;K!&9?4!0m67hPr@d^qWPZAch&t2uS<P=7M`rExGa;QDCAWZAtq85bPhSYDJ}}d zk|Tw=1!ANBw>FJ@a7==;PwSQ1<?N%Gzh_kC80PrVKlYUy)fWIpR5#%;oA|JMg0^CT z=d*}bAI$>^*(=}zBnD*UmtJ$E<LnZz)s|zwTBUYP{R1Ha1DdxG>Y6$|YN(~aPU-w} zYoAv)dE`Xp&o=#vr$-1065b|_(>T-Ohme&7Jw|_~J!mnHC&)bpQ*X$T1{(n$nDvke za=>BRWHlnQHQV1FyC-2eM(Ma}`kZKlA%)MC0t+gO|G^3<O>CWV%sh{(|8DS;_S&r4 z|MG*I!z{TOB2LP3pBWqp$-Zy>jYu$5+=}}c*_oAS_qW#&1*|iCIPUS(=}7h}wbU+; zQ|wIX+Ffj>P<msWU2G^)7j?ET`aY;Su8#c*$I7J7ZZgHXaq3-g&P(McT(QT9p})j< zhg2{h)<>>@W-2QEqJpM)pnRfk_gCnATyv}R`G~Fthp;oa0KTFWhyT>}vW~=V{U^^) z^L^mdbURrg`;o2={C^C|2c<?}QkX!A4IOL)PO{H|b<&7jTXrgw5*jT@BG#tYgD5s+ zSBnA#2j5$NhqSSAT{%C)#(~imRWFsZM|@g|7}W2AqMd~Qrsb*hwYB7igc8~8t^wx} ze4h?_c$`wjHtU2n`<g(yOyEN-o|7=&P=T4;EF+Te7W%az27diOKnh;p1O0fF;aX+M zFZ6xVV^21JOmYcKS2YG#mWnh`8+ID|qUIMKU)MoQZ(Se#`5M3s>mKLwjSd)Msx*)H z7Sz^@eKP#)PW~A51d;wNGWy_9DK`>pv9Ysze?|9q)TarjZ>Xc}98O5(0YIA-wRdPZ z>Z;2HL%UYGmoD?mSmUG=bw8ZEj|0UB1eM183XslP+$jOqjP0aNPI%-l7~r~K#1qab z*Zr_QoAdHG^LWZHSZb%C<$aHJr@|B%*zwvhTIuf~W??3Cw&yFEk>F8gFQGq&*CZvB zVO{H>fTqo@FQq>u>i7z{xF`l~o+XqFy&eQlLcRWW-7}j5Ulh>VP^JQWHXM5o*jfgj zeQhC<3SndST)(1-**{?xC}0ROl+bUA{$0KQF8RCo=UdvdTkR`0@D%jI>(nAPgTp?f zj{_lfdQAt-p_TKwnKpf?<SVo~ORoEQ5P7qk?K*gN?JY#jS#zCd0>gs5c`NjN=u@l3 zYzt*<f5&?8NAvd-&)`9{>Wgz-f7hm0mx3N7YS;bJO|b+kzVm%S>lH|FF{1x@=5pd1 zJm)ryYPY<+YHE})roC~V2J@Ggm&QGUTo)^Q9`Vp1--O7L_~8v?TZf+^XSb?>7&A~) zD~}4A!2GA%xyOL5skNWkC6vBRR~@L31@}9@TkU8G%vbpTMW2B&o&cose^;LH!Q&+z z)E{i9O4HUtSSgyUq=Y&Hc}7S9%lo8{Y~6M?Ixr3_(Wd-^wMj@N<eP#VFB~8;JAi3} z?r7tH#b5KKlb%woc9-S*xE5wxL%vavsnm<}8hT&8ST3V6H&35iv4gmeDXNV0Ln4@~ zvzy58wN0)1l>u*&acPu_6@_XN4)*e|;w=o(+_tSh8Kyl!dY5OaHRUFM3`&<{HDSjC zag{bv0HImbl%qhy{tuUyp&4d7gDDJeWiD%*68E*AXee4s7c!Dz!wOK3o0T@=U1A{b z$@vh6B7vVl3dw9irSxiwK@!5Gm6x5({?DPT3)jnc82-e(sI{dyFxkswP8MG#I}AB^ zXSusjaA4<hvz(3ie(V%B5t{ClGPs?oY|vnHuiYR^W>m;)j+ldAx{lNlYY=Sdl(DcO z`x=uNB!Ud;-uV7^{h=_A4D*WeETdgn_PvNitLd0lEa?Xv$JGrrGGsZ(^>Gd9Kfv*! zM1r~aOW3sH6XT4#@(tV*+`IUnV^&2Z9jhwYe#;k>F^RgSFo6-<L8*oDN3<eDl{%K8 zPgsS^#P!Xwq9zrF23taY$i6+({CE5()~6r0N=X>;euzpwy|mH?Kl#50p28QDzmn}V zuK85=y~1n^Ift+e!^??%k(Z~;8al(T^JSf->$gDYvR$H0v#U!ce|C`fpV4phf457I z1O~A3wY$-5(bM2o!La)+C43>$wKku{)o}lqYQYTBw{SNDwOW*5E(er`P~;8GLyTb| z;e5Zgb$=y2=djJbjGL$VQ&W?Ti6n&RPUjj*)jhkF#_I|*<}8V#^hE{WZdl~uev9?s zWYAZ!SD83|?&A9{u{a3>>T0n0?Hi17Kd$%~whu8(`wVd+I*R0qT8vmg1;%p6uPTMP zM~7Huc}y~P&K7$x16iw3bJ~GzRQkw*EmvWB61uERZ=(OQ|EorqcePLLp3fA@sNSIs zbsg*mK|ZFyD*FgV`}v$vS|RVoKe}db>2kA#kbw{GuVn#&qW;v(tYp&IDBif<F)@ss z4+J94zlDeZx~m%2`>;=CfxUOU7i&)Dqrwn{Q>Vq}V*>S5DdeH{8+JN5DHk?R<lDl~ zttF1H{{H1wuQVtJ@^^|_p7QUnL~iE1%xSD^8x!L4iks~m8Iq|S&5Xf3!Kd3@Z6%GY zms)pj%?zc<w#zF&)8_^C`j8}}ZH+AZi?u0Feb3l!Dg{)eE18?6r!0q{UAJpPT{~?v z3#h&fjqvUH^p!t|;p~#+ow8*yDJV0k7%p;3wZlb3pjATEVzJ06;FGGWpy<Ne#>CXq z%FIZ<_wmwdBNh1<rPQ8l5B=hw-4>+I1Y8>Ti7k%*XdUE5SF`n@1(mDVJTdq!xiurB zZLYK@s{Sio`131}wq0a})zdYGOpNwp_|c~s3we^(vVEQSc>FmY4ys=VSt;dNj)c4? zivp98>=RRmR@%tb-EvW)(ef(yRr-y0SDvl<yd3H}u^bH&gi%6FLk2Lppl;Uv`y0I7 z{j1;hy8F}C_@__cS>yw?OC2Gh@+NDJ@R)`j2fLoiL61nkQH3TiLw^taUKF%A2Oq3K zcE&Mk>Kvh2;T2!K0u(ILeer6B3CaZ6d2`a#vRhLFB%T%0q4N;SLjWjwguXh@@?e`x zhEBv-&r@lYF!V&u&~3hYYeG~x7S&w5Ymr&^tKXB>*hS3u>hP$=K*wh{z8)4?rDz@v zE2semiIyKqbcPjWwVO)cx@C#y=Yk`I0mUm0qfW^-33CM@2roeUs_FX1zE;Qqjj?El zm}`6k@)6)XQ!D6{aKShm%N;Qj@3!#*e?n@zT2=c3GR;`iO~|SJS6{Q3IC`ZsJ?nXI zINSYm8Kj7cKt8lSGE#GxvL+9M_5o{aYFT7D+}BJP*u_kIO2&xqJyStt-R}QcX}a<K zpWUItI`+rU5!bwbpaVA`0{}knX>XxN=|?~n<}wWuGhb4bd~*G7cONCa{9L6{S*epq z909~LnVsPV!@B%T2G#=kx=%7X^5kj^Ku4S+4HEkVr%X=S&LYMo9GxrB*Y7hb>7jzm zpmG5P%Yl)NJ`+s2Fw)-J91C-#U%LdVuO#~qWCC?n=rv~+L`|q~c;STT&UO4Q;Kow~ zOC%5T*V4*fiC|s?6m^ANljxqsrM1$q9|O(8<qJcGdWxuS-m|j#%y)Q04i{CFW<FQT zb<;D?ti(Y4j}N>JEN|0TE>zLLshUg5#h8C1ZsKT1fhyFer1aPPug5!JrYve1?}{Yq z#$UwbOwaaR^h$&=_DfL?RS{xQKqu;t7grZAojU<8Ko47?ppla9<L|)tZ)BK*czA3W z`&81Pjp5n9Gi}m^`xzi*df-*5{ldrY)*Zjmzs5rysjBGuf42NN-RErb6!-@pc|`?W zUwn(tRm3B0NLSzh)c`^B<(sTe#?Gtc%Twi)rN!syy*D=1g}iNF1=PoBdLG_)tf*HV zxKuH@>HLAdn0wl-;se!k5Z+qm0NQ&Q%mmak)No%ygH^d1QhEAO@hKE1pHxy=|5HMP z-t(WEg?{5FydBp7c;x@lQvPr1|3_?dy5>dzR1J?mJjS<PyLVB4ExIpNFpE8(7Z_CQ z=8NC@vV$u#&R;|LBDrqfJXRk0FgDSqHE*8M(&o<gP25g~usD=oyW#xa{V)QUGy0CB zUoKnd?hF->*4Mkp><YYez5pC3ZE%x>eQTY<6waKPp&E><GjZt)vpvW6Ey@@))ni+a znVxwYF|X=!gr5Nw(q?8uRa`FYhpl5}qt8Z<fWnpo56u}P5O^ecN9Y=c9#Pg%Sr7$8 zH1g`Ws_u$I1lSLx&TA_?Acv)G&Ohoy<jUNDYOl?@^qWWMwDocMqayU~37e+(&#!vl zxxAl;$)2jfWs0a>8n=68FDL#FWFU(^TT3oI*UKqgfg2Y&N-Smt@Y6rp5^iM#esfBR zgx+Lq=`~H=0FP1d22Vajuh3rAPKg%*wf|xYhX|uKlUj6O;P(rc{%X|PVt?b8*ZX+q zz%87oNAVvDS{QbEO-^;`{Lql_BUw=}?&rB$1_1!=g7o!JlX+8}kOm&KDOZ|mLiZRC z46Bc&`jmT;7^w~RZr$26ot{%$l_~i-h3d%*32-D>$&=F2Kn53v2PE~TfL|w~A6SMp zx}~IN;_&Jdx>i%-{YQDDYWdLseAj*{@bDjkH6jE3kNGHireq@hnUPDq5s1Cs6r<9q z6|KP+ky@HR;-4gya?}hXZJdL}ri9k^@0+A2xOkTpYBQ|6Hug&XoA7~Q-HCletwvOK zwA!S;SY*<t?4`_YqGOvb>Rued>YDerJjNu_7qUimX@e~SfghR=N5W%FV|4}|@+geS zTiASWJ9?bKU1>t!R|0Co+e?|EZ%wN91^i4={OJI(rWP1To-mY8&523^&6sy>CvH6Z zW4T`(!j0(*eKw{F6Sc2vEx*nFjkXVQZ(-vnt!TL-9lqbH&L#X%MR}Xb=t4JKC{Klm zBFP@Rl970ets1(YRot5=V4#0}^1CpGTGWAnGOOh6Jlgh!hB7(%qrXa_@Z%z>6622= z4(TE-6X4~la<E2n?jJ~>$pa3O-}!TM&v5*9%vC8leNTE#IyQ?ZE~6Y@qfl~#L$-{- z7cQ>J*^QwIZRE%6E~l~|-$wic%}o9T-aF>77k<^fwq}C%qO`&;-{BlHhU*)u(bVhB z%BADBw&X|V!VH9*y7!A=&!$P;O@fJnzq?H!SI=-2@N})s6w}QfEmbk&RcPn-kGlHk zm<&j5v<j<~mL-|O=`~-lqX>#@%if$2Ss1A~{c!2oR{33k<{w8&dw5FiCBdGMc%WQD z<ID1Betr}_6SI^&_JS=6pD<g`rQ=W5jkyycz7hNqFs(uQ4VWbqs`~aZl&Vy<2w8)A zZm4(IitN(cTV;M4|8XV1<s3<LCcJ)1i;#fu#E`VSyib@MY`iQv??EeOy_uq0r{>Zw z5T1B^b9llnTZE!$$o6>2bKt9t@aAu(I=|kU58O^<jS8fX>&UT@dVaUoqH*P`q=ZK$ z0kMTRVWER(%BKd9U7J!sXEmTf#fr1&KUQn7JH!xPfz<+>#*3BJ*RGNx8){oSGf8?K zT2({pR8ZCxVpuz@wbVSn4X+$JNUMEzD?;V#Fh5#6?aY0LaX5Z@&ALz%p8>EqP7;I{ z6yx$tt#Og*T1~9_)XxiQG0_z}S`>!y1sHmJYFaRf?z^zFiyQvRDY8lMR;%9T`IUvj zG@uOI9Ig9LtikxhG}{B$Sk(DCtz5peage?z(4&8`83VwMj7v!?GicYphckU!F2^J9 zfL<}K+G5Q&gr?(G1r20cZrwL^8CN<|iy)H2h}aW|g(dz<A~D_|AsEJYYfHzZf<lI% zp|Wy3k@!zy=o_6yy{pKx58n;EqJG=by{0mh=och0B~LE|RHT2$6g#N=tzz;bNyFV% zMH%{CWoga*=gjG7IlMZ`;_LLIu^QN1n=sFv-@v4{eG(8avkTS`Sf_(m>x6-a-TD<T zC${{_MUj_J(%I)U>q6Y6Q6=Unk|a;6NaDb(r<rT(3}tOy1K1Fa>1!MvgCMKWpaPF8 z4CI4HK_=`iIivb_?)&XkAdT!;k>(vOvcnxxWS^$7q$RCyi4wM}H`nO`_A)*k?{IQk zQ3oM|`YTaj{o|7#i_E3NTa(_Vk@58jG-*BcM0ZSU(a`<VSlesO7t+eSMClD2SqA18 z;A*&0ese$vTdgAT_ytf%&~YqrU=+ah(W8VLT_VP<SX@|V3(N*J!v;Iay1XuJ#wFvL za+ANm>?Xe$iRw#MUyuW!bB*DtqHEc@4dFi##Yme%O)4)P7pHU6n<JawIwN+lDqzEk zjL7o`ls`60Q{)wP;<0SwUQxj)hOuadX{~QvC5Z+7Ct;6T`~X&jbi~q&R#2>J4ijoM z%y_(%#|eA=ABa~*c}TL5eEr~8u(g#R#j~sZy>f+er&8Rn&jw*66i>`<L`Z1v&9q3A z&(J@R#A$450%z*0VoynfJ)MrOZm1Jb^IX;(C?0}@6f6U4u}p8ES0Rtocm@J9J5Avd z$}lGnnlGtxY_FK?`EyoG`00{2XV)J-<+8yOunPaM&Ii{8G&G#V*wB<(XJ~o^g8|M6 zfLz#4bV1EMW^(PE-WwYqQL~?Imc1*|tGwjXj?1WPWmKg$#P^C&ROm@E*4LXZsG#tS ziW<FQT3Sx~18?I){egFF1@5E5u#vJrSM-3XPVQ<#n}|7ZXOsk4SrA7!eGqVUjy^YK z_LvQPCiB&q5#X4e)0K`dc$=&ETqs`5{Bi>E^I`L9Y{yhLK*TMV9DU95@and1do%M> zZSEnLuW*o^HhrQ|A@dNE#ri&7^v@^WQ@nzx)j~AwD^&6RJs9e>|B2gV&@{J9RfbK~ z#yMmN7Xr4dd|H3fwz+HGdgMKBfz-SHl9EvPv<I%7cyRs)>X*8p>n&XP7PTDqsv}}O z`~1cd$UC$WYUA7OyG;rQXZswx<_;&CqVy7V5lPX5{_j4QphIZpfI!GzqAK3<{pU%y zbZ*UNj|<LMLPtYE<Gnbf?A*?tVDhHsSz-n3oliN?vuA5_*@wp`)rd7ieABb<=e00Z z$#!HBM<2e(288BJNSFlw`2mHy_s@_kRQ3iia4Yq&W#ca%y1LLRNGy6usin$#eueP4 z4L0Ghma|J&UEf!~v=+Phna}Cuc&DtDluUvA?YNQg_lD<{$~vl?Bi;pddGgKBqi#v= z9o8&&e-KqxU;*?ibju<so`78|P2f%y9dNr#R3Kr(a)qvWyP9($HYa8?+WN-YR9wVD z(<O9_=M%G=s5$MYbSYBTA3_+GJTSfw=XYZ)dVbq1Rc;*pIyV>R5DKK(2;fPmhU4L5 zisPEPj`UeRDr1LC-1!$a{pW{spOqk>F~cXXjjxa^BXc4YVMV^E3jc9?!bF+!r+K>> zT=FiK=}pjeSJTq>!|utrpV}~(%$rdOwQq_LeZ1vmya1outgPCEsI~*TGv%pe6G%10 zar-V}VXB8%hZx_lH+|_)SF3(Xk*D0kE7^FeGtX2;_8L2SZPn4@V>z_%>L4a)aoSAD z+=(va%H~OG`5$PQ1GxQq!J|`B;|l%;$Zzvk)6vmnTL0nXm0Xa&%%#UYWKyd1Y{E5p z2+ZdgCGtS#3WSt)5KxsR@19{dxU*Dk4m2E+er<3R#mmE)*{r_v)7b^=MEs2HY8>qQ zzm#My<mo*aq<WgI@=#kd2+WeprYh5hfw8{C0{7<tq56MS!t1nRXyQ5HnK$djOlqvF zF6=uSI8!=+S&%fAi{ECQ2G@)k=JP1?jF_x3g$0!Yzk7AV?!o~u!TB!Ua;SjxT|}Dh z@K6E2T#kCGtg&A>-?(o~GG=a(-^?j^#AiH~utOJS)E^fX%HKC$)cL%)#3epvEKA3^ zAi2gxWAZqDzi47|i*0rzjiqn0OzquS)5G-D>%z9N(<q8pow%~dGzheoaHQ=fv`Y4V z)YfAUns`f``XG(~#33KYX1-4xUmPR_(i;P`B9zTH>*c$1!>EyCXc9G8AG35XEB4^) z03kI~Egr^xF!1QI^z;0r9<m`sM#lZvD&XVyUHObKp899ptIh<jq$STX_s}x~Nx)RJ zgI()p&SU=@$e+QxxYuEptLBcyD=4$(ai)c&z+noT%3VMx`E_v}WqFh_&%|l?Yj2OE z+-RK?^``AIiS!}|B)lCqQY%UmC!KXV#}-7Nio~P6+4^~Y%D`<>&+T(PI{urUI0bXA zaBXloe{Rku3kNSW=o9`iJ!UwuIgAHF({pqJZSqtulQHmkdUH6j{qW}aNFn-S55wcB z?jNY=V6rivzc`8!X=C*dbmTJf57eXl0QvC6%r*}D2T5rMu#zal0|;|F`x{H%htN<H zzo(>(6W&uVd<E>M5I|NV9<>`X-x<_TbA4tQ2^Xm2ye#0mCPU)O1W0#|{g~0|rDj0L zM_@~dvp2V(I=fxg8XO#)iS{>%d&Fj5KRYT?PoBj}oem7h6-iK<k@4mS=sLx6ocO<0 zQZmYAcjtINq^M&_yj7u-5K0RRdbSPKL8UY`q_T6_h5?e0HqDELt>;(gezy&uuBP;z z+CPq<O<Bl9VI#}Qn1JWhnwy{MqBm($G_q0!h!dfEeL(J<!M>Qe?Wj~-uJU81R3<^S z*-D%d`OwS{Li=<Dad1gVA_Lyo;VBUIC{N3Q=kJ+J;vk0qK)JVDQ;`~4JDYce7PmA8 zz+a-7gzUF`C{8H5MRh((<h8_v11~7iTw2R?E$9PV``x?e5I;dOW(W4^e;^%-0~`6d zjuLlnGID)9$ud)>^6)I5Dm1>aZ-NzFf0!bFe7Lv#Y8k7ox|>jF%Bg{i`rUCXoF=br zsH@HAs6duX8g5~9s&K5KYB}2OK=}`(30&{Z;HK1V;EsRLl?iYFXF;qs<~sU=4Y-Mw zy$P$<FRj}#P_?(_`#9iu)c+yvEx4k5!>(@x1VK8bYv>ZBM7q<VQ$XoP@=pyzBMn2N z(%oH>(%qdB(hUO)Fh1A)zRxFk?hi0)%~~_pb)LsQ_Wo@XiR~0|ztv!sYHyDZ53({} zaL`ieL*0O%f9Ut|1MP6)kf_woJoz>-oXJ#%Xv<BC{jCx$vB;nqe$C}}G!Kdhm74*0 zM7gzarY5|FsC-bS_Nd4K@`V{8iH>N3gy+q1{*B}@-CecsGE|g1^B$b?0a`=KOaMRE zIH?@^)C_yi@4dp%e6=$KQD>Zt|GT>y?|*a`K5f2#dNj7!6Q|~j*dLv&+5^hr|Dn9= z?R;eUg(_m_Wiq1bYxBAf<6-L}=a@uNS#mEI_!9c-;&UTofByjgzruegC`OBtvumG@ z(BpqBRvcw%i*39^p-djctM`jOUl;se#{r@LW*`1HJ`Z?T3A9Rl`K19GkOOQ#0+gu& zx_M`bqBUS*=se0z=g>iT><aKW{LbDz-njEce)_<Vn|AS-Q*)^$OJyx2dZt!mJf!Zt zH74$!X021}Qi{xf65rpi*UNW=-<f}~5BPc8Cf?1MY0&U<L^84jjc{*q?0nPWhxOm= zfajr18w=d<edfy6Diwv=Il2Tvw0$#)j+2LPF>Igb4=GP4QBQt%|3hhrcjvOJ66EJr zseI`lk@Mgg!R-ceAxy3F1@xt_rr!c5U0pz4bf+LUr)NMI+F;Nrr6>g+R23~Sp)3%v z)YW9Clf%5cLEVPK_f0KA7CXw=$Ake8B|F9rg3w%=cBPHpXkE~@hwsssxfLS@NW9Vz zmFg!pCIp6Y6a<+&4WZ*(er3DR903T^;;tq-ML@OmC@~vxbabqAg*59iaG@!K7cn!i zSZU$n;@`8+Kso+L4lwI^Q81{b;NqK5Z}6<Eev8X`ShBi2s|>QU%N;UrEhdKF7`kgS z00G>h;;^wVlN=<k|DiZ3-q8J|4}fGbd2xe1MZpExy`~+fX-qIP(V1yz$^-;&v(WDQ zHFe1I$`0a@y}hNJpMFld=p7QT-Kpng`YeF6cJH5_NSaaCT1=r*z9s%DpH7uMB(V|B zLRgysWAnaMdLwr<!gshxCZy%F6(Iio`Ddz8ruSi3m%Yfjh2I*1GGb#z3i#9NcdVp4 zTPDCf>6QV)xqS-{sjSySTvWA(lTlcH8n-eDFK0uzKF4b!>^F0(um#p9NZFOg&_yeD zTVNZFFo$<%_V{y>k&3L<)3dKgTa1FIT=HO=)Sc;mnRSZm16GVcl%Z0bv<*XC<7|ME z7~KKn?`lu(y!#E)N1!BI3Xy<HBBJtF;w3QF%Qycc16#*|vzw;(YqYZ~|u<$F4D zo9HHQ)IXH>FBM+Djid>UB!Ov+1w$DBd{VrXoLzTyZY^P}e{aaCeP->S1;|5Mo|&bd zW?5iV<U`8XIAn~fz4r81h1cb~rY0t>Og>VQZWrype2Y`i^_5kRbuU+*QL^u@I4`>2 zn9ZOkURw+<=3zGRX`ZSV*Va6UFn<c?wRI|CKPqGJ-YeDVoeAT(;>h@{bW`%hlMZa# zQeKAZ=u9O~s#R7g+D{#`sbOeiUC)HXobpq8YHc!FqKMs(7bPy8SNd}#@(-nH3*t+2 zz0|m#4|}a7XWSS<&M%0agiMQM%(y+!TzPN|&6)03$BVQ~{3h+0W>Q@>0Dy-tc0b9Q z>PqXfp|qetpuN$+v~6e6%<6b4OJr~BKk@+OiF*)sN%PtZ>uh%++BY?Lh?h3wmEP;E z*4GIgd9DoZrQot#_zlWXp6h|43qMY_f0<fb)czK@z=-VusM-NtTf7edtr;5D^$j~- z^ZGz}SYIEl``gHX{G#@LIEu~#<yRUB>(X6Eq0J%eVC)a${Kw=!wT-?lnV!ibvmUEr z=|d}(W6+IbnFXk_C@Kf)es3=HE}2fReNv~l*d`mkE)?<numX#xj<RFI$$kgFIh1F% zR#OzDc{{^|GGO5(6;$@e@lCNL;GZujXJgDjUm+RfE^vtNsl7FeFRs^8S1K*^6~gOP z!jn3X{{8pJPGCl;r6^1fgAIE{^YF$nAopAI{iCv#tx^;gdv3g0N+6}kEKo2uv*x#I z+|O$#Ps5?5JUl3Kwz5{&YYZzYFE6o-f^l>2E||MvCg4}WHWh%kNOdPH6G{nLPUS~= z#T4_ldG2UIJ{Fn1xUmz&9anNdHRpQlJXeUXSui@IE~vQbkZ2N=K=iThMg5D3a<p4I zu=7`~f#Jt^_Bp(K`YEN3u+!h!ceUqgN-PmHEOvC7`B~|l_C+>dz~GPCN0FXa1SqUi zSk6X(8O6JtHI6yz703qb*2^Jjklbas_3q>MBU55>O|5cWU3D;tb~ppIt*uA7<f}f> zP4!ml&D9jaPj<?0($@=GcGte+0|TGMEf6oC#SP#ut819klp_-{*dTfZ0J12PY4vqG zyRsq0!HNqY#?69Yo&vg_CxDe*?fKiQGK^BVpolpc)HWBe9dxLVv1kl68*F|uFQ~B` z#`chbTZ|7R4M|QVo?euHq#^mF4sxrco@o~kFnRgdS?25eT+Gr+Aa+-x+ZJhPjs0!S z{qT?C!Y0^J1YyXGT(x6(6D&=>D|}CUzM?-{CU}xeN4_@|-E#d_tLYm|fZ1+ZmkZkz z*HMbPTJnVVR1r?l7PulkBdJ6UAhd;n9Z&WD_c@4TXQgS#z}Oz6yXeR@^F;Q$^@x2H zpsPMWyh+!B#YBNy)MwU>NAJljyOegWm)ep>MGRr)<bi#6S)BdR!>`JGh*=-cP@2rV zY!T(KrA7l%*GQuR%VFEsq<>a=0<LFWzwTY}ura~UYL=GOs&Cw*Iad0sB#N&jn!0BC z=)6aD`lN+!kjls(30O~$P3g~=S4Y`757;4a&sl!m4T1OpYD8ECMHl+z?fz;Q>6i2Z z=LHu|REi)Jl4iP7vB#ynJy*C#-<HB_sRoO_c!_kkB?Use88AA->st_kKu1?K`4gRc zYLTmnT2yZzU!8V1tmviG^axI<W?fW-wWhfa!o_{?S6^9OQUU6GHISSXSR-NntrQGP zGs>1=idPR04?L}i5xkjC)X}uzo8;oRxDs??X*1zddr&HTbDz+1lO{tQ%i-eU@@OrG z<T>6W3LCH<Ev%%p@7I03bb<Cd`cc_Ecy4E^Oc(bOJNRV)1ifP&yUUpMftoL)d0%^= zuE5tigXsiP&ruN(JWBGdLgY_GQ&c{6dhbr8|L=6}-%0ce1<8q&HH2-ojc1AytIHHs z&EAeC@xg;FKEf+v2+qydG)0HX0!5e)Z4{;%mk+P%vcHNYSe0Z14|VEG_Vd|<VQLfb z-rR*p2^ST|8DwleB})6~F}p*UWbd<-<{hE9P?u{Zx(@Y>@fy^~dnm(9+@}`q(Oh+V z=+bdGk8Wkjs8wNjDmG&I=#Sz%T}7j&(xi02X8-1v6!Vek`aC1WFabq2O|%<RarW5H z`1n)=IU<>+s*_}t)v--5gv>!pdZ^*{Lk3h%w*Qx?Os>V_Xt1>U^q$&pQSVqZ%3RV* znY8*wX2t1V%+>uI<UTyIwwX9MGZ-iN8ws!#2DwR56g_>SQ>=AA%cM5`ewGM*oX})3 ze*a{;y*Td-EGjGjFnK>;9LvvlQBj$OvfY{0hiuYV77mI1L6~d8c{-wr(NGQOj>6O9 zG|E<t#|eL$t9D%0;nnPswnka3MFA`7S}>UP3es;4BNviys=^|BdX`5PXM0}c7mD%C zY)WJk6{)1)qE})y-ZB|9?;qbtf}I6;EDg6ihB2>)c8)}ZcAp_jD0zVvl^*G9Dm9bx zM!O=$X$e_0_gk$({P=(koybQW#H{&xj0$`R>MXyKLjNfQ-`3Ws#{2mVwO51W$cjYM za-D;v1omn_a&M-(Nbq9GaQt`h;!TJ-<X%u~HZ7CcMp!K5MpB*i=>@zrm0lh7=9hX` zUcmIMsPY}DJ2%NGc10M;@hjwaZh&{mdHWRqbq8PTiiU^}b!f#fIC}ypiOoKz9d)5k zVbMgxuWR|UKl>*)zJ)aBAudgz@r|SIOOebHP}OD&3rqkedyf+*`<?LoryvuGEU0j8 zS%yqc2oX+(9uy#6Q|?Yx9YccFt4ufX)2`~vjK^W6Sp6bSxCOa5f4wS09Jgbv#g3L< zU%13~U?*+=gG@+1RhfhMcbD||Ze!PAmSW=_jz5#^l%MObc`Qj6sixbthJnniDA+Aw zBt@n?ql_n2ih1Y~cbCL^c}@w?IfXVS69Vm#0Frq*`>RdNIJ_lMhd|%10ZX&sWx~tr z+e2dI#JHbof3(%iT60ony4`eJ5uY9-pFV&p-Ev_I&d!o05}Q4^zZ(v(dDg*GXMkT$ zG+9^z!=`R%dOip5pet8{%HHg<qhr@dD@I@-^!ZZ_a{d9(0LYq~bfxD@r6-joD#PUQ z4Ou3i`vCNtmF@5Q9~4rok4BDAGMBq-ABr;bpP-on1DWzK=1yO}+_GlFH^!Y9!r-ep zm!*`Gw2{q^*A5LM{pBS|Di=>}pJT)H6tcou30osMWKmzdko@X?i=1p^bH(Mt=~YWp z8bTe-jRZke&6XYQ-c*H>6sH+nI)=Efow-SHASZsfySM4gXw^4`gWvg9#e%%p1}`h@ zh3?2~7XW$+LBxU6=d~=!KI7F_BQSyz0CU${%IW-Lg@ciXCCi}Xu&GKbSJ#>OC)FM1 zT#2Y7cwz04z)RbQA}+Uzq0}I;)aO|WDFVOjBx_!n<^okLm!OAT<uXO=Pg!KeytZU@ z_`;a@Lmz%4->K)M>;O--u6pkQDF7m3-S#ngq>JNyMw4@wPzymzB5(|hGP5X^@<jcU zoFWZZ)uiCN9Qjv*#P5WXEN)oW;(r;h7(0Cx&sz3K!;vb5$P@&nP_WL<#;0lg?dfs~ zzMB3V8`_rqLPmtJAi&hbVYK$EVVN211HM2b!FJrrIc>{??ygnTWv_DN#yv+{RO?Ld zI2cvdiz&!CAnWKK3X(W7xkvFLy3y+=$?%h(m4Df38_lS!dEK<C2`!$ZW;z6ITwYCz zPJ?(u{*eixJgzr4)bcGpeYz-rPFJZKhY)~jLw#b%Yag>m$rlNO%)QDK0?cJAAb~$1 z9nR}dy<TxUlm08m?`^p?<$oOTsfTCmY0C}Lbhz^-=c6L9v=f6NQVAQLz>=cl)4I*c z+uDX=B}8D;_SQ2d0(qtId!vr>2VSde%f@}9HHr2yvRXp$`k?R0^>{X@C`KnQs^yFM zjA*jQf9A;Gr~CeA)+_N4s_>>UlGg^Vw}`dZ@g=RzwF|2*@^fy!XlI~`s3BXCopQ>R zr5zGl1;&xGKDts$`JummcQ==mlS<}4nBI{y?mzn>B|7{0ycl1^g}D8qFnCAlK|h-p zN~U>3ciy!jUUX!yX0Idx>&*2F=AEcMKDC@FQw(~z)ljU~a&jrIS>-eak>c;8sxTU+ ze4iUq261fO00VDRb6t-gBC>==khZgfJ8pRijY3f3C*an&O&^{@pIJ23Ic(lR(9KW& z`t&J(NFmo$Owss)IWh!grAzeo+;%L5nX>>HI%{z&FC^4~%-%W4HC0T_j5LiuvHqbH zMN{H?SF_Sgrv_Bv1&(Wd3ZsSrDkuJ7fOxx8kU1iP^5(<p*`}r;rxZI-9Ky^Dsul=m zu>xI@yxsnX^6^D8?RhEm8*q~+JJbsf#9s!=N*q0<XBdN46r2q(G@x63LFGuM%^-|6 z_u1epqRP@_?Z1Fy#QQ=!k6Ui2wg#o$Od}u%8r1f9sk^N&PA+i}EEClo=zG_MUFM^? zp=(W~rV!qdLGg*e<(@tczwUeKeoSyxujC#LrTkYIK?5bwGJp4JTu8c(h{JYBOLI5a zz->k&)6N(?)tvgG1eklNuTN#ypt0FFDKa4usJ_At^ehRDtfas?Nf(g4^1&o-wbF0C z5Vw)mvZ*c9tsZ#97vI2Zb1qBvE<dI~xI+DH0Ocpdr#mV|1~9t(B`QKP;#I4+;w-|$ zjjiJ3=%e&22c3K7qjk`pIw5|mq`I6vnue;31LMNyeX-`%T5GZTh$AK-Wbw_>1B+WL zL{@U|;l{~ve;_EaMJcaS%AWL*55UjgwSkdd##(C4rvCOPmr4@Hq`K`wEUr^VlotBj zWKdGI7@laLC!%H6MM>f!<otErpk3Ewt0HOzjJk%xqO|>Koc+ObKK^WWlL&Wooz_s1 z(z+#XPhP4Nl>e6QEv`~O`vUP{!i#QtKC~-U;P!qR(c%7=(x7sfW9<Fz<(B}-g}XSI z9XnN&PF!2laqc>&1`t>iZkJd->iA!9p#FcD)W5m^l?HSrrOHM(=jTOY6R^p3n-BPb zqL_Bot*4{kbDGOfWEbzQJ(}2NR-b^->hxy1wkC^w=h~*x6foL|k!Dw(=Y9zPfc{y> zv&_RM#up?8os~;BovessM^dpXj`;Gx>nz9(boEVs{cfjL)Yf!==EWrHEim2S-{N&@ zcHDP-*!*R>Z!(~42-x+tTvx2FZzOp-P6lM*8Q7oMe+z^Xxg0iDLaPDGdc2!jIAeCv zvba<lmUw&6&?&VYpeUB+z}UKlTn;?Vj)$}#YPuStUTK;(<8Bl!;Qdehtv+21D*X(A z0REwL`;{gtfE7gNYfZ)d*4iN-Ga_)rnU!Z<s_6h4aavtLYCR)yNfchvr(D7$KxC~f zG18>|ntxz;Qt0wog{t)*3Yxii^1wBJVGD8$XMb`sXTAGAGac}K<wP3!z*6aFdNE2_ zKJ$<oHXL_7QH4C?*P@fS;4S$}V=k-sB5q9aL*utCGz5Tln~umYj8=x0O_|<#(eti= z$=3K83a-c6QOLw%dScUAr2WZ23%r6%Wrg|srl#0@psLCHDzw}s0m@H)YqQgOpi#ov z20jf5puDKX{Oz&43O}jRskk&wZXhBHj2?1-XKX8H(`SSgXIwB_u6zMFUtR^b4eE~2 zTbYMK<OhWiYi29GJh{32VC7799eX4F)8kE3{MvWsf31Rx+$%T^8hk4XfibpBl1FL_ z0<NikYrrHELk`+MJvg^(@m_7YC|0JK4UOQIvMVBwF#I`4-r#VHb~LabbTX1(DKDl~ z+LGvr>dpA~Dda|CyOn90DIZ^=K;Vn36LD!JbjY__tpMEkpWIDM9v{xFZlsHbz|5G{ z96rjB?t__kw~>)ihLmI@`phR|jhUF0H2ng8K<gqr*A8oWe%JE|Uiu16JN+~B9n%&+ z3e#=Bh~`!wZ@1$2=Q`JhxmLAQVH!hXj5w^%L;BjY_eB}%3-A4N&7KRgJ(+2rb4Qi5 z|LfPji`KbT{PngWQd~<BbXCV<M$TZcn;;Ex08JwdHsizx#CGl+SJWG4ulo3tOFb{T z(ZlJ!@fqQM;t!appIrjNOoMOz{Jt)P!$shCwt<(*8oC6WFQk^M6RF=#6|ek5!9J)f zYGz${|GsACo!EKpRim5EX<0o9!OX0DY$YH!zZvS>$Z&a;)b|ERnSaiIY$>$|aIaBO zN;6_%<k3<#HD-chAFp~CBufsbp+k2OVm+r_f>&z)eNWvdIdowA?0H!9?S4^}Z_8U- z+{PX9=e_J2ca?j+Gnq{~-wvb0Vqqt0qtu%pb7-2={FO(i=QUR(H-7m}R&N^{X<h0` z2iVy;7Sb~GNsYl=Q{&16d!$RM7B{<oNw9&kxn4~zY;BHYJC$Z84TkQS*QKa;<h0$! z>f?V8oo<XBak(1j*fem(<gvc&scChnd`g+Xm`$}@Eoyn~r={zrT>WC@jsC8k>|jp4 zo3^&b-_Y~2w@0AVp)0}lsyl*+&%nl`!sS^!pTn);Mz?N3Aag#{sQ^?}6}Ed>ZuB9# zbRXeYzN)IMXK=F~B2zJhzqj678j)~P<;+}IV6K<ea`0J7@C_(Y_Uh2iIMBZe6E{X{ zZl<;@tT6RG!&~o&#Qo4wB20}?6sRzK50t)^nQ%t%v28cHpI+x?U00PJhj+4$O$c{e z_&*^W-OQYD+4#GvV(Vbzm$JT{WQhrG0aN6fOe38g%#!WU^+Z*5MWcCjwW4qHxbj81 zUUQcFS~U)r4|K<m7MX84XG-2_H>D87GUooM`{YKd0<A@uEeEaKsVv_4XJ5u6@E<() z)|{yGK{4HlDoi#fCDc%!6alKdu(HD}`+nj{5Gn23y85H5a$68{XkYY;0Dv#Fgzn+t z{<4MfK)LQnddLgGj?A8WlxF@dV~I$HF@{GNy&b;ksa8xJ1eJVJ(BJK2e-4emxasO? zgm`JcBjM9Y1bw>_<2!;0q%Z5qw{V>LkxKm0D@dA=8LDkK>*i2o$=lp<?`C>}6geCV zX&sAjNCJ1D8+_eNESYc)HpznGCjjPoQ_y4Kur;nTdtcVMk%^hITNasTu+uWXLtg`e z0eU6A_m{^~jeZb+Bn|9sToXe^s^Hw>l(-xOiwyq#u5*rq$~D$_DgyJGQd9*+AfNSd z4SGWkZoJR<ToGPLNJ!|}a5WAlaeQNs!GBfWOolnbCEwAL6{U2PA2=ZGq?S{MYc5oO zNZCk+dtcMl3`35{^E9i1(}94BgvT08@pXe+QwI6B;aPa1<|Eb-M6AA(zpQl3+8{Og znvsm9BqCmE{-_NIbj_|riMe9!dkf~#H{PV8KRdQnpHFWZjr2358RH4y#y917ba&Np z=O5|oFKzxkd~ITuppu@#>@8QJ8%3ZR));ncV1(T1D<%J~BOypz-irOo8Crb<e^erw zD%U?b`g=93P79~sh|qsVTYSG=m60FLZx2f6$q=miDQB$OSx#>@j?CJq5wiFHrdc&5 z%3oZ;M4*zN+(H>K1NhVETna8kxM1@r!`L@wO=TiJGhI9h>++y+l8d5pC$kk@nScfQ z_%RP!FB(Cu#m}TWeBwMZiGQyWqEPvRHP+Jh55Jt&{VXFAoPHx^(toCZq|7*$W476u zw>q+i{|IRI*l}g%E-bQeQMwCZRqDo<IJfoflh!0RV{a50et9Pw5CjHfyUV4ZYP2bv zy!fuf^>Ub+6!SZN@#1>u6m@jybBBX0kVFEuSUGK~>ar?xMRG1{g81lP<`)pUN|@6K zh?$u)y9SNp@GV)b-u%t>4w&}*)+<M2jYR^&YFl+=ORny^mc4%{vg+GfkPx{`NwXAW zL3s#57$1#DVk}Brz1ns~+GsWo=?ZVTIZGk%J%zoh1S*ceBgGFWU>&}799W?LI_86K zB%>AkYr4)^RCZ8(G9myofmaM_=Bh5>jAT+B)<XV5krEOT0}#S@R_CPhToNjqdz!O$ zAMSANLY`Eh-C*rmUO_QJUDae-@<em3dWEx3D~sx%1M!9L2zJaFjyl&A!LO`*^j4}T zY2V5MIp(&GU4m>pnVx}HGxKz##p8X6p+urAxw_@NUvG_w<8M~LqlE(R#R@Y;aC~^B zbz{p)Q&23o`-`-=%Mtir_anDTattkO!pzHUE*JYYs{}J8ubeQvZU&Ue0V=h9&aa>% zU9EW$n##dz8kD;a-z2Iux8we5Niu0awYlfym6i7HmDyPeq<Hc<9T_sH*tgo<PU%ic z>rfin;}Z3y5XIt*A5m9}`T4KH>KAo}bhzk#yKC^Cwpe=&s(3`R+wtJ<uS9=;J^nF# ztz4^>-t`GVa$m|NV|z3##iz13iXIn;JkG{ftj(g^X3Xp4={XuIC(yx~p-8HMA8P0t zeABIPvKa5`UBfOFTWU*ZpD2D5(wiG4aZNu=vAk-Zq)qf9Lu{^?<Sp#GY;d0gIaIEm zv1G3NQ%<9Ri3E2N^0!N?oU0PWe^iLCs=Y+XatRi94@uh8*+v!lwPl!7E4M#2Xd>s> ziPOxd1;c=J^6lM<_PnC3LL-4JQ9GesG8iq=Ln<`zLL~G%{_nL%8)$2D$KO|H6_%Ig zAZ?Xai@wjKX#232qBf~EVenk*x_D~h5F^*yV$;4+uq@dhB0XK7Sq7s@RdH@zf$m{L zuEfYM6>)~a=*~)>`x_)BMW071`lk$ns4FdQ5M?zr6603O!Xm-%WICy^!-*K^tluQ` zjuD6omof1Jm&r$Hs;EQhm_~-oxK5()Ns;Z}NG!C2Ix$tM8nbRt&^aMbUm}E~nQKH7 zT1`2xN_QIsH)C#}Cc~#VB?Vc&I8xdYok~$Y(fe|QAu*Pi{mJ7Win1WdWvNj*sH59^ zSMM9Z>K}TxxRRv3FX#7-RvaH{hz#)Ab18AXB||*&!Vn8$6hRV3_6~60#nD2-Ua=M> z;|FMVaZ*f0>E->_f8XqW*-PO-kCJP#ox8v}cTpTLnk3z&#(RVvW}G+R;k9S7V8kL5 zo`T5ZpB!6b1<gz+!c)+m#LdflRY{DVxN<~V?Mee1za!L#;RmrLuQ7z54uuexUI1n1 zVNF3$siO2qxAY+a|KxC}ve=HKz&Ad}s`~m1k~Ya*PApTWY{}pl#xOoPGXdRz{3!|g zQC7Fse1I+80(#->hy@1>SnMS#Du^(9?Zq&=hX509KBZ!{Of9gWO$-6#)toK#-E5~Q z<sexcw^B63H~%tOL)pqnR_nz%t*EcT@e!lV%pswog0f(F2|^;p5(rwFEw_7-C&}XW zeI>>TE>V76;ag%}p;*PYkU?QAb>?z#2jz?3pRw>%8;>c-uYcETYV|e5=w(VJA^wd8 zw0hTXBxQ=*PD``rm%V4A6w0$8zulZN!q2YKSzcDE8>#q&ep|ZLp=!r!Y>1;e5J(I^ ztW>(Cr}P}ZycHBFjJ3DPZ)c9wkzhr6;bV4C(G`D$1+RM4)JS9trZ<t+okKy125gpr zigl0CB~F|>m`}-BA+Z6Z_Kb;X^E!!7-6C7AeIArDDQ|~0J`Xn(WvTOzoaXDc-fZku zLLuYI+&D5>q9P0OjdQAmuI|r-Lm*(=acY&aG5_Cd6=zQrkx*a!rV{+Cqv(Jc?E9hN ztM|{Ls8EJkX0=wwqOBDBW6e_YTN$~iYd-8g1oz<Vrl{}x#)@hs&qMPzm0D5xm9;DR z=TbYb!f-o1a}SiSvHQq5j~o3e8aa;{@42uXVE+u>GtIMdU-k|aAefs|#Y~q4x$-nW zZxeAUx_WZq0*`B}M=e+^&A@1{3Z=0^q*k~oH$bs&nCFk>45*Z)1;_PK1QD0E!5U2v zw}$yp&Q8}L$a%a-d4wIK{yr}6eaPBKcL~vkRo4No8}2=Dm2k8CHY;5@1hclry%i<a z{Nqe^BAEl9kEu}#mP=8wFJ6+q9zeN0VD`2NOIFK$7c1}nh=uextTMw;Wn+j^+AfUC z6uKPfZkTzMX4LjNh56hKD)IL%p;Vq$nSniGjdaHNh}U}O@MMg?7F8GznxE-E@fJ9q z;`Hsz<NH-=bIP#;AoN9-O$tT!DlK8+Y>e8}fvHxmj|_?+By`EoL9fvB$_GW;>-I>t zrxu;c-St#IgnyXY^fh?^2`OF1)xnH!-$DIQW@Mo(s-(-u9rj(NtT&;`JE+GN6~W!V zh=;uBG?BDB6$v`(sNJgfN9aX8tzZh{LS%LM<paF#YT?norbYFj0Rt3b6f02a0QbPy z-*LAvXH{D5osni@ilEDUIJ!C1tV_3YBi3aqfb}rskXZ>inQ~)3m;K#XtHbe_l_DIg z8$|`FR9ITup}fvP?TNpCRBRrws?EGKc+Eh(punKvwFr6&DlY9lV%(!=$vX0-v81F$ zSxC-hiP)P-a&IIRgm4SW%e|fBJ$(gA)KX^kdQHt|&nN_wMM%>_<D0R|DC}5Z@6FTB zbVD+gH0IQcOm&7y7}iBhokig4qbxfa$jrr5r``6%xwjY7C9-<K6)08woN=+d&5tx? zicXuZ;Tr<~Py$3q=-zVpW~B5_%A!p#XbEz4ie!{od$)h3nAPFqs%bCh9BN+?m!idb z0*<#td2Wa}x4O(uMmhU@?xOl#Qild3rDx%sy(#MyM2ev6yI+<M)AE^rSL&G`QoGH- z{4=$Egoem=xQ4e4Q-lVz9qJ?L;zORs$T^;>1^$Ych$s{r9AM<^^S!)2VB3I_e#q~v zC225$*4|-wWoAjsn*I1TwOyM$r#2wfh$g5XM`09Q%hU&|q5I=v(@RnJmgtM^-cJrf zhJ2gkn&M~`Hf{!-&H@I^qbI-N`qQ&fm*#xaSYGT@kaaTp;=c5w?|#N(|FxW7XqxdS zrQNrd`T8w{A8CurN;Wq~Ss5CI*R7e{=Jwd%UHk>8@ys4m{DgZ4oTXObMn><LKWWx8 zrD&jsl1MIYzyJqj`MwF}pxIyb4M|##eIk$Qk&mYvEz~;OIR#WU0h-RSLqd8Gis7IV zx4D@p%2Ir0F*yi+3;F7Vy;yCh)7|&5?s%8Pc6RA|zqM~m+49zsR=!7@n-<HBchF_< zv7-=<&oA4$s6VJaeVVkE7Kf(WD3vd6j-4mdP2L%|x7FLgg-|sy%{|ny&$-+vzs~)N zu<wt~5f7<5)(~*dbe#A=O7lsYe3V8{LY!=_`)oD2I2HAf9m3HsCl8Kc+lNPr{f5>D zIGw+5+Ky?<he($>?Ejd(I^IWvL>(=S)S+DxXKr_@nzD(oU!xV%My*p>`2AtKGewux zkDc7Py!~1~q;?55MpXup8l_++GMg()F=0!YV%Ge}4;$uOciHwc+KEp3f)t~(_{AD4 z69`5`NF7@HP(Z{G!uNmBT@5xhUg)4G0hlXAxD@7+Q)Uj#oXeEIN}1CyTX`oQj+~%` zaak$tAU{U_eOYMlkylKgQ~QK}7yT%^sJTmD`~wttsBW3e<>)LT)QW(JcX<N4Xcd2P z(SVK8H;kY0iysh)Cm3s<%S4uOvi$#0e#kSwfoe2uMH8L`D)asxl?H4~X}JT#IEu;( zF9x$s;Z&Wf%s7EuQH0cH+IF?T8W6|}$CPC#;b0Y4%NjIAQ}kHSFD`amI`tY^I%#8( zMBD0dfgfF}o2Q1!k9dDe_QE|a>Wt}mNd?h>Hg=ycC9sNqg5Yxw3>5qvb45Uywd#_X ze;t;9$xyb*laVS}tZU<1ybA%e;irIg%_?POP&3-mD=}KBCw%+<A0OL#UIbX;MY#ym zs2Eaf@5gnVg7I)kb;?TVWGex;KxL09@2QMIgJyf}A>TuSbn)txoBD4(o?Z@-!~}_r zj`OxLm9|(F?q}_=CkWY^AFo5S7wA4O+X9-XbUbC2fXHZh0E+9yN4_wJQpkVc4^$6q zl?-x4w!-!O1l(Iv-+z$0#_9T_A!&E;#R@`<l6NL<W(ULwOglQ;{nV}B&I=}s{I$1H zD2!&3Wr^jIGcU9Xr4r^)i@n)@5}!9v$KhA#Nz;5ID2O(tpDSK;OkyGT-;4CwF!!e} z8-o?8E{!$dOa{yY63YV<pDP7ihq{dvEVX3VHO+VNm3I#Rp^(CDbMwA=N9jEEi)ua2 z_sMa>cY=B*e~RO$nP5pHIJ0j9B`+n`E`FKwirBB@ew<bqf8*6z{=FmyMU_Yr^%O}v zI~31legvAwsL0GI6oFBMX!r+7GR>%tA3d1s*99x;^-EPj@QYV`cbL^QSz4)7;^&n1 zxNo^6n&?g-ZO$<mlzQ>ZgjNDKZoE&Jr6$e<rKRyY^h&kF!|=RK_th=Vo%KA9-PB6S zg!eZzeimsNV{Tl({0^+ED;3lGy>ItQA`AIAF_^-wEIFzaaw!<9K%C>1H+P0+<HVnL z?uvUhhGB70Du<%yeol~ESy+CQvfaV<u%-kZ6Ry0JRKoA;c6;92&zQ8A(VK^r|9voK zuQ(%9+DV2flxalaF~yspKSsJ^+|~IV*GZEWL^rK>U#^j%+fmGhB<($T;yI?E3wk79 z>uJks8k4Fe>s*1cC-ssUtSamc{KE|wQiZh{<V9tH_PoPe;~io;3X9LWjjw5>p~aF( z=LDri<%f%YcrayfaLc2p8^#tb;GL{)F$MPL=l|~=3IA6cpZ{NJ<9pwLguGHmS7k?8 z!~N<r`>|rRpBFZjBFOg(BEWO39N9(I)#!y2kpG$FABuwH<ykWiZhOGTh^w7P-@C3H zp5Kw1)d&P%A*Dpe@)sl3_mbxug8_M)1qlmx(`PENtI4f~>QV^l?2nx)KF_NNpwCQ+ zv)Z$@<mTB14f)}JdI%u2GI=m{c6vs(-8qz*Hw5onkg0G!<^+z2YN?D$i=XP`@uu*y z+)>C8uFL0=;^Q(vC?9X>YR*q=2=v8YJKa=MGMcUS`^t_Jm?FG${Ke&N?eWWx%qWjg zff2uO(YsDQnQvGqCQ8DM)$R_^_D9wCd0Icf`uyZL_<%9-Dd|-z78VC#{2^lfcMx)Q zMNWcZaay}NvWjo9qg?(=kHtaHt44~9j2mJv6}D~{;_oJMbwB?@gbdbu{B)|^CW5x& zZN=XdEI!dNI_S0F={0B$E>`;uCr+sHMnzFzN$^z$F&Vh}eRn;0f*L<oZfu<^F=|lh z)1@gd0A)?PQ$S(EbG1V4^r)eLKbPax5S*S=BJ~z(3lO6ZPbMZ9sBr}*^{X)VW`#-0 z{39NG@0z19vcE2n%BRo=MmD5b(P;<}I<VDkZo<rOy-s5;t`GTziai;V|A!9KO>g>l zW20_XQEhsrD7Y`-4;Zx%Dt%8o|NAnN++MiYc(%N$!}1Om&5Wa~LvpF2RxYMxgm51D zy;2!~B6$L7b|?BY$e8-X8Zla~P)=NZM1314hgU%8rrB+F{Po2@6bMpcf@M8>Qfb)r zFpTnO@m8rVJs!@w#@7n8)n2{Lf2Wm|-2I$iik4=*NZlP(;V<DiDMz)7$9f-NjGHm@ zCC;j(s8plLghfx|??<f9`3H{Td+WEDR1L!FZr>@zo4=Rhs5hj$rtmwo?9Ub<N30TO zggt1sbOH<yG@@!sCEiG$?BuOBT$ycr9A0Y4Y-_URxbwos(8X2-y?<kE`Wk->ZJooM zz?+F@a00^1fMyCS_1vW*H>+-K=yxINaroeH!dMizH5;zrs~B)4>xvNEdX>wV@`$fb zM4GY7JdwgdD1`>jAp>1k>s9BHk-;w&Vc$tp3QGpm<~&`O#S)2XN98T{2j}}Cq;GYr zan#ez;=ka1*&UB}W9Ra6*ygAyLCHE3xp#%zOy(-`TzBz1)(&x3VFr(3;g-x*;~ZgR z<#m0b;s5J3LHAVy95be=r9U-BUGZ+F`!~fJ;hkneh`f$Xvggc*Ppd@V-g>&xz5lye zoiCKYUNQ@RM`WD!d6uX)s*WIVi4B4B5>O8>&C-zv`H;1^9UHC%*gK*kakiqO+#exE zv@9W4$AaP|k2@w;zJnNB(;we(WE1zFcB3<YkfJ6x_yQ2#<l<{3=ydiu_39mWYi6Y> z7;WEOSlZgtk^oJw72a|0|5}+XY>zpEn_ks<%!1!Wa|iOtk{3GU`@W~3PcE^J%hS?n z1hz<KYcv3p&Fl0xmJyW$+?LS^lv(bbG10-QQRuoqI9k!Z%i9sraKXQMHZxKU%FK`} z{+NGIl0xhAXX-=dLDt^DG#?FAXgd`rR!(Q;bPdU3A|Uv+snEzjOXcU2tAOg!vs{9B zyK+h;|0w+05zz)Q5>x18c0&y`P~0=or`vIHUGc-8nEt?cUdKgxi;>;vJMw|96TH6X z)K}{cN)^Tw?MawLyYyDbwi111FUni9T@*#pCRy`RjsHV^)OeZL$(Zp__wHWvT#$tW zzTL{xPb;pt8tqecyj=l>wlMj~dUmrGp5pA{#f{0@_A|QX^rFJA9#GMsr$N>DO{%3u zt^IXk#zZZZ2~~WFKPVVo{k@Zg;Z()VPVz*9#&fHiR64Z|@gKEc-4cxC)X5OkOHmon zDSXC)Z)#Np`}$lx>Jd^$7QM1(A1kvgdQ2*4wEW!vX8Ue`scLjN-j@v57*gS8T$+;+ z$Z&i#CXJ53Ud#dTa{vbC*R0wb7jjr7{t?%B{>d0C>nP2KsVAScTMBHkQ%PMoDf35F z>N+b><@!Ci{xRK(FVvpWUP!_aWsY(q*yRR3ksz<TLuBL1mtHk}kWE{s6ws!jn&1g| zOg%f&?mF$5F5$n}rI7S|aJxkBr{l%^#{<Z^*TsOz!q%dwNkIx_c3&xK{7@#sqtQ8d zU5Pd7d$q8tdU6aX$K?}`fZVrkI$Z_vOlyYDb`%kbEg|u;f;h^U4s{Nod83|36(^xN z&V_A?(B^BUgZl$K=aKtdi>Q3m?_=UIDfqA2!_nr27i9I-J(uK3lo<lEkR0)9y`_cL zNIfW}DqSJ5ilJGLmHuJ{q(HPV2Pq^eXfC^qe!Y5x-`<dX(IlOj=V&G8@H(QrEH$XC zS|Bi13;&ISc*LCFCvn(ar)$M0nJ)Syx{L8?-2J=o=S{9;LnG(r#6E9D676qc@0KKf zMj;oD1pgbvotEIU%3o@=w?7I}FpYoQ3%XE**QR$9H%Y6e>9y|6$al0Y$VPuFl-$_S zQusVZezL(5gY@K@kTyFUY?53++c-4SU%-Ll6+9R22|j2?$f-X6+35F<(F4L4uq>sd zOh{w>^G}(<C-;qn(cOw;!?FhU%gE*dwpeiQTY-5P>1Px~5Y)+sFe)sE)bmYWp%-B< z*V|b&Ym0j?qP%|S(Hi~{q=U7F_U`afq82$x09}|!<GqXaV~{R_JmE{7z5!TXe*C?s zTi!7qJF)kB?d(KuzVIq-fx4l<bAL4ll*GuHpnV&9sP?I5GoC01eXO<s$uZN)C)<HR z1&cZ5U=aNzUf1bb0$3P6^1LQ~*7mZfwGR5d`>glGg<9k3z?Z%qdBJO5#e=5E8n?T< z*7IRV@#5F3WRV9ul$-QTNs3=ooIBZ63w66JK^A=Dl3&c(ii4(dF<!>*E*v^z!Fw^x z@EmAi_QR#QhGlE&+S>6}j0#tbWh#Y&>}1F$)^*ThgWp6TA$K_V@lX7{mE_i;%JTyx zI+NL*w9MuiZ^1({gD^8bt;$zx?#XYHSXOQD^IyF>xtMvmj)-tu^Jy|z?T8QT^x5gm zNdk=1{cE9I$vF@-=h!AK`mse<@9&+M*{a!vu;0Sl*w2bjQ<67yMv?*e=1<9QZ>QOi z>ICBN^!*(-XYRGc!_swSn3?_Xv7P&tf+gt+{I(u`h{DQ^Jem80yPXx$=A^cFj?c|5 zg#0GFM?d2~0Z5doXJOXO_@&)K?j!sWFk*hLMdF<cL*Zui@oi_S*N0<lOB_eT=txJ? z{bLvRoQk}khUP<zqGdI_%s5iPJP@~cJk@aO$C}4Rzi})gh<rkEVXb5;Ge*SeQ@jpc zK(+-*Fua!V9|}&CA^G#J0xkqUO~Z|jqPe|O=^x7UhN-P3r69CGjAj9-%$=DY39UwK zo<RDOm~fbrEw^H>z3iXNi6kPF2on_Pj33|94nB5E88WV>SG{ns!+*9Rd3-kwdqyD7 z-AWi&x(P#6Kg3@F$Burs8s@h1Nt{4bK*xFt>R#HZFmmFuTIjh)E3w8cAXAzQ@%`~k zWrc!WY{rI-T~luybUi3m^|YJ=OU>BPoCH=mU^`2A@%m}r5h!b5c*|9@NJvAXACWbE zCFue-hTL|Z6aQ+^E1`)skb!kqK}@U^a1=y@(=<W%xgC0j%mvGSuqZ``Y@X8<PeJqq z6rwglG$qAd*BkgaU3gw+roo*qN>--w5sSZv6KfWQZDBZ?3D*Gy1qY3D*@7h4#^E%( z_0INl%j!v3`_Jl}qtCJ7R<!Rdu@gLY;zR&9-Sh2pxMJL&mvb>RN;*|VLzRMZFWHmK z09HA$vU_uPD#Xf7butq1odF!HRQq7E^UWqsEx=l=cTQo+w;|7mWy<1nw}BgvH=OAd z(Vj70zw36t{PV)_C`zgJKEg#%%^SNZ?!BpKN(@ONxBIZfX#-?7;oj@--^ZSXlbjiI z2U(*ICCnilNC$?`=K13(TO+3V(isuNHoI(xwAyVN6aTGiuQ<IQ=60I9^K&sRCSfz5 zH~Z1u!0!p&N!_;XgxPI4^7{8~otw6_nq_8@lI^zFi;=ZJ%*7SPOwZxzZvObK<Q@`I zSDmR>q2t=voT@b7RINA}w9oh~8v|7O>s}|!vyN;1602W}VC1KYXkoTn%$Src0sg*` z|HYpZ?v1yM61jz)9ZCs`IZ;+fS^UyMj_7r~@sVJ)-XDPy0Icp|+Th~nz9tpwJyn6P z^O?z+d6DILg^_#i7J1ui*QW#iv0}=K9}T%b)(uDwrMkspbMSQVbI!4UbUNH$sl7A$ z+xT_#OrX@|K1GE_CabU=IRSGt+fc7FV&^{fT!`Q1#n0YHeoQY%63&Aq6xyf<2I9)u zwd@^myw&!Y#d_lM(Gz;ok{je(TRuCB|3Q9`=H89?3RWf%6|;HCU!3eFQ$hNr3iB3Z z`~(Tzd(1!6j3l$;L9t=&v(%zSgwORMW|h!l{oCjap+0G>N8oaJoweDIL?EV9n!HKW zoPsM79OCB0YS0gRl|Ru>^qN-JzMxE(+BhTtTHI9jboOqTQvGLXRb!^08irJQMhh?1 z^C)l9v)}4EH}d$$d>yFX?U8eoTTdZLT>xRrl~3C+SuM--yqz_Bo@hkXDc@3KyU9h% zA0v+Mj9;y0=n_p6#L;b4y80e_#SW*FL#-mY(Onw48XM-%Nu(g~o#qqpM7ctv|ICM| zCZ)W`8#jq6E3#<?C2ZulK&$JbCw~>U;fR6a##mn<Tw{DR?pp@4WnP23n(KKsi<|h& zUDq|QCc}EKpAZAU+o>;h^Pa5Oit9^+*Aym>&t-q9nlSp3*(|bm!)DTHm{iw|gk^%t z@G;$)Y<sb>mG$r>xARou0!^vEIRcphJ+@I)y<3Hw|M48RoJUf|ubR-f7TxbJeSO#F zq!wNjnRM4IvGns}UU4W)oS{mhb5Ce@wJiG8@U$HE+AxiO!#K3?sg4tv`-WN9jn5v= z73L{^#eGoJ2q4Qxd%y~O?vTXa)L%+u{QgVcz`yU?Rq3Oi$dRvo+5PI0E<1bK3Mo6w zFNNW>Wi8}VEn;k1_?R`C@XU$E3!_=4|9iMhY5vc7Fdc{x3YxPmxW3<6#X3#vh%F4C zv?xi8#d+x;p6D(mA*H=HoezN{&*|UBZS-~*v0I3cJW>d1$Km;^f-nE8A-(Ny{7<jR z$>{KkihlNJt2}Sepr9LznmdxbSz=df|FWU31D*6<j}L=L@S}B<o_V>eZ^~sqAAko6 zc@~dohFU^;GwtZy61RUPagk`TO<ojwCS!)Z$o(@5r}G2<S|6$UgI@FogA+^W_y_*< zg(q?hkCo<Y$$`XFZZVyE-PJNZ%eO?e`=U~Fv*6kvS=nz8jl=YQ!%q%$VxDhxl|xI4 zzbknbO_(oimi5PkE8tF&(fi>oJ#tU1S|4NV(cC9)>%B0EOVZ+(LTxDi>H7xollg91 zSSNy(?-rWZ)35}_&PwnJlQmulu;g4^9ZUQ<>g-E%X%sM~bKMab@Q-pGH4_BWoz=#c zmJp(oqrXQ_>K1z(#`U87nU^UFT=I2LrYvjgU-K5Ms*O0gA}>!6*9p&~nWBJ%sNM6L zBfeGvb8>F(h9~}<efX+uQ1X{huyo(x4(WtzA?kC(b+wd-xOOX9cwTC;%~=#jlQo7R ze8Ihk0oSiwR*|ojE;`#eggdr0Sdxa;$z%ziR-z16B_cn__l-2eAm|yqxMcs)_zvUu zk+JS_rSng3zO!T?y~?Np=_n?B^fg4bJth4@^WOO5_^$2(_D4@#@OB>LbTwG5UUN%k zjXhZD81NBQci4LMR|<@2=j;1Vs;zT<_%mRMIaF_dVtHA-bEEV>dhZ%3lI7}6ux4t2 zYuC;y{E@XKUJUFytf`EeStm;LhAyiX+$_%0a>tuso2@(1UM#BrTE{%#_iRF!OSZJ0 zYUwp>*S4Hr5~_ylUri}N_uo_=q(+%^#(H(*Jx+5i5veZf*5(GA|6Hw9!rU1<>2gfn zKNPGX)T)6iDr^ib=+f%)#d2#XHfuAPZM$9JH$W(fOBZ`SCF!^Zk8=D&xi%u~ooteM z_vov#C%P#4bJ)t>!Jd;e{}p=*XZf<VoD5UUMv4lGWN~?RaciYA_a}{Uf!4P41sCl} zX`?U_B4_kO1@+1sv}Km_XYaZerPKG~3~^HADl%hTPAV3Qlc!?7VBwa|S>w7vmfPml zdJVR)gmPcq#5EL%QMRbZd86+tCm2JjR?|$7b9M&9sMvOop36~kSoT*uoE&~CD_b=< zn8d<Q%`vqo414?gXoY=`9vVpo*Az3|{$#izO~*QN&9CXtX7E;!f>mko!ET20+^wTD zYDIy1+-3W#ky7a5^KntxMowA(?!i7kNCeSV!?E>tjty2Za79ct_e%FiS++Vd4ZfK= zXk|4lHs`$-*v|oieVCpbsx_$b@)D>Giw1As!Qbu0sqe4irX4l^jQhhQK&+^Mj_rE? zk#k;X(iigMgS!*p7=E#4^37-{Ws5C<*R<YQywC)fy*Ouexg4xzR|+aBjD#t21bz_x zl`IRj$KhG28W|=2V_T34(HR^j;m?$${xhbFQ6#4{zQS)LurP!vG30gFn(e<{ymBsN zXW1C9fj4r4?(-rq6jCe!k^gkvc7lqW`D^MK1h__ja=te&>-&-*$bm^pXCn<qoH*Cq zpDtz?b@r|soUl0m(6i*MZP{!YIrE8qDL`zg_1(pMmMJ~y)ZQM3#j(O>yYut+J9dPa zo}>Q;x#PaDwXFqbLBF2GpCJooJ2@;>U<lDNYx^9@rE<`g^fA9YzPPn{Wz&hfb=IS+ zURcz+*3vo_yTtsJImIihSt)6fAu)N;kj~qH%fVA9yW8_1i+BHi*iYu@=GNCQPwz|S zhPhV{eC7V3aCap?OJASwZder6r1K1~{&K*oedo6w@8KNfXiEH{b*F!i?|v_@pnq&x zH0G;E8O(gb(#GJtQ2seEV{F}DvqR_uTY8%^b1DPbs%BV<1y7&cZ6bb4Dmi9KP0Q2c z#Cm+<d*AM%>ytz5R#rKSUa40kC@2B6D6xW7ik}+Rk(UyC*Po=^(W~l*Qusx9nei`( zY;$~q`t~c6;P?YPnM>~4d#-*Tv)O+HjU}&!xT!s?71(|(dE(Zn)tWJJTrrPCarU|U zUTLq6_jiz(Xtu#qv$jMr)vkcQi^nv#FcsK}tiI?5sMZ{{D?(0%Gn2MvI7(RW=S@Wf zSxY|r`0vB&J*{f_1CJ+V2sf@I58dAb$OUPu;hPpve+9dNP9TnL2@~Zjy6=h5%9cBG zliQUW8s$svjt@3WrpY>w_(lK0cS$Q=5b8}kwOz)xuq^sV|IU9hUZXh^?_Loy+5S&M z5-g9^efdgdajzV$bn)zNv}<A0t@U;4R_H-ndo!~l)GmRXM4;MM%olLPB|z-VBuBFV zADkUU(VEF<oy1vJ->qA3SOmLa>cpJMT0vCHn5zPg6o)eF=7`yg_GYrpjW-kB-HYlm z>-Vvf;I_*QC5i=mwObZ-caIdl8e#JkP4=09{NsnaVA!g>7_N(3?$)#d<``kfqkA05 z@sZoUUER7+Ypd5b;}qC&ia$gpA48#f@*`%U!`}S1u9Us1Ls4)UFUh4Ubz(nj7Q3Lr z+-4g#m%B6r9<)KEvEx||D3`5Up%dz4@dQJh?91$9n%rM1hMkB>X1;O|YPQ@u%+Fn4 zT8rhUz#-_AA=XuYNX_&fiSHigb$DM#`*|tHjP_S-nQa2yx5HC?rlFP>9sK@n4?d3z zeu6E2l4p9)qiI4ew9CwW;ZvC@w#xGa`tLuJ1S;oZv>cWAYCG=vzHa+qk?&B-v9r1~ zG6H#C$S}&>5t3wIc>E=p5KkO4@X)BMY*KH(?Ta18r6ppQuzAObZl9USZfaw$qlB0& z`1BHiO4$dk-Cw&Ti7CnP^$a`fx5U<ue9pc!F`O4`F2;$_u#8RYUo@%<*MAXSxPW`& zL%zRCYDGgQ`VEoYjktnV*T1pK_=fWJP%IIp|CajZQWb=dpL>=pMQmo<Etg&*EbW4q zP?c&J25*F)=&WB04wKBrB@Gb?5v2;=9NCMFt$3^+j!*8cu28W`hn5kw*Jur+TjH_^ zpa(nZG|$;#EFvCFNMw{A|4_f1b^6ak;Jftz{_-qrjU(h1n-|&93MX@h#5U7r`IItA zfn=Uuic&7VP;!=rkFA<iM*#kAP$l0Fn|xLA<FDcJXC;qH$6cTMsCd{i%Ki^)Z~Ye4 zAGT{#f(l4TNlHnB2nZtG-6@?C(jhPmLw61-D4jz|44u*?HFPQ6%>XkD?)kpYU-0f{ z|IRw*VBoWkwXXZR&r{3^k|@7?>rQxM<S~FMfi}uktW8D|iUd|p4IxI&bOhApJZkUJ zCK!8ROfnDDaDt)M6H|FEf!f*Ny7WB+_9XNrH@i^(uwisXncP=o$LzROxB#((<3bX8 zQmN^N`?NfT#X6bdrzY#cb>wS0Yb`nxG^)ihSG}U>f^ALPyy$ua1K1WrD{}qa#MJa9 zRr8NdGG>0k`)UP=-#@tzy+ft!bvCpNMtPO&xSP=Z*Ksz$dhXXMEh%s|*kQe3gwpKk zex=o83w#2O8eNf(JLNa8x{_6iNO6EpdWJ4nsu_Hj0227pEAA4a1`-EGa?;xSNdD$M z*CrPm3;H3>{)`^Tc4^Y*^4BK!%Wn`~Vk{1C&=HqBK{(%0qnmbh?8g2RRR4%(ejQj6 zs>(5Kh(;zsyyK#jW=ge`A1klXMG_ASe&Du0$clIN6P~mN$wL(QC&O69Uh3oSI{c~w z@_K1p!|xjtJLg+@A;?yQ?B|$C=_g-DH8P9*nUcK5QnTO5zF{EA98vUeKBy=y3~=NL z@@_CX_ott|5xUk&A)7NAF;cA7i8QX<Nvkj5FShvn)CRX*Ns=MSV`6WECUg0z8>vt% z^w`#YZ>*_ES;BvSYhLL%Z!PIeJC}9cKlb;ajC+mY>|+4`)Ypvrh$v$mkrzKVmWDS{ zbamX@J-<7OMuF8j1E{92m$!oyXCCh-=qJ9==<dGX-LCrLW3THjn?3mZ?KAn?3Q~2d z$PW|&Eq|Zmp`ZFqf{9v|x6TrpT!rTWgz@hr_B=RL1IGR&5yv}m>N%E?YP<~??lONo z{2d68%W=PDb};!wC{vJ~#|Y7pSms;y^L};8kO9n?QqSr8qV{IJSpU36z|-}Mx$kGc z)^`~_uww7*vq^|fPzFVJ6Z>F)T0hwG>T1}CKZ!ZLviK_jdFIGFeG<-hLIXonNXB)E zbMrluh7xG(M-L0~{sgtyOyxzL`~zC4%_jli_im@KZS{)&oFP$Zc03lR4-few4~XY` z_hIYCDcWS@bk2=`63MKL)k9Zlz#zu1x~+h5J4qzQE!wqXjI*pWiXIBL89A13N}X4X zZf6zucp#l$ld2HG4!4ns%@#f=*e+zXt!qxqI-6bpQ(inp%E(;5CAN9l3teO~lB=$# z>7sqT1_H)<cQI_|(8bU^mZY?r7kK+#nL_a_aOpY{`6I8vUF5qW7%iU|%XC6abMjC1 z`lPr%Lm<CYOim;k?X$DLE-nB|{#3GxiHXcENoz5-7f|wk7rA%j%uxGN{_H&x%9y{~ zitAMpnm3s!I;>%itVM13R_b-G-s`+_sg8=?>We1D<uBySmHez?s<2<a8gb;cf(*2C z=DFQ{RzHH{H7BicjwSIs`}<G@ix`F!Lz3;}-u&{h#P7TG4dB<Gmtf`G;o~eWj*B{N zU0vN%S`DW9RQ~k-r|etuA6_aZdg13J|9YG?_EHps9)xAx4&}Mp{+TF0toY-=&apF9 zYYSgl;%WA3UH`xgp80*Vh5Ke!U68H)k7+I*-pS>@#lg~fl&{aDYgevHMn>g_(Ee@D zIcO_EB7?iZ%=$M6F0<*nYk&5bNRNX4<zPt-?@w9A(Tr)KJ&|}?R(-gg(TowDak0VC ze<4rzHJo*|We?_J`=1X6hvTrrn7e{EmZyWf4{xK^0v1eB|1+aK3p!f+zgmI<{hR+^ zO?R&pW<j+~t02wEcCLC-t*M*S&Xf6w_!~X<h5)<r;^=TnAdexHXg4M0=K5(SshlN$ zwlWEXL3XfPa&JC2eDJqAnZF0Nm;?~8G0pthrMPgh9X)#LKVmIi%rVEyCi*i*Frpbz zpjQ!^Qs_vWrd3SphcLQ2HCyVsbsSr<pQ}5~@dm5g=i($hsVgnPfz5H{HP`4Pqfm<m z?YI0_Z|;A3HNMZWLv2kGc&@TsDO9PF2sBR3R4LO%y~&~@F246~`+cxVrTepKys^cs z*CMjC0(ibS!sW;WfrMtaRzTj~sMRtSSW4jzbmhX-L&MlK>Ewu^w5poI$&>0@cJ}pg zP$7<Q=q9@M&^7(wisA+r_!h9@Qt#4rN|RZW;n2u&Q!QOBp;u<KGY(p7IJMwgUR(dt z(d*dyPL(=3JyRD;-LdrRqxZdcl_W2Rhp|xcS`Kv)k&F!G$d<5%q)1#<%79?js%i_u zh?!q9$0Wmv*0?ks1Nk92n6}z&C5LFN&pb_l?ID<j4FRAvLG6C{*lOEqz!-f(*WqVt zP&8Q%$_m;Fx-ov0byA{5%MoA4z!~{ENpo*1N%2y(20~(Ap6Nb4GupQK13suy@SLH@ zpFfHPy?*y6Y&kbKTPT*p&He8`4Cm8}c@|-qnp5VOTS-Jh_t0Y7(0Z3aS3=De(Vw@4 zv9t{4LWi{LK}QldCktLCvLlAV?|YpZ<jDr`tsy-F!GwN6ie(5__~BuG_H7db?QcVs zo$;)gjYDrJu9~iXe~Ho!y~qOq81_<C`z7%ecl;ADhePQ$2_oFbOEl@CxxwdFH?2k+ z2b>iw#4W33Gbtv9z*q+mp4o+cR6jvYM$nsFm;UWT!9_jIcMkVF7!dMH0o#8uC$}6& zm{aYu%PVPuFyMcANg<4iWZquC>(%kcSf|nwg`Z?q)ytHj30}<ba%94A-e&KI(vi8` z@NyhrlwPcp*o8){CERAUHERTA&{j}`BVgU7*f3UAy@E2*jyVRVMZR~~K%qpm?X~zk zoq8Up_EyFF=w*sN>KP;&glJRaxz)AJ;LEfrZY<(o^>lY`0K!J<*9P~#oj{7&e`%qg z=AE+xuV!xGs;lKG6(c6qVh3&<c)K6piQ`fGeEVoorRL0rP+VdmGu-pF41QhiR%w7a ziFn*#9MF57C07SIOn;rQ9bk)f5h+hdE_aNsa8)PWz#+VW+k;t8c2#YdeZxxLYGR{m z<(lay0m*G?mqGh@5ASBYEmn$>@{ghrW=E7=xdV$8Te@87>mRK~<eKvJwoftVZs2vF zI1$Hzi!CK&KXnuIeO_u}H4N8Q*d)#5LAUA<^dO0T!U+?am8_f`m&3xSCjQhs4#iA4 z0l<H^Q!x#_mfFk%^*+>A4Z{JbGBBn5crE%<fod=1ZTlCj(K6+!js9bx1uW0`L8R=L z3nD(eo;tSTU^n#Xfgb+?@pA?_?cv!2+CvKZ#UR(6#?X1;U1t?`@;63lxsu@{+IgG{ z@as>HYgcxu9m1xq?!30cC=Li-{_*5+z*Y@jljywD{(<BS{qT$6Z;UkEU%)1zLXIQf z$Sy^i8_hT;O-yMUyIU%l8VX*s-zf2IVb6Df>flvg!hGuN%-^cBRp_7Nt^CpfWh!-- zcg)@S1HAC&Rh3gf?`Ojf4n1b4j(H--w#{xDV&hB`CL`FNA+UnrzxE!0W4*dUa;?u7 z``V{qbau?(!`N}G#j-mZKu6#~{YybdFS7V2*ox&BTae<UhNwK{8h^g*>h*^H!ni zkG7iUgU)DRc<Nba35|^18d8D$+ppP%&vgm67?;1e49d3>{po*qjTToXMkaK)mDU8S zm4ta}gz_2%VIy%*Z!;EHccSNfK$Ub)Tff?`pU@R{uZ$Bn)5&E#9Fn1a6w!ACu;#r% z5?Rf=aBpk7(bCJZ#gAyo;uyu3emLYPs(?r+&EM1{?ZjWXYf4<g$AH~BaCxIYy$!+` zZ&nY$p$@b}H}k6K?@8U}@0zPY#uv^+81s%y&<1ZYK7eWu+LG6Ik`NS>?_&;lS+{@j z4}%|pU*oaFZ8LjHH>+$R#41%<Tbunn&{w5eC$|P%&do=Qe&|SdB@4T`RcpBytYABO z9UnG?S*U7~69gO+ME&`7*!uT=z6_olMw$LDL-KIa1b0x`zIz)Bv@plC@u)2QxUxYT zF;&UIIr_|iqGb*}pth)Oc#7R0qC63B)9uc_q^VJl7Mi7GbNV*c#3zu*Q1{aSr77Nb z-)yb#;xoNXmu1}Km;uBIWys+QaR$jgta-3U9ZIrn>`O}~%STIG2x(q$Fbu0z7OBQi zb&jARWzE|t@hNC=gphIHGP|TY-qmxi`-MtBg$c|AnZ04m1-01u7O^1Nw_+fjr);Y* zBF)1DgO7=}da7}c;wp+;2tZY(HLBL`8M^BPti5l8j1$45Yn*p6ALU3EUh|+pi(t_D z$Ze!;MHW^=YYUs!Z24+XZP_6_^VWQ3v&gx5@_`4vlMC%((2h{>!<0ro!C-jJu>d;j zYC@-}w=g3qQVv!}3;3^w-vy@-*Zc;o{#8^E+UKiXB<CQLpO2B;efB!!eztm|^|f^h zhJdNfy-$Kw%u_Dv3O)2J+xQtw4;11GY*AGS;aIYc#UU&e=T~?}*&fh%D25LEngdCn z^4D36U;i>!8JF_p8X9|`^O})pq7(z-wUDaP_y^{-OxNjyX6EsN1rIu-Xpvo!WdPC* z^fRZV%7Hu4!EkTT#??)NRRR{II{g_ZZ$F`2YVVteJV(*;8$IE~LsLg<eA9C3VCeA& zhD2Z!{U1Vp(@`RXFZz1L`G#ReJHq84DYTobDjUssJBjL;8sm1_8igwrKjh?u^h)Z8 z&n-W2{KKG0&dYiENPW|!Xrsr%AZi1jt%>D$EP8xCo_BIh-rbs_-={C<z?z(Kt)l-e zVby3*xpP?iuA6=_cK4w08Bbr6=F7QPuCms;vPTOHWT5o9M{ZRH_^~p4FNBXw=;yPS z(c3m9dAfY*QW#Fq>pVF2vR_aWn8g1rR`-6=(PK^_5Z0iMpM8k)`SPfa8tbXn)5q_i zVN~h6BDvYO*Ll^aK?R)$2}hP2>S*>F!(-_FmYc61rS8iGJ2I3WAmNGjOvCVa-@_vH zHw>R$gkG?EQZuo+P9fY?@@(k8uaY%ERV)8ZE*eLFH1OXQd8(pHMqQ=tFCRUnZzTWp zt&bb(!`$AWOSDP<Fr>X>{fxcSwQ$9v(DVay!G9P!;^mHPMvLz<o>X<$C?J3$_RL9c zL)_@fJ8V^Q<Nv!vm&3;<e@b^4^O4}@jQZ2EJQ~d(4ZqPDbePH^EtgqSoXWWv>g>=N zb!sO4hw)qSzXdm*P-O!Jdd32YPQL*f6CIA9KU;A75CChzbX^q{r;Gl1%i_WYcWFBl zF8ZL-|K_pO)BS}8p1(y%RT=io6K8z1gtU^y_BR^z9q)aHIU{B;@SROA+zw8rbxeVT zW+mP{(h`L=uBWruUXPWqC!I@cGaUM6jrGLRETJ!mNI{ba)863ffpN~^;!OJg&a%<8 zp-j2{W1P=}Nhr4>{a+W+?;)#3VcfIN9p6-!+Rpc=q#G^&>`fxji<CUptRGXFK08U- zdP1&JE$_h5SMXBWx_*>;o>_`ILYnGttnMRMjl|Po{ntMg=G%TOM%Gcb^RpNyF=%=m z>{?DUrj**BH>U8z+UAwE2>5zLLD(zj)m7?ZlB%x~vS8%6P)Yt7)xl!5pO4?s^uH6l zBw`Lz1bWWVfG#xi7jPh4^U)>RPg_ZD`gimCl^Luv_2Yd?m<fxf2YlrrLr~@~D@7=| z`uMlQCv;>+r#$pwpGbOwF&`2*MYl;5r$Ei<HJ8rR6X7?+(KGhfrK!wO34#sgs~An) zi1m$yH`UHul{!DF4C&K!@Dv@pRzI8%jOU#xKA^m(i#{P*gs<Cc@4QMP{$VU4!>{vP zyQFp(U?q|+8UHZihRP05z!P(Q-bbzsD;ZV(^6L?CE<_3|wMsj~Ci+t&i)uQwmj`5t z;N2F;u|tVvt~a^9Ci;314$SCWlVFKr1bXA8@x+BCR#r&`RTgy@AkgTnIq;cKlVMg9 zfmdUtXY>T)1qKGb$${H}n;!{;sBA)D$XfQBt+4aT{-=fJZk`4?cC*F+C&|4dSC{Z8 z^+p?pp6=bFe(W2{x^5%`kS%5Rq9KEtSI9WwG$<Y@1&W$o+e#3#s|Njd<1sPTewPBt zzQDjnO3?-MA&7#=>X}K^Z}Lu_SWRL_;vtY0<rRh5n6sqo0v{>t*-p?O6O%x_U{Qc$ zlrmgLizQhGWtCtO&Dx9`U?^r_$1gF9dR-rk$B-yqZ_{AIjBm@kC!{rpzW<i*$8LGP zCH>og)I9lclpb+*j|c)n4Xq@8yEtp)3T=)S$WJoue9>&89vpzEr%9gSKe}eFxR&}2 z^AZs;qP@yi0CatWRj7N#e0_PBl-)Lk&c?0cV(`$9LF%vvr0Mf9X#H2@FKSS+#g(_S zv&ZWho3w~)s-tRVgIfI)H`p?0JLjMqN#s(R>myp2VOhL7LQvmhupvXB5$<(1^TmX$ zzVY%)Q>0D*of6Td_Vf<}Jx5n5PszCU2ce^SUY<!Lmh?XiS8l~tQ2#rop817VY>gsg zo##aP&8NtU9aMBB(dzuFWJmy{vpAqQaryOD$kTKNs}e<XG@jXM579vK*h4CH`;F^} zhe<hoOt~6J?aawtcKIat0l;f+yfgdhI0IUH{-S9Gx-wmyRfm->_$F7Tn3jmKucnc0 zL1i`PG35rmZ&%w5JXIKe0~r}3Bfx&<boiV?vLIo*v)b((j<|eWB7Og(jgIVZF>O$w zByTnSL7PHN1~R<7wx26mVia4WjepRezd+I?-Qunc`hw4WUTd7-N*}Ss4~F{ir_zQS zD<|sz5x#lM-ETKoAmO14S6R>3`iH@rtlsWczMSflTNDiQL+q8;CJZSFjz}vq#fTNy zmPSgk)KSAk-x}0GhoD_F;yMYute4~#&e|_kw$3U^56SLH;=P#P5gUSm2=$uL+t2Ha z;3cDQ7K75fAD~QWz}U&MIL+$EVzl_%=7-E}kk9p_nWrEbs|+=#Oz<j4S(K6_j5iMZ zc;9t34WL^k8k_RI)Y%6OU>^DscuGn@I(pfqVT`vll<rr4c(gr8@J-@#ykP1x?PpJG z9?#3x%<Ve1CCu78mh4t6*Q{gI9NkmW-41@Zy?UJ&&B2x+u%IO#yWNT!f*%Ii&`!F! zv_8w_iY~P_g-f(DpY_LGyw>^{d{Qbp3kh{n>%Qu|kE(B=$*TxBa&ZHOtpYSXT~YUR zB9M=)`71N7pLm)RYRgM|BwVaweN*anr?pgx(Fbjmlbq!8WcQeU{OEDMbc3PiqBVP> z-NGXJm!)0F=&aVfHJjSFpVLA4c6p)N8f{t<&IlP)K?tS8d?+6ibo4Oc<<sRw$$v_5 z^}?<*v!jR%nxe{XL-6|T3?BBTUI2Y0D3D%#`2<)2|A+Bq1-3Cs@PgpXf+Be2)6at6 zoMI1%Fxb|nTOZM`-8qu*BC$-US#*p;wdMV9_}Y)6*z7RBvxg>BEoAA=^x+k4qAYpq z=cQL6#&imAM8C_^VZSyglBI~rZHHWL^sBs8kg5U84A`pMNLk-{;?s0?=Qnp1`^~KW zlDmkII)+FV3eN5&whEJ{Wt`LL-hrtPD{8VWi5hn1x6|U72G`9b6f$IFEMy?fi9$Nc ze-y${Np;wt_0_&nIVZo0SBsZ^9Feu^X=X+>x5+mT#))sMDyO$=xJwFX?o55mBBN6C z^!MRftbQ2IV{YkA7aninpU$XADz4X2Y772cT0Cs+e02ZEXu6>L2HLrG*6o|``TJax z)LJqxKJAE4VpDjUs3Wh5UNkaZjm$8Zhb&goh8V;+{q*?QTf`#$9|rxq+CovaQa4W> zVksA&cP<1IJa`aw*H>(i_jL@FS8gCdw~lY*q2!H;cEVVHz|}rhCFxN&I5ZyDw_#w; z5x(eORTyYEk%-SV>=U@L=O0G=OEYut4@i-PqUGe!Ts+;aq#jwWHi3~>hOO+QQU=mP zQz#;OxZgjF@PV)9h{1=X)1b&#t)S`O_lKsnmU@5eFb_DIbH_(E9rnE%!A)hQ<$(s~ zesA<QEv;F?sBU?YtUiLK=ava*p3~-P3>i_Jv>DsByoNld$b49h1t{P%yGwgiqiNai z8Jr~lm0JPo;X7kyKU=Stx-8D}P8Yr81w9<A*M84MT`F=b`*mP;*6S;V*wQJb#W7c8 zu-5O57XEVStkkl~TB9{nbr%#`{iayL%#~Gs1=n58ED1VB-?%m3y+gRpw=#zYeFyv= zW-`Poj=+vaZ(ms-I)5O=jl6xZ+7aW>V3|F>zV~LHkrkcOtAcHc8ZA8hVU6|Zdz4iQ zDn|3tr{~hIRB0m>BdFhSQ#rLhP6R`fpbA$`4MG{{$oP>#b|(5g`Ur7roq$!uvfl|Y zT=#6|S#HgKPt}>>Y?vR?$b=r|a@tzrR&uFL<63?YLx~q(`jDp+D{wG7ek!yrdP2+@ zV`dyOCm@rF;dS_cl=YevYi(YEj*VVQl|}2wE0Sny&^_e_*6jfi(oZ6%Wm?_H(>~Cr zF+|N?9k8|nRBPY6&}@HIfUg*F%9(Vk``WysecFLYI2^m0{5ju!_g&AJI$-0)hmH$} z?81OIobhnpx?V$ppV&F{z))c#Q0qrm7<4MKuv9<)K7AGvHsND93O#h*-e{BT2%Igu zmE<;6B`Jj{@@7*9a+Addyv5OF>cngX<#f~3rN%0?^Jae3vLb)lu1S=!nw_ap(!R-9 z6@K`bN%>f`LLj95oU@$kvhgc9sUlu1L7X52^89_)pL8WkgvBbr>lU(IzS$anHXl2@ zB`Eg9Bl{ml*wvxuvSfSQ`pG~_?r-+q%>)*QKLM#MDcMBo7`b47iyv!Vs)p*C5zBN* zE#T7cW90f03pE8X)n(tTt4a-Gc<0~TGR&?3-)p{n-bhZm{f%2s>|;P(;liDjxY{!+ z$=Qoz5}(`F9b{H%-J)>2LR+;GaSYdv3b+x8alcSu;`n$Z>3M1wYdlUWf0ue_O=>vp zt@0`DF=kS&bt8KzHqgC#;6~oo!A^^H>{NxCm^j#ln^2l1`iuWwQd!Ha!CM{v&6;x4 z{Zw{`19tC!7`Ec9q61o=9^4-{Pj1ijyX1`X0a7L18$@~O7*=_yBv*$4+mojQI};TT z8s5-0!`DtmZo?^>S(YOgy1{4<qu9`R*UIGh^ohc<iM76)8ILcg`UKWFZ`3=eAB5`U zk#qe|(LCv;*tOA!X_S`?)N{yZx0lnNomYfG6%c=$7u32J<eRa~<=r4ArycH=PGdrh zmsL=H#Ch*_YI6(P83T|sU50gzgUB*93Hys5V1v)-i%B#D#YAy$KL_~c`Wl3BbO_tY ztHF_r{4^QjMNlTY8=Qi$7oQ!qv)pSd4R07M+~1Kic;`}7RE~UTao9<_d!AJJNhkum zVd5lwOJi|G`PS`){O=Qw{Oq(BQh0l%=|?R7UTLi+8J4jN_B-MgB#vw39S(PYH;D_& zX0P@?ghrimT5zT1mqIFijY%uH=;&RGNuPPqxwx&3546C%2bD3d?(EA-Vkwxwd9<ag zV=Vl4BZ!{_dUZ2?!jHM5yw$}m)(xggTeN1A52&f-&pyR<j3JY0w)s?#F?3v+B8{-& zV>J$YOb(*O{OA3I{4;*cS}qg8t$RmjVN9~xuPz1V_(|$@DN4&ip5pt45X8MH!Mb%- zPl5q_J}u8|>@Tl9zjxAtNjhWCAmvF{lvt$DsT-~&G(_C<OSOgy8>D(<&W3|Y=d$nR z@&__dwt;HE+zuD$C<xeH;(e`_rYctYE~Ho`jMbZz`@2K-hh=&Gyc@O|P+9#*(@B%3 zns16CGZsE1!jdS<4t*eS=u?#&O$>+5rR+vZ6fbdq@Xu~vrVif|#KDG$xkE##V;l}3 z`AJzUe(cM+Sp7Nc3zDg)PC7Pr>=FyKwG7qExrriak-~NJ)r@XO6W!QLKeA70oF5~R zUP4f3U1kqvuA$%(QT44Jsub7o_&`oSVE#3OtN2{Yr8Z1wGCE$nj=%VryxY=))=o)y zs_f`ScpEvv@>cm}f7&VY4Y8PG*_HjA<+3LFtwHl^$Ew>YFW9)&&R39}TSNSvV_(@O z7-Qe2DTX#BT~5~mp8t7o#?4JuJ;95|R$J+~_b)FtH-Y+GVN%vs3SS=v&9Z%0?Av&i zHzmx2-j86S2i+Ch(cMA=LU?ZYGwQKd#*}(Nmgme5odf`YmwvBSVRnSRnjK@MuJ8PS zp|U}nXY&K#g)r~a0kS!MR4b}ER7E+cUPn)NjCSxHP`>d)V*df-B>ntSl!45tai5;v z{0~|qvlYj;($6a~%P`!xG@>R_I`_xxwjh7Wm_lnJTUn#Q_SKP)57W}E7cF2`VH1X- z*`xrCe;9AO6G$K2?=im@6~w$4*=R64<H=-Vj1>*w9BtK5d_FAB!OPn^3JDc|0vyTl z*)s1ZU#{n)r*#~nV_bD1tZM7V&rX`G)L-!*SQQBj(s|1)5T_@{?LA6%WN}WNfJz?J z!azGan+qS)f-aD^=11!&w5_KWaNF&3`u|tC_;2BVH8T32iPf77O|t7N>~V79VZ1x; zSTxJWRU8Swf!;-kqu2h5B>3*+egJ;fW2yD#u{FS>zcF43G4&Y+500PVC6>O%z`v*e zdouaWzwtEF_x(K0*Wm7@j%9-CqpcJYJX#LZ*%HH#J`aHOE&PwPAoMeLwVBQ?rOO}N zV~r7~?D#%zns%?9H*tFXCF-vB(AV*pDV<3X!_8BCTXE%Y8@3#F$<iiLY-i98gqJ)5 z<t$<B8KpA*VgaB9xWYlVg%Z<Rj!Wd+;~Oa1cip?Ea@c#idAyA@9t{%jR;1q0it#XY zK?}=>I2R_XOm^d5(Z9N)YT??gE&Ggp_UeRmNv6@->DxVv=aZz_l}H_MnyAvCTe&r+ zf?i7tyG-ztuJ0BrV3+EekZh^9uM(ALg#u9l*BNL2R4-d`wzn_D0@S`t{7rRIc4STN zsHrUonNqd^GH6>zg@LL&8-Uf=logq;$d!>_6)DtBf7UUdJQjmgk!vM2dt?=Kx%d7! za#IP)nqy)T>tNI6<pjoB+WcPn=bv|mN?d}r3&GhL*@X%8W|pe8vQ0UJhBkmmCbY$G zHI_}OFomP;x-n6HGeckTJm)b#j+8?3k7ga`Gsbv{qt3Op<ri;bo*zDJS)s}3f)9un z>SIW~W_qD#Y$GY!n#0NT-FA4@fo()yLEO3K8MHoR?qm9;Po1k(;*{B`<5I)~@QHbP z@E$<)_gf0q?QBo3$OIpme%T{Ml_+ePXRzApgj=`K@hE`1Wz|1W89{@)dofE*zw-;d zGYZ+(^WU3<oYK6XevxLbUw0>c{2Kd8C~8vzl&RUe)|uRU^1Vb*pUP=;H0w_?h}h{F z2jorbrKzISjz+RXlf>SRBj@JGYqdyrAQ@4^kRIv6pKoh&vC5CbPk_~RYyR5Y#j$3! z9>#HC#W+u@fSKAz1_``kuBJsnm!dSDr)c8@2LZpPCLV+3-x-r<6rO?PDh4%+UyWED z9<-*Z4u}<u&6oq-Nm(SrCAD}?EL2~pTS*J~ia&7dOw$pPw%DQA^rY3R)bDTY);_%3 zly)JnxoakHsB(QnO_XY{$)_QX{Z%|;&a}fcr@2l9rJBi=Cwcs=wrp=osP*-#5j=+# zSJt(Ip97iv)~{%<L~_q+7EX!kE4#RZmkk*1u*J$Hs@VC?49Y6L#ENqs@O-wr0KXYr zXc%A`LS6+3kQhj63wvBjF8dRBW32nC*Sy8Pj2xwVfC}uLIPk;W_wm<rYMgM3YH9~h z&YGf?V0E;-{1arC92Y_ro8jPbw`&_TK9ykbE5A^Hydv74G|PA5?^IkM@0l;kLaZ>n zkwekkDeu;}!XK%Q3{d?#BAOkDa0FXvjzwi^e)<06XUTW;J8{#|M}y?Ur%lPGwMb(Y ztqY?4^0JeI+nR$*(qpUE23;4}*ba)kIp5*?4647BBi%A)D>_XdgM(GGm3Uhq?k`x$ zii;8=`@+6OEzZ@LC^(>Jo9MG|g9}vGc^tX=wPNEHO4?8jQs-RQM)kL{->}rRRWaku zF^S2+7lvD7+YY}SCy?dAlh&gnQb;|S(V^o}aU!n%uq{57eKL0q)>8&O6=g&cTw=$v z-gU!+VeK=q-UJuo=tvftRHheWP$<6_7vlCAo>SY}(3N#RC&#mD*gpj^iPnlM|NfPN zXGeCZeC&rm4Y-PXN2xVn3J15!CIL$E7!n_jo~;hH{+;!`S@&C2?(O*efz{w$E~k^t z8xz*Qu~ho9oua6n9!j*U&+2&a3*nd<4(z;7U!R3U%+Zj}Hg#@Publa0xzwMW=nk3f z&z99%>#}pQ<<_#Mu-B~&BuII>IDGKD|A@sdNMi4GY{-Rmk+|hXn`!FU$B2Qh>I9b~ zeBKuCB2wl2e9jKZLJ9x<7&yrKltA~hXJdM+!Kd$yAM_de>#4&V&x<{3g;;AwL)X`k z%^ytCjwHqt6YUZc=^(H|cfJW%g~XKfXz4?{au1ZqEx%{R!TYmQheA-CiOb(-m&Qh6 zrBOnY^b8UOya7AFycF^DFh3*XX5<9y7gxVrh>s*Gzm~X@JCW+-j^-lGQG0iep+?Q$ zky1CJI(P<h)zMfk&r6-$7AYW3E+(K!U}x<{*4$3a%dI}qIj0gOpD;Y@T$(GcmUs)m zb~EgN&gfsK$Urdfg{gfa)qIW;)V<7`%MuX^B{PPzH=R9y7~t=3fBRDx2n898%J=-# zePppXI=O4{QR-E8SpVS#Ppl}<eUls|s=sn@NBJIIp$9k^8j)Y^%j575+u*A!<Tcj< z0)Kr|*O95mGL}vLAR42EN%0>U9{wj?DC_3yY?M5**b(dFOHDM1Ra=cm?xLJ!Rk5O* z!o<^6J%%2}P0!9_PKQZ&tUa#xwj>A$Sfwab+D3ruuwPk%U>nJj>)rIr)+Dipk)k@T z#t`XO;(&GIBbx*YY_9#LeDfVwUc-Z7w)KV4PN(JZ$=Ci*2t{uj2Y?>Ai0J93oKJ;u z_|b?B^egw!WPx}|Xg}u7(fX_#*YI(n8JCBZja%95vb8L!;*0okego*y`kgF>zimp3 z2|w;XjH5@1=4PnSfkwV2edkHNqXE@w<dgI&^a>W=K=(ZJvU3DCY1l#u|B6^Rzxg`) zCs;p&<#D)c_-G@lUVGiJo%^Hf&v~)|-W{J(OVi5GoQ{U&)aEr-!3NwEt4afln*2ko zoDjEFtH-{T=SdYFdsEH<&B@&1im?G`6Om_ye+m9%CM@q<YQSw0sfF-38BLJqS`}?t zl<61pK(EtJazf;SgT6@QEk7gy_UDZF^FN^F<o$<rOIc<%4Mh7=l(ZIkUr7fso-{0E zizrY{C!wu~&1uQ?Ol<GtUrw~Vf9-_=wpRNY7Pj(C2?%+hO-AKmRK&zSnAe;C>CuMT zzP;2kGaJgE+l$&H#O(yErhV@*o1dq|Isecsd4v)_{z-n2M3{i)#x~B|f&qgzJB4x- z-A6hity@W7R+Fb%Xg#KjDO`vvULCEk)x8_MC!ifB|HPe6%QO^vsl@yY#aEtmP6;=| zhOBbET*j>3o9*k@@vcB4oS!BNf>UnzQk0D;ZaMX1^N#Ly&y+cte%cr5<t+!SP!O+i zg;lipoH-u`*jAOji+plOsq!e1v6ojiyA*8M>$Ni_<D2_IFYQ=&p>sEh{*AGtElF^t zn?<=PjXTBW?)pXq`FQO54@W**1%e_p@*r+>8VDJ2FEVn74Cys`EVe*cte7)|s}Oy< zW?w%0wnp6vSSIf=Qz`uunhEV{rrp>VKU2nP9FmEq#kIFl$-X)B;;0xa$8eu#us~;W zGDLf8(s&^Utgv2tds*|&6GRS94R0<hU_}>86N3idBFRnh`wMYKFiMcv1npP~EM2*D zw^gziqX}^iavQ<EIGHTf@e!t8DCGLF$dPYN8G-)8LAWgwO++}2Yb3=$KhYz|I(XT9 zC)t|06HeI5LMK|jP_a6+edA!|jP>--dfz1w@!dhRGxD$Cr)9<!ZS((ZXYEe08NTsv zVattAoQVz|X%m5UJbD>6($m+pzeb&4W#+fMVfKtk11z?MkHIGLXqhOZr+7=o%sA)E zhRcJ>T<7HXRA{T5k9l(}1mVb1@2UCYABGr6(;J}ALL>o3w+Bn5RBp~ZD-bYQ``9cl ztS_gkd9_S?d2T!*SP=sE-eX?gm$!xtyGQF6`p0C6&f`itg?s19h(tas5WsJhgA9$g z+l^7ixH)q)O`PGZ7f~dD3}@){MuIU~x?xF<jlwb5x|k5qXGLGJfr?Gomb%yF(fVKA zX-!oj!N0M7Md&%tmcCeQuH0j>8L%`yE5|p|s)SXx7y;(k>5Ix!nD|Nn2mEMjVO=to z`0^rXJOB-|sE2gkEZpV(JUY(UUe(jGIe@=-m(hSJFORFsmY$Fw$vTh)?Kw~T(R!(p z5(c;c`Q=Tn4=A3#xcv-lRpbuB;URy;;y@=RwKTIqq9x_Rq6gcmDucUiG{VnPg*A)- zZx<H6RtkDyC_@V!eaU2Xh9aJ!jpuW^yNZ|j_u7|idlY}p%#$kme3pAVIS=cQSNsMo z{y)DNc?uan=ed!6R5#;QuV91-9?<G}shm-Xh9WxCCQdFjJ@<BU80xLfq058M_+gEH zwR9T$l|?b1IPzFCGrk?{R~)5O!yBjaXO!MGFRV5U2Tfk?ajmTMLQf`M4*KA-XgO|5 zFb|E<o8jC~u1mP@9-Lac9Da@F!0u;Owh&OGMz<CY=28PyAJrwPB*bkU0K#<_OOh$w zjJCsltqo?iOHh5Nt!ueGvLK_^WjVf7CNh<gP|s#aS8EpL0L|t{T4ENi2z`4sHR#<} zpq=qnhm9H?2-oww6Z3aYvLG|S0v>|PN1J-jgUqCwSFYbTO)w-t<k=FOfX=Z#Zm|8~ zY}0lTi89Y-PoR7n$7^;Tt7`;+`T`I{;H@|HD_rfO>|5>FyPS<96kY>f_>jZAH|q22 z@*9Rfs80DzWdFj9>ZUsz-DkLSGoA&XTcF+9i?ZAVF6)g>*)Crwogl=V<LENSnN>Z8 zi1?;0{#$s8b285<<1N5Zz0Z1;TV-w;cW6SP;9*F2Z*Ae*5I3@Jn5HL<r=)u4XUgOg zdZx(QT#63N!!C(0c2-NI*C4yJE4?o@TZ{fm`@M{i3!e{G;bY}Na@L&Oy<Pws{3{!` z^mQ9T@Gk>{&C4`25q#5A=<a)h;GvF_SfpsFipuDRB1`HI6^G20qw>87<eo&6+tV&= zR>R{8^frg0#izIRtIGpG*WPMly-3=R?HRfG6*wgE*C6&KwJ#xI@7gmgwT=vZ`BhX% zyW5EgBzuq@Zg(0(R@T14SJW#;@vBg+zQr}&r5w^w72AY<<4+Jy!d^{Q-L~Yk&`fN8 zXPQGa#WDJWP1fEHuhnw1LuTVB6A0Y>Qd3mRebOlBb{T%Uu$pA)pXt!bv$;&(7R^z> zi>>ghEhDdV+%rg)tq^$C9_V@sxjX0pe766Gft$%Z|LRA2r<2FhaM-ln&qdUTto*lp z9UJ#_uW4Jim&8ErkbCU)+ogv&A#u6IRaXjD74}~<2DY4Ak)kty6bnTn_!B|PQvQ;8 zhvE}~$lH*Xo%R)m!n$`$0ySk?Mq1S0!(D{PUooZAc{Hr>wbe3hoY455dAXZ)XuT4E zgpn~p9}tD=YY+2}s%~O)@-LRlmrR)H%%r!578GN1oAQHlCb5)dNOrXdq2%VSgw@Af z<*WYQPSIOM1x2nt;;cgAx<4OqA>YzN<IG1p7$#dje!cygb33xUJJ)Y8ewoJ?v7zUj z4YL2yY5!vS)(2kc2vFW@;1<LzEvEgvkPl$JwCNDG-8cm_eC4&eB$)oqI&QF~_a59| zEg{ClN&_T!Rgz3{7(M>&TKy^W?x)L>vWy&75f$6$9}-F_vW2aZC&GUKcpFy=H-Ay) zXDz?sYL0)0SfZcNsx!ND<qFo0--sR4uOMZelBw>^{=`GY<daMz>dZIi%AUW#F0Jyl zaCd!PK@kuEsnV2v<qWw@%O%@=pFz+*f~{JyqQ51X2rTe4ezwncp)8+J<4B{+e(^Tl zXmxFZo!XS)kXJmo8CdDGYqX08=%{}|#6VI`v$sj=XwE`?pTt|wme%TJxndQ6+}^bJ zq>B<s`b0#hFfeeK!SZr{;%30!I@bYR5Q?(;#2xsy6cXSas{FY`o|;)HLu4~$N#y6# z;tQ*j51SJoJJk=YBN8@CqJJ?*D7VFB$&aqAG)-U1HyXf+npQOh`41dv6W+O>Vc0UL zBuPZo+CblYI=w%YOe~AaI!$Ro<>kpJXnMH5vg3)Z4YZw=%rF|^*4e1W=YfHZfRbfh z#d$2#cbqVFHIuQw3E*|3Dl6X^V(ju2R8(8e3(aO8f9Of*#~H_-KLOhT*Qly@CZo+C z-|xILf3ZB0sv(49N%R|f6l*6emaSmHe=AtJyr9gqiuDvDFR*ZEM5l+k{J<tHhHfd1 zPA*ih;s(8Z+}Pko@zF)DUAm2ylmCAA2WK12^>)o);EkOBdF`a&5lbAaHFjJL@<S2U zQy1EJYT@gn8OmAf{wue#mUI~#0o@_?Z#La$k!agEYm4L@aOPtc-|S%GY9GTn+H&n( zKQl)lls2<9`yk1iS6+ASvS{<jCFXA$!m!;C=WLJF6ONLic~qQTH7=zWDk~MGxsrSh zg7PHf<N-M2&OetpyM}g3P7R3764XdHGAa@mH27sc$e1Fk#4qh)-%=U7;?br8Fihee zizbl_0SA8yq_q30h`8s}=4Bjj_!BiSn6<lcC;C9ur>|h)exGK>d{~HPJ%ZGe=GT{| z*0T<Pjnh<%QA3(fbflT|BZ#+xv+(TnEuT`tjM(cWQ}5s?#fbAksF0UwGo*jD9D3$~ zayv7p$yDLX`;_Y!t7~`{K-f%a#j8+S^fDoyEJRy>=8w|C%<!vVSv$(^+0{HO+$$_` z+zTU==GN8Y?HwGt74RToMy6YQ{=0a(noCJmMND5szxxZ{&Gk|h(VraT9=@6o1$ZFl zedkF~P|=c4|I3e9b^PSlNBR}{Td=&=zE~xW2TpU28wQvNA>wQx1Ue+K%vQ!ZDHu{> zkFG3M4P7_a#s%{YLHQN_O0i4dUo$o3vP5Wq$1PKNa199WqoZYFmuYdd{j870O9mc_ zGmC0^zD(Vg?vE?-(@aH((Vo*V6zj?w*g+-Z%$R@R6Mo^$Y_5FZ6T3>|3oeb#&f}No zQr2ov-b&R`<ai#zl8hA#*ZQK4I%0dctE-z<y+GLDt>}*;4@ILF$t?}UyKRauF}v%U zxmTq<oW1@^K$Ouf2|m@^&&pQ81?9T3@Q}Vry}LE=m)zgA26o`5@7R--X`&1hQuV%6 z6^GX36T6+6O<J2rxAFfkLeF(HO6|7$-_-wu=<|Qd`qOw2uRgB_-~GT#7&D0Su%@*( zes;+wbqsp(sNw2El||ly$a6QpDQO1IQPJ^_<xgFdWnCjhMcTlM?HN)V%OF0<Nk~F< z-$3MP;zB9$Xf!fY?C-a#l>Rv`bP`^cL5^=UAce5FI&o0V>fhfpwIj@0N5^Gx0L|uN zkXGFwmD7#T-&vNociEi^!$TUd$)sEg+ng~+k*UF7J#ijxH}?|rWLMHDk*`^?WI~Kh z4xi)G07c+_rO+WP#p8ogLzZcD;cCR@(!{f=;2Rwy(RRX?0kd2#lmhSk8T>C9Nk5tg z`RaP<(`Ik=y}2vTiGZ#$!N)*^qA)zhG<Z!&)xwZ_TABM&4$>R#uz09Dxey0bY;n=7 zSgh0M=2pVsLt45Lwe_b^NKjb@9y)6u%o<Xw0U^@0nlUM|&sm&iRE8e*ukTJk7O%l> zL-cpkvp>EdWSCm1V+-E&2ZX)tOoD!?e2g?R_>Ns4i<lyoln^IvOgtRjtu~k3K9m5l zYBY|h%;*bH>91dAz$VL`a1t@`uWw&6fkw4kELpS>%(S=<KTTGaqvHn4{xaoSmtC>N z+&=<8x}m;r9BAH&rZgAET%@avXoaf0N;fWP5j|MMoprlFSS;6*)M`%=`xQ}VhWa5u zRluLmgoy=1RF0^yBD>wB$2KtvXg%N_`<xR`317%CR*4owM*%-&-jF_tNGiHI@mFLo z8RC#v0KL+Zs-r}Zs-NA!OSiPTf$G{jmxuJLq{d$rVs=gxQQf=yMRpE$tM`a3&W8$h zIyEBdm(@B?lesEpEmcK2hcG<czUWQG*g>+j)FO3y|6x#W?MwEpTtfV`!L8!YdF}L@ zLw-#wQC(jid`(}adT^D<A(ZA<ags{g!7C3?Vmoj)YMx!K_EgX(@3prx2Ne<1pcSc_ zK3p`<$p!D}zkSn^=Zd%^YhEcs2fj`b`IWiKwq02<>;v90QaF)1LEo=jffX0{FQ88- z^_QeE2w8yY-NGyY;**~}czBl&X40#O*%3NeE-Ohop*WYRVU&DdlQ|SpSRJ$GSRha& zUksL|s*(*QGeJ9BUc$BQCS=txv5d9H^Oout9cORe-wGlv$%PlV%&3Nb`4J5^+FVu) z8C9Rg_UDLw?IU_ReC0Mdc7JWw{9BZ}2KJ*U-MnN%=1zw6lG-n@9~TcPwFuIzg-Uue z7VbYTye&&8E?M#5O)WN8+ra~Wq{(S<Mt<uosp>czpP{W!2HAr~=HH_J@XmT{yUN3E zWWd9EHmV<RULHx)iB(qaOqD%^zn$rQ7<SsrGFxzV^Pnjd679v6bexl#YSc=P)fL#r z#EB!61WS{9aEC{0y>!oB1-ew!dIm+^fI0x<r9jV*Guita_8gjdEAwO#=J$Bt%8ri$ zCjD(a9gB?`1+&#dk~t7W14;DjNq-*@o+J#>1X&j7*X$o2>H0-##En(GvPSMy=jbDz z0{X(+DwJj#>Nt}Ec+&8aIb`q$V6nLwKJfnix5HAMn`34(?r<Ch>PcA64=I3PB2FTS zK*76IjgXIn*Rx7f&(ki2u8!8{4J)&8BB`pV$bIp>oKc#0B&SjRj^x%dPeh-`keD`f zYNAEyMdVv$DHu=Y|0d>`1q*k-8Co01V=OwoK`YS@WY*@w44XOui~iq-V2(dwm>N`A zgy<WnpTv|Y9=6h;-!=D&o8^mVlY0xD`RQzl9M+=WU`tu6*uBlPDQ+%<#`<&F(0z+7 znIoDv{6`C&e{>L3#IkHSx&PHi&lK0wv~(+<6SJc;obAsSgQNcFzTOZl>5!;-?uM9S zp->drcH8)Hn1r-5`G%b_H&oJ<0lkN>j*~Ioh%=ZZGqYrCp}DWrSxP4A*X)F)kF{mY zhSzKl;&q!u8jYW8f5GSgE4k|&xC&Wj@e$LQ!gWvXEXroq_mz}w=*tR!ti>lQIR=ZO zaj>yv>vOz$Q-^VPKN<5-+j%aa{wSp6*fcO*W_D+xvy2Oahq@fP$Z&-I-K|jKt<%K} z%Ry#<b!Ih6{SfP()QN~;w3&^;C9R@55YgE?bNF8%xJU;EiYnuiGCr6ea)TsR4_Lw) zZ_b>vKTI>i4E2X*{(Rax0-fKgOP1G~)8U-*tPFmlMsp#*)Kuyx>V#vK=hfa>oQto; zuw;Ae4{`<ES=I#HHi>Y?gPXwymW=N(<|L$UaeFuzwEW?#-AmnyJox!Ao$hqBm;0k9 zL}6r;WAf-xJRt?$P&R<vk!TSO9l}j89C?|;2|A*o*RWpadxK7i*SO(tp8msNGc|5p ziQ8EHFsv3Ho2V}5LryFvx1!sp%l$RpboGa14#4`;9eQQ3O7WP*@TyFGlIUPoLG+j= z4r!=mIH55{7O$o7ooAvJD3^C|4KEe8^Ja^Z2CS%m3f9$K2%0lG?9b5VPc|z?3AZvI zIHSaW$Sga+?%G&e8yt#YzkUEVOcE3KS(R?sbp}8Kk6pF-7=W7v$9b{fgU-^!`h6;) zqxE0Q(1aUq{$V3`(3fDEWLM3t2OxCQKw#w#Gy;oyS*h^M75`DA^Q4bllX2FE7JvC4 z#%A}K`BGA107qA<w%~xKIlOCVfNJK|Gs;#@K39n-l-G%=(85rD`qqR3=Ne!+n_Z^| z5<#(jPS};%{0C{|cdf_`7Q!~91FYM|@^hx&5EV#XSOgq2*>ft?be_@qaDVAt0;-^S zL9Kgv=;Zj2edhSn5A$`y1T(IoS^!{htEk+9B?O8&tb_5M;yuOI!Q;^S5p7C2n>b@@ z8<@!f;Jnb;JrGx%5Mr5um-Yu;c(1i`t8FYbd{I@V@<N<!!pHNhgggOFEQUsY<Om7D z;S+xkMRU*FQig;j3Qk_Nxsjy{VOMek!W70y(77TS)fca?`-!g!|FKfZ3gGoCWdc<h zd$D7gPlEo(t(ykK-K8C(gS}H94a_(4Ia6bIP%*_Y>t`%RXx07PuD5$*wWh{^kNdA` zeKdBp5f|Vfg)cg5u8fI-U0+y#(F(w0KF4duu9IBKP2@Rajpq1#wBB>vwQ)Ye?qG`1 z?=NJUbp8{8T+}29c8Ea+kQ<$<$;_+JcOHw3+Sr33BH`*xPdk(!ze>el9m+DAWb6ft zmwO0uHnwC32Z~1K)u;X4(bR~{pa~CpNtvpSx2wc3R;BPBcfWtF?`_JDU`pI!Y>ckC zJHkvJK-KJ^nF`Nh0m0x$zdkwxMo<GvywIe*ELo2|_B*=U##bgivG6UMc<f~!X;)WA zz}T_A8jCmC1w%7_u_KIIQqsgE&l$j+5Up{Qef_*;bvMt5C*!Kt9DWs1ME;7s24B7` ztT>r(AZcrj`^R>Rafyh$U~S%go935w<|;+P1-ZqSDDm8&s#3v5m2v`BE8R-<>&4YN z^JgYNm&xnB)vpsXezUj4qnbn(lMnwe@^s2r6_`3Y2w9&H`G`<kJ}N>+9&NgJP+aN7 zO6qzsieRVr#so6Yz~b8K<z_uG$3xPDLpXr@HqeI`-v_)uUs2x+G`zpYS8w)eAC&mj zCj+UZ6>L8Z&2gWXCvzZUam<2LJm+!8>vC~j@y^jW05y;IWYySV1k3BSa)td0qXQZM zS3V57dca>MH#<kX(Y2%Xc~hDal*KZomz<w^wZ9OkvGIy`^zH=qaYb<5bl1Yx+R+P& zCx0m5hsnD>23ul|Cu$$IF3#?8)Mdm@vdX+1K3PP3+{+v^yX8(9lKRL+1g(Gb*;a1w zENgG+W~L#(O|;b1(JjCE&SIHndzMyb?|R!X|7unx2_D!+k4$Q)xgQDaYjT*|T6+8U zlg;dL#Ub9<>Cd85iv;o8d3|fm+dChL)1opkotq#zjOyT>QB<f9`-$*HfWvo3t5G75 z;>i+8KsIZ#KsHc?$-cD=+CN+)(eR8{hz8vjI5rz67D88om6)H`)!S*iD-aSPqD+Ig z1$ZZ0PDx^9M(NVPA04U`D2KNesu_KA0}4HY!M@S~m>TC)<DSy)I7)w86{jRJTErNv zU>DIH5b3&BjP>o*9-iJ?qCw5@(ISnE63H<bia5*zJaZ6pA!50Oogm7<?^z|JH-1{8 zFBgV<bArlMhK9g<^%JLQ8|stRf(N{)e+V;%u!e9`A^_3=z630p6L~|nlYRfXvq_<C zU&dKI>D;tUvJlM_p{-n6Q3~JV%V~p=WNq{s>Msg|*@AgQFYVz^W>ojjuKKaGbRS%j z`hHhQEv}lQ$6}-9^)X?A!U<Q#6Fl^C5}&bg{TK^?|A`^n>`q@%-uyp|)(}!o3S@9% z^>BAOHV3q;^zlwvL|C3Vu8xut!y_oD32zD5$fP6}eoLQF(1=EP5)o;nHR!^4j3p?= z-u}Y?qj5n*<;_k1FuJ5Z30Z#XNAV4Bp`00<=6PNxm~n<q%7se7ut$GL^d&&D2G_Y{ zC{-JX1)-1O^y<Cze@9s%)Wy>TpJC5BM~N!lTTO)AmN;tFw;Ch~l1)ggr+=tyUYQ*b zYZNiyQ^l;V%9-XCoJWJ9R9};FR`5HrpiNeq{)DCyFEv%_qKyJdW_-XO%$3m-pB!Xi z;TvW$jqgUIQ01fI9=+#9DWfc*O<cufy*E`(x5_|^l1A*_pYW+IX>Fif>LCKit%#%t zo|OgY|7zRR6T`tE7t6a#Iyfu)e2q^r=PpT6*L*FER>m)QL}S4SnS@Gbc!#363nscU z>fXZ(`?dnXLTZCYfGT;7{GiM^z<&wKj48J(S&euAV`uxhDYuKfI38f-BhAcMOymcn zIl2GC+F!6m^}gT#Fi46Zh;$8&lnBxSL#K3iBSQ)@NC-oN^dOCN%+Q_E0z(SI5Rytr zHw=>R|Nb2JGx&Y)M=;0U``WXwYn`vP)`+yCKA&UiGiIU=+UOmIwR{(b2=)5Zc5cKp z!N$$<J?S?UpU6S{097&k3C(~fzRgcMQPY167t&$+Nbqc1PaB!8>!YhDp2O@xV1`>k ze<cWFteSD0^j~+oBWGBmS_&umFuOhU;$C}tXgle{AhB%HVD&^aXydRC<`qc?!T78l zGoYW{2B>O&so)hjR9&BNo{epUHAPLk!G=~(n?)A>UDu)2Z~S_=&abcjcBCuEN<frd z><@MqKH`6zslC3G*5zgv`GC$eKD<joz3>+<bnIw|)?3GPRUeU9ng8=4k5Lnme5^Cx z;SSKm`j>4O{cpT{r8w^?c66KlCYbpy%6I+eZ(n(gE}F%zQF3x`6T|Iyx651xh^nv1 z->5G-+W4%ZLXS;A|8#uT1195|4JDs*eEE*yclgN(-wfpyy%*M}1j#3sJio^9ZOST@ z{^mpsNIyQxPUbWC(PaqiV2r^hH<B)lrJY_TSAfTw>2DmRYD&uVcSdPke#|j<TiPG; zh1Sia)2nJk{hHD}gFx{A9&y+IyZ!#ZpZ|Z75`6miRHlCe2eTUf<Dj0%b7Sq3#&t>K zN+RUV8`sVnKT_(Y3RzXq9^~Q)(8h8b*4l^owA%aIBk4Lo2=^PW7*nM{19`g4+96~; z3#yj3bU{jsRbNgc8PI;wd{i}b;Qo&a<KV{01Mf)~d?+Cc{wlOO)ElTk47^dysdVCh zwlN#kJsE1;T?S|Ud!_6+hEx9dDXQEY%`EMAK|y0Cz3$ICLqq6dC1lNd;WmETi=l0G z)eH{v(X|E}GYa;F>3AQ*B>E&_qhy*vQdrPOgj6`o!<By{!>%=tZM}jC9$f)}F;9e% z8rd5qiG>N;OerEuzF~#EDR7dB9?vP6)|(ep&G+Mok*eso2~~}dK(fv!a9fz##{S_f z_G4aDSxKDPE+f}IrmCJ}_s?U>1(un;yObwPnq3q5%#}qij*5-)NJTH=*3Mw4mCM#z zHP}Z);jXlHi&HXfC9A2=u>H3uWJqkB^M}f1UucT+^K{&n!lB}#?P>qknfsnrDpQ&W zWFhJ)+ydK;LFHs>uKjFM>$NWD8&YGl=Wlffbt#x(j<Yl>sHqSYw%tQ7)d`kUVI_)x zz^JhqA-8dJelW|Y!<^PvksW`1?Ss4gk@2OmPQjnV*dH4s!iJO_W<&3xC;Z3<_rN3< z=Q4vbLT&u@0RVey#Rb&n76!kREb|Q@AT>j_wO(jh!}}LJ!8JShM`(JKi_107`V@JY zSmbBf`Hxa$?z^H_v=vrdk0_otBsj{pQ{S!Kkh-RG_!Z9~iZl7A18FsTIf`9~>{5Y0 z>TUGvs6<ZA7UzqoOE!Tc{vVaXQCF^@@~d<T2<PYJ<*2*&gwy^wJJO};F-KBJn5R<4 zd65e1;J>=y@8NfycO9RqU{6I&n)*`@Dj<w@*6f6YGHM)5<%R^D|6%DKdPWUs3P+Vl zmhf;m5S;af_}}Cs>XY~uG(HlhyM>Vwzu#w&{{EMSIXzI;Yq2MJC-+Ll!TnBI-M#p2 z{vc0+h;XCvz&J8xT!M}|L>&AFRS$OQuiq?yvy)2MywZ`0I1l0qBb{GZOKzpV)$(qx zQx%aveJzL}FAIwv4be%Gsc&7OwUuiA!1pU2d5*}Cv*L`ZK=cN$?6GicCZT=u+qD^r z))aPr!{h}HZ#Uok3t$UOD#>yink<W5w$M%~BP_ndnfY?eAzj&WBJl;*W}$k;lkkxG zHP#=L&s4X)Qr)M`e|BfrO}A*naQY6ky@Q6a<<=T1-3_#UYA7h9Nhn{?6DD!;rKAww zQ^IZ+19`zNqdO)``eqF${=`l)D9W+_es9(n3#%z!wfQHK?Kfp7N+ctnhFCh^e|~gD z=x(ql&OURv?*n#u8$1g3F;X_w4Z#hAfpuZks5|XuGV?c##9lusCtvL+DVo3l3PpC% zJ;pgaGtEMgFiVB~HE4;h8p<3fXs(S7txXSL?pS7TFNgmq>`w!@g*meI^=vUG1TRZG zL^4KCmg*u=1^|_6kL8WLp=1GU{v{AIQIDVVUF|wEy8ZIQkJ@5NYx@H8+#gVjF&5E- z;CGtOP2p;Tl2<M95zmHF8seJ1kT_kDR4pX;D79gB+3S1DrMai^39K-E)(MH4II=uS zRhkco9ay={)QVZ72x0H;+viPuPphEEwk_{Gg*ACgY?W*K!1CaAf`)fdO7;qgk#leA ziI9i$T}3YSQ6!p(%OzuEo-9baw2kSA4U5E@T0YMF_a9m5Zp*+W#Z#X0IT8S51RBtz zTSA$`#4jKe$zc>mxq(ac`d?VUvJxQck*l&}9ODcXY<Lk1aIzXtd_~sQyfi(3<K@+R zL+UftMYXw68#t*tS6vEYO5uoc!8UmB{=pNn&EXJ}D8eY_qp>tf55Okv?d5k8bje@w zeMFg`2Z1*JUG0&${f8BEF;RqhBn{k>x+vcS6dzr0QfYTf>!oDZ>_WI~fWZu*LF)#9 zjCB7>eKif}N|)PbLG$JT7oU%<l<6{!x^n|ppFolu@7-Ri%*?pLfMSwr_x-u?n)krL z^c1&9)!|7$a{DH{Yy&=wS!a};FR6-U5P4HR-&JQsJ1P{#B+B2%5NaPB+&#TK$?hf( z*KRC+VL-ECCwbMDD!ezXNT*0=piG3+M`(ty2dO=D|Dq1Fo*_YPO<(1S;sVKYFS`!U zUBB~staMQ&SXs`tLW)>1oEEbb%#?S_;I2WQrAfEt$62HYHY(@43tOj8Q-5QFhx3Yz zMp+PfmU3o9{(BO1U9W~rPW+^nmV$qmrYHFvnbGtm21t+O7N@@N&Hl9FD3S?65*Cm` z+!#IAKBQahz%OQ)pl2I1HTEoqb8SrBHca()JQKmZv=*wItsoU6^|(hakd4e_0xZCU ztPL0i@yb7ktG@jrYpH&=E-ZSHG_t-r5u8Q%=oR$(U4i5O62<B5zY~ZviU&@}_PE># zzwMwQJG0gZ=ni2LIDGrI3=S*e4Di4N23E?`y8J2yP~>`!WaBAGX1qMIYOiaY9P2yM zW+mJLzpMvKEXstmaV=C+{XHPA*=81&m(~fUSsCs5+ysY+^cdcky^`F+`3?I0A6D$1 za($ad=1O5T7QkVpwi86=5-X{;oi!%6bTuvQB5!R%+8F{YFrS6Z%2(#Qkle3d@=0YY z^|8?HDRuYk-%c+RCTf3F{9^Kg-T6>Da546aaFyH9z>^?tu6u78!~@$>zJ=PqXr|9K zEz^%*q!QEy8cxktMM$oNKlE(G-I}~)<svA0l1azv1+)^4>N(c4?#X^5J-7$s+$04; z6o^<*`D#3}`>TKTXZeMGsYynQvg6pf+x~04Q|T&t8Hy^Ik)ILKe%f`&)pqq-fw2Jo zIrs^(xt$v42IOe`F+hR+UE$NQ1{NrU8tdlr*p}aEz0xew=Of<NhjOAs3EU@f`Hm;A zso77ZRjF=88s&cv2m=Jlr^1!?LHUbI4w|Q6?>(8Y8?}0jbNMT~lVotd(ntEItK$j- zKs$L-F67sYBgCn&CdH|yBw6LnFeV}TXAswPLFl1DYXuWCdXV?QDx`B|4CU;{NEWli zCV~Pa1Et!Pi>*hgrX1~h<nlr6AYE(3UaQOsHD)JRrQkYVq)Jok(0X%!bqbma{Y4Ei z!WGnn(bZ3@u_wF96s@8aKCBPZy%Ik8Gh38pU&sdquDwDUsaGOmn#iT=UG=h{mP4tB zVCAT$4END@P007LQhiSj{qOPtSIr5+I=U!wId%oZns_XR^P3)A>m|vJFU@`z+QLQp zHHd**ZJ-?LNJib1aYH3Vs5p`k=ZQsdWsr7D%}d+>-5-Sex`M_2fYMi;ub$G?uN)+X zmfhZ+=wVtu#cGb6!pz(uiWxg`1lf{w5x!J9es<V^ZT$QB!5=s0U#xn0VRgm$bU{?M z9|ISJn75)g1LOQ-_L5&e3M^N@yUDm#Y0~g+dQPUs#X};)Dz57X-<~!3^lw;0^oM&p z&X=o85n(|^f}_^uhpD8AAe+W#lk;oiW7jnwpXzskGr2}`&odD&&O6@(xa&`S#vMqX zk~%DT+K023Owb$fKg&se$-c2B(|SL;?+e_UW1Yw2XOCow2#tAON$n8)`*d?tecSCl zi{QN_9X5CWnmy~r5@e+~+|Baf=67vBR=ht&PZwR);aP<tdH&|*$}bukPJa#vQ^072 zXZHv?*Wy2{R7V~s*E~0%rq-&hv()K1NBp?mrk{{seB@KB*b*wEs7jD7r8^rN4zlw@ zZw;6^$@yiGQI&)5?tfT>lltuN&vwo+>{0<HpOP}<ZwDE&n~^sAg`rqc8Y{_RgfD<i z6-ZwCUcN)2@ib^@O4@Mw-y+h*I0!oQb44d2Wk~Jp9}#y&MCJbd@P0(M>$4(5hr2-X zAUD@0B~P;NzS}a^2T+v9r>*EM0pE`=&Fb(qi<H?jI2<(Jnk4(RyqB?bhOOK~zy_aU zkB_>CL)FY1-*|ez6w1{g-0m~lEE)g9%N(<$#IHrofg96;#aHQph1uY1Fr<V$ql{6u zE4$*0NNc_5ky#&XHKp^$hvt>4vRKyq5Fje_dBo$IKe@FvFXgT{Q!3R^$6@fu&?f=w zk+}85{rb2X>l9&^B(&h3731i=T3b7-($xCxE$1w73ZDC_DXB7|m*rnIiQIFX4tk-f zCGCPAK<hiCYc-`f&#S8yY2p9;Dw8imnd%!UX0^8y>AYQDZc05;ZE-?6h1Z+}DluPR zunbb5e>`Dp$}JZjt|k4hdmMUjs(PpK=u_9@y<v<d-qY9Enga2Kr+usse|Sg}Xh_)2 zfH}p!NuWL7bWY_n-T*mb1NyZDaI$uDDK5_UcwMlkeCA7CJ0b%208>6)4VPbJG-P{$ z0e)S>ZLKobh@~0PRy`L=Sw559=7&;orX~31$#8tAK)U424>dY*=wM1CyD7vY^Lt+! zmOkzJpAsBhNzI4u6AdZoO9ixKO|7~L4V^)d(fVTaCC0keheq^LrU|t)O&{0rmGG2z zcY8YSS;F^qds1z^`q_?Wx_VmeP9NU?IQV|Ku=sF2M(k4lTZm?o&k(Pw^F$`(^eAfY z54D`m`8*bpf&a$A-si6VL0i_wS`_9kIzvlPj~C0nJVn?3o%4978ES4Gr0sVxqppwA zN5&-(7pj64CTF8t$6K=v;zTDkGzUVZ^i-lTQ2dhbvQS3-s#YQO?zHeB_tG@KZ&84| z-!t>>je(@AEsd37&bc?B&jT$novv%w@*ZCj=2&^NgL0X#I1h(53bSl7j>gl^P@vK_ z-3vB&FXra?VVZ4};s<_}Bpn?$;q+FiGnFos#}y?6HZc`&rn>T7w03DqS2yKcT3M^Q zQ%7h0;onRf-Kt6!_f)h@LBY8Ajn{GH2Un;9Ok3y$J9&>RxOe%(p@Aq-0}t}ZUM>$# z9l=YNzceqUY-Zu{gvFdqo-55={V)4;XJf#Zi&ld3V-67m8H4Z~NzstU_pbkXxWW%s zx1Bo^Tu4ZyC1vNik#k*a3uC%T`Fxw-O5NmryOIZaNz|XG>3dFYoE~{sOsX?Fnv@7^ zNFI&;z42M_7I<K|eQIqz*}N9SKlhU_u7GSR<pMRZAQ_}uh!bt4AhpcZ95atyQtm0I z9-l9(ByNnjQ+~uyYN%--5uon&NmsaDDJ6k>;YYEKV#;afer1Tj9w^^^Hez}|Hl-%2 z#`76O_zfcAB@U@!_$yH$<;|twFG<3kNRCIRM#y`Ukml<oQt()@t0lr0*%laQjrSpn zIX}5FJKJ9$;TAlwOzYnhw^@pki!aGk<hC>7tP$KJC!bL#ep`7j)3UwD+9Gr@{f69$ zd3pK%$&24GE$yb9&L8iTpGKOxE^U1oD%r-6W++(3dBs8uxr~XUvglmTnTod`JmPDH z5V6@VHV~@3T;Zx0VZB3GTZ9ZG3iqA?l|bZ0JUT7t*OVXi9Bc2zk+O5RMdf7b3e?WY z`t;Ql2LRQ;PzxLIVn57n7CUq0h%SaHa{kP`C;Z|;L88X%R<cPTF7>_sfz<3<-IyN= zK}ST<wywjt!rV_JZqkW*tS37NFrvEz%J%=~X1VGAlLUgHsBZ{UyCV(WUw$usc^tH3 z17yF$1L-ZE<iUDD!L-5*0l$nGRIIKp-t4$nwSnS+=l4q8UFHcQV2Po_p0{|i`SjA( zf+vnvssP(V?F##7n(Hz=Fk)EBsGYAMkM*mZIAZ%}YNTVEa}nH3@n5P?O52?cUG1%n zBqDdVZ4UA6)?k3RiPrK@imA@>NG%?*UraW+hyBapaW|!YvF3|<^g!&T$sUr3+9+)| zmkiq##H@z}j?0J0?|n6krT@~$BLb>3LY&pAP4CUg8@?>!@6XP-LzMfx)-hxU3N15> zhxKMg?UVa-Xv-@|29mX`(G2Fh!yBvOf6MI4)sw-x#^yK=?4a6jnH;#`9>C01b?xtN zyVGX}9!Img1X!Ogx+2q2wO3~=xbhuSfnf~mH~|-6)?txSB@*V}lSwhU3w~Nj?{$rF zbUTgU`1%5o2mY?roUS)of@F%mTj8wm@~4Y7g;dO`&ScREE!uCuxp=UxOYcWw%i<!_ zDCN%sD{}Hrf9GHIxTMGHaO2h&l#*y~&B_mZQ-oe8om~7h{5+`$crM`dq5Dp<Soj$J z2T+o?V@l#;5p_d#ug!9bo8Jx2zq`|;N%~TaLW&Kmnu-k|+3nX`9(xt_ase**L^wS} zv=ukc4|5n>1kX8gVm4V7KipF!NKklqa)}wYjjvB85g|RfQ0QnVKgkM7NkimH2dErN ze<v27>(`keS7U2L7dNkDg`P#WwQ^!G)g>fl%92$k$v{l|@9=vG42S8u$%Ryb1RDWz z(hCtU?qhgGUDnRn7H?2i<~dU~3EWjuPqHX>JWI2|9tQ5sR`AT?D`f<e&2~{4ocA;_ z<@z&{LWBLgb>b!2zhMIwnHC&XF}9_Y{?vL3qo;p+pWKYX0X|pY7z!SM<*ViSQ?w}^ z-1=S6b9gQLgV&X;2qB3Hc(LV%RCuY399`h#ptfn<h{zb`H2o5<k>*|?;Kr?(AaC)N z%aCF1joD^?GOaXT>yurlTuPl}6IgUjV9xuJsiH=9FZ*4W$55MRA@TV}`SY+-4F7?; z^Zw~MwS&3dcnN)ggR#E-V2#nk_Ji)yTdE}L=o_9NeVxd)O_Yi5aqu=2GTy*CuxL35 z*^&oO&U*WP2v04qg)16Trac>>a0^jAVY^{haQ(L+_Af9rzDhiyul)Wozq5`5^UItH zj-(3@{$q2_oM4}&ccJ0<%JKRW3%T46nKO*~Ut5gUf;!q#RZQ(8UST;Ty~f~#PyL*4 z&)gTZcD)T&Es#~A>tovw0$agq>z{WP(|RT?p+f_{O4@!Xr&}Iz(=z*_4{6;ssB!}T zMy#p?>cgmy$978CcKdL_aG)6CT+T{_t%~4^)LH1DK8u}oHN-^<n<ch(%#bc5fGOeX ztY-Pd2A1W{k6T2e?|-1VCWow0si;YMtX(8Bjo)7Kg(l)btMZnahG(#}EcST;Ut$s) zeSq3#I@o+^|GKrar&Y4L5JzC(X8Nmktw~Ua;yV)kWro;K<#TJs)Xmq;hYui)Tk~ln z1-8*Xg=ajG+=I#V_`jc$%AiYXX?V|fcttwl&vYQb9NWNKpE@nF$XKtZNrW%auMya9 z_S|{~o_dhLG|Q*$AZA|xOT(ww{#6nD{^j(IOVY_HuVXv4(DL}iCR)-wV18efu31>C zS4xzoB8amGj(ZT7nI7>M{s`9-GS<Eh(1Ew4EP4T;#A(&AicnP;HQrDcIK+<`K2h!I zw0@{+G^15RSsxEQ{*+S9QyAZFC|C{(t-w(CY%i^QF`v!s&-{%Zh90|7hB`ehsjiXj zN;tj-p}h_&_BWU!poMH~)%bwT2;VFM->z&kf0|GBH>_@Z@wNKb`9hJ%pVQ43`>Q`G z(Imv-=H=$gVg4HrVuylwa{QrFd8?ZZc{2d3QYPN4A|<D%EKu=kSu_P)e|;B-badGj zL8!2PB<w271GtA+Gbc;AFRIg6Se9}i>smCB27BA8#`SF|4aj9$J(rU=s}kvl*m$Uv z1o3HKw6+jydFx#II~pz(aSRm!bMx?2#K-a!HIjo36@~^}wvqp^@Iczo!g7L`UCr<U zTo(rL*nHPUWdCRc<c};!F4*&b{W)}zb*{5cdIqshU4cs{(|xoi&pB$a(Q;yFaZr-S zqYxsvnNKJ;jVF1?Hej4tG0GEim^K-LzPMS5#sck0Ty?4KNd~MG`wM5`y$HDK79XLR zEzuvL(b<+Ixb)XIN@s9kKAoId?IVbO`n6M^0vh9j>jVfo0oh>sHv5uvF(bB_z+^(O zw&OW3Ej!C&%3XFb;^9I$&U~4!BVN6MDAt5MBGlAqS)b;QN_vwIhkSzH>bPlv^y7iX zSIBtyd{^gh_LFy*Epq~1?fia2*;OT*3GIxcv<pg{v?m`Pn(uF{`laNw7=nM!ZnQb_ zZ#ui8rY_dLe0FiJ*frO=ja5hD2;kBd_{D(GyWYU84}#fW{5Mc?=rJ4#b1(l`6Hp{v zX9TUPa;bA4oWJsrZfsI-;J-F=c@Y&pFF${MQGKKx`iy~aV^?0~TGD+{sAS37bA+Y= zICY_hux?24_;kp?1)aeQg6#waDQR2ECYcbtHm0&Ixt<G|Nw$h6Hy5m(?6ZlNhP?qv zLXx_H&)E<~wYYq75JJCUF=|JUmP`hwT3zt4&@)3eC%vsHGeXavF>ra5`yJ{n`rwK3 z&NB<|LT!*Wq8TfK&YEC*TBGO5`w)ruOj4nHfFcK7D1;-xnl4ZC1S-Ql_xzbKoB!PR zwK5x59P8jf)Q?iYRo%PycAO}6IlMx#FFH&iW5M9nptLb|mlr><_f!`@5*|&7*4KOm zmVd`w1vd{C++RL88|!B2J}?QIsG8sOir*>%ADv8#jy*wHUGc<mYV1SEMm*K=MZF%7 z0#o6~+KS#ZSOFRzoSa{@?XRLQ%yY||*b@p|%|kqTEdr{8-LJ2z41Cm}WBWp#q2IbB zuj;KSvkPit9&TQpx@@Hd$!Sx8tet+wl<3}s(D@L3Td1in(_snXL_}L*V#kkSka}i* zupe*87@B)=Za$!xe)-RFIp`22Cbdk&OqQt`oSZWTE_hsLRQa186`@qo{@_EY(+iBI zcl@||^@pj|HAoa?Z48f8R{+3FUFxMy+kJ52P@_xQNY)1QI>ABFW*J(9#zre`F(88G z;A_s|8zv=phL8e7h`KgR@XQX2uAJG^tn^va5&qrzuFula){5U^Yj{fC4U-k;`214D zWOC>qz^l@`R+NcGX*f37`dphbK{#Cl&1o3t)tBob+X??;yt38RKk*DZDd`oX&&W&V zdUnLN^LITRD`61g2G(9(c1!kFEb;j*H2?az`%ex%2Bxm#rwY8pZf3bU$^|djYtb%Z z`a|%t)l0@vq7s2}0|3LkY^z@Ugar+N9z*Ej&pNB$YXIqQ7EHlKn>TLKyMHt3Iz)K4 zf0n$FO2INzV@ni=9(z5tJ0PqFs<lW#2QX?(R$1zpp;4(+htFXi?IiGv6wO0rj*r%V z^DM=s%&)KQ2L&x%F0MakZOQh(@3M67e6bn_bfK<}zZ~cOQ&Mkzt{M>+To@RB(NoXZ zr|%RBImydv<PnU$B&ysK=<VUUc`u!#Sx9E~ibkZwrcloCo%uq=m!X_sFrp0+WHVDu z7eV)6Fa4IGQt25%FB-hwBSW!=4EEk+9<s9^H{k3DYBhYRC*jAkC67m(xZJ3G86P)s zq}`KOd-p<nW8)(F{Mnj$5^d5gW9_OR9~fefYfzS#>wVta7Bs0zL&OT<J~O%ZEcxQ^ z;STc?J7P>aT6JG>_bS{x9TeBI*x1tT^}oExy`sFrZeKe34{Hn)zgXMS&e5FqxUxW* zFT1atKn_0|8={pk*>8C%`tWbN&b4yy;r4Y%0lkKVY1Qbvkw4_j2aT_VQ+TEg`Vq#G zv)$c06KV6|$N)Kp{KF@4usWH3ydDEV>9+r{2uwdMh)dH81OknH%r)o1lmF-itmchM z+Vi5;Of}<#mEcYf2S;`vwPw=rTT+~DT{`xBu^@ERzxeNG%(W@ZVm{|^mW6f0Huv#m z&MXRX8U8e$o0)cp(~@w!Q9$Qy?8|Be%%ZNxeeT|fZ6=N#RvAf_Nd|8&%r<4U7pm?; zI9Be1Q~zO2lxrAHT8B?;EGd0|rd%EP;b^lc<a5Z3yMR5c;P(RPq_u@P);Q0dQ1wre zmGCVjzY*E!-4?x5_(ABO6Lar8o{3Eo{A{nX+*_yWwVB}~TlUoD-LrvNY;x6J>gfr3 z?1$DiiLAntRPsVjKlGD(^~ybv_Q7)C%6N=B6Z6I(vDfz2G?QYO(_d^M#UVPQ0=uk9 z1sQbNS2+@ksS&I&v&ZPVX%4!h$GkVGOXrsuA`e0<362Yq<?5&u4_Gf&aNhdA`Tm{{ zJiAN+dEjUJ(k)LipN_f3@nkudrA=aQ&6XT6biFk8{F$1bXDY8386iXkk<wk{=fj#s zF0N_hyBpMTmzI5YP;v^NWosyZPQZpLA7<4MyY+PO+-O(BVSgz{Nns?y)K()2tr=5Y zG|_8X#}ATOO>H#Z*xiQjxp05CEOgOsk9wnuiUdk_j)dW!9+hGK>#C{JinUzRG>gjA zt<*~3!>;za^o;j1%mt6t#XN#hh(nz7oi71@1A`L#U3Y8N4V9;%lCdJZ3VFi2ptg@+ zDVjvQ<(2p-h^Bui7TOUP8?F@E(^|6Sqi!dgqh8mhym;7tt;@k6L}p(|NLBORz1QfQ z3Jf7yhY@yl45d7BP@PXSRRZVhN*{l_%5Xb%$LtHo!m#z^&VC25Nr#^u`=aO2hc(DW z^smG3+FXgPAJqQlfTC0!<Br%+{bf>Buc+p?m8X63#4(4KhKWV-KlSx=r+?0H$D^1d z47!23RM&6XCHE#l(ot3@R9jx4-&p_OiqXe<`0so5@-u7IMG>p5ob=5jP=R!-E#|v@ zZJBzvlGRzxqFU%N|5)2cEkpLS3f|Z5c(~6MJ5VRYO6+w~2JzqLtDmM~qRRTGHOx8e zsyFPtY&x*pTaslmfWwK0+6Ox3sCR_F?b^67mn=#Xt^AMND@h!2a85y136{QllRy6m zHYL0^oD;$I(iDEeSFX>lyEolJU{KF2Sv%R9#j5I45%w5&DBkDW3$TG6wqwslPt(4T zPB8HO?jk+EF#2f9#^G22&g6^;o|y_%(9N0{wOl~4B~Q?*7$<1<3!g4XJyL8WvGpK% z$XhDkV+pHf&5f{q!NaS$@zG|2r-%ew&56ekWFBaY`(8?NPRFqAS@wPEPO<k2;`Bm= zr{w#;Tm#akMlA5_=^Vj?vCa3RFLo+cE7_7QES@}8T{WYbKO!+xV{<ln^4&EjE+_D_ zzPTniS+QAv`Sj1=9Q|_6`3Au3RgpG63F<g3(MA{%Yw?uKAE200o;uw?Io*%OnhY~g zQB6RF@ve$OvlugqgHz7iN1ozo>?N9<?*`447|5Hsq4ScI;r5GMd`(3;xm@gUU1FmA zkc(M1;oLaMc%!21MG_LjoRRL|;LeM`<a;Qm`AU6lLMLgonJEa{KW=qZcc}=Tcy3qK z#=Ih1R-8nOy}M0Wzb_;npDtyeU0vy7qfN#3EPcga51!1VZvQz1rPwRR5e~V^V1>() zzZ<jPc_DX6Rb_U47ZSWKCD;L{u~C@<Id%3NX0+3HsEIDw<C?!0W|>oZ|KN7BB1>5< zSHSRsK<DqDt7=TKYufL%YP)Z%FI4>ykr~>_*ow_A`l0&PGEo!Jry719X8sk_?>yhG z*sJ^@$xS3lBP^VzqCn8ndDS?^kXnUITuSBJu6URe{biy0V;0uXpxWdU<=^Zr>x9o- zpA1%GUtQgNBk&E?wp3u^@v_>8_M2Lgn(#GnWO+cz1t#?%TIt^i9o~K8kvy@JBiOpn z4GyBUEI&bg`l)<u95%GEznHH{seUvhY~bP$A5#ewgsJiD89cSRAtZ={A6I;>A(Nd) zj&@e!=7~F$Qmh(331rP0V~<u7lLGoxZF+pNs!ESKttsDo4cwf1E@~@^fmij}EX_OL zh`&;jH5r;zs^J+W58qP2T(H{*gO0uEL_7`_cXgUWOJ*ZSXG5~d7P)h{o}H}4Vd3>X zbYDt61aO+wJnW8U6{N>#&Ro8hZ-&;-X-y^k%2tH<m=BruVFf+D2_!zZW(Huut=Y1* z^iw<ZY8qo%K6Ahhc%^l1;o;=2_S^ZRg&aS(TBVjQN!`hahIX9~O8jMchK%$-surO4 zADer=r9si{5>YOVoILgD$wb77kreLG)C9i<c__(H8M_zE$M}GMPQi!g_Wvb^>HlX* z3jAM~(|=h1&HSIVw7(&X0FMGpSUPVTxSfc^^Qw9%fnin@8m0DinTDj((n#uuJ!vdl zoqU2xgIDwI1ORJU@O9qHq+>RXQbfkgftbF~B*~j#OEKwXa|imeeHJE$S5noLp$3l% zKJD(yp=!yo*iQ%mSu5n~LVHku##XdOh{$4NG8VPo62&9F8LL{P%3WuWdQo4Z_oMnb zib3{Q6}Gi)A`G+PyTm8zkd(oZ&fJSm{EPJ<q#Ft}(4Otn2Nm5;or!gt*BlOiY9E~& z8Bece_kK&LFBq3tgsdu<E~;gbB9TNSY}JdkjI7eSbzhbCZ)_a!&0!|oBf|x?W_}6H z%qnS0q_Ny@l_6uDewM%0yzFWH3e~g&yci8^+%c5Aw8a@}uZ7Q0&zjVPlZ3uk>ViT@ zFELL$)+AmXQ89e$aMx^J>ku>_Bs60UanZf;+S<d{&dAW47(m!Y{fFiL<z0*A;YAw0 z{vwZ-EB$E=VDFGkRIqib+^swpU;avQZD^3TQ-1!8L+VRZ^7SL0JO<KXrfotb$zOGv zO-gDuD@oN0GF9g>!Z{%}*0uSY^P_{#C^SNHxUFzHB+QSB3-vE24m-rpC-h^tt)U^k z4@9n4oVDA*&^U<+=Y01ur+MqR6;FlH=Z#Lu=?+>p!2)iEF!@t1UVGQmBT0KIX?eO) zNpJ9PCBCx)IF87isHqMMNico^|KV8WQ;Y0bF@Z;}W6w@TdnwuNBY-861$qsf53A<j zm}5bpph~}v?Hf1v(1m+uTsry}B{_qKKE=OXDF_)fCB7;qmePW7Z&d{%9Dc#RwlrYw zK}$AbEW=up`2u2>7yxF$T?%E$gW5h!lh#En4aPs0dh>L)Y=<;XV~R9ah3Mv833m_w zz(LH_PV^rkzEZV|ayorlwAU7pBbo7mk9by}RgQcwEw_TAB2k(fgUN07Dy3=N-aSq_ z`6?pRR>HhDZ69CaTpuaMJGPhn;X9LdqlWUb2lDFY>AEa4>=g;@Wg5!AUF$}4?BA9a ze!M3AxKW<`S02f?^PY6WCFoDtra0vga#1Kp)q{+wr)RNg5AuptG~w|~&|9s{3;Lko zkb7a^Ubr7@@6h)?A4=1K#Lge?#{If_@%%0*q@x<UhI@D43Vvx?Z*JyN@yR*9WtPKH zkQZq(QXuoqIw!dHq^DWwi9h!!Ve0U(CqO<v`SQ@(wCf&-H*U<QFW;IAd-qiOkr@40 zWLAZ<%xakQe47`?)@N7y=N$5Q8VhZW3HJHZgjXp%)Df=k4tqD;;F-Ob;y4Q#SEX<# zdpd)=4z|}8^<0Cx*B4ZCM%u>Kcnr{F@QVIK(O^gdWoAZTk`C2faL}k3H<K<Kfk{Yf zD%{Qw%fzxqo9fD|?bkC)?pw)UdB3@tn16q|A>HR)10^9!iYj9RmLq1EqZn;BVmZCo z&t78{38*!<V@?@`;aCo8j~<Cs#SXVni}JY%pcw9hGAxd$YQ`(#km2gY^J4`O>-U=p zwl>r0I`|*-2qx#N{6<N;1Us8?`!=m5QX7@gL#u|rpO;T1gQ;(BX>a~oIG{!PX=?hZ zy$h(A>slqBU}@D}g&+B0W;|DFMI{mje4H3dPFSNOU&k9)iCi6ft{h6#|L$~Dp0(I3 z#eqo4o>PJ*i*lFKXb2mjO@QBUQcds~KfsTX;!tuk&R;Z`=bXSm+K{)KoB<iJlAPom zp9BXUO1U&jB6|o_K!^8V4qL=>sKIB)pI7)=<CyU~WaSOQt%WpI<fRcG!hp<W?Mju@ z!yg0B4K4YU^PAg}wr7rwji!ujD@)Uj%1Vo3vU2}q(?#m3U2?~tPcq!|AK3kng5JCS zx_Fr<o5RKZ>T_xu49E|&akuj%*p9o+zRxw!`P@q7jl+J!n{CvLytc1;ex+Ks**@-u z^KXY>u}J-4#V+6W+Q&1P=?}6le>`V>E;!HB_KSebc1bqPQISIGQkpyQht*MaF8&9w zHZ+yfQHigr^rs1Bb31m3*j=PY(BH1?xA38=^=_K|r}1*|?B-)O47BaQX)}Cmh)T?} z&z<KX|Kt)D-73lOHMMT|XlO_g>-643oa^*Z<8h)w()RR|9K75R-B@VG(sE#ga?vz+ zZyr{_wSOO+w@N--dHSA_N_6hB46fC69Pur*gql3T!aQf`eE2^slfd$<WgVufZ#uY! zfShuv{3ly|gY6qVSu8ePi0(D@4bEXYAH(Z+jEpbQ`Pz}CKnRS<2l6o}pHgJP^~TK! zjOoD1ZEB-vMD$W#2B?h^o^<YbOal?U=gK*Mpt`yBeG@%X)V}}DO}H9Q@~Rt>L;MH& znrbuW1<r%|l_yyW_Q-DQl2)#KCXjoC{^3fG0{DLHyBsWk;PaOsY^%(Cvj2X79}Nc1 z@4t9n6sN5?VnzOjKlJ<tAv%OAxs5~^>CG+bu;v*97kzXFNenkg?1Qzz8vpxpdiLv{ z#QFrZ^8M6d0OnDP94TwE10mW|WH3=6CT4|@H+)7Ou@2-FEf*k|bbRtxlxOT+%@bh^ zx3FQTMkDJ&?_eZOUf0o6b$GgwJI;lP#p%5};AfhVa<trJ*GbG@LIf}!10a?pi?1&^ zpDkX_-S9`Y8+_Mb0-Rqir=G3Fw(ko*k^{E9JX*apg+eYhm_i8s?0ezJju7mJ?kn{v zQmZc`)M)Br?D?+IkCeiHMvTtF9xd$Y_3ZUfVYZ`Ii_11bi{B3b5|3tQ0$xBJ{!dAX z|FFoUMH#{aZ&yA^fn*ZR1UV|_H^CWJnA((d^^OmQe~f;U59S@H{$6;`54%bVB+I}H zrC=*KQ%xQo)xzFiUF3$0qUGpZu)p$~rmpFIv0-lh$F(2mb?n<OToeLjO3?cw#mV_U z=UJ;jt8+a0P_GD+7PrmK0;}N+6~PYF{FlsMpW<OZoN4hRHUSPfU=6YC6YnFLL3@Wl zT0R@{KP-Pwy9|FF*pSuQmk1r6N@OKAgN32p4r2D=CO3H6E&Y%oqa0~0{~s0#GNWgq zONnFi{O<h0>PsMtkNaCbm2^Z|fDzlyq=v4@n}!FU*8{3OqEfen+a^9d;(Vlpm6vFl z9&5xc%ns{Mh>oPbJI{_wxK*0fs+Xa@{7J}RUp*j#b)crd-`tkZKWEV&xX^AhtK{v% z2k0@O#3G4mh7lwsx_YS84sUQ$lnUd;l+p}$S$R~N)L(uEc!r9KozY}&b4>ZW4|LIg zQHve2#!--;O20;drNE@|SS>Zua9w`1iWH>dc_DQ=+2EREn6=X;72rGO)83ToY>%i7 z#1>;3O%Fv>B4U*ly-W4;H7_Agu5WI632oZ6`c-bFHFBthv!uk?9Vda`V?q-aOhcvg zx-#*Nuz_VSs+5p<FP$~{XPmbnYUjP8n7x7b%i^K0;Fn#;g|CvSoU89&4gpq)jqBDX zpUYkBc;!Wn`2%1Tb1P0&sFB$BuRo+YO?%$M1_JBenMDZiwwqEUP2=WU0YcfC>bYx* za#~Ld!2MCR{sRhnd<iF(QPKfyj8KXKncTo-VId8H*j#ANiAP0o+?u`_u)6>SpDdw$ zu63}T!~v<4qAkmDW;2Vz<oN~^9{JGGdoQz`z-<Qq>C<|OGAt|yxV2dwexVYbnld|$ z;a<z|AnLdi>FXs0{UkNLlk8a}fLwm&sO+EpxrL<tlShkX^EEhyWVoy87J09_9GWaC z7|Uz<V@QKHZ+&*;PyV)5*MZJ=U&%2BUPGqn+1rK;?ELURr2XJE#+2imU?sv+mJ#(O zY<!j05@fr-dNY2ETo_LMljV8&ln-1hcI9k)GQyKtYgNBxNz9~dB?Zg_nwn+yDpSf1 zew5pmdVtmLaDtV(DpIqsuvNh_5&CZLmB{}wyYlA$n(wM{_CDAvsh5l#ynsI}J}jzc z)-gyvwp`ftCIw!&;=(*Nt~fMUZ*lEZ2Cep)74-7+%UL5aEa<Tl%w~X2Qk$~@y1W4B zX?M4;^2(j#rRMMM2@ZxTo5D}U@-s9qf3Sr?hB@qv(FA)kaRHvF?Ozj%uX+}SH!16x zgk=A~nr-|x17pR8NW^vI*kLk=V5=7A;<-fKyO<H&jY*2j^NYRs?jX+_=ko>kh9A>< zMoPG}JK>6kUe}NHvi+OV+l1kxj@!Fs8><bl6Xe6^H#r-vs-Hd&O@A?;U5S(#{4rZS z?sFRm(-}nvTL%1Ue&-caKziwLI|#hO7+>>=IB0^)j1jePFSCojwHDiaO$*Ey^``sR zkFUqW#uPIj-7j^gP8|xM%=w>F(GZFPmpn#+|FF`c9;ypRd_QU%y*-}}>U?wMX4DkJ z4HSSiq#Sktki995)`YGGBYpc%o4o<JR}ihsV>JK8RM_45Zu8(n0sQjrHoVP?(U<uV z7A|_GGCNj|XODqQT{8bbwNui1XVE+^k#u7fh$G3B`RiVMmeU}aSo6L~x^+&^L9kAP z*)YfJ@;<=PCM*vNJj#U}Fq)&nLF<7|Fq+?%nug=!@xNrxqhXC`rQ^KCrB)V?K05_T zZbu-wE5K_H0n0hIvcD1-^jZ*-{|wf`a6b7s@9_28e^|sz?Z}c}w|Tw>`rJjIKln5b zS6&GJiRnlR%u>};Ei0#!%M%cm^q@cmvHXXHB!MC3^WS}K6#Mt%_ZW?Ede$V~y5u&& ztL1MhP3C?j*hB)LfpE3j;Zx9LwQYT7JLtWxP7^VCnx?d(Z%;z=AdkFi8#;6Z94B6X zbF1LccPjB-D{ho8GO8F>2+otq>t9;_u~glUsl7?kCuq$HkBf=Q{12<~@%Nd@gGuE| z(gd)bEg31p%y(ja;h(H1%%GYn<+9q38bVy_MKgkck++b?@SL5A=K(bLgq-_c_u5ST zS4-xqHRGXZH2iD$rq}kK<nW7H%wgHgzUT2Mavd8dn$93Tz_>7j<g10{Cj*M+*xnM@ z=9npEN!}>2oNipM2fvL7P)4?|(3l~uuc?w4rC?DSaaQ#fgWh4<(w)LaTAW_o+&(`O zot&iaEb5=0*IMS4MNTLoDa)t5tgXk8t$EUOTR&u*+E>I#`Xv++yas->BiTjiWB%;s z6~Al!WH>u*_sZO=M=Y&8p}?;I!jvdkPHS{IQD%mO%qCSRCGMkGAI5{lYA}BXP}$3K zj@br?dA*uXuM}?tN;yoMuL0P>+TV0+&38$<U&Hm=SQ6D?2vRUR_7p3lx7MQkD)6|r z{rIE4>Ee<++^<=6HqumX-KB<iD@<lmCceiQFyXyiKc=ITxFvl}HL)b1s>J`2S_1Aj zm#4=?G-hm;CIY`^h|+uv0@g2oU?c1>p!^KnzwtwsY`3&rePM0n9nm|YOD5SWl@g|) zSh0NIRhadw>uXjkm7^5=9qUEm0-sLu?7o@*+G#K3eW7(hNgRvXhEs!bRg`&MVO-T+ zO)fj$;=vOLayHVQ&n=g3J~R4k`(}RRIU;|Q*iEZ#N7(Xh$8#!iZ%V3$9pA?IjBF2< z$yYC&bSOz1PFe3G;{uZox(`0hx|-9w|L|!96RUEW>`3cd@_?Vd4;Tp~?WsY1aBI+R zMv*L)7r8o?z1B0#0NK{6e>y(fTTV!)%#6WAI=*nCxcq5su)zaKk*CbiRB({yqZ_~% zV+i~-=*u~VVy30j+^|;jToy(zxDX!O7Z7~mbg?5iMKz9TchF^}Z~M6Sn`l|8y+<5W z)&t`OT><f+%a0mm5Q&ew>&r?$qV+74rH~uD{K>Oj)944QH@Obs$6`j)We7*ZO6Af~ zhkeMDcorV<p|+YjhmbPPnVj|R5tV1NuQ<Y~*X>iuC0{%;CrQcyfm`3GivFUh*BgGJ zmwxl%;G^r60p=b~E@SjX_D+*+d;T=w^7-Ud=T+I&#j!-zfR!1g955ece7XetJ-1c> z{nT<fHcYZIM4lK&$*RUxA*TsW)T@t{feTIA-V<QsaJ@ge2{7F`EvAksgNLi8F7P?Z zMS8TT^6nL9*Qfu7wHJbU-GBkXMR`?y$Kp(|YfSnJlB8Y@UtC`@9(P~ghW4zDLBT@H zep=jrvPyZ@cGC-<ZL6Om<by!pSEIuZ9Y)--zqueC&Zg1Fe32f|6nUbQQ>i)y)?GVU zUe~2W>5{EA^J)E8IX>^)U*JU&Uo@SfMwnyFL509&K$vf-O>Y&3)t**&JDMgN?C6^L z8|z$N?7Vqsvsk<$Q~wsjSMqybvN9G1nW`qxf?Dzk&getuhGX-D3F~3Lf_w!pn7tvN zq7x<Tj~G?#j;Uh?MgNXzGVsKK@~ZD4_idZcP;~hKi&NXEAsy|yh^^o=h%L^HKRN6> zGxOtX0N<*Z`=o!B`6%n&$fH-<!?T${WTu|2Z}9SxZl>=l>En=yrWC(=npisAvx)I( zP)jT)KR1psFxsTJZ56N}*0b{QAZH=p|0w$Vm1iYra(kid$$j$SA>PKw%P=hyY;6|V z*VhlLVJB|i3^O6DdJWH){la>{B0zfY_u-3zXCSRIv_beDT{{R~v88=G-^_&L>dSdt z6C3C8yV9jB6)%y_WaE^(&M6&e=R@<{YdkBy43>^Ml89a8=8SW1n3m7wkHy&wa6j@T zd~7(gaTiA(Y#?nj?C|2$r(tgXZPP^^H^k932kUV|QP`P8E4Cch;)YoL3sGq$sW&}d z`VL|Yk2!Sv{<!EhB+<W7X7hh7Q6gGZ9xqP95D<1Eopm-Xa$z2EJCvTKQ%6KND85Mw zwo1+&ur#$^G9!Nd0zZv?b7Po@NQc}d0F^YNkXn-1ks_JYl8GO>9l5(`GK{NUuTG^) zZynZay_#?RqczaJ_2U?_4*mMO_%Fu5h(1Fa3%Itvt|@$!jvI>$#UTY%L*a%`%A5qQ z1r4q4XH}Cg%zuwWpY#b>3jkVRVeQeumPVrGZ5R6$p`Q8J-&_|GQz`M{@>+6LuIYT4 zuPV&JV4yn=A@Ck%3Roik-))v%x4>it_8<EHsE*>WhCguK*Y$K&Qb28oy4>iDYXe_{ z$$W1-YEz~0!t!v~C76CJt{a$JIhPB08A&d1OI1<*eHfIgSrrgDH0u?CA`wi|N>;Z= zlXZcDN1PW)WhO0^30%_^1pl<Hn91ZenF#;G!a3f-Nk-P9e%qh^%T6z<;ptf9=3dV& zi{BHZ<(#PyMv<|t&u^Tfx<7va@qX`d7iF<g9dOu3S5`#N&|LhP9|OGzES=~q9Dg&r z#^;#?31Xi#mMhI{QA59Qfmo0cJY{72Xw|zIxXPm{rp(FEMDdcnIM#c#X~JEIhK`M) z{xSMRGB1f71qjhnVfj5XEQ%VTY6R!}n4X=^_I#h&d1|;1jmLa?eFd@%lo);et3+&x zq$$Q)U)*pGvN@r!=-MCF-SzbE?f1aO4^Y1OugAVuirCe;altE=9CkZ-<&m3yEs+#X zhDz^wcw<<eP%?|?)?Wr1Y<%1@A8r_3<yH(KW#xnOG!N@>lJ0@_Z`HfUZneql;!=Mr zl}j3>mXEMj_<g+yyE?`>W_b3P|N887=^wP&3nNCVRfN6^+B5r>C0gRLy?3(do!ngQ zY9=`=%YRSMoV~T<>SC9RtpCu2G%Fp~Q+5FFEgf!09e{yW6E*0eQ=j{5_iv0%R5|Y{ ziI6qeG6B|~uU&Mlk`LDcL4hsa79ZS4pl*-wwabcVl3F4z!p1(m*^z?hmAO}@#MY?( zGOEb%J_5HzPE4lbx~0ND12QQZZhwKfS5OGiF#2B`ZbYXyt!s`(o_NAOlMv#DEGVO= zZn?|9!nNyH+^6b-M?J?uPu7l4kG+<*7CptD^OuBc)HKjWgJR?fZ{-NKr~FM%*vfbu zDTUMMv%?(hF3!J}$rlx6%M;GiA}a)=>u*w}0^{Ics6VC$h#RD9+($}&R<*a2^*0iN zEvzjV``@-61cnJMdbYm6LO8r4wa(hn9|gm#y;`#Pzq`NEq_gpBohiri*C`jKE!-6` zKf!VO!LF_~w2J;9@zSb@=WSRz=VD@eu~g^<aRE%*J>~a_VHS~feZE*gnxcX84%mIM zFZ5(gG?!1C9iT~-Kr30kSJ57pe=#NwI(?lqpK<?|M3oZ!np1YSUNu*hiOalJ=2(6d z#-J?>PxixOV@bk7VQnV846FWm*Da=#Vz6|3Mfn943!yZAoe)iG%pONe*kI770dHu> z(Ld|oTw9ol+5c+cH=FWlNM0iI(KF^^fA{+$%R84*()qKXtlE{7f!1iTR~B<wv&04u z%FR21T|n&61;3!VmAXoKZ(zAS2;v=KKb>`nQhI=Gng5cgMrme^9^bdeqx@EBcb%UY zi<1j6LUeUCv>H^i3901*Z$B0;WO`C6rZ}F(Q@(6Ppx&cSJ;AK2(>*I1&PIoSHDZD7 z#izl$#;IWKV}eyp`kSoQG&YK!43OtLrhGBdGemXG7bd}=#$3b$qe0@iToaPjNe{p6 z)1i@)CNJr*;pQz$rQVBtGhL!l0^Ayf?KdbRp)>611Y2WY+{lw&wC>1TOs+%-X<zqI zU*AAsoxC5NXIVGno^!l#Z3FZtvzw8;2Ls~wv)28$Vmv~Ik7)Gln@$f!r07Dw91d6M zCC=Na^)Q_~v{+GPjC}fG8pbACh({b6V}xx;VqCq7!MV9rUGq6?aLzOMYo1R*6v7(Y zBf~XQ_Wbzto1B;D2k2-{Kz|B1hi9pM6t4aE-`b!ItM4$L*bYgq@OvId+9V<#OpCNV z?*n?*z)e=l>k`wsmvr>Nnpbb@vdhC6gTthkl9#Q-bK|)lW!lUe-q(ha*-UZ+r5=<d z2)wLeMdAz@>TsLw{*0do+w2Kkc^(@Jw3~0<_O*sR+V5XAWv7*FU?AZ{P*kgw3dN4V zxNf)`g|8X%?$mbYB-4?CHjv_U^UTo{qk<?N0oQQXTVoFf{5QN}E7!rj{Y=UfbxQ0h z_3d<eda;Bct)TkOKmgSF@N}lTZ0BUedG;CMRFnw81|Q+%*wz#{L+0f6$Alx)Zpq>G zlGck!_J(LgQ6!#0z~+3?Sj{UKFMs@@np*6d|ET>-g~Q-E{>J#cylRW|FGsnX<XVQA zp~KUBU~0_Qu?|3QNpR5C``Op%jc$}0c{DS|uMD0}JySlIRwMK@TRMMW`r)sV>V9qR zo#PqqeFK-e895oHlo<5W%lA|}mL3fLxCIG!uak3Q=rRh@G@BtZF!<DVeJNA<r<=a; z!R}6PrOspIyxElT8t>(6s?^Lq$yi<dwXwmh$_hoN{C{pRmzdZVo`L0@Q<LW?));7t z!H{s$&Z8l0K<`ZQXRa3xz^0j!j9iDCni8Dm)yA?orEJfk%weq#RG^u+_V-C`6T^<g zgHOMd-C4k*|J|PF@*=%<&0oYLamK1vPdgjJOC;)<Aq|TIK{6Sww;=a_5urM-=R@@% z1pr8;J3_n|0|nxX2=*^~=JNye6?9Rhgh&%#QXof&fh9p-f(zXVcsFb3?_q)LU-R<Q zc9|%EZ>xWp5teBZ)!!>o01KAc|EXV8KMaU1zB#5-2T;&3F06|*7`MFEyofjr;O+8o zv$izdmLK{3UN+wy?CxIGGl`P1)LmpGneXcU$yYNS?31syoky?-luLf(N2Ub4Xm<`v z%D1{&k%Kz1c$feiDs+%^OFbU*p0o!Zz9jV6y_w9}FfEqKyBDu({{JKGEyJRGzb{@w zLPEMr3F$5ch9PAD>24K7y1RxH=|)1jyHirSq<a9RyBS~x@I2rDd2?Q$-)pXGxL}^= zzV}{x?ez&`t_7IF$kjS^_UTXo0Xn9j|MK)_xL5j{f&j@|k#+UGjpF_aA>|a118#j} z0TBNjo>@dC-OOQ0<P0^hdm1oGVJ7;x96qxmXEH$n@Ed0i!myuR#h+C~@f4<F?LXeM z-CA3GD#%}oP_O_}%U9ne@Rq)tE-pZHRXq(oDk6O4|MHd{cz*hrHWvrjXj>R{C?%}E zPg`DJK#PW$l1_gREcuvNEt{!!o>a%yoNPlP(@C^U6t^*(hQWc7-D(aM#IDoHrBs4| z!)|nVmdoc*O3XrPR2g&2WZ?LX=xww5GKe*DXT4V4niWWXo4YIDP7LX<IwGpBC74b& z29OT$A`C**gK6QLc;cT`E<U%dlciqeRPrm3C~NZVR)pDsi+>S%UjZ+D6A2j$>Hq;C zpKn7(HcLf56GgC6Aim?%xY&VS^41?MC0a2P7&;HV$5-J%-L*zKFFu!ygcg(_$1nhd zs^p;QX)^u5$T;zVS<v!LT`T5JdGiE%9m5>CDOUYN>=$oO;vU_cy#w(<ws4JVXb)iO zAW)<W7o2GRyFlvY;*GGF!G3JFJAH>e(kANijR(rQBx3cnLzXD~b$q>yl8`&Vj!UhM zF+Yau&2>E{Cu58bR#99RA8X_>gy(C2-1Nss@Df0}?yUcsCu!niy)-WrNee_{!QA~s z<mcR%HgbA_A=ov%`fWTn<zDW|PH}lWfhoMQuuR!(xLd909$~DXG;+Jy?+N&r#GUPA zxfK8e!j+nKSgRBE?k*s15<S(yi)o+A?!J|b71qDY1JdiR$_q(G=t5OzI5{zaWqW3Q zH2KFmA?hMxwh1vcdzYzHdka30?~+NYhrh62eM6z*+l1@=I0)-L)kK^q5Gi+39NS(2 z0N20gtxt?68F9J!1Y<GWb#)%|58LC6l5^+_5LCRJpWNEVnnOze9qpX#%PZ=7e70M% z2|UJX3c{Rxw!#y~+e5ZcW%joJKJaunuR?2jT&vd^(3X2WSv91j5mwA`2Z(PBvujbL zVp2nDZ^Derlca%X<^C6SR>sHuKVQkI<?j%L(3<uLqba>Aq39cRU@6B&LdQj>7%aV5 zJW*&ParvD5zfbS+O0(0-uNV`F=gf!%Y?(HYWlKwDG$L8{7SV?g=*{Ewcg0)~(FBd0 zz>uEl?XUb)ssfk&x9`YN^cfCOv2q*@;D?goRV${-jsj#KmF|I^<24<o8Et{&53z7% zV{5D{py(UaW}s%H!`o`3;6u~40o3Y)kKYdT5>!s^cOU$ri?=evdzz?9Zo#@j+)DI- zmzb-U^iqKtbGTh+a?UI&$ku4R6eJeVcOx4<A_4wl3NQI(p=-4z^M_B<K@qL*2&%WZ zW&!FDV}zaHh>K)*9&7c?%gyt%ySoUr&_=EIxIiVxWT{x?L8zYkbRRu%`)YjOn)s_) zkRImx=+F{WEeDc!BQkR=GxG?S6;!F!7$$RzXKCvln%e_`e2jzdAhDTG<+~2=k^-RR zfW`KqrlBgdn9rPji%A)~H?ar3EUfCOX|h!b;YfU1xHndryo_SxZzvC4KGJba&-~5Z z2Wu<e==>NJ=^RmpUGs{lr}yw>5UQBXYaNcl_5Stsv7E7us7#7oilJB_wrfdKMLF=8 zR%uq#l0C&@*uJwbF5QKadn`tek+q_4@mIZ-<06s!0}|KOatMp>_;UHZKB**QiqNYR z2>P*bF`K+k#$)2G$Ul_jGxxSHFeAE(qtq39L8DZ!8Au_f2t6a3#@8z4!NyX`gN9=9 zNw;f%-O=Om&uIROs$;F}x1Dcd${Bo)CB|zPcHl6fb*l;9B!;50!?f0-3VHt*FLI!q zJO_tWV>2dgOD3SMSBc@SH(h5F$H$u;yp)LzSY{xK^<Ra}<;KzcMRMHNUPZdJ3|`Kl z?l4bKgL&VlZV7sD4t-ZD+<tJ;Yo$4BW?jm@8elhR1roE_pn1(SR$=&ytqfV7Mw8#J z7=7_R@!7A>{NnM45Bc3;I~PC#m<3OoA6&LZ4y%k@OvY8jgx&A0>}_^?>`1=xi#Rwv z{iVpE?Y@5Bw`<GvRUxMQocCoO%uO^VdF3aDhI;TVbxwRh_E=3mUL2*5-tUwmNwcvk z2c(<P?^Vy{6mHUl`iH{+x0eBvl4-tL)lMcg3&^V~PcmW9JBk2%<K3lU1ufMSbp?9M zU>j-exl?VWxrTOCjDdBQy*TLmaTU{fSf+x_nHYDk0{QBOxE?`!bCwRDPo7!i<V<ub zkEed3dV;V-k4P%3=1L!|^JK9B2|sIgFzSTGSusP2Uy=;qg`(i{p{qz^jqimUQ{L8W z&}+hxnYRIEZmAkA>tpC`11#STGC*%Y^_T_fI5_SVw^C4EvxLkcmY?jGG!7g%vE7q% z$FQ%;{00v^_!IHHlai7(+_BcByYqAwLT*jii!X;XhGti6ioa&ps^Lp|#2S7!eO*{Q zeAG(*Ic-uV;dP>9UR<LmgXbF3t1bHp^h?Qx6XDbtrE?!&NV{VlZLwIm%CI52N|(K` zekk(&$U4Qx3h9pZ%s+XkY=7}X_jfa>d@?J49Xw>~Rq2;lZ_i`bMM#UrpctME`?rJZ z=3F&Vgzx8&M>=~CWc0#Ss!b|&CS7!pfEM}V`@{|xM$O>C{@V5@g}U9<TbpEk3YaJI z+voEkN^_f-x4D?P2u`i3geh4VZ#Tw<kkKQ3-xv|@N7I{B^QV;*9EB`S*<YPh+%*p! zo*?bb0;h$)we7)$1oDc}Imd*n%0iALYC7=TxQfBwdWX+%VH?c*weiiopY0GFZ<PQM zJz|fupe(T{4Pjvj>@dX8;>OPegG)MMcXI5H+dQpS|1`95JC%#Cux-wDxz8NWzqu?a z`@2Bv3KhQsznNfJgP%D~O9~0{Pk~5Dq`>Q4>#@8h4{wi5G8z-N^WwylicRcIFeQ_y z@EX3#y!P7iNq>OR5RTnQ_SoO9`2O^qo0Cr&7n<vepL08`)0iFc_puJO`#m-}znlrn z`huq4&lLL7&y#H;0CtVH>fsBabExL-Z;?_LoXc>&_eQwc@-O*0yykr&mP!pd4(eb_ zXv`pGZ2VnL?=2KJx0Cr`f6QAttSsK}79$dS39T2RVS9l8VF<Cha^c^Kg0-lqmIclh z14XAv5#?jhiWB}r5qJ*HQPpLb%{c<5LW{t_5N2R&`HD${IRAyvyWF}53Q_9e2eVZH zm<hyP;q~`>ju51^X%2qXa*4fRmsG&SORdWzhA?5Qmkn_zB$LRGpApdT4tyQ;K3Sy+ zDsuizD{!D4xTKqV;g3?`;(M)GDx^=HvZhW!i+bNAAm<Sn{|f62tFgU=(gp-%uk30| zm#$?Z>Lj32TnZ?@^V{E1t?9;cuQgEsE&1d?POiZkj4!rxHmFDpp0g{yb#R2<=>3Uv zZ-G1r-i)%Orp@Qvy!i2mDJJ)UWU+IHtYl`!CoHgj$(W!NyclQSs5`t;wS}!l_onnP z=D8td+7TdeikjXAm$BQTed~z`U)UXmAlA1MP7{rK`4}NVl3o66il->_f~inVS2eY< z8s9Bt1&%k+ZXBjpfiY4au%<Zar1;Pe73zOxnsjy1^}AJL@IM7K-#QC42B6!#85!CG zDznA3-&5oE$J(uS9-kV>UdUXGU;E7Dd&X1BIZY4>49;NgE<AIF+S=Q3bJ`UzJ(73r z%H#dD9%9DPn_S2uksgb34p^+0F(ecZ{A|eW@~)C$H^AgmDoZk(t$~qx`@4rgg&8>~ z$!-nv;~##-_uxj!x9(Y<AJ1_1*~?3cEAm#B!n1TMoOhKa_*qya&hU+sD>TskVfi8# z&6y;}qNiA`v^B#&dCtfRpSah?KQl3-9OAz#ka0HLqe6;}LVxb99i@ZTciE2NCS|_| z$bJbW@zIs-KBrEeHf+5rSzZ*DYOmBkv{%p~G5^r4ajmnp|K_&H%+W&<+APlG?IF2W zYV!kr@q2Ms0B@2h)F%8fjc{0YzobM*+DS4;&2P55EqnL!)HY%~Q7GD-cA+HB4B}TX zs!JO&nn}TKUoKx2Q$Jvg){m-bg)%aUy2$%6ywM+9?6&*=teF1;S9AV<5-G<DW_MI{ zn`<VzGE|<r<J>+B21(b9RhUZy&B|oX<#6vvtE8)+f02@BDXDn7UjyTs|C<L>nc|(m zzp@-x0A_~H*W2p0<7kYJlsTf;k0JES&uxCg%`9`#4J_Bk<y+fZRZkzx)zQcd=a1@= zE8gyBzN<Y)!g!6YH7D9yX_-3LOCm+I7q)T!pjQ5JMUv?;qHGC*95c2PL22ik=johU zhl*E{KtU-jYw()k<s47>RD%sh;LGqoM9>=aqhoPV%ceg~NHJ0gOkfj?bVf>|_2$em zX>+S0n%WYz&|ViWj|Uc4LP0+!hl&hQzg8x@>SbY3rT<dn+xOm<zYA?;RjDhtJr1?X z{Jyhu-*AY;3nA@1gjP}{s?q0!FEk%c`(LaI4)<{VyE=Z9GK!x;!&~u?y&Rj86y}Bu zwqXP7eszY?^nI|%n64+TA<j*Ie#@b{{iA<(<vOIMdVf8)y^}fH0psL^)M?@io{QJc zg;vJi-qn7rHlxy=8xvWHNs1<yRLgZs8|AWL&MOrie#=PSSL)mGfmiO`MJnO+{kr0v z!#X?(Rx_B^6>6)4ot$4kVw*633XQgiZ54t{H~MH}YD$Jg1@k_&cGp3fx?dIK4SY*T zo6FN>`1Z>XI1r1w0YA`OQ+Y*Z@JfNJnK}pfyHeq~qn`0RwyX4$L*9^7U2~Pig>3+s z;qMdurN(JoD;n94cPwV?MoCT`MzL5x0-eo@Gz*nX(!JYIP;(FVYiZ*@3=NfuavL(Z z!QA(@(D2!y6KVPM;ukaGm|y6d0nqy$f*}_^Fc4*pQxOF0v>>Y#KYMfdA$qL!AJN%R z!i=22zhL@iu;mKt!`|)X@R{Ak?nA_*c8;Bb)yEILUVa_zwc<W>8XbAvlI>r!i<t2l zf03d?DXH6@;TkCHt=e;Y4d_VxQZ!)-YofDvq52Lna_zk}y39wVlFnw}aEoA$8+>Fp z&Gw17xjfVwoH2RMLCsFS?ay0il<*`uuF6qNTTD*91aaiT)|kjp4zj#)adbWv%jU;F z(98P`=HHnrUte<+L53N6HTGUTbT93<?xw>};km-)0UMLF7tb(0n=@f2O6B(-X6zTk zS{Tf%vj>X99j`5KxwuUdhg5b{UP;+C%iw*`0meO3i!BQ<BzKat^?f99U`sG6>F-?B z#{!U|ugIK;gUsU=<5x>&>L+4I?={#_rzGJ-`;=k#WPye}$T9LzC8hn#Wye>m*JXJ< zI~;&t2ThStU{Wh7hFW@rKkD*LrOl{qrIG?8HjKY1Y;m6ec~;Vv_7f8S^J65Q6A(Fd zQZz&LF!saUODRtZ@T}=S6#<nmMHMB<(cu*eC#DgMzbphvEGh5rcXKjR~J;wX)lM z<EnLU=+I!<s7z1N+nGiOCme^|ku#kVq}nKgi=CVGlEs4Pxi&LVg$=P>3uw39v#W8Y zD0?M|<tjix+B)0Tk3=}S#$Tzr7Q}{;@3l{<?ZUQ2?lVBdaH~~gRCKC1{G-o~Vs>y1 zR`ZZdIbi7<%)@4?@pXxem;lnNmU3=01N`@8^nMyJ<sa~bVKcj*x#kzRyFGfgyzVEs ziD>HVN_&<~$C9|?mH~j)G}N`qx$=MAl8aD)WqTixOKwbCr2F~1@BEob;DDlZxIdY4 zy`lIhf^c@wew55Uld-X~o{94Ps!bilU=Qp_kG5Mqttcy$OCt;g?O($VnGmZ$Y-c(8 zj_LQ+iE34$qxBiB{tFdiv4hv+Qer`PZ`yWm;p54g(zzh)Dt!{en=|Ka+-hPj?C7tI zi}<DIkO!o^TeSfd>70*S_1;S@VBr#R$e{rek-${PhXpz58y@?&qTjKXQr|D;i&(}i z2Liw1VvI<NG+0U<qxU-MDFNDZTh5@`!Qu^J2B>*<H>IJt`By^+MweJK52kx@1CO<+ z3(kAfDFu8VaOpcwg!Poav>FgPxsygHOgWdmm4sQ~N6zl`<w@XHu?^Mu{%I0HOYXo+ z)NjdJAA8KiXAUfRfJz<;6-g&EOqSr+zpK<ib?y5<{-DO#v&%56XxAjmn2>jG&_?@# z%*eepysyQHpSn6p96rvxxk0N^t9|nK1(rP3>uM9KN6FN&{PoDEs(M9RS{VL<$Oa2D zf+FcVPYu2)K0DOaD1SabK3Y-KVL`KmfH0#&^I96Oewi-#%$XNvT)z)>#(N>TUN`K; z5LEUYd~QIP0|z{r6ygDwUqq^$Y@_3h>}+;LyUUz*LldDarV-r>qHvYET!TVd->)@U zeoBY`P@)&QdA}Pb7ABQM<RUS|AjkbmKD0#w?FS)l4q-$UxH9#Y95&>P-KRgf{sz|m z4SGO6E4PT}dS#NNshuiK^VX7?^s(o~S8xcRtfpit1rIjv|61bpKxGOiICrbR9mV-} z%IJ$oZ#64>6d`|D@^yaB9AcVj6`Q{TY;M1bc97Fl+`%7MS|aG_CU5B$o39mfwht}S zHTKW)T|E`#8e#0^?+C40%Xq4YMFr12g6=KtAz`W+v%>q?|4?i`F}oY1cma7~AMrna zfME6*^K~yq=_3VX6mlFd_k~&Vj710l_@SnT4!Qo)A%M#Ss|`P8uL&GPPuYMc^NqgA z*pdR@+<F+=e86d*Cxt{+&qu)VX!HPTOrRn&iNISffnbb;T2lgVn0ZZ)wB__C@B%gD zAIc}muIs5VERCAs>(ACdl^UyL-OsGvr!FhJ9!<S54mF$T2Ii?M%wnVa_CnYDI_6!Y zxdcSv1d^R6fLG<X1B2mQ2MvfC58PZ|LFoQN31HZpO5E^aUQnp=S2KmDRjsConZEhP z-;e$lTy?6jb!Z1@D0?`h2~s}bmbC&7MYL|fp|+GZQnqBv0$pg80Ctd<7O*y-{3Sdw z(kamZk*p}sAlU|y$86Gu2_s`ZSLl2&pxRG8332|<5qnIjznTdnC;?y4Z#16}n3+aR z05o;qRgeS7a_)CP;DKY!6o8qHYuTnwHlHjoGFp@Z{$vy16%jAy!sJG|_DgSoh77oH zolnXI{z{|gJ<}}uW%Yp*I5NDLlXayTAwv5<`6Os~#VWnG{XwO)7+%-)lrar#(uN4j zoZS-BTk_z1r<n}-%^x&ntu6#ewYek*5F&-@;3$eEnSto*e>ha%W>13HY7%xr$cnTg z`H(teuPlLL%hs|q6o09FZy^iA^CZh~DlJSNANWGdYkcdvevE1A_P+mp(O7pk_mnv@ za8{U^Lcg{~ZQ#-RgB(=c;p93rTiRmVk_Z~qA*$n85)wY3=-Tv&>DEmcJ&&)%3yMTB zz^Ph|8-aV2mdnl#E~?X<W8V-AbeDFwMp_L;7H6%g>WW6jlNmFpHiev>){m$={`iqJ z8(i}v)#Ha&qM-~(tFjmS3pf=MjzEryM=^!5By=(y${ML}FL4s*5*&KL$LeIF9^RuO zB>=~fX>`J_)o%McMyml*lJ_lX_mR$*HPX5YsFm9GOWALYJZHw<Tj0^A*OjN#zhy*Z zv75UBjS%h={S$QKyht^4;0F5JnBmK&)zj~gE-`qNlSQ`$cF`J*ruHw3@<KNwr5noo zGJ^<=2vx<~Z?KyN{LHjlzMLhAu(^9rlwN<n=YMy_&Te7g9G?5hWl-0m3;I!WSz&XY zFbmszdl|)&$*^}~-Df)k?Hf!NnMCy&)z8b@ON0}b8-v9{)B0LIU3~GF(~-AuRr}k& zotmx@{B40S5T%*4;36s4^ZV$F!=+BTTI=){%=^(QtTiEJ7D-kzMT+&$uI_fyvnp|U zdWq`TT=^S0A5B^u+F?9-!Rw-l-eyXHxXxQj=qZ?uv~Pa6Ck5;vLmGWGhG|EBua3Q{ zt5+ioQ9TiwiBv_kEXf%Soh?8bXeKu{3FD1EBOH146LmDt!YGl;c!}T$3VrvKa&XcK zXtgDXp%Q1ZbgpKCN)HECTXUCmn`O(2;n(yjj6QLf;g*pMt=7~pL5w2mZ_NDZ;YT(( z-``61S#|KIHY>69zMXJIiro|4cEe8g)(D*23erVsbmY3$9IDB*?i`o}P)47V(Yw<s z-W)X0sV0KnNiz~c$-aKAnqrOU5B^aYY{X_6pK1DV4k{Aux_b`<k>KZAv$N->TJ1n~ z*=gznn*H&}qRf`^e?2K*CupuURm4M#r<^YLIX^aVX$^ViVosq9`gq6kQ`SCpn*ppw zN5>EDZr8H}%^7*ooUB>B->0NLjVqL5iomUL9h!5Sn#xfGO(X)o&<6x`LVo&*SG8T( zSCK9EFjg2YhB+f`7sX$A(LQ=10DsS^u)Gxx^;^0v6zpx+LlpE9>F!qwvuhSru;AU( z*N7hWo`Vv=%L-|spulq=(sdrtRwo#CI_j0$Mj@CD#A^PieU+*l?_daotAH2wu6%vZ zqol-`a;vSxbkEa%C2}qX+(%#`f!MTl$0u+ma*ZJG0Jxhw&mt1c5edHj&lygRAqd|a zw;vhw)!)XgNC6Wz(<oHku}lQD8ocCsv$c^v51H#>;QEIWQp7)`bf~rhM5tDuyqZ$q zxWpFST|gk0&!~nUJ3%GVe`Nu2-#3LR$=CVjA#SKM#r%T($WUs69jLnMbUsOwGt6wL z9h?o5(grQ`a`m7CGcl4mg7eLdv+gCNYr~XV&9B#K#lXjFZkGhkdi`!)(Nbd)A5gO( z`l+q#z0Rhu$&Rs{#&J;^XFu|KfYfEIqZTjHXjq*<(CpPwUqNTLb*5{)G~Sm1CKNQN zpi&lCvcn7Ts;jx*T#DZ3KiKYjT^Tt#D)KtQ{mEF2N%iep4e1KvhC4_Yf1LVQ|Md`R zLY1rSp%q9fgb{e%PMSVk!h0M%CW_ZeEHV5d1N{cA4X6A=Z?dz;9(x{pZc*%fcQ4u% zzKp2}sj(&K3l5YLms0a5g2}A!iRJhvxr+R)@_3<!Xbu=P*PU^lA0*}c;*J3@fPa|D z(ppm1Tf7vdJ1hR=%Mo%K$7wkO>wUh~%XJPb#!!w_+-ez~{v0pI?z4uo@2;j4A7bv@ zi7{=Zp1+wiDTJ@Cs_MYwKB^?8h+M~_j8`J``s2^%AnI;GVYDcJGj3Q&OY2zLI!Cx4 z!K};P@u_0fDyNFuNdbzi5B}jjxpXfSS)VOd2VD4W7#{uajJSpWsg2)>2(D#)fUZk3 zoPAlXh1!MhEgY=?A!bh9BeD)ZCdc*b5lo*~P5j#qN8D%?4QSn*1(c)-e+OkMzfhaK zI<<v!%!vO(aeeuY(#U)1F-Y%G$~~xiowxgG7uoJWE;oJVKrUff&H(}7QtP)Ig}Z6| zbs}QYPlZI2NRl(jNJ0(;GeJG3_dTLO4I|uE8d^J~g9qIq!UY9A7{$xq(a)+pZ?oFe z*vN46Y*^1B%1!Bp%;W$SO;G4?9Z_Mlc2ouXR>ITJ1L(Ul?km<|^Q)@^(<95gsM3>{ zzzu51NP>|R!}FYB6SZ#Z(!WlJbiOZlYxrVb1sEwdY;IXf;s*cPy6|9FL=afhlYQYP zoIE}-rl=~+bLc#k=l-li@-p3^Kr1)IgMvXFT-wrp$mgL1R)`gx@s0Ab#OedB`L66n z`q-5Ak*lf;<~D`XmZCpGEj<=4#y?pUhp0+ePnn*s8+{SZD;xN4hh@twI<-`G>qi`6 zoDv%kT0Dfs4kLVTc{}nK?{k8K1pu#pKDbV^08ANmf$ETOPJ3p8dU&J?JK7q>M{##6 z*1v_MLx2OZS7a|58}s8odBHI7lFxu}r7yL1JLF?8{AE~&oR;xy;StUBI{?HZ!Qe!G ztCoil1*<~QuTHo&7{-dwL2fExk9D7IEhBM)UnFf2#o8Y(RDx^BwN8@=a2Bi$S<H7# zlPI&OBFJh?@n{YLKxwh*mEYML8L_lFg>5xK;kcFi^c7F3pX{I|tp(ok9Sg2v91ZL? z=#lJ9@U3v!Y=f4>UE%~SZ8zp0s8x}rS0j?9>H9@~QLv?$ti*7q-Fl(Bb}rLl*xNE2 z8w+g=C&6AFlt6wBuG)EROit&|0I=^~w1d4IZi?k-UUZ?L)ovlEOdImh9EUGe1B9GW z%cVBg@{>1hb#E;xuV!sHuD4bDvc}(C4C{t!*4=%+`0=A!aznMAVBORJ)<$BnW1qjf zB>!T|(%~EPBA#X?8=FmG&<{dCL~+e_Nb9Gwr_1}WODuM3?H%|al&aQ>rM&S*n-tl* z>czUE_{y8n!0CF5^Rg2CfC<1?t2}c!qKVVY#UnM56=h~`#n66%-z~YfSxMkIWEfy- zVs1E=OWZ+V#DcW1?k`1PFhRhY+&A5K7iX3tny5uSI@VCFL@%H#vyZ*d)SeeJP){}m zy?e*Zu5oV!FgMr##z<x>-r>YcZd|!}lJ$wR=>bE}ReVM!Yuv+H-hob)$!CxL`TXF> z$w-DAwe+lTTA;r@u$CRh|C)3l+A?=__dqb&nZhaMb)DfGuKhB*^F)XKty1U(#{so_ z)rf6?8e0GAnkwp$oXffu>+4Qm*j*`+=8AXsY^JXUab2`|A+hn#CRw1G1R6b$mw61* z*R8b-oBmy7IAX2Bmj@C_!|d6eXQHcxhLL60*>+%H3g-<U3JlazyFAR62)&^5U4Q<; zob-#V;n3lm&Z;5A`aH@n3K;<(eP*9qQAbnw<ex_kNLM;<`LC^5`I8$2@_i+`KsHFS zLH(;%UyZNocHKqdCBHjo;XAaP<0yjRroh`>2+&u9=vPch06x<Rh}lp(nED^exrU3^ z9s+s0+*e?co)d!Oe&(QbLR<O^{;AOD5HnLhZm3<!i9kCohSuk{W3Z5u&V~|w1Z;LN zFw|Hfz$a8gpC~8P%39-VlSS^byPoyl&Z{2SZ2#|4AHj&A<Bk3Nj(Op~yrYr$s9`eA zzZjh#45Xg|H?B_O&P5gYR1v_!Rpa(`1Pb$7wlbl8fMhBH*p^CtDX3Sl^twz%0pz>- zl~}j9vN%@vokB`_d`lsXKNbqf>90_2x53K<n1Vc(`j*CGXL<SO`1aqQE+iZ(%dzIG zx$icI)}rgpPj2>w@gggV+e(0eKJKBQ^h>PMmjXbcuO4|^!?Jolp!XO7Z_8RA{ji?1 zw_gVE$&T*YhZrep$(z4p{zExYE(`z<t=!h`l98I2i8L_Lm#t1~zugv!|13(!nNNdw zutNbW`!X*<8)1^YPpC5`FYBhygQXrjnVYZaak6F^DC{To`0HAMDt+h^hxs1jAl~LF zd`Uw{x*$nbv5)dzldBJLbu9vSDA8g=*}iGbl>4T_K2#Lf{Ffl6KXTjxE9x8-yfuPA z`gGCj(<0nDRKtUg)^}v0XPQP+n~OzTZ=f<#+eSG)7vJxU;BIpbWYlvpAYa(f5Ov{; zSo2anu0rG23BzLDSdZZ?uVaej*t}k(8`T*K3uOcU+5rr?#@-L5F1oN*(TA0dxS^I; z)^7VgTKSo)Zs}u#3cz!+E736c_mh*lOF2RL5CZiYUPj3#yx*+SWTB;w>wf0gn0^4| zl4`onQ6|KiiUQtOD*f3&5Q*^#8#ab302`3cj8rxioVc4a{)aLmy0<2ow`c!RE$jgS zS#=%K>;NvwY~9EYFA|HwdRpcfjwZKOBXJi(iVF;Me)_coOJG)HX~A!{9-!!}*))E@ z&VIN$W_9NYyOw;hA}T{A3O3+~qWAjad_u#fNvw)BTd?ZeqqR9{dQ0UDftA<X9Z`3$ zI@zCgJyYp%D>zz_#0M7K@Rnv73fWpPM|$=ixQDu}&Ic2e+}*o*6PE!ENE)JA^#P67 z@ir5x=3&ZIG2vpQh#$1rIsBc0Ha?}LH0g#MJ>*fcYp#?PJx)czyQt4*(iHw}c=H>v zp#X&0_yrhz<ZOP{PiQ@rJ^J90<?p+qRG*UTT!u{|Po0T-(PJCSCDq*wY^oH0?ZiIe z6+WDK0Q&5?ctv^M3~o7bqL)~QE!R*JV`%fOhf|2g-<jRC(J;zKK-AM_IZQ0k;t9t} z82RLY%`oN5^T%j!mPa}mq6U~kCc>4^um-i|=1FkNFhUdeHF<lK5sEE$$VPT|S56}Z zDqHgl)FiWp(wejmlWc1qZu7U2vLLyt{NSz(L#42JydGPIY;5rC18CWFXzkj-$7<%S zrn2pYz*~26j&^rK!UhTy{Y)h0*{aZj%c0jd5aqG>im}y2`obsYGJR-LKGr2?LHQRz z?<W4`U%;p{uc9L_j4wW>K`dvIN}cK=8~UtN%$=SGRMDb(IB;So*Txkl$0&_gKdsf6 zU0(Lds8q-3@B`pz%y9-ZnQ>o-UPqP**eFg&v~E|(L#G;T!ie~gn1ikdxktU2AbTl( zA^O9+wEF;mP1P&dHRZ&ZB!z|MfS!?V!;G$ki!A(HLy!4d*nrUZFmSpi&4<gO3Ysf< z!}AX%OvL>6-9dH4aY`&(?C%q<>ctZGn2e%*Pu=y@nqDSrj90NGc=!=@=8lsc?|0Wf zW(L3{ON0I_<o{s9WWTzU46i<aoCE>=m(jWo>;X4M(^To%w-@HX^RAFxBT2=S=lDdj z0ZZ|nY1jcKq4PhLTJ2}+Z@<27_<hNu!(vPmWjB6ATR%R?PD6lKWm)G5>`uBb|3i5` zyn5$XGu}10*2Z=~t&%W}jA9Cn^wx&Bh=&M%>s6$q)qT9jV6Lf%lX+o(?0szCVB-3F z%>IHSqHq#<)i<qiuXj(MS94~tQ6r-EvxMg)%9pX}_YE3aXTHj-*i`gQQwD1~J81e> zgM!>bh()@s3y_20&B(H4Xz|XPUKT3FLf)6H(s@$UC5mBU5f@$TfhWtr3@v}N)c$QQ zE|>gab=~V!RqlLcfVhghFyv{-?2QQdc+!}=JG-|*s~)W_(np<<gM%ZGjFgSx54YoO z&4=XV=PKJDzFHS3e*VGt63q@*TsZX@I<%6he`^2NEZ_ZtGD|K4$NYD?WGCK-)<i|d zIX6yfockPUrR)w1b+=1>6t|#>n3GR-V1~sNx-y9d?a_0dZ~D)Leayus3*Q+IPmA6l zj8?l9{l+&=d!-}|7vVSw4+r;d{E^C~!E)b;pQ~cVd)=cSWVZ{j&2Vkkq0dD3Dn9V5 z>)7A##o6)F&bvU+_P?^$UaW0NJ*TZlFOmn$hK>W~0NUAxhh525_I~Q^{=D8V6GjSa zRs938h^vQA*G0R(j=juic38{=RW%>{N0r|ELG9W^GvH}S#(YU*qPxiW1Ydj+bA3se z_yY)EajGGiv9?H*(y?c)LC8MQw|Z}(Dz6Llsq4|_f`?RdK|}l^)f}4?%=;$zB@Z<7 zRnVm<<QMIcl`pXc2-$}<bq`<*n)1anVQ8;vn>SBhib<b~P|)yGSAKJMhroDwgI1%A zq7GS8c^enyBT*zGj`kml^oeC>_rY?_tBY81X{O`hXIsZe9|L<TEz<ErqM%KAg%v^7 zpTz8lfSOTb8-_npueYILkInCFj4Lf;Euw{^2g!+eYD!9jChODW^gN@;nirWBk90pj zjG>n7C@t=&ri|C#eSRwEHV>T78P=+n+BjE0Y_9KlNf6W7Vb^EY-O!XuzzBNb9xu-a z<B#Gvu8m&{;LEG{oWV#qUQrinOwQ`DK7PE1C+v9@>hD}#Pby901?KbtMcS5%mcJA~ z)4bztPF3fO^0&e4w;-{2j1OZyfWLFwMnz^pLO;_uWElQO(}jIz3b#1{(ALYkclRy{ zK=X8*P3=dviz@P|C)CFo+X95+b-Eh%4<8`qFGCBw-y3))5Al)L=OU1Uj!^I7J?jQj zgQoNu#Xkb+3JmzMiA~jlkv?Cl)1aOTO}<cpt1jf^?ZU;K_?K`}E8n<?BIDX}_=8li z<fm+Z-}OpYB{L2DmmV$_TmvB}it74_<D>eQ0WG&-r?ZM1%wb2nUO6q*%MiC#&R`6b zYX%2!?_y^!z){M)IPFxgvjeW*4UoA?8^Y*uYomS9^fqpz{nqsM|K*JT-vZ44A6OZX zDYW=McCzRsKtZ@Vjbj7lUx*cY7zRqwUhr_Z-Js~1=jnkR1x-uyF7@+u@dLkXxe<XQ z+6v0=fj}<$`aAy>JHxFkUQb=p^W$w4Z#sv%jO;g<`PqIxJ0-Uzr|&kB?gdk}eC}pO z1fk?<^N=BQt^WcS;ok>`sYRp~FZ6j|rtU5}!m%#SMAL{1Ux1tloKTV1fIru@xFtH% zhMB1FDM!6&l45K9<VKn;IhWa+9`F{(+6`DLc?0+mx>9fFwg~jM7vec{LI+QI_P_(y z->YArLS^%`UYIr`Ue!!eNagwv1LSn#M0T+jhd#&Xw=X_{PHsN}wjKD>!l%LHdpCmi zhNQug@Kw{!ll5$Su6kRoCzOqxiF?U09Lh1PN%^Efd2JTp#=eZfw<2m_LmeYT*}ecG zQzpC_7et$DmQCX!mfvhNw1AS~iAF<d<zPhT6Rlx?iU=d1bgS8%sLWZ>4oS(&+uplP z&MUAy>(L8e1{}{C4vK`O<<3NsUvYAzayGqh2x=NQeW&!j?j*Aq+v<3fWmEvNO-tHJ zMzNLvTQc8VH1_77N9fRUzwNV7C6+fx$@TdDFM$H2&lZ1o`mP&FF(oXekh4@8W#jh% zdlY>uNV~TM8E#dEx;TFPYC!LAq^=zSnOVd@Ujg<ITVFoef!1{uUZvElDHEZ3Ep(Fv zA=Qk{u5;iFXXY@xMzmZZUFqV7rqe}Tu=Ehzzu0=ko2OD=Q&?S^*44D?qfz&|jx>V0 zObsN#X=Wzo9&RA>hFax@<gR6ABi29dMQc$G#=LK^4@YU4i#N4h=>TN0-C`wZGcQ~C z8m2inR_TgJHVZkU>zv>CwA^glnlQA8Yc0X2f=jGc>@(8+v*tCJ@9M>@JI~UW<$=eN zc@}X4<p<{IAk|WW`}v3DrK~TB2i2{xq=mxt5QFxHh?oq14WoMgbYpqlt-di;6BBm9 zH^;WW=DCs9r9dW}SxfY%pZJ(-a=`Z5>WHlVq~Y|B9~;hc`89Wb-#_kTe4uJ&g)qVJ zk1g+DE#nk4yC>ds^CO>+c?e^a7*FgX18rgWRtjlHpn#``7+${DW9=uIT5=UwVQ67o zqvh+b%5(pgs3Qe@7|qUq@JtSVDiuctICT7I<-g=?6{qpM`z#;j;D00OLk+{wWLQ*F zZI!6k(BjCUem2$Ni-Cb2K^P;S`jX{E+;ifuqVlnj>0unO-V|~lziA@nq23?#@H#pF zP?D|MW?$-+UkDz_&LX2@)%x(AT_WFGhT@}GGO_TQ5+hGeVjd~<D4JmE&08Z90H<OG zYcKyogfc7{lI=LTc;skFvI3KHSIy<dd?zo_&J`|`C1Z>*f7Ch3I=hBXC*>tiZRxmM zW#l`*M1g*=VNf<+T`vhH!6U3J#Fx7N@;kghTsh@Y(<l&dB;Q<D;br_XmW-Fy_~2&) zbH3SIdG6LfSVG79)h#|{9yq=I)vL=ei)JmmXBPkXdm((~k_u}IGu+lzHB?ip0hOhQ zIlitbgbz{m6dHhagYY$=2{r_wLk9dNSEqb7-?9~w07m&8_JRbtQY}cWlN}{L0vD=g znR-~nUg#E-`dQO@v9tZPFtdtODK<#Ax#0d!fPeR2))KwYk#`F~sEp7YF0+~~%{bv< z?Pg!j5!54=)Ppe#1oQ)BCX`><w%9#bXA9B5q5ugLzLh&;)pj%o?gvKGgMHd4U-+xT zo~`bCBVqxE@cMXz4dp?9*)toE?}|@-$cTrwSip~1hc^!3qNnWHF`MZsY1X1C)mJ3} zKySOF`^J_+SwC|L9SYdQO5te>X<O4xohKR%8SZCxfH#3dB-K>b#whN1DQ=?b20j)( zt3bNn>p+w&Qr&sVEiZKOx|0HLBj+v{ya12ubt{$9CwZ!-6DO@3?g1Rq7gF3+2GTD5 z)~|cYwjTG9=U72ovol(H)?$G4MLZMU&i2aDDG(RhX}YyhqFI-i4iYCD%)mEmsqy_d zqLmgX68j{AYJrDw!=OHU(^9`t%%$k@^4k5g2lMYr^!GseF5?94x8Hd(ira$K6<^6k zP@*t8yN6aac|%dOOhXeIU2|hP*@2_dTHfHcF4a=^6MnT<H#2agWcpUjq50sN8fJ== z&(%x<BRz-OIj{1J^rn3q`Hn!JDDRK;M%PPu=1+Yqb1oN(_-sX9k6EvSd)6x2Tk#0) zfkPV0xnA3zKDW~7>1h_GJrnX6K6DC;Ec|rK%rS>Pc>`CgJ2WeekBdo%vgFnnwT1Lr z$2UPfPp&*a;Ef@m*K+O86>K)A(MG2+rE<DUYa%Eb7OIEVnOWs(^#kc9uuNCidkQ!! zsgMa%{?#io(gTfCH_Ck$Y8d$(7CE`8H+XJi?HB6R0qJ68ObG;i0Z@7R`}@e2Ni<%D zl)eBLRXugL=rVuQ6ufs!Rq4;3yu3N{+Agp_m#Jeoast)+8Kz7LIX$%3{vZ@b+;*rz zc6f7JL|TyOle9>9Vw<$q{ES!csUP5uppTV*98kdPmbG1d04m*$3Q^=SR5Df8AIC$T z5C{eedODEFcJSW)0|32SC?r>fnGZ=><J2JVD<T~fpPMspww(%MVu!Hj>Ul9SPlX#- zCx*51Go@k<hqz`KjZKE#Je?<>mG#(lUdWPsz$J12uXA~jePGIMJbyVi>H@rbILd^f zKLd*Xq4-Y%Omk8Wsth2HDT2wvMFD-Wvw2R7E3zK-rW-w8vU?GMRFRO1BdjZ><BnP3 zS6Zy`4gJ=B4UBP)Y@ZyTTH0lrEa@b-_wh@CpKP874<5eRTp+(~V>*3%H1=u+2v+F; zTiKoDp#`cf>~1VR_@4?lQ>D68-Mf`B>%9~EoeR9FGK8drp@^<jurf59X(M{NU)Ex1 zTY>s|p}7VSB&yp}@RwHjV97X+0p2K2pk<b?o}`v$%~_O&zZFp~0H;X0ot!&q%eR<M zds@Xwk9OGP^b0E1j0&3yxn+it7k~GrO6=RLMg$;1ehnGWoQF=|75xvK^u*7YAY##j zUVa}H=A2OR9D7-`10ZnCTM=9;<<)^$=?paR*z}5nxR=9G$%U5{wk`Z{PB&y+vn$If zQJVTtWGDD?^heHx3kGo6uG;euH<wo(SXtY+2vgJ#NK=-g=)=Y|2vTE7o-j2ba8lUY zmQ*`p1D1709aFJ%tUCkf*i@h8&dFk$W$da|ZMH$$6iT@8{XMiJJG~wmLef(9xn}Zn zoxTs{mbGC8s~){rI<bK8_wq7lqR&{@v1)ZBc6L>Zapgtj<yHNE55_Y%UX9FC9e#?2 zmBdCcSxF>i6VI${<v4={kpEB`FOQa+M<ZNmLprzGB6H`WU#FHVDIVIcNFLGNjF0?v zSTz~=CNwpHK3rv%HieBOkdO0x_<bmWM>f{NAwYA+q@sEys~i}nnOK^~j(n>$_%#DE z9=sCI*DAugoV!ZX{@z%fN5n5WrB(u{v<^;iKnrr&vDwLTpHv{=-%bi0(7lZg@~hSe z`MOj_2zBUm>n7()IVIx5R@{sF%vFHb-Pd5`uP21-TZ2ceE>%AC42O7}&nkc<=>A5( zo3+alzK+N*UG?R~PdfTR+sH21VPGJr^H#~Q-1UWOVsSki`&MyTaj7fy-5LZ7klssg zbv-%LhYAg-hbl@32XC(1w-d}06DTHLC(<?5Cevg6K}OzkKka2y{UK3%Ws^8`QUvZ@ z&_J3G`(=($U>u9c$@{#@FR>R^|BLBjJ&$?vBX+ak?ESkxFtR3hN|pPhq{<c?&G!d- zEuNB=VoL}0ylBM4@mf8R>zdj^Vci<Hs{c?Xhp3b@pNy4TiyqrM9KYOD8$QS5WloA* zcA;#~uP2**{!I_n#b1}>?X7_W_p$vkmm%@z)OHm<p^PlkloHMEwIjE6Gg!9EYSLiR z7xJ3d><B|kqF@Eoif7()*8$LYOCk}5-)7&%DX2&^J~^A1gV+YWO|9~h7?$2duxFW! zc(SEjVzBPlGkjdUG`;%_9^8nVTwCoXGEiQHzI2*Pxo#wDw9MpfXdlH)Y<R9FpiR(P zR0?xOHQ)+h=6(@{9L@Qe!@SY!)3N4@uqKM<NGQ{PQ{>KG{zF@jUZGkO>OHiMM&Nw^ zw6OxpXD?c6bJ)>`DR61_g8F!5xO|o<M`p$+r2OiAR<(6nvlacqkN+?}uObnrj#G&i zLl=}oUyei6oYSBD#$k6ACZd*AHolx2f1Tl-7nz+w79*^zYtvz)DS|lIgCV;e;U2oP zu+`ce<t*(FMb2<qJzBB7FYGP~d{?ix=&G`!BTR&NX~>u}w9aFxdcj}bXO8)wWQB#6 zO`iHy@$dwvqZ0^y<FGMKuwpx4_Od9;3ZK?qhh>B6yJ!d^eTFVAuEcgZS>8$7`1sMD zqRz{z5ONCT7)3nGFal6o<la|jcp{k$B`a`bI1I0l^dd#dY?Gl-MeTUV<TX6=)U3tv z$Pvu%%2ENdu9<0S5K8zHke^W>zb+l->pG3I)-BRP=b>uHGqXZU^X~cK`!`whKV8bc zN3knG&gW+BAMC}Ngl0Cbl?90a(1wblVpMRVXW_~<8Q7iCSXW#7cT`a)Gv@*yYIljq zE#}(eU+-CG1EvO8VPK#<RiT()*#(=8(;w7p^vb>+$2QUWcs7;8!%r&c^w%|2C|@J? zg2MiY5q!C4S|bxqoSFAf#ZmI%cA1N5tG6hqi1^r+AtI>#o^DI$B|)C##ZX-T*1>jh z$%@x^r5l0yAuS^vEowy|e2md1lrYfo1Ah3m<gCKkP+P+4<D0cl&`hSqdnGZ^W@H{x z^}C<~McivxoLmfLus`hxGgjax7WG5>L4dReriXlm!!V3gXwK#nq58)I=SyvV@E*tO z3@Y&4Wv&Z!;g;cq?FptS-|U1-`N-Fydf0OTWgItw7S$ZrgMB(@TM|m5{KBP$g~Kw! zx8JX4n-Gwkem|aW#91X<ax|mjfF6cx&~&uKZIr;^Umx^g#@xd4q|S_ob(;C{!TndR z6P<tjEJ5rsLaS;HxuVo%EM;GHFmEviG(7%ft4S_Gd(ltR$sVt4)$VhlN1l+R)1(uQ zgXhgAUglwjuZPC($e0$#W9d}l+FJ^A-_My;b!HRs6vlSeo#8o`Y26s=c*pC*ob-(Y z{CoUsLd%LEi!?gltdy-XDN=pDCYZ$;lZ~JAq3_!}-@%<*{C7Z8jWdZRM#>D?-&RDy z=2Wz(he8?A=DDK$kHetm4IWE`|1UPO33*1gsP|KRG5UJLHk0V4bX@2EP}qMGkeFU* zpMrjN{`s(*o%E{Na%DGLSlQUZc~;dpm!cu>3&QN`EbdmEipy!HNcW;j`^DrljgM>F zl3A=~(bn2~k5Cs-95AqPb&nnMsc+MlN&KD25wGyw?L9X5yN1Ws<GMKYfV1T6MDz48 zHr<p8m_bEQAwN57ek<~`naY(QKi&bomW{nl5GRMi3$Uw&wT(d>ma@(k$A?;GmlgXb za41~QhReAnoN;O6xR>6C%DR8yZ}{4bmZEB9C6@yz(p&A!v-T8O>49HAPML@+{418S z22MJ>HRnB^vxb~7k}n!qpB+!wAR~KDm&ra{g>5}T_4miczVhHhDW=!fxcj;;4y4B~ z#sM811<UC(PmznaEr!(Hki{^+r@Tu(=<LcxDr=vNFGw)yPNnB1k8ITxe69|jUY##y zXG%sX$x-Y0)^scT-Lo)O-u&78>D;Ky!-4u>2fr%lQ;jeZ{ZxUcv%hL=5px7X+Jc{a z0(+(>TQUpJNbslFB9u?1<tM-EJcDBYVq;omhYP-Z!v0aeW*e$vJz%ggTU9Mr934^= z(;luSnqka~&kU(PUXbty=;z3Ikp%NLMi|cpVM~$XRR7d@5#>J=$$asc>NC3^eH})M zHWDKy2K4%~Pb5?MiNyV>vQ#>3qg9sk!>>lz{=^-el19q8Bvpf{Z8;Wp+Mdz8poj%a zAdQ?cW=E-ep4%U#&Wy#??p^&om{q2(tZfu{&S+U=t+u0;Gg;a5l>&2X9hi>)&T$e3 zA_6>6vzGZgeL+63_&UY--+F6gP}MA<KW0Z~xB-`1a<DCHYud1ABR83nNuyCCI4O$H z+~QdE#y=E~g=xC*7QZu(p1XPn1eB#YEU4ypBU6~z*%}9C8~A8rT@-z_WY~SWWsDq< z_Rf75wf}dg#{Xwu<KNW(CUr$_<qY&NUHW%3z+JTBjMQ=2B;MSd`GTN~mCVlW1fCAo z*q7U$@b&(|cJEYLQ<3a)Idcb6>X4_6b>qiHd-oBjAl?B7LH^-<aaTr|&lbgPv7?~! zq$JEf>eZCU0XhLw*)a2y6aldC)BT{THYgFRXuyO8oS)i8Nj-?wbsfVsOcQxmxclJh z5mCwvK2V59yH=hb)?&plC-nY90X)k{O#luo^>@ntua(76jVYYu3^Ka#^_hO*d$*hd zL4zowYt|KSNsgOWmsb=mQKo=52AKWX$s5n6GckXQCsXG}+WHcAXXd~qPXq%((;q<K zHd!19%`Z~mvv@!9^Uw9tG=>KAI6@CGx=ZNCUPa)F*UzD57fk#*=I$$x<OO6&cDyvW zZXtKYur%&HL(o0>tzK(W)yoK(hq9r?$tr_JPvHwIy**$8%eP$aQ=6d6AGWCzor0*~ zPOfD*VLdn*i&}Mc##5o3VZ_=obVmAc!tBUJ7xA>osyP=sTNgfVWzx<s>OAg3w$0`l zoAYrWr6hCgCtDP_&<?aMf{;G4_*?HbC1wE`&`zius=6LO9$Dq6LBWv7r9i)=kT_>W zmN!{5R&@493EheeSZ$6TKW{3QDK*e0W2U#9HJ|AHV1HuL`Ouq2+{bWud9dAxSFQ;# zfq>wTFCTYnLVthsnr0tZo|q#p-Z;ucnh5>Et~Bl3_VmeVDIp#|)KaBlu=+b|@9(0; zljyIYn&iHhZAY-sw-Rwqeq*icX=~G5V7s!BfKcmX88>B4w5A^2225t<@^<irpN;W! zFUArjMz)5i?%k<uHH-3tAX<E>;L@%<sszKn9EjGj?l2drIMl&BD{@FBDl*^7PDoHk z+csrX|M6W4_}u&qqw{8M>}(fBwe`;@g1omiCN9YDF71Yw&ydN46=R<K`3Zqjg`crm zFE~>_fE0U=_JAf12lG^`^9kx`=l<DV<uy|UpSZ2%j53+g{j0)i#n)BEvCK~7iMlCG zWf(MvjyG^)&$ILL=>8~0rXfA01g*nhAp^POc$x9u-#Ih1*${H9b^4P!*BA5cV`8a7 zuY$_*gv+<l-pmf)WKg)6!Hi(i<I@GddXp>I;>iTWh*Y>-vRWqzS_+CDnr$w;z|nN( z;lChrs!QYU@-j2zC4vb?M&AugwK=)$>bzp+Q=f#fVnIb(Oww9gZ`sYqDHo)gNP9n2 za*_XHefggG0#Fj<DIRiM7rA^-PI<Y^SZ{vX&J-xlm$tcAa}3x9?HK#GM|T|E8tqhj zG-sF^EJHmCUdidH@L*Ag$d{AE<3m6uYQ_(F8z%l*IwG9z+qa*e-AoVZMZ%hN(m(2L z#weELrj}|qi$I1J<Y%bt-nKMzgcesu7U{iGQ|5!LM8>fd1G)5)c`r-KW-)|flpk~T zNzS<71RZ<aF#8q-_xO%z^Qq!w6Madu=FX46<ns3AUIn@vtf9r&q5b(qD&<<nQcloX z@*8&EwC6e{^l|UT?|bVm2Npd}V#XT3MY1zjf0NJnRiS^R#${%&p)}^(a>Ccyc6*rd zd_9W1{@uEmH3zu?TmC#rgN)F7oA)UkjyDf4{70Hi4_tOnOX2Du`8Vt(1RT5sH+q#$ z>yMdx>&9)VxLu~4uBBxdIgk%lHaOg*gtnS*wCJd~>tJhUKSP-uJzmU@jcIHG)U@Qn zua%8Gln3KlbKy2gPDN5(6;T18X6?V@HSAVD%AZZm^m1`Ir2d6mjJO-wATF+NU&R%y zkw51)w-M7*d=>z8)R@2dd7}1L(e100)wV!<O-8BkzB*uDphZK+4TLhC%U5GFt>Wtn z|Bcy+6h`G~L0beH5gdIy;X=|jGaVLc{iW5KLgc75ZWZUH6mB9CyT2PhHqXp8aZGCP zF=Kt6HSc>Om_006QTBJfi#*Y6D!VDkd1pBedxq1fj<xp>g<`SuGXHno7qR6&(Ld+* zH?LvNC;e+=5L|Ao6)_wCx+Zlw>%rvzZ0GekB3*nNnW(-w!w*kbv&$I{{8KoGj;YYz zX|Cq~hqbqCi}DS-z6Vf|l8~-JnxRV&fuXy*K}te8B&3z@6p$XeyFt3cp_J|p$&r}( zKj*b=&x`x@echjc3FbKWW9@7GRxMScgZQr{*;H2p^uP={b7XI;5pJ{CSC?{H?J6q5 zsqygrjX9)?Lv<@FMHF1bZul4?$$qjFf_VtA^o&>XE-5`rKXXXNYgG)oSExoBBjuqN z;tQ~$LeBi9Q!%afVvnU7ZpWDaK+4zWGHH=FHJVYDpl6r2Pa(HLcO_YK8nLl{v`v!} zs*LfSceC?7c6}i9Kv<wUaGcbhy@{8{)TY#rP-z}ckuP`dJ}c~eeS;<;gpqxH1N`-6 zLyrD2+E{cy?dQK<?w04%_^-T@rX(sD`j(=AnT@YL8z^41%>I4S`d{42o>^>RCz2`u zWPNi&elI~b41$7^Jpm?se_5Pxy&`P=VhvL*NzJgO?ovpZ3W<Vj#4TApJpy_``)f5_ zUMoL3@9M_0@mPgT_y(x<<B&eqFh2^a1sD@)VU(mJDox+y2mm3>!OkiNZ(F;MwzwAz z5XDNdGzUuQx_HBi4;`1fi}(%r>cKR;Xf<lN7}>5){*RDOnnAQ!Pv+&?-K5k;Qt;{R zg?-k$3f72l1u1k`a&Zkg#)mPe_DBot8M_XpCu>(LnKS(5bOPXHI#nB(8#1ea)9_*Y zP?+RtKJOnJp@zvMjj${qHU-d3%sYa#m#z-+{cGS}ni{Yl*!Nx+j<~;D&AqyQx+x}~ zQVU}of7P*cw<iZ{kd$yD1I$KS5HI6^4x_iiZi`<~R+R{mo>vl3;CtX!^s|I^wf|Iy z=eY=zJ@DgJ@CVgs7y$8{CW*K-qMs=UEmxmstj1Ko1JxhFmibU^1r!6Enc>}(h>ri~ zk@-fQfR@Dkp0p2BRpJ?_j5`>^4?CCNe)1m94RivO9f;PpW6ZRIK&5u`BY(o7h*^K9 z5`sIC921ZDsN!aetRdi|n<B&_x%q}Z8T4W`$b6(3yDzxN;^{%D=2(Jpr`A7HQL4J= zFIGK-OX1;nOn_J?vFwUc>>Lb1#)LIrsy;*GzdBZ5(UHCt(%g(+dfQ5UeRFpd0uL^S zW~j(|Q_9XgH7@A`LF5*ZSgsP=29VKaARuR9AX}SUoRu4`KNfj*r%6Rq22OV(&{Yg$ zlk;DQruc6F1|~qz?%xFo28riBAcQULmt;c5=<ttKyZ7Ruq-mnL|6|`fD|~q+#&hKp zoX9G2vAW1jd6bx~Jbt_h;t=lcW!MN7z(s?9dFpjhLDSSVdDVS({{aAn>%zUVe<wO~ z&R{&rxBm8t)}UOPD?DPFgZz5L1e~D#+I?EwmWE9sfzI#5^TvekW_4R9a>dABTo#=D zMDO-Ac!dRIzNI7(Zw)B+5>%Y*rOMEZkA*otL0MKRp5-f4>Im1SiwA8T@d@iVX+<Y| zKk5BqQ~0;1{Fph)uqkrz866O|t)Dzh2Uq^jkNl77Q>RmMS1WHP2?>L;{=KiCRgr4T z`hi=ru=k2?PG;!lZptobJ8-(Ky@9vgm`x-+59l{EFHN}I*?0z$nM~9hC#<n~w5TKA zHMA*rV0u6~o#F+Z2Tn9C$Z$&TPDnng<^4sODn80LiXUDK1$nuDN}zHTr%pqL0Jj6K zu(*L0F~_C}=ABcqjBh^EnXehz>Z5e3&@8LvAVfNJ7-InDrX62W%QC(Y+mD5>3=83_ z;$i2XD?>b^&9H)0{4i7Z>KYNb`$%NlL;E5LSsF<Qn#PXvVpwdaH8{513u+JQ?z|mz zTXXMTC2ZX$R83f+1csUGL+Y_F6Ni-2tOG9k8$<&>>nT=N4no8|ZvH=Ic`&E*L-qAR zVP`cJ2}~bIoODHT>FpWA!?5%ZG#b#T7#rqv=J-^)h+bra-OPEAXYXVx1{$tSh@j{% z6vd;Jy44c}RPJ9jxTO3C>WuJCUpv3g@K0t>`ylH<4iBJE8K18u&=C}^oc)k<-)sgU zd7Gzj(a~l7mo&!hyd~k2<xbMlon?8kXL(IXOi~qLwJdOr`m^<O8+S&uetuD9uNiko z8(ZNPCrMoyU5gFlktua$m9sZs3-Q>V(^B1eEIAU$iof0eBY{J^s$w^<J&*b7^zB6F zS?Ci3`a02YLGVucM~5%vd1fKiCs!FQjkXZG&!p@%<rS9yfmma(VpIobgL_I6c-e;x zsfc{upaa)F2LlI4S-wFTq~K8&LK&SJ`C)%iB8gp2(){F{aqV5$_xsp!q3le$hCmUM zIJyI39e`crmGt<rIdJ7H<E5`h+VqCVoLaLKMDzs4gRvVJKq@+YJ!W%4OFjLWrF)zs zW$3MQN9P-LU8Y-KHSrqXa(kuTJM`edo>lzEq;yu>!UXDK&l5;R!z$9y%IL!zmMdk# zQ~npfK1>W<ybJQly%?M<UW<xqlZ(cgh;j$F5>tz$o_?ZTgqv^hGu^m)50~fR`OLmx z{PG~N_j%f=m?4EpS~a3%WZ3@MjgA28wM48b!3fRU#QeN|lvu^(tX0_pbGdHRI#xO3 z<uF=OL84DH30d<`5K6xFv|31YYJN7qKybl`fc<C6yjRDDB4>YG6Hlu!r^?(ZL&(-I z%TuRO*ScX|-P@OJd~DfDUWw0uF~?IiEfmaLHh9a+PlD%KYYDpBGi2_v6;^h=(Y+Ox z7ZNh4j8bxQyEQjxzPLfqovPBWNK+>mh7qVie6+d=jLSo>U8AU9-c*8E{l9tK#YcIY zQ_+HZ{AK@kn9;1&m-%<PXc9We#k@NGQvs>vgmY9wX3H~YtG|XF1=)cUO`q2VDtV3Q z-KHbMYg!$(T+Z6NHfR*p(uxF)>m5IqhrKpdhKvym=`VrO)P7cJ{toeO%kY(V-b2o~ z=FWX*VJG_<F0jpL#q_Ge+KRk{|N4Zodj!FsjcnYz@VvMegRZR~v}Sv`E|c>XvDJhr z32VI+m3R~pgpLh}nYW?>o=R8$3`dY=a#p0ak4LXobMb>og}jx+R{upU`q~Jkkbl3R zL=Rr89;?6O)7`RTbKEPgGEA?D7a$Q9{gPTiS8VsyGf`hBav1`l6!95!o9iQg12PU7 zY5xxd*bscZFI;?GT=vYg84$2084VIaFcTg7yjBsT>+Q*pz}bM6&ows732oF@lF@fk zJpxs_Ua$54>aM9I&x0cqJ=D2sSvtkNcDxtA0Lvs*WV&AGbDz(8R}&+gYV#wijJ5`s z-~ZSz7JeCaFID_7&ceKpf;V3SI?W+p%qwnu#rG3&MMYpIJQMAjVuwVHOdB22?xB%+ z|Djh_2>dK=c9Mz){?O^j)8}efWjc)aTCgu&4~Oy$kG^BZLgy3aE}P*9S8S5Wq+;S| zB3b2E!qu273jsE*;X9`$@$znWq(Qt%P9r>zc8+;;41;shtjR(JC7d+iW&Suw92L(4 zQY_rCBa>-K9g9=)A!VsXz*f9j7Q3PaTRbemb#>p++3eBBTe&e>r`2xLK~el;)!MF8 zqs6jS?t^{0*pqyTtO5KxIp|w4pO0tVvc1MQJuC!bf5c>aJ%jskI$Z)h1o8PbN_qnd zXx7qE#;$T7HG3agrV9cqyH;SQJ^4$rky$s%1ed-@{kb(8f4qixj)&85Oj09Ho90!E zpR71YkF`qGB)i>w?in0gp#we^58J3XkQe!3f)A%W$h+0+#awQ)NUX>SSff7kR*Os= zANub1)bTO;lD~;*0fzMUB^N}&D*0KUi|fk;iQSS68gV*nL5&qpw4753%^3tdUm?Br z6y!5A1<*NOU6LVTubhvpYMtqTISFQtJXxLNVC{$~9&Nu_T@AaZ%d=5-DwcB<L7d;S zX)=+`Rb47-LywE24bZx5ro-}S@k>=l0SluKZt`Shx%6->X41K-FOorXUOLbv$f={i z3_XWCde-NoK7MY{fLTeF!%OMf?=?~-LVgN6rZ~iQ#d}Rj&)E;bQoe{aRWFm5`;ns` z6V&4cq-~%1#pi3RDWFPMTI$a-gb0@}lE2>-zAC|>i^rgqFHLb$E>+0U7%_W>9Te;p z8b)b6Bq(6h@Nx8)D#Q3G0pE!hqj&5J#=v)X3xc+C&4uwz^|9_Up+AN8kG5Sof(AEE zP8$%`dnZ_`s3}gf?tC4MzzuOREWP7^p0{-kO^SBUb%N1qd=jp2-kgn}{wT1sEsTal zO+eBfWORQM)jsH(AHRiaUx*58N~5KxO%V$|51pT7)8Uw;h^;g0Gl>869GPzD?BCQm zn4#fJhAuk_iw8D{TqjP=*P<*L?WGnW6WElHV}^J}DAv!ZK(m1NHVxkwib^Bc<5kk7 zGA9%@ah=qYQZX#b^(E?c%P}666tVHQQ7evR3>CDP1wVJ)KZ8Gt)@5N&eT;Auz#hGN z_F(Gbaf=ZCM0<N3IKcde_6#>}B1!}&3OC-+N^tXi+YfNW*$J(0>AA=00neCdh8oAv z1jtL&8Z1U10fuHM<CQLVwvsYeEw9wN@01_0?p`{re$(-v!+$@BrC<?Xe&onzYx}U8 zNI*E#q;|35+384ud#0%|t3N@)WT_|5a>+sCIZeq_k#a}r#}*`!^m@cy5bS$#I@9Z~ zFZ^5Hs$Nl`<+LPzT=})OW{x>Nw*D23d&~tsLVw@<)4P&$(@<8}-l(8Bl~yERhb7jh zEYFKBOf8k`+*u0Jy;D(bMl{oq;?%KirZ;4|T`O|odvATPDt#Y}PpK&LD;-NynR8c8 z>t{oDoc@2HAGaJypJz<wRk4Q<0WRX%2FFIG6Uk9{eQ!cT97E;*<+<jq)547deeoHR zt?NP>uN}h%YC`@4eHPp{_g9}(FTIt!+nULwUqs8L7Vo?BbeK)9Fj|fdU3MwCBby6! z3M%dR2nHh8<|xL;5mX0|6`-IsJjv*C28jl#`yY4vZ-4_&*8DgAubq7VCq4VWU;iti zbN?%iGO8De%@wqQ?@-mpUq-De@u+UbXeZ{s4)`~(ehgiDG(aLAds`j!5Y!D8Jex5= zp-C4!s5A4c3Xe~DhxOwH^40d{`+i%ch<t^U^QAZCG^I9{;xnWgK>(1`Gf~CDI4>XY zoz}NK1P1n8`R!#gX^4y5PGo8i(E-dE2Wm6feaV+(ayBea=*dA)I0}I?x;*(EqcA(0 zm)zcblUm*EZXz||g1PFMYb^m|U^brj!J+E+pz8D3V*_~8;gWO8(Pk{hN(%6@@3Mv( zXXiz|;r;y^vp4}Sx7U9uBY%9w)h=?pdH61BBNz!>glKsLW#rL|(2I#1%@&`sLxV8Z zmW8sW6^cOV=2y4G&WRxc-+=s(!@+>Kq)hz_k<egyY{@p@Ta<JUc$oVbr^WXrJa9d7 zGiUSPhy@UoFqK;>|F$Ya1JT#2Gv={)9)<fV#XkVE_57~wH>eB9lj#5XmFmA88dKRC z0em9q*sD2AI6F3g;m*E@`Lgx7mth1M@Y6kS4RJE?z4ww2$J!Tq^H>Jdod4^f9!x$q zV)=W8uJARzt-TKDy{*9Uy~PCCJ<6*p;=ag=T3AY;&da|m%>QlZG?W%14u=S*l`k8{ zH1wWLl=lxpooWC|YhWi-<%@kJ&Y;H9{j0oYFI9!=l{+t5hwpEst`ta=QrtX#K1&8Y zEU!=PW4QE13~^H1S-XI~Cd<$N1^$hJZd`&@AquWql4+vmS|AJbd%Db&c(+z!+VSme zrf`-SoBi*9M#Fo+JJp`RKn<+^3j0RM!|)3ny-MJWpf+%KIxFe6jhC(QyWJ6SdRxcJ zEc+!vCBAO<Aasx2QLwuF@)k!<0mx-rv6h#nYd8V^F4xsImZ}l0J$jDbfuq8b%2>k3 zMC=?lssLjI^(*nPJfE104Ng;2#?=AGH)vM8y<?G-Zu|IUKd(>9dfhj?&Q3Ya6ECDP zlC=dpDen|4=>ivl_n~<r2R%5tG<LNVm)Yx)u%g#re(cCiH>oyNd-uofTGo&GQm(p` zu0b?XJq^94K|JQP@FT^?HQie_Y<cpm@=r=>?vgQi_c-fp>uasI_(y~B4Yf2cVp(^# zMA1%!FJM^;pQ_@-3xI_H;lAU*)y}V$e;->?oNrxBu{IsK{tP-Oc@j6l%?Yt+4JaoM zjdb6F5{`b}MN~z7NF5|%>M*}sT(TTYV>y&~f%-MVuj(9kOZF;Df|kJn^c0bFEB^gZ zy@9XEb#_+=+~oNgEAMx;Nf&YqgUIed{)q8BsIr&5chG|Dz%T=E4KP+$cEGLBHp zJg@q~=_-l*6JyTRyOo^7s7%5rOv%znXsFA!<StO~?3G3|LL%rSs2DBG=Aaz>mE?-^ zSH=}EyHyW$H_Nh*h%k~gyH)UwZYtC1xjIX}kmyi;1*KgubJp*4by-`fo<wW=mh1r5 z03`<p>d@wzDkn^2d!g1l)Ak3N#@sbPstw-JZsqE4u8*Ltj_%5;8n;Q1xp_x)n<U&C zYE(0JzpVGe#<AE`ZT36dS!F!a7m|DC>;b1XJ8{8~#HNY{lQiZ#%s&aNkL{By)SJ%U zpKEj<4Z7OZw<*m}57By%8)Cs51!*pC$(_if1C!;-Ci`n{;C%S$>}KH{m>*n}7-S8b zJ2AX0PI&i-upuh9dP-lv?ozNeUT}&e>i7XrHmLbX*Z1XSNdezg>^|bu3&u3^s{HhK zOzz`)B1TyX@b@D#FHn0Ph}yOGAd^n#8Nsp4rqLSHc{&mj{OSZRSNy$$Md6E}wXw60 zf;05)pM`jPSPV_Eh?T@BCgX>C;kDS7-Q>vntC8OATmzfc6@0!4MO<;3fv~I)06Vk1 zU&|R2sVSkt!Br94n~f4Hu2^!JAB=f!D^G)?tMW3F!8>GZmq;Vi^Yuf-(&cIY(namm zXH6`wcB{UMc~C-e0Bd2YW|$KG_d^B`<WjoIw0@9&#=F;oSoaPX328doa|InSdm~Pg z71Z&4CtZVu>1=&{7fRzkC5Ey^4)zXid)BX^pa5B9ldh)4PTHh#Xrbn2)F1;sZiuG2 zOv?QV69#GvH68PqRNeE>e{Cu)wb9h+7Dr0-Ss9eqC_bb!17YEDsP8Cl9r|8JKmvZ8 z904W>4feaajk|BusxLej|GqZwkvS#w7&A6!eMD*f_5*cVloX>b!)<u$18Q%lyr?|g z#8dG1FQ2GrqiFjN96BC^pR0I=&tGm&1)ZEO;&+#<7NO)`4?Pw~Lbrd>t0>$!Im+Hr z)N01xBbUw2&&rmv^>(?>`{<;xfHL+=5fzJB-y{vu2W>0Vr%y(iB(^6#!y%M%@kWun z!VjZIyKzYrHVh1}IsXh*yUL5N1C^e1?0ni9d}C}yWpNcBB5UJF9}0i4!~z{!Jd(h% zuYIa(g($phbxWOaA&#aRJtjxb3wIJ6wm-N&rrpPHlke3-OE)v^d?~DH00-_+(o$V& z0ixGtX;%AWs<mm34z1$gA=y__a-*BDNKTG9ftRP3x}$L{NiQK>KXF!8)}BR|{hE5l zT^piI^GUl}EK(>yW)dVCgJ0WD=7K=1;CzpTcs?ZV0w!J64c+pQ3{Be;gaTs2IbHo8 z&15oWSgOJ9DUMjE1Yaq#NAJl)py{^Rw7vfFa{l#}!&8%vlRtO~!A2ID$iXE$L%Tm< zjGL0hjc4N|V|P{2r>M*Daw&sB=EVtRy!xj9YwpgTWo67brUgrLIoqA9SL&LmJwyOP z-ywSY#AEj{+tJ{ty#Gkk<TIJ8__$}&AR<rgqgAL0)b_M0)=P^+L-1`7m@aMQn=W!| z44C|#p+0q~|AAPtNRo7aF_%;*Vz<7j`ddAKl&u|Ubi{WP>*$d+4p>M^*s4sZyN^Zo zHs%-4H2*ko#%Sd*8zTfoV8bXMG~@h?f1Ss*C-HMZg<@*$fW~uqV|A_ZiHB4UwLM@S z#CbejkkwUYU(?*?lIF32YxZx<_hf_b#0S%#7RC{ECyg7`u!L}M2zf!-&dK#*;?+k! zqxxhuqaTnTp=PD0T6XFa5o%+)>P$^aRqC@iz%BS&wLc);wmj99Cg<ihFsM`8c=Bc7 z`@gb*yvSE}n`z|lG@2kP4!^Kt+E%VB0CO!)1)7LHs54ScJ}=C(N+-{lET_1sy*|mz z5`<JKWjL{w8+tWI!80(l4HfI2{6A!2(JQR$62eX@Uc(AI1pDhYe^UjhH|%)%U@dh! z{RA}k!?cmT`<wpkjPiI+2jR3)%sI@2i3F48cqb1lE7)r*<V$d#lP(}I(|3jc=tTdO zuI)PEQd8UwKrJ5I;-~i+e7(B+=fX-N9hx@n)iBOBMfjULJ7i~F23=I%l)2P#gBx<Z zc8$@Oj9Zebm6C%<p9KaqS2OY^kw?k|hBT{eoFr%+ff8n#u4+acdN{IUv5QDJ*x)H% zegn5$$}B}zPvQX{y$?VTHsMKog8>^|hu7A;Kiuu6cb+q%N!nwtK|ytzw3PB=$yf_W z_sZ{LgF&7|N4Y{wR`$oA7$PpN{v{2uMvh0P1=;Flp+9?biK}ZyEuD@HWj+=m_y1D| zT_~~psJM8>|9)P<ezqd+v$UqW{(Or_5Tn1EQ@$?qj(6<9LD?+e8zWlrAlbIZYvW;| z#?RYZ3OukZ&yO#J_(hXMJuy<)_$Ili1TEB|Kb)P3DAr|uA?!5%_ce&%>i+K$&F}7H zb3$%q%K5p2B@DV3%O+RyFBeHsB^%QJpwee<`VtV4BCrlg8gEW!dhC4R1fy+{>)8)i zLUub&hA~b;&3|pE+)*Lb6N^9R)d(zS`nDEa;t;)po1fx62DgX(qt31FoW*HX8LtU2 z?P~KfH=+D+-Gjo?HIE`yg6gWDvbr>)5z1MgOg)oyrzMfR5`4U?*@5+EOf)QMC8Z_d z4GOk@C0{E`8vDYQ9_Z4J;PjHv@V8_L<6bvkHMzgb$E4c-qzOiLU$M$B3%eaIJ$#$N z<Iu|0?#8KP%m?YC!<zXNO8mSspy44_{JV#id=j^iYF)fZ-f~|?+TPh{EWi}76D?vF zIz}9OMpb)MT2W^MH*nxVt9n6(vFwAK+Z7&D*h!1r){Wu*qsfGY3c$(!4fanx7GH9n z7f&kk+mGHo5)g3EK7ivrj+9!dGu(NP8aySC7PVcb6OEwUUpvGzsgI)ykYW>$(RP(x zx3A^FIZ7qneldK1JEW2U33+M7!@qS*;rBzak?TwHz&TcE8movOxYMQgeVbi%K*ffk zoLcsmK=@{$sdUONzTzNO;LM+&_@k5By^l`MYtN>$YC@DUAxAn6Xu-*+<b-Z8bT5p5 zt-VH%-J>0$uf6-?Sgk;m_oS~a!2Ej)Gi%ZpXr}VZP7it4)6mgf)Z~K~T948Vr3uvO zQyRN}`Q%S>!am9%y-JjL>mAIfeRjBRKzWO9v>EF0%%bd>VF+YE_dn3jN9MeZ3un5H z^hL72FzG}%57Y)1Fml4o|FZdSaXf%m#HP&QGKoQ4$AIELcBNmC%*}ch@MFRi>d4#( zokP8oN>+BcZ0<&oO36oQNlS}Hy%C|WEcMnE{S#7~)Khx-0YwW~ugs?v6lit3O(k}2 zjZv+h{w%8-8!zIRGm=XhjXvx$$zR6;r&LMlz2@bm`LMsVlQL(`Yh9yJ`A4ZSxvcd6 zK#5L4?o6Y71WSDVOV7J3E5SB9H^H|ZKN%ik5<Hs`I~gt@9_Pg-aa9)Ad!5D7eUoU^ zn?dfCp50u8v#9ro1twwD(rJ~icy2e#3F^a_FC!BW9hbk)oX3r@6?a3&-&tD+*T>?| zEUlVTH%B%TYRGO`b|_Zovj+_=R9uWaEYS}5x2_j0ch~s7T^l|Y>eWuXqW`<I+L*U% z2Vlk?BOcEfp1*&-hyTR+ALzs!IL!Y1JXB=UYsm0XHpeKlTh9t@X*ar~dl@~fOb8%m z#4i^tk4Y?j*?3<S?m{=heI!J~36CU<A54&95sZ&3{`T7=Z%3Hw*2r)kiqpf>$}h?B z5h-Th{68c7{2z2-*^Zr|^YH4e<leKWgx0+ixDdm2X^CBWq3+-vp0x&#uFu=lWPpY# z5;!}R5-it~8`!IPPMo4S!q?eN&tZXO9S*4|Wl?yb*ByA4|4Pi_QMN7M=o3>C9O2$T zj3OJBxRnS>oRLN*JT@bNoV4J7AOS|UYpj#&uXzV>wZ?<Rg{-tZ$6K)LIaVKlW^>!& z@mawEMs<p7t$8)7lLsHsK(VXhNh{^?OY2<nA`GQi2s^EWP^^wJ9dOWUo^rMa96NIs zW3{lR<MT1Za_^5rM3UZKoVTo<jgNI)Gm`OR#1V<eqSot!%I07_`UlL9ikq;)!?DY` zDasif7tUWV#M0k~)hO7r<=#CQ5V#-%9y(f|i%K$~gyGXleMZl|KREd6O&y4n8+3=% z;R21{ZP%WUOU`@(6U++?NqbZz?{2p=5Bb%Kdkj!#arMsC;XA&s|1et{M5RSuZC9$? zhpN}A@PDbjr)sN!SoS+jm#)l@Ve?dd+6z=_$u_hwEU3u0qb?w8j)czoho^j#m4ywq zDgU#qP0xby2qCqRO;?7f7HA1-=j5U*e6O9I-TiDMJiyH8KhUrVcA%*m71^rGGH2v* z?;iU-XIdOdA<^qxLB7R^QCez|SCx-0ySculrDYdC#EEkGKbNg^`sIp@l5gmIVC<XK zR7lFj@IBwi27B1ljxk4g^=U7xIf*mp5FetCQDynC=EORF@F#LQ6<I~}c!=$syZCy1 zWR=LLU?L|6N791DVN<o|muKnYfhp<j?(3YbF+TNjv^^Fth_b?B`MyLR_BflabQX;* z$mg7ikFO`n6Q?9`oz)HvX}{}Ks(kfUnwod~R6+Ylb7W@)&q>a#M%I_zk<y&QN<G=7 zg|{4cU12Y2fEz41(>&+W_;$^g>O8r7<qc()A~p5jqW5Tbvwr#$<sYqg)r^+kpszSI z8x~R~{^J#L9l^$S+kw-@)$3CD`DOhurBQ{nuHIh<`KQ(>Reo!)#!VURGwhvqQ2*O2 zeyz2AOgLH4M5ec}ovc96_+D_((fK7qo?FOeO=?X0ZJ7YO+E*<BcY7g1#su~L7;-Qt z113^vI@c5{&A*oLnJw$)ND%=gn_WRP_BUnwm&ww&N)_1uft)Xb%A32#2vpSoZorIS z_sfspkF<v_9@MfQ17}=XEc!UBLqkyiV(Y+1Z7E1D!5M}9(0AV$h%qK+{Q^x{+>MJX z;JKL<4dJm6u?=>E0fTz@-*Vmt^Uxx=JXrc%ss=`{qevce0&I1<n~*Lj@$>Q7ycf$_ zMN81*8OfEh68tT>Jq8pvv>{KtPJ6ffPrD=^iFh34^O;{YPEMkaXJ-6Fa>tRx;phT% zL{LV9$8tq$xp?2^u%`}-<<)AzaqHUJjENeK`T1sc&1Ysre@BfKdP*0L+Krb75?q?J z0(O&<ooC}9oPFkHyGXHCJeXdo4wUfta`;1QyCP}qa8Z6yfUeG*Ebf%v-w8eC$k4`$ zaHdZtrh1!!LAA~M;%SDOIg<J|m>M0+z_HBbb9Xf%>RjKytbyWIpUAg8dUL6k=~=IZ zHuL>3%jIccDYk_-icMZoql5JHljiqt;AJE<=5eU~Se6q{M_HW$U+lR<L!5{s@2!LN zi=8xGoS9jaSo|H1p_VMCP!wZo>3H5{0LELfaLx8U*CmmhXRz8Dm<7~c48s`HUHVVE zSi8BYI*phf)b37iXtLhIz)~piS1K%vOQn{{i>Nw%e<W9|qA2CeyJn$Mk!nU&w~drE z-uHtYLs{P7BrOfT2S;g~%*yWw@3yvhWs=s8ziPEnFEZIp7TUlW6Eu>mBaURe9wS0d z9%pwm4sTit)xOO!IoPZ7YcB?-kL#Vq!_S4cN^zhcoB|6I-k7U}igaKW{)x&~6~sHt zRrS>DpkE8Vi>Mx(hDRy#Ewn@CPS|+2ca^nKegOP?czu+m(>POb+G`wth~>CDKqg@| zV!uoHRXpdtIQfUSW4Lb1;m|_4GGWw9d292dl+L9hWHYlbx-~^w(b$~K?MyXpcr~F^ z7y)v`9+NS|G0*;fpX+?#r8T!}#hhrm<7!gvmHfdUrj>UbA5u#vUojMfPqoAt60tft zEfFIH;c7laJ_#AMQiHGORiBHxfixNUOzO0pi)C-A!mQ5Nl6}_FG{4x9n?lLJdOHc$ zD%SHV$qp{&8Kv4wT!vk0S84*4OqhB9L<n8aH0DE|YfIb1koCuAQhqg*OHgT4YfFiz zv{yU=S_VEr*5(WS2f}x{Vb?5Gdhq;fcMP+6%$jq2AkIF)xGAr1IHvaB9Lh^1-B=hd zKnCU+KRT_(FOUSr(+jBF^6?UXR?Z1ZBwp@c+I8zg%*j=BrR`50J;OipH}H2f*|?~G z>M;H!=@(Dq{){4dV-Cf#PEhUw*pmZL18cX@pTFoFp109_geboT8s6QI0k_^z-PV7r z{%RuSGg`WqYPxa%fr=BLCzlsMYY+fo0-GPY+}zFo2SPjk8x0`vRYm89Sp94kH{>Wo z4e^)Kn`dZuj`)@OMv8*54j$=%x?imqB0{f4Kt)?Lkdhi=f%<>sb@hyf5!Kn+Zm3u4 z?Or6x^6IdA!QddL9|TL=i(vEGX1$~eJjZkj8c0i3eR{?YwTSW%U`?JM{JRLf@0$oC z0J$&mkzmXPxz^n<my|;=j^?L|$(6o>H9Lqs8w~<gyL&y{Pn_O1RS-bL0!^JIX{?rB z)SLgA+uyE@&w_l>#nQ7UmvWFc$z0Qw_;8Djhcs7rG6p*H(%+UqB3UZ)GoBLY;?EFJ z8#XuUSqkNjX3h`#y4@F}w7V`YT$?Mc-HmRIL=v>o#|@*Ev808gg{9M>J+K>e#OEYT zA^(7GA07z`t2ZQxjSgVmggRd@jH>JVxXd2bk=6G$y!T2bYhsGvMtxnSKIZ;z31rz4 zL!1>_Qlu`<7@}<C`upVE;+L@BkAB#xl}_dJ-=ir;-LHlh-c45PsQ6IQ+HqXrPrMHD z80lOc1M_*%PPtKk+m5hpiEjUwt~f^WPS{4Uyjo2Wv+C1hlDjE_<x0JcRNs5}-R}O% zNU<z7QWQk}T5@?FlRoqKwBAsd@AoloEH19@jjL{fb~$Dy@%N`NxG7Cj(rp_;jq?_I z3SI9W3b3tz=5{pkKT8eM|3^!W|G(rD{=ZsnjF*tP+%^Z*&jY@TF7uUo^_v;uWiNZT zcpAgkL&>a_s&Z*h9f)oK){@-9WfC+f`r|5fF6YbyX$`?Y_bKDO(UECD$t3qV0`&EO z{k*k?Ul}(0%5)A)uFK{9sY9XwCDVifb*(&#!1wiapXQfuSSEcd>bR{mGcX?fcghD? zHMCTWSkD(-aX|YQpMv6$yQw2XX*OHZ12y(5`5(1>g6fyL|MoAbQum+hVl`q+cg7eU z9F?vwl#$!-%(X5a{uPi4)6WA405xuBQWu)49KpkTOdRq;44TwPiMw5Nk;vcs&4-~| z*0J9D3Lv0wru&Q}we-;>Hh}_<qFwu@;}|dinvXOfD8p)#YNc@A4c<wiXPN_s>?mVf zS4#zz+#Ve&^T%gM=+a%u5~wsEu(3&WZ_hHu?^@ORlea7yDX9v~CC(}lzi`GT*r;-w z0ywAt{B>!58p?1q8R>PS)$}J0$-nA8RWFUlZrdg9f15q8r}Ya&b&SuYT8kQ4b{w;| zkL49q%UUYz!&4sucpTc6Eelv*m5oK6w5D;;U@Q7d*&%26QNkLvSez&Dt!iSfj`>@3 zho~>%f-!KeFIlzj;=x>bQ>fOvHyWEfsO($`(pi!)@S*BIM}PL@t2;3x$<ks!LuxM1 z(1qHoqx4n=P0DQ9Bd{L6%Sj)lc*JrqIJPtdI<_U_7}!u`pb;4Nue&>S)pS)^nlU2b zRr<(B2v9)QP0#n|K)2qdpt%{Cvu8E~T+YtP6Imt4Sdb^VLBIWF8R|uX4<8oSLg%wy zg?*~AB&cQ%<%EH#Gy&_MDno)Zn*A?sUR|7fU`#iYKUkKt&kUB``IXb;T9s8&M0d4i z{;wu%a#)>RFV88$(4z8t9*Jybe_9<~aVpiuLxNjHqd>KMb~vZ|$Sc8pi^?O$!+73X zhS>xCf4O3z-9rtMW7(@2ZlR%TarkA=!k&6RYS*s)v2I7Wq@Gp7Y?x8>3fCGh+0=7u zpc56bD9;r|9`M9Qlh7hU@ucxq&}Wqv;>&nBXN4!L!_l0my>?4t<W`Pm`e|uiELn6= zT{*e3*rTH07!o}yyr^<U_gwMqO+q#D+IrNssWUO!7@h@P!G|6U)C?>03MHQ7MRFz< zP&;Pn^?o0bh3*u8GNi3M9#gsO)V*C_8XIDC`}%x;P~%OQg5|5kugARyM`vt_r6~#( zn{kw(F@E3wD6Kyk;2_0%DW8dh#5yDo0oL^zr|i!LJ!Y54C34}zVVQL!Xh|Led2e+S z&hZlvb?}cqZk!wJM;JHam!Us-{Je`st~h5AysGxR56T17w?g?IAc{`?+FVTxT9ehK zs$$b6`^FBHby4t%ay@SWDORFl1IbCKR<tTrUr=l(?PW}G{IiwJJ#UmOGCG>hdiTA_ z{zZOtbjV!-?=i1ga<ws|=grM5o)k-L<N9)R41q>GH>$TQPC7QyG2G~{X&9e_2;td3 zt#$bNH7W44WYkdQuw55`clSojKA{50oFAJq8jNMKO)CaN!=vSDb96$*$8*!DY(wL- zr1dpw;Nyx5yR1DW&xW5zg4Jt#c;pf6S}Cg9j68%T^eUfB5*BLqkI^2Cm)UBkVC)>E z$Bq26DL)?Ic@sqeFsHY0j>T-B=$mL#eFtqMyUx;e=|p+ElJL&ByhyiH9|yf&uv-5M zkyX+p+2(pP?h)EK63<F?O^r{}39N5yMEZFwjAXPhmjGCBitCz6tZosvi8r?XcoDoy zUV6gDP?Mdmnm=9)=bzr-lw^drA=L8gh%T{qL(~VK92t986-zo7WaXoiUR-Xu`SiMh z3?y)wK!YReV${OrrnQAW8ud=)>bFs)xb+(dMORt#&)OCEeq*#`0mmEfn+xzYB16e{ ziG)-Li-ty5<|CMWLT&lYm4JJUB2`!9NdU{Oeecb|x&mFe;lAy4+pLMqr9^!)2ihA; zT!q#b+w1erh;Y-66sML`-C$ze-Z2CH>m!z5Qx-<XK2|L`y_62z>VnU|SL1)DyA31J z6cqcyANx&N`emi9FFUh6k>Osr!Jw499Ro;qDlt^`KM?KZjCx5)E|keyQ-RUjn(q&- z40f?ZnvOjvfb;Bm&xMyAiPLGmE=VJ=p!CT_jD_@4Eq_&QPt7w90;S960^o9jJMd6S z6=!9d<WEC3dS(qUxXhFxEtjVP+ecqg5<H2?t%cxr(V&Db-rXw+4uNy+e&@N&<G~c* z+TR(ba(o;_hUk5T6BARt9x#4RH73pzM;_O^YzLgg@(wiBc=>IKf67kf{{H5Eu-b|< zpO`2o@i9^&S7r47>}*}8_fEJvKYA2<eI26yOwGmF=}ePJ%{5YLhT4&lR=w-ngpM4s z$#8Mp0u#74Ph)4f)38^EXRW3<N=`RVVg|P)3*`m=D4DbLVY!BL-5A+Jm8y-*u83Uj z`XZ|Vh!--nFJPX8l%|Ch@VMH9;_A{-Z8X}qMVyj*_KemIqmE4RoRrQ205d82o?|wg z;f3n%oIhJ+CB{st)`VqrVCbs<yBLpjQcm1Pz&#-doRm<>KlfCL=RcG`kG=P=*No!= zfh@3jK=^OeSC&3Het@tyu%(1SWBtkvvm643H1#=LdwHK5{f0#ao0rn=FmF!FzLsNk z-AuQ{9j<0{heu&yH?)6r48#Pe{mmYo&P$)s4ezra09!1%Au)bt1&w(gElc6ajJ;TW zFFWH?rsFm@I$HkCkjMQ^3v9*bK}1Pmd)0S5Ozwnxtxv-yVK7>NN%7lOG7VkuWGo&k zca>?{CYdSy%UeF9k8Zx@HMXUc>PIB0RW(>B*!91=nvKA&mLemMw_5^5<Jwr7*B}}G zy^#n>#|w*qicXQ5cT$V8H80Xlp2pi4wMTbKPCc^=Md4H5&O~e0%1hk;%9nDot8_FS zel{uR`msJF(R;9JzAc!hq@|)Qz|<_aj%!83ayS?*1KI%AerkUEq55Xm{2Ce>-$sf7 z0`uizwfI@jY`1~`$EF^iqs$;D=CS*m%Br>OYZ?s`MEuTcMSBPPql!kJAtq@m(XRo6 z%M|?}`5W5%=zII3za_$Wa+AZ^n8+Hc^pYji>c;Yli>L4S@Sp0lkF5|RSVt$n-~0)M zxHa0Hr+ym}j!anN+V1VxH74%V`L&#^!GcHMXdMzf+ynDsr}@*J=gspJyrJV(<e+3{ zUjbo&M`%RtRSxNrX+3YoaL2niXP?;ZKFm4@*@?g_3*??u9cjhv{BdF)z8Vm{fU-{5 zgX=!#A~&`~Mi_n$y5g1L|C<OipD9gRMiz6<f1bW;fA(O%&IjG)-ZW3}wTz{w5ML~V zgjVl_y)ca;Yct2J7<dZkSU<bAI$Co4H28{x>M8Yj0%3h;#VXcakKIO|5%IKp6t6aZ zc4Q0BqZi5%7JX61H}dk9)=YiVcKsTz!X`aNg6HV_JzJUdl@Qrxc*5C=;#<TnA|vO_ z@rK~f@WEtJ>(i5nx*J0Aj>-+K)|VcV*B=K74m2-wn=d}&n)@aTOoVi;leOrH4u=Ot z>B+F#G68%p5v(k6&$UFszMZ2B(RWN-my}EfnaNw_u8VJs;oRHX2kQ}?>)Se5L-{c? zN?9mf#vrcf8u5d`L8k7(yaj;@2B9Ht0(KPgL`>iAuJt#p{`2|2E1hy#H0v&z|3G#C zpsl$YMY}!pB<WaO5gv~9l|=AwQMEV?WB9u?W;nT=dgam0{?eTjS!dTDq9}?FTxeI| z{loO7^V(BwqkmfN?i875^M*5&+pz}`d-Eox9(P|S5)1(1w6{fZ)!>eyUNI?5RYX^e zQ)U$Iw2KDgpy>T=l2=>*o{%cOWWBB99n-M7W@ighCTTe2m#q>c46&@IO^~yYiKa^F zjMYvHU-obBQfxWbd;EH8{gBF_-0{2&6Pq`e`G=Zc!pd)Pxn1th?bKKMV;^Z3Jgi)o zu-+87C)1Zd8QFm`g~6gETw`ZBh#&XLb8XdgJ&h{^)N5qRx5RUlSxP$Pb{?Is*4y6- zhcrdz8wN1$X~4tu5HvhjvWfUHc@p7I#ik+y`cjv3-$s7cQ?Mv_E!QYz!k{T1X8!rJ z<^5={pWu(wqn<~!;K@lkyp}#VSTyl?J23v<38vjQmhA|P)Rl9QB#hz#^0+rlXGS^c z*VvRIsuQNVPux&WB|T?cq%1Zk32`R-l&<0{zBe?IA}=?&Z=urtg`GkQ#6y}m-z5zu zWNeJH!EBIG9Fm6Lyk=Y=1Xz5*sYV}p@Fn`(phCIWi<S`^{AX`=ktWRg+j;p%IeT^! z9h*lC!m|9UXIY+|!!+F-8uVwP+RbA01)PSaly&wvnDjzgSLUGx$w9SkJ8W4R(+h8m zB+Ju<`}C}CZLcU^;s&9wuLP8gEk<~(vY+hy2b%3>i*fj~nQTbfN0B2@j;&BS{X^W$ zRAl%@bCjWE*$3@;n9sTqLS*GON3INf^be7h!II$<tEE4QK_tdb4qTu@Jm<x`OdhQe zG~vlqo;`EwL%i(j-$6uT5*F%ZM|~e!SSh{#HRB)Ff=Bi52>c?FeQ-DuPp7iNiKrcl zYm#8r`{#l8&Xa+?i>P}gcP^c-t44>`#Cg$=s&VrnhmBNjF1A>$SKBXd%uL^)e`O<P zS!&}C;-5Q)_sO;7Y>P(|<y{urq5@c$rxu2%&syHbK_KaJrG?e6Oacg!5|N~cx4Bsk zitrjzENG162YpB-o29QT9a%dtkE{E)H~(RdmCH`sjt(LUnx!#%Fl^0!({e$kDi|E# zr!-Qjkw0+w^IkjN&rWqGz0A-a;%cm$e??H!wwxkTlXN7P)nZT`=&L=3$W{8Y$LAPX zFAv$+6T}7Qs^#yTpuj-lqGsY#Fu737PtkbTQ+*N4?aWw)!;D>c04#!yYy;_E9a|d) zC&|Ud^*6{AXbBa9?4ISkVz5yL7r-%Tx3GCd*IO&%jP4XFD+x95YdQIOcb)<wwf?UB zuC7>y%|v%~WG3ul*Iqq}aOVDw2_Ex;)E3L%ci3ceT#nzWKQTuWZCQrKU?x~cj=6Ri zmAu;n+`8F7$t;3Tm}lNrrdcP2S5-?Xa7!at9*;i+ymUqRALvvp)r7Ro)3D{y9Cs1M zUWs3yHU#~sq<8HRp)};rAiW>=^TAp(V%KlNT)XW`z@8b(Zom|>bhZ=9Ve%%aYA}oh zeT*g=Xu|P+{m+tjPC$l0pi^2dXe2T5d7Jp0_^$?q4prEv0Hd0bGh0FN(5~m084BlH zq4$|J-oG0Ut%0TeJ7^O!M?lc~%I~ghd`VqjfSw6PQmd(rCrns+95X@f?)ME9<G8YJ z&y1(mpt_UQ<II#OR~Ik-m6Io$M*^I5Cf3}Q{rO+Zvf-zhL9BW8gT34o9P5S`ICez_ zkynfAdwS4KF}boD6jLIsdX?EIlkI3qI7^!vUfg~~KSmm_PD=M1{HalMN`qD?U6DS$ zU%|P+$v=jN5*Sv@p8WY*loS~49+w!Z1IykNbtDT>V;`aXLQpzKlE|$gZL8y>%fdpD zOe>k#W|d;I$<>z$r$dhafvnM8E<!CabJ^$E;v|gYJ<GsJfQs?@^r)4+K^FHzfIxg2 z=X`wc&dU55_ZY+0-oF45n;1514ITJ`%9mvJx+vR5{xMr<8|EUA{1|D4u0-z(7pX#% z3nE*|FfMQIi~;cN-nYTNbf7>2W%j)@PB-OfpAjtL_i!^KDJxee;80_o6qT$<Jvv#X z$a{^1RwM6?$Gs?IBSgLgnIBEpDE4-sp&l(u4}N(5kmsTM_aA7YJie|!Sthkdnx2?Z zJQ)*y2M>Z?1ny!NbJf2zlLJt{1%WW*bW-ut_YmPFG1eV*vy_-t273F<T=EJ2b@_QC zX~+DYY#ift?C%pQtsSo^I+EOBIKQ*6U2NY2HVOy(x%%;>P+TuYznhi!0wVOiC7f7} z8^KPut(*8h48612muG)|8Pyc#&}XeN-Fu!qS9+#ZvKn&xWO8{Ue#zHU*9)^}Y#!Np z2swMvBYdLj982t)f2919sPM|#5eL8YaYm@OwTnK5Ie$s74HKvbI{~W8{H4p%irIeI zJke5CzSy}sWIPl3Zc*UFsH|3nK326(#mZ>3UJ0yN6Kd<tX($|czCQP4JAMc&4hs+l zoRBNY1v+6l()WoXaYA3&-+mJmIPO5iyBj&JDJ3fI88zqTTKZ@~>~~`{jj7U>f^LD8 zj_nbzu7=Lb-Wj=OHI0Pww4|ov2S3j|ckh`}XCLdt6L`_y$lZ<lMAC#+r=l18zoc0_ zYT>0B=;k5e(YuNBwhyqR{4e201KJAF%HgG@m09666CY6CxmxNs)PQxnKeT*j8SwVt zp4)re{^F>>2z$1`u2lnEKbK{9&9BdTEc1@Hq7B}gDhlkNoWoXTxcYzWbUX8o$h0>{ z{{8m_@;Yn3sT55;PdfY+q)L7)ZmMP9mbiKC{i1(p47CMsw9`b)U`}V!cbyY)ulV@4 z!6ivnGc6P_MRV1rPy<Z#)91g#V9US|ul|A5H&QVFxKSo);b;WfisCNXYq7IW+RkFM zmQ_%aFiF!V#L3YkdzN=}#;>)2y%$SVhdXey5))YS?yi{o(=sn^{Ijr`rJ}Qv{smaL zOF3l%p&EFmKq@>J)>8aTP9Ekeaqw^%C{|Ko<M9R=#UUn>od3{9KPkzQV4vG}PTt-A z%=Or?CBwP(Z@S}c0!DQG)8CgUuOr|1t3I-_E0dO7@pQ0pka>XiAUw?HkI!bk`pnU$ z*nglEkJAh{viBd%DOYrTG=srSn*pU5MS^+GNvR|41xA;@Sn$mwiivgEjEV+CYQM>6 zmNanKiqmAqotFhcw=Nh{NhVH|-oeyMV0Od-!rO{rfzZ;1uudn<y5tM5){qm`XPrsr zA|q7zT(M`2@jFHVQjqv(QGC#K-nnkE7gG6VxzS3aH><i@NgPf?{qbbCW~lA47gFn0 zPExF-TxRHD*JFm{7^lBn-m(=g*-v`IC!Wx3aRYyjvj2kdR(Y}WwcTSRv}(|}KAL<b ziYPBg&lRM;58(3MOXKij1(d#%a}4DjCJF^FMneCqcg-W1E4p$8+){xuK@3t*-e2?B zDcagcr~nlU8AnLy_>RWB0F{ShyweRLRp_OG-8|OAIv`U<-L25@y?lUmXh}paXDmhB zKWt~gj0@A9Ir4$M+uJAq7%>@%U~=B)wZPGVe~CCqFmcJnD0clhIjVDcv>2bBuMj-! zM7Nvb1eFkOt$64iSjX?y{bhYCc5&R2O$6|?xQzW0Vu~0(-R{6M{^=)GGu|b{1a81L ziR5b!)!VpitIJt093tP*t;i<q19rNRW;e+#Hp)u#j7Krf)`&Vp5Z8|rH7JET*Y~k+ z1Qbk>e-;dbJIcsdG|2#|Nm*J1mh^_6pE8$hUXTDyXeYt5J!0@%(Sp)V$GohIuam$} z0VP#=eD0b28789PDbrW&L<QFIe1l=HkLHtuofVDOSN)x9Dn{V?i$nU<;Ta4nGH>Tf z-Wf?2zq3${2I!QJ<*jRfoTk^9(-J~%#eJ@herIiO-+AeSJ575Vi{)pa(8glYb=ffs z%XDIrceu8;A(gZbHjlbVD%X`DWk*f--h`72C71o#dj*t}w+@A&$}mlD1N}&uOq~7Y zh_j{u&$n367R=i}*LmdR7Qua%kwMn!RJoR;|1$dP!;xU^y$Ba_uK6@&edSEgl6*g9 z$U@jB_C+J=H@27an(<v+T}!1#IQji${S5Z|Od1=%=&0`}b&QT+WgP#iewQ;BBTjR! zuL%ME2;Kj;$d$SOm0UkjETY`{kcj^jN&`p_V?8%^o)`lf$Cq{Sj=oqwgRW@1FWhVQ zBtua6e={m77~=@`jz4=_8R2oisaz#d#CadDs7caW+zo{O*Y<xo`3Bxy$`1pAc1mr| z#RcwKmP?lqk8wd!H4KE32+9Vk+#HTS3R!ReL~#R8sM>QIdB$Khkv{hK-{uQ?7vj6m z(sL*%q}|*%wr<c@A3Z%ku(L<M>OME9{L4=?$h*Dsfb*^QVPHKpczFce3Y&7a27v?s z+M=z`3ATbD8qN2IC?wQ42Kf%CDYrAgIDuDEdB??pfSiAKIggifW7C7Ih`g{macn?f zuF=^TBhVzV;^=Cb6C&0n!mwUI#Lh~>0%XxGk%F2z^pFCzq<l8!oxiH*sxzHy!nUT& zF)%Mm)lV|9ML>JT7LAh%y{ZUds^A)<#d0cGx7GpyR{z|b#X2!wbSnk#g=j~pD7sLR z|J1ImrS(Detg(I;GkM)#tIwvVed}aaSsgb6HGcHw9De>`2c)qx$8vfy<NVX%g9g5- zJ!vywa2po2-7I5M8`F?r!diNx{cqR6U98IA*xu4!RFpQ&$lO?mE0;#HqA=P3Xc-CL z3uXqM5<r0vs<l20FDG%($X<}g>}5Szy{6#yDInk*cgroVR5fY$%j8RJHq90Fy;-Xt z%fIFY@|<I<%aT=+1(6I$%{l<z*wy0sRaxkm`u8gzp!8e(7Y=sot2p}h7N6(}Yh*XW zYwM{!{UjWaLSrMzExLF{^O*QtJLjw=hN3Nt>Cd;+&s)KHwtAhCw&mo9J&yT;Uph=J zTRtgOOsUf5R~A<}zhhi^2o!t#WSh;P+Ih=M+P}(L2Z|~rBYgl)QYa$Nh0vYrIM^+= z{PE??Kfh6E$EP!|3v<@*M>5?p=945I_g~lt=FW~&d$(i-+;)P}*Dv&2@-A#QC>0qE zEAY4CqW%N<-92}1<~bKeW#qCRM?p>bIeL_76_|Qg{P(K7m%UH&<pHHHKf*MhG~s6D zR5KXVrfU?v*^x4mptJSY${%myatG!Ss<koDGRzGm{8Ks7Zf<VN)qmaur_NV!KsAk# zE@w2<wqGPU6hF$+rA$|{6UonAU%+{dVc2f@p2{7oc%9jXQXzEk7<CDD)J&}&Tg2Bz zOYyCsdWPPcw{#&Rb1T$D-v{g0f6{4j21-J&)R$I_pWJ{N`*xZI8kN;fiw5%{&xwbs z!ct^>y|f`)akZ=)j2#D5n(H#vQ?%lc=s=~=SjO}cHHJQbY<-;h|5$sgptjqv>lgQy z;>F#exE3uI+}(;3TwA1gaR~%zf#MFu-K{tj2=4Cg1(G~p-kE*y&Fr)N?(<A0$xQzD zeXVP)^=s0tSD?_`L3n<34(^(BSCG7Kc&R;xw6TeF-<W`*TVNMWGI-g|CF7~_Co-mt zMgNx-_1@)>>MNCt9Z>6BG4CXk+Bb79;5H}n8?Sm~rZ3xEh2w7!y_ABf{d;--_4}QR zr%W$~aK=^6xoRXgq@}y#EeD0bGTlDr5UB=-ec$lNuzII=f0n)E!FTsiQ$#;C`UG)Z zV~FN8!pl9A{rb?@6o*TZ<s-Qo&#RKPS4N$eVgwed;AUuYEXp=D{}FHc@~Z0S*p;xq zH5$IZY?MuKVP~3D7(kpqpDt8;eyo|%%(17U6^CNP9A2T-yPHo0-&&8u3$ON1$OlUk zakAIb(dJ{G{d-LHvU%;Xyg!4O*30i|t~xJK<g|0^;}x7*{A$FAVYg)&_rK51VTJX! z>w}xsX8&DLP)5px0H}<DgHk`HJK0Q3oFCT{sDvv9_^i!OBo3*igPY~+!)aVy!u}SH zyI`4q=a60w)G)B%sK7PpGYUn*fCh=u-e@|7SJpY%hEzo>CF9T0z4-JFRqF3;`<bZz zt=?Z^6m{mL`LK@v(A^0UBlw79s)k28DWh!zlQwKkJ1rN4*2CXsb|UqS{nMnKD3-Fv z`4SKSu!=z&ookBJBDLo^L2tKl4zcuOcQVKM9)DB$=ky@@a2t%@+RuT6NljcCtz~35 zaZ4qB#hesZapV+NOZl9|E2>i-p<<pf5_YK@%OXC>>HC0T_g3%LmwN;)ur$Tw{(${p zE=ZzE^Oblhkz%%k9@2>X`CE+iU(Dv`m#y_>8^WOyqR}`v8!t2G=6K`S)?O0K(O{#~ zB$c?N)P)6*a%wzX()B#2pN^3xXwG_w++cJt{#J4%#e`+nE;hJ8!ixZgjSfIEJ(fI2 z67|)i7&6L&#!`PAV)A_{bBR>*)oCf^77#9!p-5MTpij>rNe^x@TS<H)Di@9v`HUWe zbb^?t5tU~_A60CAyKu1mJk80mwK)|JavC10b-zG;cv`9?vG-C>ySqQ!nnAqu%UdUb z1!LS<C5ngL!=lIZb=rmFl!d6gV_D3*OOdUQ=yrkcuvoTfYe_i}Nt*Y|ksN+W>DfsA z$~5MlYTrZ_#K+isaRuj;$!U{q9A=jOLn64e6TFr15(|>2;jxE?O%Qk?t|98;$0l%T zsrV0j?>1x!69{ta40LpXICaI2SjY?*LRzi^kqldwkMQS>73xLaUPb11ls^O><9o2Z zHP3Qm#!L4YJh0z=?5Ja@;W+W<-~sVJpA{T;mvnby?XY^iwOr)Y^iuab-zoJF>+8U# z%seDiohnRZOi$G%`%y>ULqkA~@uNwlfa_5FqZ#Wp*YhZc_oJ3|{@?!HXHV+PDkSzT z9x{yTDM6XAdNu{b5~^L2a0O2JXc^ckLiF1oTVLC)*E*9jiyJk;Yu>b!#yDJaa`f9U zm29GakAJruO(6H_XfU;4OZr(i#E5zlI4RNHmhHH*-PmTXAS<r=2WWZw>~UP@7C}Ng zMw`^k8y8usQS&U2P-tv0WcP!pg`f9>g`AB#;lj8vR`U!9h_ipK#H?4LuUX^7{YG$t zfnDu8&i7M7p%|Ncs@Ddd80NRiBvKAbWzPxReMzg@M#OP%sjx~F0|a0BRqQH@hh|kl zHDJ8pAbS-&J9WzOR4b>#T(a^{B>!ZD5cd^zc-dAusjFdv@tA(9dh^K_(s{~k4$wKs z4pP1#J6I6<J1$|QJ&ow`2zpe(BQbx1+bm0QazcKxI6f=TXP%kWe{Px-w3`*BKxQ)( z{pdP0eGf&FY%TWnOn6(icjqA(CI>Lsr`-rP1&xY8L7p@M@0fyIJwk?;Xe|REoP%YU z$Ph3Qslq@(YDO#JsCrNIUH{Vua>L<{;Zn4qkTnEY`vSC0Tjy)AGl!u>E1^%w@bN@z z(O`Zg!s>wq8kkvO%V}qd>}(Xw1E)F1q9O?SeA1AAbCLoE$Yo;jqX@PnOG@CE&1^w~ zZXuNv1AVs$YwYyv+n~kJ=5J$X{iH?IHh7Q^r3ZOupD|fqkgcwMd7;|41^1f%holiw zK=o=MdY=?>5#U^&q@zOQ`?6ea)!&BLwMzsZXcH5KG9|PW{7ut&6WTm-|D^mMQlr&^ z9D#}NB12_Ecy&pVJ!+_asNLK&vJs`{{o@KZZ)DuifdXkmB~tjd<(xCJH+*ZwgyV!N zz*m_A8>QDl^`A4)Sx-noC48yFQA?Jrjf<G5>I~l68sNWUG*g$EGZin2Kt)@?^U9ht zivQWZjlT@$d-RE!ni=OH)ZayuYU1G1yDmJ+#iqGK`MXXFcnQ1T&*g!muI8rtY%%%F z%GH|aj{0J)hpxs@g;MNp2WHqa@W*^VZDZ9&D2~I?Et#Wkl4g7;X|#1dX%R%Y8Wd^~ z0>7sjz)SVsm5(f|A$>QOVo{3bpFScZfbWk_ZATm8<yxGwbAL!?=f>+>N>lPa@d~%M zBc;C>!Gd;;nC%-U>;H6J#}vbH)<HL{8*{N|8z1NG3rk{Yevm%nfL~bj!TL8cvMgU9 zi**@;Fi?-Be^Qv5_Q+A{soizgqA<i@FU&bedSqj~VvPK3<)qTJx1ImR>8vU%o8m81 zTq1)!M+T;LCNZE7_GWr-libO;Sa(mDj}Y<TMAsAt>~77icFGplSx^Vo=!?F$i90N> zOOlkQQN^0ic6!-pT6=jtA1x@<5pyPjOE1BWR);Cl!)VL{i*J(j^wY85k)mqOpMF+( z0kMp%XU>Rzj!;HfdVwj;!9WeR))v|pXdw}`KE*=@UAiOtpS4V$5R&uGG;N;S>lgZ0 z-jwn2QN*&AbisXOCeLl?rgd%B;slm;kMnflqn9#~Eb?xl%`cf~;2-!SE=;kww_XP< zl9csntDK!N9x>obMQ_+MsvO<aLj8QnF5T2l{~YER=qoA<m|juKmn_Q~#fm}^Jwm}a zFTG_gW*y{g2S{GCp{p`z(aLf#?tch<cPvXbQJEY|Tp1|KU$?6X$W2S}%!vg#*id;K zwsOm(w;Se$`>iIWIY||L9G8)}l`Q0K9Bn&&5dnV!o;se|pikpI{-s)bNsd4iOyp<v z(9}w$U2QX(h=*0rSACnmL3CPKtzEg#^txV1UL+Q&m`s+gPPra|4_3vG)_aGXf#xQs z-?a2bEAiRzN$Z}HSCu%*WVw9mg|WxzGBEbS9v&5ZdA=7PSH2+19x%+2%gafq*k_(w zPZHd5hO@%#nhcrRAcECbl`lapwgP<Uev74fqg8t}WhwE<u;kJw#F+)MdOFCtHI_eI zp2?Ae@hF<QWTRJMyIsY2@VECt!zAA>n^06%`o@W4urR&-G%t&4+$~jFe%}BNj>rT0 z=UC%v8z+p}=C@u}qf^Vx)1}7jr>c3SmG#c7<p$-*O+Y8U@+&nDk9mYwAA#!j?#kpQ z+*}VhmwTe7Gk5CzUNdWO9l^cf+$NkX{6f!3)NChp5AM84g;`8;?rY>&79EQ)RXjck zdjWT41O&|X9SH+<eMj;9cXky;h0n6BM18mwv@<9{G2H&N>)r;*D|I2!TIQeH$!|PD zR&Q=^Pke&+ymD8F<9XUlRmyc&|H>gr#4I`vPU_7(=Qi|k<QnqM?VVYJGvtKyYBJdg zDhiw@4x%J<hfhb}41n<w9pLvuGY$C7>w;1G=<G&VX86?uUFgWDFRxN-&$RxXzm#}d z>&?t6lW)J95VC{b@ruD|FhM`m+&5pmx(pU?i~}PZ^*$hdgEcSc&J}-j@YV>g#kWk3 zOGuZ~)J$5}mM>R;hAPA?oS=fd8T=~ttPHYaCcz3cag#w-5sRA*3%q^osw6woXs>bp zc|X3MnePI4+Br5M{m8`!#OXrtNj~PtV6K3B`UANC+Sg93tAfMomY0AN-r+y(RK8J! zILHH8fSO=)#EKF%E0xI7Vr8YQx|nY@qk<zp(>P{+r*VG%j7eg-kdx4T>fh6wgT%~U zl}W0|GQ&u;a47$&VVY5ptiH(JikT-9PN^-VX<&YDE-A!a?&F*B&4YE>w1)@U(-9pG zwg#eMuq4mgz-UdgERA5fIa1>YajRo```)_B%7)>v)-!SZJ3y<%IxbA96;jLQKwfY> z%vYTO038Q-z6vb5Nb!>Il`q!oa!8dj8ZvWbZ~;Wzflu=d=6~X=l8K$Df|PqvQxV3p zXe7n^`(p*WI4yslYHc-@l8>>UP?+g(*(J)`_EJI~V22n(w&IDkWtPQjC!<t>JQhg) z_fCHn5%u<hm5uNLT=5{+hkYm1v-|p|{%o_qs3f?~$u7f>bzE-?d0;{1-+v@DdY)b* z@3b2I;UH<s55|9s7Y`pY)nWuWNw{#4Gj$KdCo5b^FEOoc*xsHtmV*T-zhQ!8TXt*B zty7Q`zxvi8_zN;^T3nL5Q*(3s$EUfXHsHc{Eodlx^vr@Oqh#(QORH5nj%3<%mKF)Z z!yKpi+cu>+wQ1FB#!y#D1b-9fWZ6H#QGZntLm^1!6bo|(CV3(WUA?Whlk$079b`-K z^!fPl?Ku(!bHQp`x2Lf}Qgo2W%gv?yO6Qk<eQ?G(%G%dF=8$M;jv2z!CezgI-av<` z<sz=fY59P?pHX5I<i{zEdW`&Jk~`JGeXRb!<xBnFtrY){fph+Ur3g(HyOV=e*Hqq# zwp;Ii$PFQFm%1VjIE<b{<b<#g1=rM$$rQqHQcMItBybMOvW95~HTriKW>gewSY4N% zl;TY|Z%_Aw=@jv0osyNMSKWhHADyO@7*)0+8A+rvCBOB<&Yz~wczS70%>fVBJjIrc z8p)U|x9<_aL~ouFlHMXzPZ;tGEGVgZMNLW4h7dp;Wg!{3YHtFh@aD1~<(Zx_yB3u` z3;cR}e2XndhcBaK@wjf_$(~S$qH7I6GYY~-N`Vh<cAY?)?KoEv+E16w@gJ*vFQvTM zFKxZzZEet$%h_s2nLbIDcKgNX+Q8|r#-C2;3U)NnDpxNa>i1w*n@1*<v)>SU803q- z<31h&ds%OaZ1&9~mkS9E(Ma(>K~YfCY=yBL!J0-<4rJaBWQlNr+RE#K6N#KW$7|8A z0+T}udRrZk_mhOE*8$G~_X@CVZcPNc<Nkg^Qll-zm3@FahKdByG36!u2sRZ0a^O2o zug%qRRQ|ddv3NX)ot#pxov;SNv{C;STC5_PT&5AkStGMHt^_vZd{~a~qDMG6Wm3q( zAfn5dhF45m`%}Vu<6Q&HY-*XjGTPJ&yLtL1RW+I)CSla5RQQhpPSquERq+w&U?izu zH;6k~qs}nfGs}>#m*4-aF?bU&g~(5wM<X47bKag}h4b#HBG74N*E}HwG^-6$uU3~X zfTO%?+OxY0^{NtM<k1ts=%|1k^<6oJ-(jHAGcT1j!#%*utN8<|FjcOlm$piA=C$MS z;(>2rl*ry`4`Lhsg<IF_AfmZ3#8oTcXHfhh<zIuN?q2`K(AH4-X5!N3?cSfK1&H1Y zjj0j$>9Fipm;KTttXtDTr@8*x2UUS7&fnTbgy!k^ukOIVTYWqAA8Xx|-uQBJ>&{BT zp?*Lo{gq_KaoaQ{=Nn%}{pf$M1D7t36wZGY0!`mc3~q#00kF*jjLp~>iiyh99i5*a zAo#95|42<Wj;4M{>{z$H+f-pzfZ&J!GpoSJUkb$(J1yuzBy4z+u2whLzpr=dgj0_- zmWH&C?B-S`m3C4!j4)K2y?00_{ngQE6n?n0ZMn>2svxzR$bQP6TVfY3qRgDKA;-@{ z@J_;n>rKqjI%d1q;<J8xAp1ohb^mg=tr-OljvfvyNBz}7@b;?hHE#Vvgm2AEPoP2_ z4Ykh@Q1h)4IQyeGf5K&<y5B(8klB{M$=5@ZxLyG*EXGzoqYy9{wS6ML9i%z8%E^eD zpX8?{lm6zbEDRi?_dVBT>xO33x;^z&Dtu>9bQ<6CLxX$r6BhTIp|rt~9MY`lvLENu zYIF2iSv3x@UXCVZXQavAiXf(!_W#K}T-PRP6=lq0!#iYO?t-PI<}V2b#_?R<VE&k> zzzt{@SXE%apM_wqgn@2-DP7e~>i^RC@C}!!m>hlbEIW3Qp#^!7(mU91pR%3=a`*Jg zowG|1H$9wC@9}eI${rj*dcgT({5;}DVZp9rRFsG?Wt$*BZQF0smV6Y@gMo-NVg>t? zsEp*;xLII~rmzVy>9OiQ<?uv>$Bc}6BmpE)R0BfFca+-a=zcc$6hiR}q;E7N*FC7Q z!C%$cF!6j6qkivg*pYGktx=dhK{Lbeo9|s(V`|Icu2r66Xl%Yu#)1ZgFuhh}@UHqM znqyz^EYQywIoH1ZPUUu8n#|-WKRa#)v~O=fJ$-?@1T`{Yh#jH<H7Va1pe_Vs9)YED zSJER$3d&D!H@F563&ZclwQ(kZL*+s7>Lw>I+2#vGO#Emq?fpZ@M*G}|2q_hkk_0TM zS@vX3yG{>h_`A=q+6@TAF^b?IXR3=X03_1hofc1fc9F^*QS%G?#Mln0h>7l4faLS4 zTwY1s7S#1Z&HeqS_i=MYoX-FHAV#Q=c;_#|!r>jI5p=8tN$MQ8Je7{&FSUe;Djz0n z-20`rwY8W2^l-#XiTP()OZ1rjz`;rROpGM;<Eh2Apnyuedtu&-j$(u+-QVR9qg2_M zowcf3DbXi$fWho7o{0=|;`82W289ea<vXQ$-qxeMlcDZnGyKVWWe7&CXhW2b93j|H z4h)#{EC;*!1|Bg}m3^llp<|bzy>>PYmwYXKe<C@Yenui}Ns2_M*q3dV;l2<IXnl}) z9Yi(IuHuAzH&;q;=aL>d#e_O<zEn}f8CH;Un6bFo3btdh+b7X2`BP!555$DFo}q2Q z+`S8Hi&D*8qC^;0jR=j~(Lg6&t`AQ=Re2Ua?bn{;*cB8cs+l<Qx#IdN=rW)Pv>pJS zr|G$vOX{vdnL!m{<rUm(8E~RQEu(c!t)Z6aHB{PBDViaAAG&+6V9@u5?sgI35=*En z<XaT-b`?SJMjscSeDcLkPtT)@Ksp7L#yb>m^Ih%D4O=1WNUkA%P~wY01uHo19=w%i zeO@}g3~i2LhNgj8PB(m_ymy{`X{y%Q5~|q)5%<fGUnKB-!x2LFRvH8P3QRQ0n)fvz z6r0zZe;9BYKaG6b1mO#IL86@xsU&Ug9tF-a`WiFRWE}h|glEwHW@mVM@{{EslKmI0 z$9q;-mtU*isG}^9d0LRI!u35rwyb+55|-{+jv;M&b5Xz)%g%w@<R!9+;ZB?=z|q7T z`N0RJ;66LVl-7X>;d>>eMW|$~@_|&5?X$b?VTPy^Lbp5fnS8g?(Q5>--}7;NI=>Z7 ztYn<jd>grX__)p;c$>poPgtQr)$9_!@A2BhPj?>wF-P`fid7FH1xLeL8&t73Sl4It z;t&g;5)qY<;H5F6rG5P87vztu)Ne()dp@XIDhbJ{g05azO0~X4fy;mwkH8Y#eg(ef z=3q>kIiZw!?OFWJUn)9|z8qYs;<Rn0r0hsQS-fIT7=E{_@8|mpA?btTkCW1?*LSo# z$HS1Rgh_`I$`DE+CYGok0>L>YSt7nQW1|_SW-w%g9xQ`|qsbKb*lukFi(r(V=b-~Y z%>w$#sQ73u=<$Zq7W{&PbFgjTe`)U{e)G=hNMNXh9q1(xH@-1cEox=@$N)no(+Q4V z`=w3?tH5e*&e&cSkq!@G+z|IJ9x~CL*H{qj%h!MRZ)pBBCc8PY*Pupnpdu2N@V&`F z?NsQ?R@lF%nl90#JiZlD8bMoeVusOzIB;QqWdF;}Md<I*{bxxQO;yuyu@~IHCByQE zV+E%K-+)gHrC;?zGwh^`<8k?zXk`DVz@u(LOE9!=>wj2}v~yb>4@$Ml<I8*+<Oz6! z;L`IDQelMN0ct<vD(#EUE7&%LUBb^rld6s&zzQHPUy?pxjF~vq*K+9JQ<MOuMt?g~ zp3=jANG6M|jU@0P&@{B$DFACdH`^)0$MD@Bh&*w9vdM&-vdHI<O~kXlzLHI*qBzvo z@@yIcpO~55ueduFzGeGTWt*E!(ySqor^Upo@bjf<bKeO5L8|9=ybs3ZCXI-nweEw0 zod^Lt3zb2d>OQ#^6moPdQ0I_-;cY&6?(PJdQli02F!$6IOsZ@vEmXnJcW<tlMx{it zg^<5`%zCo#trA_ma63Hr#?h~TW>uC}*G;AuHGXUDRMtxFXtCo5xBJ%c1by9iGPL#J zUd{WX`_~;pO-A6-!;8rxD=b(t8*4U{DKy|o+3CFa{XRd}sEkN2%kWXCAmV^&{rNdn zuBrnb;o&~|67nZd!IRi&T<)zT(Z)1~C_3P;xQcQci$AIDL#1$oEE+n?6FlD=#&d;T zmhyA>ZOm4*GYU7nt^MWIEn=>C_@qy$R%8A(N;J*jjrY9Lq{r~zxA`HuxS<pO5OnX| z`^|Sczx;lO{G~*<abF0Trh!@hP+r_Mp_oSqout$1Km98CeM@Y#NX7L8o+%WV{)Nk9 zFPu`PltF`9R6n-+ro~Pw6fM0+!>~YKUBA1Ww=L4i`Owmqj8Q%$Zmhcl+%GX}f;2Nu zhA~C~jRb@BIVw*oejDtaO}EvaI&Pg51BGeNLM*=k%_HCYTrCnD%$FaN(+gf16JIx{ zMdG<gh1HgB<DW{78=9lLDUsQ@=XXc{Gop2U=>0AIAwSEQA%!ck5c4&_aCa^fGd=vJ z{Utc4LinCQiOnI)XfpI&7xFZjh1&<r&LOU(!}o@|weK+H*M@h#wVW@0IaC)FEGS!> zLEZEh<)<_hP!gO~dTjhTC<f6hcJeu8c$#+|xqoCZVE9g;r2Qcyb}X=@DV@&flx*Ri zHuc=Dc|@Q}>BDA24%4v*5)+D-QUM9ARMCp{22b{|b2^3hzNo|BUjgMDu0x4KZg`~z zy>#q<h5a}V-l<j=FvJ;c0+Dq3MyNhFE~ge#2FCD_b9F0x;K4;}ckc)%1p^k=E`}Hz z0gHMLb-@TP-j&qQw*ngx;rhBC&Z?^2dgek^{tgqw#IFI2hnoTWxf&}a8_#)u!hvZ( zJsDJNN!(B;%r}A<&63!Z-sHaq9XJ~)-NILJxEC_lfq2SK>G}~~*7d~8*JtDzY_l_+ z!ynMcBxs!iEYd0p`wnlPf2Z9)V-^(FMo_A2%@*4#b4x0%hY4EwD6te4<vmL6yB@9P z%%we>W<H0X9bck%nH1p@d|_ha!p0oU!t;$@%iH2V3pxC2bf6BhY51sWUeK1aEmtFu z?90oAl0bW~(4)CPj#o<SY*)pR?!<2WclR^DG8KkTMK*PtRk{sWK6UhBUuzNX6GCIY z{6yRzS7g3ZQ5|-mqM~naWTmHP_q!&CW=_|@q94&rHwt~_dz4<q7Hj;mwLZ~Sm}iJ< ztmkNA^FUCid!TqbB3s+_w0Sv^eq^DwKcu%YpueBQkRa!;c$!~9(vSB>;ze?cq=)nH zBMq2(smqzulaeOGy`?ysh#JkGdsatK;^B>sH5lMn2t*;%_L4=tJ15?tY!w1m#wmQ$ zNAo$hdAD1Cf6rTw89tiwT%5IKXpdlnSZ?aAy;?O)UaPICm*ybx2T7P<zUb~O_U@k{ z^)7|1n-7kpKaY6Jr^Ex34)v`pb&MXN(YXOlP)#-8V8UD+lMg+s_PJQ3NKV4=52ZC< zEP@X!YgFvBfM)mxwPU^i0#&5GvRoa!uHI*BfL5yf^i$l6`D+)Kr1PvDVO{(2zBfea z)e@F`+w$0m`PyMte@omuecgg&OZbv-b+N;)EoHUGmiklPhxDdE5oa4iMIPsiRV?4e zpFm!tQR3098clh=L=7ZN&`7U(#+W{iZ7Mw&n`BFOp98QE7}{^w4MN;pbi{;y>ZZHO zr~ilaZD<7U3AzTg7Dn`3{;sL*T<1?wA*b5AtGz2fQ6k1hsT`tUSke7d>`1^R$vB*k zCc}j}6F8DwQ=VJ~ZUGkYnaDl~*^Ort5@-7Dt!&iv+d1|S8uz~`5mk%<S#qRLiOzfM z%n`VeeVBKy`sGno3qyr=q+Cd}|4I~NS5B!6fB#EDyW3o@ob&z-UIR($ipJ5HS$B^d zb2oeruhIi=jB@YbH(y!a+RT#8k(Bi^Yt$WZ>xt7jU6fD5238_?KlHQj^Iewp<PKl^ z_nS1_lo9R4VdCcTvx+E#1gFD6&~)qa*C-3$B%BvBdt)Ybbxx7%)#H$IW%hZ%BFd9J z_a*URZ)YC2+r-XAr$T}}4FesAl!>t(tqKz7BEU;z3v)e5Gkc_C>k0Q6lfaW_o>>Wp zt5OCdO;G6mn0TX?>We+1K7Dv9eMO(Ip7ZT$rTpN0c6ft<)j{+iBkBW|4Jpk+k@tmI zMa9Yf=z=vTqXikG`cX+rXdB;w)z_*N&4)D0O>uWiUvlBGt6HvBWh<n|@+z@s<XtK0 zcq;KpX?N)^si!ZU%@LW;0Yl_x21)2>HWNeW(vG#)S@#&1?un*nG!|ykPwroIWA>SM z%hX%56RQUqdh@6)Ibe!~c{|gslRm?uw9MLxRpT@H5DvcE8kN|Gh>}4WbTxxk`aM=y z{S#>IsFh|<_=uLL<Y%<ItJ3MXw0**fHBRLZ5=KJ`3fd2ioi<Nbt8mBF-}|f2nTDCA zr7`wCk=`Q!X6?Q^A>xr*g(|+rZ09j%J98;}khCI%*__3h(mb);OS2d@B?~7ri?u1q zcV?4-DYYD*l#Dl0`Mj&Jx4%hLJI1bi3+DLk0L8(#N*r+Q{d_GPG_<*6H(Ych@;AQ) z|Hs$2f}*Fuwewnq2KTQYOPc@0AzyF%B;%|2Da=EUHT;fBzxle_%NAEx<XwQ%;Ok<5 z;<Cctqe1Be|Akl)h{?!$JG*oF`XIMBwjkqXu85;P)rN^O&M|T|EVyT8m5UoGSIn7Q zRIE((@$z%y=EcxD>9jim7u)!_vYcW+(srIW>VZZ5J_kEz3Gyj~)xD4$kLwTW3xhb5 zbJ#yxP49AW2tNw$X&Xn&eUenya!7QefMoA?<P={5$O$!;4wVnJLq6%eKY(ez!D)YU z!g-yj?<vyOMEua?=i0@7d5Ln9mo`OU28G+V67|p*N7lQ`#n3q^hmu9iuo`jCs#3MX z4~Zw}w}L4EVDUCKQ)yyZxZtg$xw}AGFB!tPD2Jb?pJ{TH*~`N67sKS>B5_tDrIo^w zog;V&hZy(8kc8OaC|Rjp$z|@fW=j)sAmw(GZA6*IlJw8x-|oRJY9?$3*ju6?v7>h( zzSlV8NT_<EaiaU2?f|m=wUv*LwZ$;bp#PWr_y2`a|9`*9e+&OB^{{;pNYUWPJ{KWr zE{F~~))cIG55$6D;d*V!UZ`hBw#TGblbc>3|DerNn`VtbUmtq^+oSO_(RJLzRftSY zO@7h9gQo7z$w0-hi)(?>>*t?KLtzCiQFY?}PY{HsS?&Uw&RjidOJ1adpIn=QDcx9` zrs=!i+4dy8elc)kRI+h?S-d?n7AiZX!2WEA@M*E~B&G||XcXLw>=Phz*(gDA5CON! zEas4`;jM!*tz~Aj<Yok%MX!QTe(E5BI3*>en`=wcsF&1ox~aj9?@ynqI|pl~XEFZa z0^FQ(_;S2MFRA+y;}bV;zo3qco+yU7MQZI!JZ;Iv#X(PfiriojuKc$N;PlS|l9AfH zAF98P&0<lbpV=87_oP1{@X=BlM(?V+gaG@)EDoWnWWwlLQ`IPW1&pgPtcpl$br~y( zB=bwUAvo0>n*RdK9=+A@;+i0$T8L;tZD7t`hC!S>hi^vb51E!(4<XX#k<MxQM6Kqo zdsf(^(m%jSdTTmrtd*dZ>;e3p1VXh$CBmynN{G^Tr90)qf^=;dt2(hfLd0n1GH0L^ zGVtu*4zT@HP+p#l`N%+ECW#G8RCF{=M;on<WL4dY=wX_3t}x^b858iiKm4?wz73wy z+tecw?SWU{sZ^M*&n3x*)&EQ|Qiwa%VQX%WF{g4ZA-YEnYd>2;Oth_!<>lc31LYLG z0onEsWV)2eKDVw9k4;UR2D)=F`r`)Pn$>S&ssVluP}tWCLDaS^eLDlv<^yCG%3Cb7 zy9<8P^=N)9JMBBg<89M<ws(@j)_FUUfy?!cx4IJcVnVcQNW`wn_wX^&-NBW6Ewg`o zV+;oTd?pc^KN+Fj%&7yBKQ&n+H5uXkBic^yfiHXze}XENEl_1Xljph4pR$di58=iI zI2XmwZ&PhPSMme~%6;V+;W(kMN@9F`02D`#r}$z3Xjk#2C(4N=Y|q}!b1c71N#qCg zgrelbG>_K1{+QI2R$EyS^9B9+>r%?r4CM0lTI8JSt!<i_r0+~&qQkklWxd6WR$)oh zQA~75Tn<5X+IzMsZvt1%i?exFE&{a+JB^zPqtNQzuU|};qalCmY;U~*Ne#-e#Tf#{ zb-78vq)d{?;cS@fBmPlm8*3mZTUKpJZjTIVSv0M9#LQL7O0`?p7iQUe3J`?{`DurA z7RzGkRt{0?<8ygbZY_*)hb7F}K7#^#jZ(qfSdg$wOZ@=INFh6C=UzLvjCm_3c-6HR zi$hva49a*YtQvp!N6(99{SHnu`2|<9%wr{Mz`(9c8W%^;L$o#r`B<!7U2^l;dv_f` zW{3nl1pJ4Dsd%?%uoUflZ)s|^eVeadmGJt#L6PFC&NM^1>i5rF(v@qg`0kf~jEJa> zt}=#OVobp0zvm#MsQ!o=RPzYM0{jY-J-}X`)nB{E)d~MK2lMh0jr@U~1*DNb-%)im zux@FB093J;(;{9OH#-v{{HvL1;m9_nO$9rTOVLKJU-vdg&Y=p9KXBvn*Im)B6jF<d z!@<f<33hshs%W07`UHDX*2FK}M-@X&1BInofJ)DE4vY9f_6&O!BWNmHnBl0z=her0 zspqH5k!I5?#!VL;QzMuf8F-@NBKD_TVN@zo{&4j_v&WlS^Z3X3%^HI(m&aqWuh(Ds z%KiY$Q(8K7AF%Tog)pP_hL&@vd&Fy6*)=<FQfxeRO}G@a$^+V*O0Bm?P2MNy)sI<O zUxl1qkR!dW_N8_4%KT+PL8<6x?n?BKd5O1NSLoVWbxBF>oY*FPUyV<2>+(N6d6}JQ zJ)}eK-p;=0>j9ApUAdgV2V}h^awGyvi?E#b#445+LLkW%8-1lw=Z8W>r{&xvIO?T( z1r>j~y;hTLF-eD&tfMheK`K{4dqQbsY1!VnW6CX0<HeRD`I-0OIXFnH+tOs2Wo1_* z0YWFJtJ4hS!W`IQgf!v;rkQ@PhI|f4gSnLiecD||Y=A&b`$F3N<_ullMko>~!{+aA zrSg+Pb|qtnXZ^Q}I~1^L<zR7a#SL+Q%j8k-BJt4YhiUIc1$`xI3HKU+`sfQTZlZ{^ z9AcSq*sZl+s6N_`!T-AmR!&>+xHQv-MouhUf={afVvM!3-V`r?zK;oS_&)Wx0OsPC zkgx0&H&r#^1XU4Pd5*MCN$BW~gsAtu#BV_*CGO4(11aa24L)498XKq+w6r|?94Kom zVe1xGKq<>GK{tr(kC4tBOy_W?xj7cO*7M<T@kzg>J+5#Y$Qu0|z}tAEmX^-u#147I zPF2?4<By^!(w9*z6wC&lAPz*c*AXQN?Ge@t8AeRV`Z8Q|E*HPk&ta*^-->?OiQ0B* zW?p|0f<vJMY}$yNF-`iQNwdMudE(?oGFv3UwoP*M0+U_~bT1Uk+Lb`fvjDe$BlRXC zkyJ1j$oet=cU0O4Cpc;g^zF>C5Qm1DkOZyX^j(*SKLT(BmAp_a(x`Qo$oi$WnRfbt z%zOF(z^UU%Sdid}O%8ZN#uI3o$P{R!oJ^x!$7Hc}g?cjt>-kJKr*-@%oi@g&xiT%N zH&p}dhV~Ub25nW{aIRE!Jw(%gxI$W$z<cNl5!oqgG5^MoJQZLszo-|5lH*I9(%@t? z-cSuQ0koOIc`gRT>;zXe6Y`k?(tf<8J|N7y|K3;xIS=kAlAQ4Aq$!&<()JQHdf4QU z(ya^K=el}cl$Y>2Fgdy^p3BrTK;wfjJN*zCD&gPkFA>l8b!sKRSb|?2Kx8dKY|B28 zUHEh7pEw+96NSs(Y+RW12sG$*P^w#uN<m0Jto)2vS>l$KKRM|L;G9~Vi#eBVxk+c8 z=}spsvAT^|9Ms-F6&B+xRrx8+`*NvjfBMr$7OKaIVwhLa3W2{8&s+-$^-|?M<p#}F zN9=4LcH67<C}j`S!rAgoq4-_ix-$8cjb1#~A-kG;n=e~&(%&D%vpK;(k7udq{kmch z?w3DCf&ENAL&iR%Pdd^%%N{P8oc9cfVbmSruyUncn~R>fIMrRW^AS~vDX>?RWpdPC zJat31tAFE6@Qk@;t@rb!k^-}$9CqryN~EN_Glwk9SFF6dNS0*sKViVk&hHycmGHOW zADQC1p6H~2@VCW1EZ;IHomEZdf3`6fPkvhPvO)CXPU(G~0FV#qtLsf0fA+iPAnvVv zA~l%P!|BrS<seo0LM*yMsT0Xu;WdE+uCng<-Uqs-85jPS5Z`5ifo!0!;tw87^waXO zYj11W*iVltVARqb|2Zx5)mrW?ZVQ}e&f1O~z2@&%VPPPLZC0fvwc=peZ^!Mdz_wd% zB6M0AbJsqpxn`341%hrG$+GfnGo9bf+Vk?`Q-;(7-6SO`KeQ17XYiU07vGS8D95={ zuISCN^c2A5e0!bhG;vBWfk0Eo^I&ENu;ALpR@JDby|)df$b6?ekvOJ1K`o5WSo+?2 zJOcC@PK>&%CENZ2^8ed*;Mc4~XUxPD#qD6v3L34`eRv4uOdMni&m^Sz?t%2UcpKGk zqI=F1GTrS#Jdd~>rgiz!*p(O=$m^SkX!4}vxp9A`P1DW~QflwdkW}_EPBz-QPaWCM z)9m!wAwG#S+E2^W2t{T?L6KI5!f`2Irzwo(JSLH>9_l`$*M^j1!a&_&I+!LWg$`L_ z#ERd(dSFY><Y_-qIB9c|1(CfhqhLyGZmGU{$ZPIW^Ura#fXm9_zD+jOSE5`h&z{h| zVe)=SM08zjr={3a$Uw->JX-eQh?d|xMSWGG!K&di`Vt@{Dz`K@z`?-7{%f0#e@hA+ zVPZN0jqN*!SA?i#)x5V1=MF>(D!p+GX%vKZXzv_mjcAv?ayb5&kg)#L!sjTS0?)!0 zLGK<b4O5V@hqx_4*F(Z+^Bf4+{c!QMA(U(1)wO0$B{AF2J9UcBXu73X%;`f9!pM$Z z0=1tDm-6Y+ki-xPfk)`8;$uzL&$<F>sTdlD$@jeZ$0D(nr|iLd77YzRf<%*1YmKUS z?u^a7`MLvQ<H+{i^-#Z7L{)G8_qe6903B<#pXpgnKXcxjDU>|0qlII;VfXfkhr{d3 zlkqXOz|vBireA!y-rLj~mN)7ty!n)mIc#*QCA0hI8lMym;qj#ebkIPTJ*4Tf5r1j- zuW^`fbeB5*-j=5G1;|-yAy9RFd$5pizzOceXYroAun^~jNPspuYvspEoYdUZObWoM zo!xvqf!=)`)8=N8v+2BM`<XGZEw)`8zuyBuVftT7HZJJwf~BQ{IlnbC#aVZ!8C?|C zO=>>EJbJUOu1!}(kcB)#rMWB)jBtg#j05^>jNQF{v?I<I&7MSSzT`n|wSC3&cbeqf zWyzJ;NbL-Lfy+sT^fs;Opch4?Z(~yCgkAq3m8Ucjp|Nu3pU?{E@Vy5A%{{(ux)~vU z5Ktlx@!YfJ)f;~iY$TgQ)1l^eA4P=*4JL_amGOvH|3uf_jsnY}$3@jA6ZXicgAkE0 z+_wkDJk>=^!K|aNj`-@58{ZY6s{fi$hm>vLcQPRhBV{HV)1YDa-_1Xx;HFRPx9L7k zZ!X;*`w#F!-?|yzU5x*nE9RlPKa+9uDX|&EyugL;cJ^{$O3hyKDmyc2S08j$%FqoT z{D;I%?}R^^k|zaL_GG?HX>4jl{u46}YALDGkh1)trW-=>`v=8+o!wbRbtaTuopX$< zE`|Pw9*WV5J!3rb!xN6IqTL*cf?$f5iES<s1rOB6-dI!?ac1J%E|GyvGv2uRK=23Z z_41Od!k{mnggf~E{v0Q&ajPreq7*0wInT6*7gXeC3D@cmYjV{WM0~K%=flQcVw?~Z zmv%u2ZKokLo1Q<FZ}FM*7_!+QHCf)WT8;kFO?oI5qyHh<oKIV?JxBkvH9Bv1_fg4m z0<WYaOK|LDDjm&GX2FII{%Xb#Z9VC?TA5Vw&uiV(RQlq3K&GGc?)_8}g10M5E7g4h z1L;W(T8zyBfW@d3+lo(mxyCM%u^$a>^$%J7sI3b#Qu{I27AGVH?=oYND@e5WGKs_A zZ}{ZZwy&iJB|lNX>^j1@(2MPpOqB4-65nIRR7<{3s-Mmae$HL-$|+8{Fx_AJs*{z9 zr_3g&fzePQE&evF`sRjgU&MLVj<H~coVpqG8mCJgd<70_O&DW#6e-Uiols`W68~tT zS_oE%hQKEu@hiT(=*rS4ipM%2qo*I<z}K+Fev09|>0Nq~IHy@!y?ET^J|Q~AhLZ1$ zNQn3Mc)YIMmGN3qCgyphe}*VMYQ&p-274e$S$q>`uZN=5E;kcR^#UuL{%rY!MyaH{ zsy={~m_H6*GHc_>0^Ee&DJ?CBMXbFyuPM#OS4@t@o8oHdO4moOEOqcCk$))$OkI>d zJ~6w#Vr(>Xf0+R6?PNJnc}Y6Y(@%BXO_@D;l05NWS_tm)e%DGL`H30%4Hh6kNm(Tv zZM4Vw4Ug}jfkd>+r(q)f`+PCKj+k%nhLEW@p(6$NC|O%;Q}B7*&t0#Dhj-72-jdYH zSNjG|NkN@1tj9X*&5wDf>ac|EaL(i}Ck2Krzal*b@*`CC=AavBJ6j?&maQGxuGe65 zhGK3uq~TCeTHV8d`tGnLqYqP)t{f73)9QkOweLQcjWi|e#Qd3`uz)C`-46~)3hp3g zS-MU5*~hOA&XtIQT-5=GF+pE}CN=}ny_pXB1RBNfbqIXdK6k_LbXv<<P`p#PP$Rca zzLtX^YI%zL!G_3euO?T61*grBc;lc{(TuNGn+pc3yNzdC0ymW?^b5-`h?Y*D;E1N0 zsdgkHKm2JO?52ElcK+^nvS6fnX3c&Dm6wvpQJ#~@?xA7<q+C>|8)rAw7#J5ilE{mY zKZdMsco?4c&DK}95WBYO`^Q~U&UL4N`vwdOU~c!-YGj{;*>Xq^t)^>MIACv1Tpa=P z9<Irj)z|heT-x1IrG2V(g{j603VTB0{T{v=B18{HTBcBQ-IqKH=~r5<-foicosk$L z-jN@Q6*Bp{wSnNY*#`Legr{|hb)tGCOyp%9<Q^vnSS;p2oy#-jvSpCR0ckIFABHNC zmOAt2sU(*}*lb2I6m020wKNd3F(yd(jr{bT320dsT0bX3G$Gi-Pa$0bpuGX(-<=hH zkS;@HSv4Qw>$?>JzPd^RkowO9fLgpNoBJiA?{<u5z9Nt2o)g&^H$dMOH$cl`>d6ho zyDEB!Q%NAwpiVPmQ?lrN<y^qV<8xtKYj3)p1@TwLN5n77c2e0&qzMkII4dZvOq?o? zYeB0m^?}x|>0^YF!aFpZT|PGW6IlI@nHME}g?_7*{aK1+axKCAhD5a$x?%aRwv90t ze%V#lrQ)z=<J{dxamvOxMzU94jfpJxc=Y?%W5`w5<=5uy1UC^`sm0!7FYQz@L1R)a ze($ppYUIwNy`-595B*>23W|%B<@HV`M!Tb~SwHk~sx4cbB=o@SsOgbAWJh4l)>Dcd zo$xpigV(9)G`Fy>`Uf|WUl<|8p%e0_Mo9VNEk}1FU9v}co&O<y^g7pZV|RD#3<#HJ zXY6Vd6r>@nkou-5@c8g`O}aO_CHG{-_162%#Ko@-#?gjBV%{Ow(&b9|sgmR{HtPi} zr$NS<6!$F<<qtbOs^aArXom1FZ1$(OD`mN@MC6QeR4okr5C2we@vSG#-z~$Cr4DPH z@nv<XnIMzt(+qv{EFY&S>!}1z35<DyWRimhULCluD96_)fNn8$G*<*odW&f)NxSSR zCyO4g80wqD)Pv3#XKsFjTq)s$Qu4aAuId3iff3}!`Sl6=>{ddAA9OKw6@CoKU(*5U z5fA@rdzIuaK}BvraL@KWjgzqxONjxPBZTR$bJ$dsDXv^2!>XTQ9Ujzv-(dWX2avFs zSy3&?E8RbPkd{2~X=D7e5M#Gv$Y)mCJl&RN18I*rZHU15-0yq6Ds?okxp|dL<xOy| zOd|uYLawlZa6QBJ+GaxUWTg`h=oc*mbAeyh>o9HCJ9y+cp0MD4QQLjvpkc*(&;{yw zP$Qpf3k%1mR8T{ibRNymUivFLy3hD)VV|UC2dnB=njpHa=FRGyu@>=flFE#^`o?gy zKP7i3j&{*2_wENW1W&#wK&t-#VUGP@;IjXa{#*QC$+T51B5~>qt-wk&u<pXq_57W# zQt1OHi%KRM&+kYAF=r9RjrlgdpE5UH3VSIs2us2nan-i5Z^A)`TkbjFT~i<l@NfuV zvW@r<Qbp>5u}q#L1l|noi$ezQ?}^F2uo~Och9Y-^=C)>9owuvXTWVGdz~QM>?0iOX zipB?Kk#>9~BmF4D*E&;FHZlFUwUMn|n>5hdq`8I*Q|iA*4x~p+94Pf3gMd;Q9nQF$ zv+!G3U-#t^+~u(Q)|+Eh)svoUFs^>VGCX2Nw=kl;$L)a>qqHZUM5*g*j)8cklS<i| z5z0J)CaPmPQ&nwciTheI$$a@Z;IUmTm_jJxia!BdR<ejEacki@hV&~;t8j^NS;^yd z!Q7~6R+c_v{u}@MobQ=XA#k3w6o^Z@V{l^vY)_^a)`MazBjw9L%6g!1f=o7@AQ@6i zw(ttpgEG6hyKAd#Esi=PQpNq6ZTfyxfIB^12Qy`H!0&WX_90%po@!TWTd+Lf<(*_? z$?K+@XC^5nD_F4)g_{vpKOaD06ylpFy7nJZg0~3bhSoHqNeGBZAM(?F(~$6TRQ_No zc3b{V*O$bE+>ArjxWkiO^L<;V$_%!}ACc`q%~=YPvhtGhzXNI}-?S8xetyC+<Mg-t zFjaNx7WDmVUiS~lCYKgy7+xaOf|cM|geEeBb|Z3}U$f?HT@{9=r)5g0l!72SASiz% zRM?X2h6ftZoa3WtoAdJe2d51GuLENSHRvS<VeB>>B=GO?A+=X%_wR@27?i=e2Drdo zqY4{)#|IJ`(JX&1B_yEq-nn3JV3J2-uH^Xm<>r~@Yf^_)j#A3}a4kGtV5Shk4caG$ zR9zb5S`JZW1vO#b<ulGvtJ5-e3V*UMTMKe$CU#L*n{M~gp2CyKPA?^pNPpYq(Q7%< zu0#teDYgHmVIn_QsS?YNS9;M$mnF+kZu88#HXC%&8dzb|d~N(b_i~%>m~W&=Rr7N2 z@lbg$4&N)wbT?3&(SsAlQC<lD7GPcVEVSD5iLTH0DAJ!B-OZ~~-||6$>4qJVfk{Hd z^&^Vb7W_OKbZxay9>TbMulPs!BU!)4kuIX*zOuK!&brkJ_HkV(3erd5H*&Q<>*gc! za<ElX^~9GU_YRXfo=?WbXnFN-kYByeAUvG6b8T#1#4|ds8?nhnDb}~s5@IIoI))DT zgd^Neq(*&clE2;IG)FtY?IiIbBYu36|B&qSmjDeCV2_Rh$5@EbmbM$_(cSr#?dyu$ z%pb<KlmSDn-w9ZEr4$k8w5|EAd?8=Tn6RL%M@GBrijLGxwdDnJgNK@v515|crY<GS z-8k^1Eqo76*dfOJq!SK3r}(WDB?R2O8A2&Ox$jd#Mjx3*G$1y+V6yT^h)|MZfPNLJ zH8K2jku#&ax6w^<6mhP8OHTIl67c&K((HcsfP6)}F=8tsTK)TZAy?P~m!3+REgMSB zn)e)c<>IPUI_a}1HjlZ)jdA3+{4QY-nqlG;7m2YB=8|SzHLs{*3|lPpFG(rQ5;Ndn z)bQbo9x5>@RpryHcvSCz-G8uP0WhYl=ITgES(3?7=e;(3si?U(_b%dcQnV8)l|BsL zyFF22oi;zy=TrNtYkg7>;Do*XgR|{4s>o1{B^kab^*&3;=Cd!NR6FqTITCCFOaU&0 z&e;<gER3*HC7)2aq}0=fYS#Ut4NM%~#mziit)Y?GB5}h1TdI=RH1oh*_T+l|VcbUf z>A2Q-=~c_}6ErTgu=yFkF~6v;$Nj0B>pBEu*d?c)TAqh4mb&4vx`{;Twf%EBv3hxM zxLNA+(Q;X{w|^E-H<-f2!K8fhvrv%jkw&U3ii)HNZR*2g4*aoyd9`Mxn3s;0P$l9Z zM+r)8aCOOZp}6L@yB7rh7!y^-OfoD`OvlLGN3orCROq{wCn06bP0He8aJ8n#-;b+l zsYFVrcvr~=438{8x19~CzPO6g^|jwFf|Gp^Pl5QclyM54KYeersWO)^v^G7PZoUcJ zYBQ642Rv0!MN(~u7$}J?0UkiqixBwH;d4swJg-iGYzZE*Xl+pf?T4_$&bzCFP0u-Y zxvuT9;?m#Yc7@TjnnH4#8ce!R+}a@Y!ekm^T-xdS-hw+ZoHp;|19l+rplD={n>fuD zWuCYZa4SDWM<D0Citq>idtwZ9zhsiw=umDR1(Pq}R8fV$KNY0{u$Hr4QJya`yjo!_ zd5@+FW<X5Gb_pfKB<yUlP{I8OuRwb(MG`fJ>C*mDz~h#euac$v)KccyNJpoVE1m(N zQTcYso7CZ@nS-Vnwv!THr`SS1ToB3VA$-gpKGm|lK^e_r%R{1J;)#`-!8<6xP)WJE zbOiEGw{df^B9x<uN>m~^L>E{Ra$B+VYR@3(TN1k?Idhw8kAJWBmJ*-v7(_0s^r;+R zU9bCzLCeTgH^5)`=*-=b1DH*uWB#*Z;h-}}HbfZ}RAA{~z(W?jGsW=Rc1dXLt_17E z?!LEsp)`>+5+zID&wu;o<hrtT9u@w`^HBc+vVKd(%#?}cRFt_}Zt~UWATjPFaV9+8 zWBgrs2-)NPn!z6vW7{y6Tzwp*37ZL_@@I;q6l3qV&l7wd7f%A;WSgRFQU+rgFGkdb z{^7Cc<f^D8afG+zS^=#qOQL)|;Vkm@wCDJ(PS8mru6G791czTO_lWV5)qZ0mt9fHY zo4}33J_Q$TirtNDY)<4}3T+s3@j5X@B<N)!tKc`JJ^TKk8G+qAZ>?33AN78r4}*-C z_+2wK%RGl!AUYOop~B0rVB^M*AL@lkKX*+_0M$v{7@DawSDfgx3V}c!1F6!OQ>5#n zlM6oGHwHk<f7~{D$=D!TJ<@1adQD~IN)zI8V;+xD6k5NjjzG-Gu5P07$1x6|CpiZe zPFWMr;KAL)h`HEf*_Gz`#pM1S^|{-Vo%x2Q)97w7Df4Q(AO%ucp8nA?E%|Ga?S7TS z+DFdheg#fF;lnk9Ni7`Pg@CwmN2pL*9q=F&ElKn#bQov=&T7aFph(u-V0KY35yO0x zzZOgp&kmA(0=0HJ2pU<M$#t)Fk!r4B|8u4j604{@Ddqe1kkM5(`Ku@>q|=?MyO--R zicZ_t38gHOt;ZbYV_b$ncy2aLtbTfuj&d2Kp-ClQX#4Hm+&B{&NvbMBxNU!8As6gc ze$D!lz*0QRaAN0%Wp0-4KGL$dP*=t7&aniVE;wKM0*M5WM<Ld*!<wUMZN`;sUO>H4 z01HOAIa6Qcf`tc03s(zJs(0sNmDo_`8^L!4kOr7yQ&YK(*76_7YGps!%<RvdbWEab z9*_9LPQZ8vsmV9!&3vZ(l`8Q~4Eq6?kv^;}SNT#5ofJ^06AG{OJgwMEu~$fxk#V&u z%citkd-IVVMH#!cCV+3P6;rulcd^)W`uya|ZP5UtDrd-9R-<pwVUC4|Oz%tM5>!@x ztRtUEO&$lvnn$?{G%`(+TslpIfA~J#mSOG0`RK!csaB$<vv*kJ`L-=3Jq62tcWUa= zpJwi^&$HP;=8(y5UdsttxzY3a<+Z)<Z1RFpzYms!^>e)K>s#)VQ*xSodg41k*zYH@ zMefsisl2oQL)%-wHTD1T|D-5Lhtl1lv>-4-y1P51LmC_-q=nH42&2YebccjUmvpCu zG)Ti>dwur4zJJ2!^Zj+ZcCKCLZ09_mk9*u6jqEF#s+^L!Z=Mm-GUKX{h8yQCtwC-# z*5VA(4`*n9rNSjdSh}#x$2=G9DmWEn|5iA<aPKKf$}+1{Vi5VE`;5*-mImv%&~sVQ zNbsCj<rNax=_KnsB>jqT!bOSISanUz$BL^2{*n0g%rP^2A2&uK>Qyq{^&Oqw2(|U_ z_J0@`wFlBgZS7wrXUAY+-GCiHD|*x!;~LEVMVvIIUcC7y4iMe4l&7t7s`V63kv_Tq zgoie~!!0T}@+BCyiUzZg?T3KuWS)>C+avrV+POu^$%RGiNC3kM!`t5JqEI(G?b~>< zuB0}`*;R<n5VsmIpC<3p@86%pnS=~RKH=&Qg8eLpvn+0GQJ>@_-J#lThsvyiCiB&w z_y+$DK7|1okT<LojSJnKPaR?9H4&^z*HkL=&BZCD<!!cZ2+gh=cDj0N9hRX3ZS=F; z#CNEN@0UU;O+i)m*HfsAbz_x|gY%KetGzr#h$322A3DCjd#OU$pGJxAE?<GBVb}p= zPG=B!hE6+LkO3hj_;5kaE1kBUd+XHD&_BB{9Ms7dko*^P>Hq3x$6`#rU&<tFn}Hn3 zp4uSx640N-bJP0poL0n^b^#t?+N|y&ZpbdTm6wLMbA0;vdE)<5QjXJIn^Oczyaiq$ zDz=C`wL2+hHR<_3&5no`zq^ye9}#HDN)g5`6ZEuLUH-C#x=v?20Ah(X6HXSFK1n;& z{OUDqx&64IGt);d_J^ZZf~n{W`8Th7TzyIpjTP{1Y#cEZSy4ALXE<|)S0HAZ_e$xt zfzs%^@tsK2>><mx0;-2$>XeC=9x2rv`yYn;|ClNz{ODA?_ZK3}34oU@X8=^@RZ|#i zT#iGT)#wQoORhs&$&k3dr^H`fqc6wN5UOZTOoGtMAg#IfF13fl*WX-X5|o2Q8N182 ziZ0$W{=@NoxMgd#3piGg)6>3}<lgVKvZ%jiyXNyqP=N;;@i_H4@NEv@+$cE(FNpPj z)XkOl=_K_|9R7-i*|e&P2Cqqj%TrV_(QwSA*T0fjpH#(fU&bP7QLYT&fE%=;KlJB6 z3<v~9D!Lt<nr_~w4i|boT(l!#I#?L)${5Y?nDzc0(|;I_VX&(kRKdofDdmwg-RSjz zWAoEY9zn9S7y|q3gXFZTSiQd2-QwM;0Zqj4Q7olQzR0`?A+rLIpIP7D-p<|K<A!06 z<yal%0s40f7dCtubZy9&RCS$GSw%5#_3rvUJ4ckzo#olMTw5cN#NDhMcK?4E-q|>Z zMtG`SwI1?YkA5F!@&Qrspk9i0>77`Z4s6_LW?n%+H1%6yj*){fXM{zh+B5U@*u}MX z6^@a+D&5QoQly#w5e1nt3u->8OCEQEd;ZzqPW3<#&kgrovuG$qR;oj#QPTw3fl}^I zOA4x)7jy&#z=^LGl?3KGyTD-9)l{x{zd<5ML5SAni_!(vMRc3LYG<|Dyt2se&mWXF zcTat|0kWV4^R<if_ZNB??QM+b3hM%)C45S9$A<J`rz3rX3o{11F<o)(T{Xn_p*bv= z!!|Flqfm8dm3Eo!lOUQ^|L>+7s;jlZ(@OgOp3Rn)Eu53BId1<jOnN)hNVrzF0bI$; z67DH+S>+xK06Q%hIw$J?sMY!Yh9MW49MS>Jl_*SpB?gO>bdB%)N^vxA$#(qGQI_L` zPo)m==#pzf9|@e7(qtAR*3~Y}{5x9Mm<R$l6HCNE3N0YoZ(6HK?DQ^^H!?)pG)ga| zh8R=IVKAMmTnkU-nJTrStBIE)<1eTf#C?&nRO|y*G3*NLuEHtLzL?U{5}0wuQ(L0- z3$gm!+|Yf+IljDN{vn?_1E*JV&o4P2ZX&atJ>9G29@SpVaQtDdptx?l_zy!2_FdiS ziM;nI^lENeP>@bczB&AQyq1m>Ff;6LARINxhHhJrZKn0QSq!cU6&_6`SCB=+5+sQ= zJ1&vmmF_<6uL3(Vv4ouaRB97K9pHdpB^0(pW3Z8c{&!H3%*WW5pW=#gBePWDl|MZ{ z3(tNlKeV0EN*m*)1^WyaNNmr>MNw-w#y&MPsC>L2XGz=$xuc7^k7K-e3Z?Bm&F&Cy z8C6^>?Zzm?1jGK+NL5a8hCKcZH6P0gJZ<wmUsQPMA2h4_k#ehJ+maI@yEs-vi#ykj zT&92X517|2&Tpt$6z_9=sbiPwNxUXCCt4xZU5S;v$LHl1;nZ=mZI7<Pg$0foGNvg; zzWi(`Jo>YqMUY+^U!TVnA!rBMkdANB{;_e<mip?gN2B?qMV)dHD$_6y2esy(Y%y~8 z_tx4o-f1Z~q?Lw8G-bv|N_&oU@1W-}M5ygetTe>>nT#2*qA6g)LZe<=_q(>JPK2z7 zQ&Gu?P#vH0p%fU_;<$FrY(}%jVa(D<<zG>q6_YG#)N`7h#m)Zw=hd|hpy-bK1P~xy z={kQ6-_eh85+*TOfZ@6}qi7aKyMeM&i^#1jt3oZ)rsUs>I5bKF;1fI15Sq-|d`UW= zo)^N+vpR;JzXxM7cD4nl6kE#;OQ2*-mh1&|vN=1bG4mtLnXrO?WhZH=xsqlU&xBGl zzxW_KZ)U6SX`F=l9R9;dLTgz2p4PX4*&bwXXg!*QxaVkeCi}+Ts6hnkA-SmA?%gYq z8KmgxTp{Y!6rA`X`0h<=8Sm7O$a|l81E&{_lWpRT4q;_AwW9{Qyu(nxqsRp3rKpLy zG}u*`bo<ujOt(BYEptjuf+;BjK0H1eSK<>9+|{{&B*|@Q4gBwpW7YkU*K(EZ@v~;3 z4hB9{My-^1)w;Fn<389s`23qkX%uiB(r2TB-)Bnsq|sFU&cXZO*Xh}hrAJMy(~0V+ ze$^sUxQk#@qAAl?7o*(j?U49_%`7`${>G2eWNb}-(R{|i`e?x46w7?QpB5_p$Nc4~ z4-Aumd6wNPbE_+6rEd$EpQQ}V&4P8seqSj|P2jFod+)dXa6?AGOBkZce+;HYT%^;C zI`64u6~FFHmS!_js^k^Azor%tDEl>pcodn7u!}eKEHbRZWDkp3qeTG+g$pS$@>!AA zUV0k~f8V)nizWP<b9smiDbv~+>`k5A<1RlqED7O@cJSIx9CA$KRN_n67E+P2@Hzid zHKymlX!-UXn+2IVBNPSsb2^}-tGpsf(Z|w3C}WA4V_{ZC74Hzqb)8+Bx~)sw$zLQJ z$adt9vxFk9+X)r{tv}5XZNGIs-r>`JDgi>R(>AB?5sxGu@#ez}5JKX=E>9f_9ZX`z zonfTY>-XoR>7l2&2QOxE9bacibXfUd56kDdcuyrVmhT^*RSNC?X11c$z=b3d64<mb z&T4igIoWgU*_Q94Ek>9}e5)kf==cemTwt_<8;*T|i*9Zsg2%aF;ix~nW{|+Z_PUiB zcaY4J+Y+AoMSL=ehR^=vi+)2Y&Xi}3w({Ykc{y~U<rwyrnRwEn8{Ga9n7TFm_BXH4 z(LPv*WD?)!!}*zSIA~H(tXP&<79tuIv#CW)CBDBxgSFC$FeHmEr<1&E8HvO`Yy(_$ z%k6B$oe}QO^uIga_;+OYy})1z)LPebgLu?=!D}t)cZW`3r%@a<THWC#12%y`n1qKi zSQXNIAbHnqJ6zW@dx&~Ogw`2-l!X(c>(d;JYePpfU(&|Rgym+KKyD_{zm}m`Yj?EM zDyFOCgz?WLLvxXGe?;sg`nv~ImZ$1bIl4N3Yp@c%9Pm`AdeWd**_lW6t#Br~_vBQG zzsH7hL-WQK+J;%WXk>2#MsU~2I^S2=R;1+W80ANSC#8HBIHKt_yaK=0WF?)lNJ8K} zDT3ZVuY8AZiwZXk<Lnb!3c%9J)GE0J9TH?6AG)Avx$W$FJa$?Jq)rG%@5OfOKxFGT zwosHo+Ny!Q*(y}wJ*h&89Q{Uf?L)}0msj^i_vs~H)}1*>aD-zM1Lwo}MYJdsgI{hx z6>LS1^?r3{)5PobC85|xEM@A5z9#l=-K@b>laNZZUa39z9+*~Vg!$!Nl4<xZB)Bnj z`EA?hBqtdOxU(L*@i*j-+ay&%U1UPh4DNxYSp4TquI`o0zs+@k3)X3i&V9P#j}n?n z6;0wI93u$NPR%KDE$rV@=3Uxu7kqHXCuVhx*H7T0GL4!llY%*3>hpml(`sOeILIi_ zi6z@xPSd!V0pE<z^N-+4LDx*9mSCYtwrq8@;~O?TQ^Scq+Q)~dK%)KObFJAEkJZc! zgGEox^N!0KO39?ZA)TjwyZKtX@Jd|WFbaZDJhKlAEp^M%qWy)8*S)gR4*YYIA}7Bw zm~elVQMgb(O!*uw+rZR<{=~m2d(9f5jOq6QUU^8R<K6>?)lIG<IgbH7p?6mQZ@WPk z|F1-XIHa7)C;G|-1FHEuWKshcdL&V=+jcQc*(zCE<>LuF{8s!PTa5@XS*g^qmqd&O z8k;lAPGEQ0HIR8-nyQZ5abV3#$$9-RJP)Lfl18%BHyd7K)g%-Yt5j^mO}^+hxzNG~ zqppL@jY>cxh{vHC&%Jes_uA<l!D;S>M=xBXAt*-e-?MJ6OAH$#I!TEF<<&pH?)A7x zD|T;y5MXSf`>mJM@|7Hs8!H9*?iB+`WX?{h_Vx`r0@g4!!ROOxv1%;|mie>Lr%wMN z?YZp>=AI;n42=GjKc-E*wEwH9Z(R#~cf0yt)l;<i0ZY}oarW{cVdgjQyM@=OKEJ~4 zPhiAjRelD_%=+f&u{+OD4Ydw7SrB?L8~)R$+8{9uc$LPr9j8h{u{S>m#gYx8YOkN2 zd<1zaFtz*8o$`MU=Z9DriyYeV%w<zw);>zhi{4O>OIz6K{F}r9xqjVBp^MyRZ&-cu zI1z+Xs${+$<H@{KTa+ao4&V>v2Ll$bPUbzBV29YS{lRYk!#yT^cC4rR>3G<fg0X&V z^&9G7ZWjd!9*fsIh7SBA9i(=|v#TM4)au$=oZj6zhEMKUm~uS)RnfOL*^6SS)y++@ z+F4pDKp>v#`dSQ4lH?}{qq^bS40U|T;ol;ZMr>(8%}Eb;|C`}`<yDdk#V~z}P!Y#T zOu@Kw=;^t@)|3W{d`E4h<6>mvV<To_Z!ZpODxdP_`7H3-yah0^kIRw7t)J<dEA<5= zG-i;$NTO-PW>Uipx|Vz%=*-v8N?l4{K1eDvM242F+X^?$4k{qP7W=0+QXyJru!wS( zD<LWN3tyKPtjw;d{1NKn5A#vSp7NVuH+)x83NL<bzKgZD1Qj*iDZAe8AciI6ew%q% zTEn+wX%#vE4^fo9iAS>OSB)9H@VeUN^t~zmoSMOp#|tKj6wsuStA3N8Y2{7f9r^eK zM8W$j>6B!_@;HV>w(?lv&&mZ<8MPMxtB*#DfdEyBFW^_IBi9G?fj9xJx=qf_FW+Jy zyD;*u=yK~0b!MahKEmoW?^DKAFQ|3M%PBb)d+@*>rr$p6bICgJSk#l%iC+TEh6>PQ zZUSE9_-YZ^kTsf5z~8`iR^5yyo|6dS!6u8;UG(jEDsc3A*=%i8riOd;>chib(ebZN z0wCTj_;Y=)%%6`A<{2#G*i>yTKT_0Fk-@j+mS=|27t_gTzx;mj#PSC$6T9L8D{Ksu z0==;3nwFq5E?$g7oM4t~BuqP(Lx^RBd8?j@DEPLcAA<D(8bqcXMi`lekq5+jeBUHF zok){QW6aTQ4bgcctUx}={k*U>|Cy#Bq_x;>W$*YCMf%{m_iGhar|V-X4_j??ava=l zN}=kM8dJcq72yke0u=77#FeIc$fZFi+^hgR57!V|r=}(>JHI;7TH<Vmlpv1CWH^n# zO0CX<q2a5Ag#K_0hT>AlO;?horBPdc?XraYPyT0f!=t6m8JQ*dXT(%D;D5WJ1u|zz zbZfKk)37QzKwor^Z-b3bDUY#b9y%PBs=hHX4KP9R`b{uVD;Y3|yt2-={m9eRy>EPm z(hm3X>#FZOM-7-%>k01Pt4VBRlWm?=dM~^}gtASK&c7jODZ65}BmAAk)|0Y%p!$Ra zUK{OwUpC38Hc6PBT<GqBScGS4@Nzi!O)fqmJ<ZRP<+1u3FXh2=s{CF*dlvdGPW?w& zU&qPnyeY%HdO|c;k347ns>6Ve7o)j-pjGl<_LT4!2~;mJ+U4v$aho>Q(AzUAg4YiI z*d`pYl#GyU#nr(!!ukspyEQlM{mbKxbsl;rk-8=qsDi{t!y7E^xzy408x(BiF>yht zkT2*uOFf?2!8I3rV_(x$D@7F6Nd#KFUCo3BzZT{Z=2>UdTz*Z!k!3E_+?~Y=cJX44 z@(wq*FzFSMUDSxe{>wE6nY=vooNZTk6k3ChN7}d<hR`5#V`@*H5Qmv+DZ^ErX;3-5 zsQdc%+gVC0wd{c!xQmVa`1L%Wn@caR&84C)E3JunVrX)E`tpX(-_>`gU;z`Xw4<to zB`;~wfI?W&F>5>TIr%vr_xq92r=3s4v89zlUkDC0d8+zG%FSb3rX)HfUzLkz8srf6 zE-uj^9$4Rwq)?<yEed71Ja?&|(H(nJBkwP0?|ZYovBvEibTru-8S+^}jfcf$BaxAk zE=JTvD&ev&_pQgnAGte*4dm*BYcLtFmRY^dR}8MIS9k~k!DH}Pg`4~1bttl-;v&%H zj3%RUO_2YJ$DHzds7w+^=s%2SA#R+ecWt9ddo4SLnXE;9`wk(`KaY;NhG<OihYZlT zJ_*zMfL!6mE24or;##AltU;gV>aj;KqHY!G%AZK>Q4BwS#I7s-4)&^1%rAXuqB}Kg ze)*D1?n+8ITOq0PaaekNHJ|9PaoN(GIG(MVc=FN4RjTnZgz$=SYzLG7{Xo>o#7vq? zlSuHB6*ecB0NKMf7MoD|7Byl_hxQT&AH7Yv=W#s@)KbfMm1IX5)M&OPRL^tyEAo|< zgm)pa&%(o<)q21WmoqWV7WzIP!c~gto-f81jHISmsk>zzZOQZw%IJoN5M^%5wWOY$ zcKnSs(I2ikpJlNlxwYp*(sLccfLM7Y51|3<{QM$q7H8|j$ZzRpTky+wQYRg|O4j%e zE@i#i{tVSlLa^u_F~`JL=>ubTcCCz=5nWn6p+pk=UZ>6aU-Z!1+5x(;yRbVhSz52% zN@m)ZB;z?(z1cdLaN&OZf6|W1{6K`v`P#mRJI7kt>UxTip`(1iO+JT&g*Wv1gfy|- zZ?El1mz|$OPGTSJ=Ix(FTU6uZDF{DHDawc2mklfCEKtWWyo~}wyGn&jy%^+8sS2{* z<u~Kwj!hZP4jQZ*pRuNz$*a)0B9KA11ns*MLm!)ChJyDjN5sDY)Ld9dQyi&<hKw8q z-<}edCO|B1gicUTp9pU^KR&{~iv>3NB77~-Mol9W@B)6QLEI03s2*v>pRByuek*60 zIWVoiDD^Ejw>rJuW~_#r;v4IzjM(RBT#Z2w=FSf-QD<!JYC#rbit3hlxijxnW?ZtM zJ-_4NEcu75kqa%&ZYb$5yL;>RYL`o&ZjXh)iARPnu{uScU#qx+!gKXS=m~7@h|_B+ z8O{q@n52G`UG41645ruIY3aY5%bUFSyCFT&-hS61JrYaus}PfQ*2(+S=t?ren<(!Q zSTf1nX_)bFNIMuUKTG{O=9LaLvmqPtPbyTwTq|E_toJY7Q4&nK==jJfZzgKFqK5=T zLAHALCypKJp!Pc##dmO(QNf|Od28#vDdn~hFrE_I@Xm1~;hTrFQzmG5lemI2&s*CG z@O<?#;_7rwU0VL3oOu-ALCG=MRq{)v6L!iu*Tw6;Fud7yhh@9wnD;V{s{%HEdFE4D z1LO-#nVam#NwuvKIQ~vV4&I+$oc+*TGgHdZ$aN00ytcoT0!qp$M{eA&EI^@yLmWi4 zeH3nnX(b_9sq3OriH}0}-z=7de2~%?C6(yBLWzjCI#V1j{IZw3q0Sa9;kvifmeZkt zsq3dd_|d`{wDUx?$W-{UNZ;BjxoBN1CzDNc$dhO=T$$&H^U4?8DE;2)?R$8{bkC1^ z1HI$wz?xE6EL&Yg0f20HhvfswL3f&FTix9|Wg)U8-K4o#)&DSfz7_y#t9&OL{sh<4 zbgWoRH_9eO9saY2dicASB?S|L{uYC#EM?OPI`)$#zxgx|=BJpcl<y|fuE)0~)Ht8v z{oO%&eU>>=5r|BSta9zi(8N0Y1X3Hl`XvCMgSZw)Wx;rTeYz}GM}8^|?=TbsCCVS> zkX;;En?9Y69z6l8tl;<yx+5552kLT|aFbj@-o+A+b%Hn=NE5{}GF^5s!oYLTfVUZ0 zso=w&q_@&)f>q~}N)7Zh8dWV-MsQqT1Q*PINPqpR_4?M9VRQ6PZ{jxrhm6lW<^A=S zW<lITr%zyYyVlNT?eg@%l{F5(%65BgCT!!|oy9V!$Ey4`)$)WwudXg7k!tC)CuNkb zxj`0avUq!q^r0X0C35Hd!f7{xjc;hw<2N(_zvj!zm^X0U!*jh<@U&G`zn4pU<<DHE zOt_A|REXg#!O0LQb93{04bhh&Zb(wK2yH89v(;I2$(2INSD~~>m7{QiP<rO*@b<ek z105}Fy#Y=0*XaWy(25<wISkr9tY-P9?nDOVCh<@F-+$3g_-L%*L?$a&V-%qipfcmv zbE{+hN3Ef2EqY?|qf%<fc#03;6<|nU(dd~Abx90Qj;TS+^K?Nv>jTWL+Q~M5lYN@6 z2G6rqaVIQE<bsySRLZ5mp{upGPCbpf9aVk(lnRu?IL2%Xq7>K?tXF@gp#>LCdmq;N zsI4=M(MF6)>Gyg@`-J>zon#^q7u}s_IlI1`(M^ZO&hg?>^zs`^6^CwQ_}_{|F5m0f zvm2rKwc)?MS4zIgX%08)NAaJm0Eg@~|H`6B&ck>CFZD04FRu@xK5hgU?C&{kkY)&I zN4&?pou`6KLZfVbMe`I~=L}C*vfj>}G1DDJ8#$^T+j`R|RvQ_ngk&(*F0Sl&*$#ve z1Z$dSWZ`#c@RRqcYU|Ub?e?rCk@F1cQ~2h}XJMVXrGgwe^E?9X?<{*9_uPOps{h9I zJj9l^@#0BBE?AA;`Q;Tzl>OSw4KUa2_&YTu5<BE#q|!?{7~YTJqD7|_BG5JPr692> z?A2IWRn<z9@15s$86NY8V+9Twt=hHXjwT)b*?A4_qTpr6fB?Pmu<X_*OD*wN8axaI zl35k$v*Md-y!$X~9#TzJ{8^X01<VBrD#WAEa-e`m0ghXZreZiqK~woUbS36H*NbC| z#X$QWQ*#A|JvR@U7X3kGdCVt8D>$K4`R5Md5lr_?Tp=&MEcw@_-$9taM!pU?vcA=1 zTwid=Do`5*Xhdspqdq-V$KMGaUg<XU`y2~YNTodgo2AG#?7pvkXpfXeQ0*nI=CHJc zK7NuB`Da;vGeJ~9&!3$aKo9@Des+Gl&McCk@=3Ph9SC6ETAFd6kf3q|UuJjJ=Pmt| ztCTwOm7eEnME7NKLP@v|<~gri<^rzAyHZOga+ORnLlxY?7#8Xo_E%d=jBbJSS%_(V zWqx<zGt_&-PLo_gOpQfd2mZ?Y=S2_K*`9Zd>8--S+gAeXu60mnO|-Ml-NFE(E%0Nt zDT7+&Xi)JgC>l1;W2|S#1B+hhQ!y!gjN6dKok*HM{*H|Q<}2!_wD7e<Yn-r)5uT~1 zgzgF|x(oF*x#RUEQ|tT|N2q=4fN8ORqsV>hC)e@VbGoqtAId)=m}$BolnE-Lc=&jA z%&x=`&jAY}58KxnAtk0-d1Nt+V=j7=h>=EIS`XL;j>7ssV)#Bu`p4d|HH=YT{)eH& zp;~-#fy~1ILi&ckuV2F(E~m|lUCbn_Wu{p_UOpkPtwPapra6IGt4Ej@dP#HM{1OgW zjni~_lJT$2*6@GC<`wlg2spQM<ldK2P@C2nV6#A)PGCwZo)}+&e2acL!AVmONC&%R z7GUH!eGxaJrrgYqJ%4+<etbOT>@X$=btL5iNdBh1@|x^W9NJa<j6g@w4w2CfNxPkQ zLMxShy5Dz@&@O;yh~cz9gS8;PD7inxe$I^P=YXYxtjIB-&b6on<{%I}TkZ`STH|h+ z_#~CcmxFJ}AZj4G8Y(YVh|fy1X?%ZiV%Ykqa4w(bDWUF}I<)P=RRvVLzh@{UqAZkt z_VIBaxqdo@?k{oV2K=jx!8?^95xEh0R$$2syYKBOEh&WH&X^A}UCyfvdaTR4epceR z$TI#el}09w3Ff!;IrH6ebN-WzpKxSm$>97awIHFR0jhHO0=_pCOfnu7?L3$yMJwKD z_}CoU+NL7T`@_vI>C(2<=`Hr&1*MO|PlM0B^w@!*p8fSzPouQo$G&dO_bcMs7?69} zAb<4KQ_EF(62?OupWm2SSVDTe+NaC%Cih2x(~j$PLf?4W^w%w@3!A=MYK1oMWT(*S zH^;P`&4BrgPf5&Xbdfky5(bV6<&2ZFhbJ!0z~%N?KgJ*DxAU#7<@`bw@c%GcE?!Iy zGn8odJx(D@>K0aw-&k*23Q#%)8A+jAqBE2M&VMcseOhG*16dZ>!0PkgECM3A?Q-+@ zb&a)%AO4udHOL8PwIm7?V4u+~eb&<!{$A}L%)zLxZ2jW8oe&+(PtXUeJo$Lp9mpKm zv2J#VBmiiiRff&!CsQ+6Lv(L1kOz;!nqq6;>qWz!E5}vt$GY^jR@7ptyv8nK0Yr5{ z`Ek5CZB8_Vtu&SCo9QFwSqp5+3#qn{>a>shx2q<<di-ODcik{$OxcE~`zT{s(Lf^M z6hLom<LaCC+sR;*D959}D|m3tLJ1Q#_A}*8jR`2dkl`4)|Foynyob_nFwp_ElLtj^ z8UKFU$|3YQ8R|}+Fm>JD`<bQ^1{M7KLz^F`Pb;FuM?}P%zq^#*k35sjI@PPTvuTKc zE>W6cvm{wb(oQYB@dGrN1G)o^R<Q{ZN6*j|?$G9+isJ8{Cj&%!M#+xpyYjMC-2Exq zx&jZ#9jhBp4#n0E^PmaW(X(pp148!~4E_{{wv4^MyK`(UYR*pRijr!1<{f;Ta$yJj z?2=O|q@b-1;COC#kWQ|<z#5b+?e*-=27JP&A~vl~a1Qiqb9Sj=_UvxBw{PuoVdcoT zLid_6b-~~W#itYOPrZq}>Dpcw9o*RVQXwYO0MyarVUJ1kzeOi{LTE|3ay^oW%=~qF z{LKwbu2k?2iCE!f3BxksObDcQ)2X793U`@WOIc#Jt(IBZEB+&H{9h9+?LDtN0k!qg zu}uZ5Cm1VBm-B_`+b^+k=Lf&U<0^h`^QXId)3b4KWzb{zBQ&<ns$PzsQGOcl*J~Hd zj1W3wY5ezrWg%nk!)bqRiPnpP{$47w{JeQXZ6s-tyPw}24x+O@JdLTPXEzf}zbln$ z9G`q&N0;m<;1@NCKus1dwfqp1b8PA()i{zJx;zWSzQlMsobw5ucpv>h9NJ-_&j0Ck zc#oIM&Qk{kGZYF_OvZO+N+K#%u}v0HaI-X|G}J^_lpFk-!wpi$HTtxfg8P%Ib@t-q zL$=;{B;kiQwZyBl_Wl-<bhB5;E(g*=yIvAOM4fK=e;73$zUP!i8yw$%LiQsT=y8jA zBxVMH?8A%&s&{%t*aC!@leLpj5-<Ax;icM~<ILD=#?!9muefMxxG2|FN0ja4%w03G z)G;Y5SMJbz6U%@K4&hmR@L!KzSVM0dUB{n^Reyx4Vu)o*i-|p1q$s!8ad{RJl=blV z(XE{`IRwb_g#GS1dAg~3gG<MAWKiYiL{MtApD8o%6&}RJy{?sbYgeY}hZ#31*J|4c zOehwtXKSdC%JVi=-d_fn0Cku+^F9&J0cE;(Dq@f5>?^3&!CxrojGwxU1W>>313%1H zK6j{rTT75y;)HK#^A98*B8~SEkJ}bU=6PvVe8D{osS{`!M>|A^%V#R)w_Sp)=+6V{ ziPQPCg0f+e7J29ncu?dEiG+Nj8xCixtsB_!v0EIo_vEP^hB3d(!^lW@|17sgn@~9} z*}qsqoKaTwmU)@gcW3yf7`AVwUuwSoFJa~D9ADTd+WAmYaY63%g6GeeM)V?M?0{79 zhq+3rjNX!ue9y@`E??{>0~b94zUh5<N&6w}{l?!h$7WQ^&f$rp=i#SfJ=shbZL_CN z1hy=3LI-A%`)noUZA2<wW`0#?`VrbNWAE!#+Y#%SbJGr$LXgT2siBMJTCQxUmrR@K z*}I{NRf?#>D=)7YPG-2I$jK#ZZD=C9e#O7=jc$ccgn0$epFo|VIx-~0FfAS|9*pCS zZIJ~wO58S#lo#TR>IK0JQ>t>@*wKKA*+TzwpRbCOQjE|z)~7JY3GgW2LKH0}y&;j6 zz`5`S;5~uFdwL8ahUnb|YZ<!EG<>M#Bx`TPx<*=Wo*wTE0W#8|oB4dtdEVh&Jha}h zK1rXD2@#`(g@0vvC>h9~^N_XOdSq9qbm(e$goUdfKa%2N@*ewi;hD{H1uK|*9_OU$ zH9PGBWDot34iz$$5)f6r8~mTFln+|)^W1pV(3X6Q@u5C<&Sw8LtF)O!e3m#%ti+b@ z(B*M4CJxDSrpV`QOh`G#6ky3+%>LkMb?UNcdu}>CJ)YTFRkNnbOsT$2y}v1S*0_8; z<4<_m9dq2%;}<k2aLF!I9k@V20AkWprC{{<JXvup-SgwlpVoMTGlQP^{)dV`hBMTU zCWh`gM<x!bRpjrvKF!R5(<QDy1*%LNw6^0E^^NHtK4n}iNF)oQ>~agvIcW?rIU~;H zG&0qVj13nA?Oc!Gt~DTz;$RNI3Gnk{uQA&QGk~hZx}^(<VWGm&R^VvHE5Q_krFvj+ zhqoT`u{QLs$N#MBd;_`Iargg5p!sj=|4Nlf22pc?;-3-@66o1cJ8g(%e9TY7;-37w z7{tFbZr5(aX3BSFZ{cR!>~wCk&-TC1o<5z*Cw2*t^Xv6opG6j4OfG^!ir;#+H0{ZV zA5(pNP%{hY*%8B^-;VJ~a~j+qdeJ}jlKeqD9<t)%e7D-<0&b?H5+a-=e&uouFI}lj z!cf6|gV+taQYUBjw_qJZ-pVUzM&8QZR_M!LF`~>B)2{uaaw3Ort!}Jk<3sHFq}oyi z!lf2qCI`$Y5`aDk+-5RbT0M05(V9(fguDo8lNg43f7>yB_#;ODU$xtDK{X2%Wi3%- z0F{7+mAnx&dO+ktk<WK2E~(3%09<iH>|rYTHeNVx9?2tzk%bg%#Se>GzD$S&ssfs> zM<t`_LtW3f_R^^-H4St!f+?zCs#It?i%hy()F~NdN=I6(=F%bOfoAnJ9oXZ*eZM-$ z!-};8o38G4$v1i#rCFM~MnR^f@Hc&CI|aZ!VyJP{2%5{)^P|+=e2iZ2gH4x0E_=4& zvWhs`_+zzc_3ln60b1e8T>kYiLr@X%ui6HEX9oXRQ-fbKE#%|f(YjcP>8f2%n8wAx zvubi-3UX{8WZ~+hg3nIIY;A%N78j?+IOqC0%DETMf2WwN8Wmu+GFC2od(Y??L3_rg zoZM9HzBZDofhxxeS2+`4zijrBTnbH}r>Gd1E*&}snwPhWwIM={?BK>{I%rYryPUVz zXr<K}Qab-$`?}$!VFq~o8gI=G^lG4dM)*<k4eeRJ!y|R3$_HnQNWK+Ww+K7MpLfKZ zl3faKB=d&xW-mREUN^T13xm%57VT!-XyY49At}<ZqX{VqmU@capQ-hvGz&4Zo@FBR zs=y(XsXYj~o22yOa~)j7#oHdoMD;`xcMJ#(r>PVkDixuzi98!bWdS$G)u-fNKR<yW zZ+Og}_<Nd%2#2;&^FD?cuX)YY21^-U@GOd<R~UAAT;Jwhc`yNu<-P%RY35%R@M2UL zA&LZ_cJzGra#UVU7jf){?L?cehlM}-z|w*p`RN>YFkWr%AKud?1G}oN$c!hkFAsi% zwAHPgI3xtl3VaXoOK>Nf-Yx&@L&vRgsdc3yUt|PJdrNZI?x6+1URsPPb4H9c{o*1w zbgx=fDn^?li|H8N+0xc;KZSk|JG;mOUU>`c7!}Hd=m?@LWPrYv=c*!kJ#W-Qv16JV zxvb~n=^_e)rz+<4xOLTT7Kf0_U&~QWfM-50bgWG7WWv6^o!%xJYIUfaucB3(fWB*& z8DS6vrzmNzaLK0uR1qWwkn*~_8E)O;6|o;B-@Ya2$CY0lr-w#DUN}DS`5ZsobZEDZ zuux(qnqMQ7XCXyB3yTc%qVjP)RJo{)1TuY63XbOEjjf#qf`kHpDe7!RRF68+%Z=dC zZfT4nS8%Y4zpGLF_VyAx6Fnu4stwgq(4}-pdH#)_ghxV(%OhmuDW3(km?8YuM(N#R zagI$IyLaDYZKwghS#XqaLyG(=<P_5$wYkz(qN!|30aN6$kjd;L>N;(J;g{AYGqb3C zEl4C|OrJQtmzn4PPM(Swhwb&1{7{dsX3v>>>e)uz)j=dwb!Eq?I3g6@5>ah0ZlRsa z7ud%qxrnt@b)G(7&VFSD=@xaD0}eao?ArasuKHO=ctyew1HO7<IGdNoE~JRa%MGMG z*|6X;<!YX7f8aLrSV^D?XO?FiPI*r8gt)7JPyeGhE0+gIT(OKnW<X6CUO&}Bwfkt< zYHi)`$Q?i#PY=b$epqD4C)*ebnhp}bOY&*0`OJ?cm5gULUynXQbLpq4F&u--pq&JB zZlZDdbYHu*)RRlob1!CHpTOt#{Wz{54(ta%{5<P8%OZT`ZJigeEEkKHKF8#P4Hi<m z4JZPWbX4E}?6uY_tiGqeapwL_tI~a6J~)3s)FsWWfuh2sGuKZ@epRxF5Q=XH#MM9O z1btzwyXIYKmGc}h?*BK`x8Y_Dv6|UQK~Dzk{=>M|Cq-SHuvHHHPOnRIjoUF#r9;E) zTtPIHJQ+*om)dpxKMVr3{+%(<wOF~v*>avTCn?`crntA>rcugUq#{Mp%c9JlYzvJ~ zc|s#Q_K2F79pPQ!b3ggxIng-OOiD)&{txneSxWT$Hq#46xEP=Kz&d?8Pgj!-b9)mp zEAsUx?x|7X9P>lJYog&mUb(%Q+x0RFzI4n3YQO>UrGL_4XLpW0G*f%gLYy-XDS@@o zf1<Qkd!f`U`hcGmM@e{>TuSBnTzSD9e8I3lq_rDdmK9oH9H|~p6`f;{LX2*6W$F0) z+pEr742cS|L(v?SKYs3E1`DJ&(=xg&R6?DWR+~x3Dn}daT-eC)#BH`!OvK7|H;+$M z->i+m71%YvOc;#@4>&f6W|yGRtX8Vq{(_B5DeFo`9$CXW7pOM6j1ieH`th8zJ=AfO z*3<jPmoil)8<p!md@w-mdfs%N-pyS5HuF86vw=Q&K#V()L<xe_yBKmq0^Ex)h0P4| zSjrL{581B`gLX>NwLguc%fw+#hr<>TBs{?quA1J5=wfAr<bN2E;u}~q6nBh)1eO2B zM*)K6`(@M!@-jFUTbFqQo5~Jp;cK&ZzY5u+XHqcG$Ea;j-7Cninadz!%MLIev^Q~O zvoJqhoCV#1-CLWwmp81G1ARulop}VIFDh=>ok31O$$-ss>T{mPhR67JGT|J$bZx7r z#mRE*i^-BabQ70i==*Sf_fH&<gJ*f9UjJ^7rGZjS+syGlGb@s3@P6s2;WuZVGky(e z60GlOJ(JG9?W#UwZP^PAf;sTiRFH6KZ`0U6D>;R!S9f0*JeaMmuJ=ve9rJm<)-$DQ zx@5QN(Ehd+5}|*QUc#yG*|_ShwnkmPP@D_oyusud*2=1DYnACc8kID!QYMY`h8ZL* z(Xh!6#Cxz)ZTURhk|ZwWC*VfD&qsA&V%58dRD&>SDvo^j<}&d%E6=$rpfWofBr3)C zX&B)|*f+Rt{fm=j*0*{H4ZMG+9l6ZR{A>p868-L*vO~iSv=KjBcQ(HUL0i5%i6t3{ ze(h6G*4CzNlbV$LX3;rLSo3^mg?@EOOTR1a1+>f9fx_tDD&V39%=Cyv?&xtudVVx7 zwYN0JGEIOS5K}t?WWF7tUSF^4cg6(*saob(3)jHfbf3*N+-|*~XauVD=N0fPE;f7H z97XiB?tLYYD6LelIe|PBC0+k&eKviBOqFGXM8fhV??alB%<t?z<TfB^Y1nwfL#1{* z*@*EQBEmX*vaID^d2Ss4KMb0axgOl~&{0P1UF-Y6DR6D8nyb4AsbIyR(wviu^Dfk^ ziBeC2LcLVvSIoXbI}<J1QI>tSJgo1j8O)v*BvQx#YSdodpLRo{y8m$oA^(bplIrG~ zk61sd9ijU&2oPoN9x{T$^zk?pRj0HAKTMrchIJtCLBwcU?Yo^zU{$^FjQ*cca%%9< zbg!ar6Khq8sK}wIwX&h|_d)%tV?8M<0(v)+LK3xVWPM90|Jdie!1mRp)t?J@so6Zk zFj*agsmu{JqXMOz1IFKsp^#wYaKXy{H;K&5s@(c2<{dpvA_a9OMqvf3Le4(V?EW*N zp7D9R%e>-Nw{wZcZ}9v-DX__Ac1x|rQ9EeNYq-x*ajr9eg&ZCFU;UoJF1W&l`{;QF z?-vi9X?S=d%DdB>FWCeP9jLvz<5oP~<X3N1aQ9&L5P{#TU^3*!D4!hBtN}Og7%q+S z+spnqyD0{9g-tmyU0hK-_(|9z)ZyN|EZdUjPCH+cFNLQ6RBS5+zi!O3WTQ<BLWJ%a zvw>e{#LJ1RJbiji68^?+dbt|{hBa574z&iT(X6IdtIxJ&n592Thu8rEyZZeFlRZ1X z24~pnUy4fMX<zsc_QSj`f%&bh%Ku?Vl)O@l*;=E09_Ou!pbgd16t&O~<~pi$CSW4J zKZYOSpsnIHF@7c^sN(%~>x_0W?Kvyc7zrgVc#+S3AGk(RIq>)A-cESUMfbYZ?4Jup zrKn>=PMvE!Y*>dB(R;eR`uh_@N+ivp;Pv$k9}~h}pDdbPPAbLg;cm^ax$Ms+r4V-y zC7FTNuuPV`D+Lzj4kGkz!`^T6My9^{A$@d(pn~@PG~qgOV2~r%h!yb}l&!Vm@I(SC za!T8OQo%uo9Ts+0BB>MmXoY`RjZ}?r6z}@@yU@!o0mHg~Z*1Bnh}QJ(?=b<c4i0j1 zC-s9CswTA2XcvLpn}L#?fzHVHv>VbNApa!4CHkJqY44<o#DB3n9${-o7pT!zcXbyE zWUulSw{&dLAMJAghoKGw2qb>Va%0Y-)oY0AZ>dM2FQM-X_JpQvRFWIxO%?x{dlSyj z^3G(DpchGTJu^y@%~ns3CRxVS(7!#=d)wxHw&jQ^8A%@~S$#17w!oSR&sT^v6Fx9M zfEB|e+TMmBMU;Wxw|5^2ZfnqaNX>P=er*7cy;&k1COAaD*fr+S;YL<=4e$9y`fDHM z>0g-8j#yyb&EUM614;QWDeC*&{D)!t9|px;T5==+tHEW~x7>LO1W3{Z1w?ygqM3uW zMh*|pD4xFMF79dNx~!T7C5R;BI6zvT@_p|ZDygnsrM**?vmq`e|A`=gO!io22Jq74 z64fmF2$NB^-JE;Kf90$w;rmI8O>3U&MrJ@<+tBXN=!B-_h}HVMNw)=LDWBrOx?A4- z0gOE%L)Qe8*r@n)xZAB{#Q}=w=nO}G`^##V)+Rn>nu^*J0<|!pyC*5Em&6Vja@X!X z?d-_2+3L0-+w1kPi{?)_@qYY9?!${Kqdwzu*1rWIRu3Vf58ZHu3vy$c$;j>~#-ln^ za4y3Nj)B-LU&`O1LEfAi1K!iT1~=!QZ3ozMk0*LCP<)DQw*{n7dK0I7^W(fZ^jk{w z!Pns0YXIZ{Z$;h;rcHeF8Bx)l@{hANbnl5G`@=s8;<ElXWfKGEHXdF91OkMLl1p9b z$u(W)+$;UV@OoZu8)=pCPUH(ZUz7}zMMXZ^B2CiU^Hak2#oACSy8u7GUV(b0Q$Fnq z)_M#q0AO}`#-taYtplW&c?g)-7Is*yiAPuT39ZV1(fv^E%N5j%b`2wdxId*rU!Gkx z)oj_s9B4PPM)F?h7xn<>`lk<XLQECNaNFo0wX9&fX1XsQ%(nZELACct=`S#a?U&bL zEm@jubAD=pEj%=9Uwq_5aGF17O1^NrH`OW>sSH8)h|N9$M;6!&c!9=z7|p}hbHtAz z_sr?FAfn`Ivk#^gseAiV(j8n}TU8Zh37jupx-oYo3JR*zhJlP===GubGSvfnn^T^T zUBLCr33^eAWy!d(s!U3~DiT)ydzJz*ThJ-GKviOS;a%0P5Po9<zMpoFg|d{<MLqLW z8c2rX=x3P3>E5psG@30@(Mm5jLnU~|u8>eGn+*$_X=mB89A^3j%6E`hkrdDFb-$D5 zb-tiv^6~4)KiA8Hm=$9sL$<kVTV&a>^Um`jy)3#+ny5E)F}Si=|6%N$EI5y$3RzVT zHZ}t0(-*tSj*4h788w-FQT2e?)fB9X(u-PU9T^cw0(#T2oKKv#p9)i3Es(8y0!qUd zxZZ{c$7jmn#iQ9tR(C^#*_PIoW~<2P|1h?JFu|s@EFACuFvN|CWPqWYWTk4#k2>gw zD#m{p38AgwsFLbq`p`|9xWN<ydgZJgBp88ObzmvHY4)$Rx+4=$1!&&mpu!*!Ka6y( z?@Wv`UFU*nM+z3gJUxKvD9`|KQ|<_bJ!`0Jbr(+o(fkS6c9)PjVYcJdo&E5D=ETqM z#$o#uO>Pwc7ST+DcKjwTfAq}@F3S=Hwz|twrDV#o3pQD<l8_=EVugei7?E@*T+oMX zaOD1dojr~|fdAf8zffkAHPIuORqawy)ab><hRJ?fuJhsX@gX;qW=Hb7Iz==-kXl=2 z$199~w{7>>w1i15N>pTMj2```UP}-GpA(MO4HBhPqb5yCV<c$Ad;;QciqN;a&{=p% z41pUZV)gJI0cyGXoqfO2>CgD@=9+6ed1ngp?<RW<<bTu^`u|6n$n^i!L@IYOO2oQf z#_qFoDfcefolHOba~#T83Dx)BpkgTUzC6`R$oV;7OPaTiva^-J@Q}TS^HDRs^O)~N z*~<ImP|<71GmaW_(JFVptMdbPmEHJKdk@2!pH~wjGTVd*Fl+sl{Zyi#PQz&7Ki2BN zf2m+xNwOeVTp~z04zLRXw_*|Wx7buOvFsnnbowI03Vy9qa=rXXK2t!xRTc5Yg-e;t zISZi`0pG3A6G2DOPo!f;^p?K<C^D9W3ZS=HV*U(XWoyJkGV^AHdZhCkqR*G5B;QJ? z;RDwjsbv57LGx3H`9gFR3oo%=c!-I2#a`bw!bS%8VxD<Rwahx|VbhW$YQnlFhn%sC zZ@%is?@W6PpW-Kk=%<&e#iYe`NNQnKn8X`?oPz&+yMH9pJ+m4aE+^uvHw3keO9>+u z=i|2z;Mm{j2rLS%X+$?p9>4`3o{>%9gj6`PcUE!{K!G2svli&AHkJ&>-c-o)^qWKO z(<fV5w?i5pu6uId7MeoRk)Xc+ags<gog8uM2!^FVap>kjVCfIk90+>Q{S<MpOjOLB z>VzrlS0`o{)+)Yh4tC`eNrg)}8sz1_`Q6R$HBtL;8x3gDzV3UH#__sL1ICs*J(lGf znvUXNeTOBfK9>XEkOa@$f?TD{->gNNT(JL#QOKEsxpO#K?_d&VKUsd~r&DLqlUsDq zac<&>MoPq+Ysl4j{cJ5q$rM~TkZS@X*rk<ri7VV7FosjX>e42P$p=Z&Pf_X3iqh}n zU}H7r`uel_On+sAtbab4Bhi9UXVYbLxqMl#`aATFT6_QL-7QVie;7VB|6%-vJt%8> zUPSX@{+X%u(-m1dAk&4Wk&mt(QN|%Q?rv`wU^#q48f644{ToUIL)&W?Cm%gkR4azm zK~d%V{+TQw$_(8Q{`Qa%o?uG&wz4A61nn9OCm*zE5ZHz$f1hX!%p|a-sd|d^Z>{ks z#o`@?`7^rQ{^hGTmL7V_SD;%B$V*qFrf4R{-zm=TZWPU_p5>sW6e&)s@mIJ})Dxh) z%xaxQdfC6Rel=#>`s}@0>jj+_h@lWmT{M8GEI0qT6v@XF(xJIf2oRoH{FgvH&ZP?R z{1RQMrM-ej%yV%}1Uz2OM&yi({eEuTIv1u$Z=t_u2wR$%_9$M{xdB~a-dlG>yXlQY zo)YG<YZ`c=v<Zn6ymu}nUbrs1kN$!%GgC2<(`;;SEuwG-bJUNGW4LUyP3nnKqtiC8 zFSNVUJjmtRFOkrK5-1_hyPb#N5dUH1!m~yC#stG9@b5B6V{M^v^Jjc00habPD(e<5 zhseCvk;K=aX2(~CSlxaf)qSB=if6nS<#jxS=-ZePeY3sz<H0Y`){hmxsxZ0h9G7o) zrf%Ag4Tm*_u!^{K&gG>vXlypUSj(JF^>}06Pr{4Ml`<m*?G()e68^dxlDSTV#9b@+ z*#7xJL*kO$b!w_^tjQVwt&L}J`xd7mSuys>YvmhMC64cJ7e$+28Z7R3W3BQnMUJad zJia8aVCEk0?pMYsk_az-Z?9AHA;QrZKL585!PhOTpF;J^>4MRV34zB&HzRw_oI=yD zf{BO9eAxi)@?Bzw2J>&4TFQE2y<KK&ELFL#pAmOW3;<?!e)$(Th5g~x_q#|?(e}oy zS`-<!k}kS3`9%%iR$D~E1>mFAEt6fKLG9zGaca?6ON%<zL{Rlm2XwasV+H_`MfN$& zL`0%h1hNBhIzxkBFQ{K*+pJ~3LeL7bhD+!60&B?HToKpgxxd_OR@}6@P8*rQk-ofK z`~!0c5a0yBFgrpsJpb-dGVJ%I32`9$ywf`ho@4SQ9_jnQ*HCB>RBKZMlk)7|)Fe8G z6FGPdtF^jaS2X3BIKJjd@oeqteOg1lyPbK_skl(c1dySBZLxB{)sm~@e{L7XDZU4j z+|^TXbwgn#iCMX8cIB{5XHHuXg|8+>y`UQ;47qDn6{=HNc=q?FuiHQEnT4H=M{_28 zKaDK%@KE$Q>h9iEGkEsZ1Vdu~h%m0_=&r-xu{G!hpAP-3WhqZn<)^h^e)adZz6EmV z6{y5qxzG(!VUPH;*wv<4G`vt{F=%##S<eWbT-+=x$>hgs#)D*QD7~0hP-EyijdYf? zTj5jX%(qT72g9gt6HL|*ir<zKVO$=%su<uNZ|vzCaVgNjpNA6Zy7NCg{V&hqS(-sZ z{41n1HJ6?az5cb+XJ_l?2&F09ldr^*#RuqRnS&zsSIb2gm0V_Rgr4lt!7ann`}~rP zREkD%>s@w3z>)2^4UDrO7(IotwY;ZXzdeXU&8*0RT>i|g3Q4{1(J)3@f7HnFx+jhL z52JYnbXLoL%BT^sB8tE|S)fAXq3cTrx(b${6!gjzhd2`|yIyj$o9%~s2107;<&wHV z%-S=@$QM@9Ri^PD<%=^8CB9M^;h=c}Y@=yPnsGEe0PBZWUAVdyeowNs-kx2jg=JAc z(K$HCf%eY22!`9$p;fhl6#BAo?duXoVHS^LqT_NlfC{Nc?{L}jL)QNB-C6q3wIhqv zx!CK7p}(62%3@ECerkTsQ!<+}X=Ba?WSt>*&5B~ChRQX!SU)#@UU}VOJa%Yc!E=c4 z#!3n4<|WX!^sUMmXXvG*NE%l0-+lCai~MxRpI0#@eI9=pS9QqhjwO;MRm#S#5o^VS z!0Hx}dc&~RTFrP$Z(ux*-VXRm=_pVOH8*b@)||A6p(e4mH&3K17#JY~3y8Zm{RAOa zo!{ygN2zgJu81(z+k!3G-<Vg|#~-H17rVA7GRFPdXtbwl7r{4Y61Qn8e=wXy0oj`O zv9oCY0ghnCMF~qkb+|k{xMwcy%@ONH1U`4E|2YYoO?$kt+CZu=e^^Qoi3d<e3SeL- zJ}R_@Gl<EQR8w(Mpw&!~`d^CIB*sWz&@JAwdjpe#!a}%xAA0S_BULTpVl=hB8C z9%=7k7&E$keS?KLY5s5b!T&r+$qFFQ!vsh)PvT4@!V{C(h}o7f>o&YRUSYdNA9&t? zQe=04I}<;XW5O*4ZBXroKt5&-5}&?)@Yhf9KX>Dyi{_ghgn#~O2~fdP-2op?{!r)X z6l(I&X*(F4r!;lt)t?JVebn$&jMy?SAilR=+K){#N@W<3l||cnpd;wL>h<ljH8vTx zOsE^mGx|qJ^#7slEyJ4r!@l7m(j`bJHKbcm5JX^vbayEw-Kofg5z^gAcZVRYV@M+? z&FByiMoNqrY(BsL@w~Z?<9T;q&;1q{u;0FMp691z*?tG&U)G&!aDE=L9pMKI3-HQb z!5ZW*YD?2|*mrJpO4dT2W30}Gnsd`xMZm%lUToC#jhNa=|DRK}9)zkzN{NM*pYYrr zC;AHfmatKspG7&9D`$Tfj0MNm6`K~w7PYfZskh+4NFeLGzetu|*2-&#B`icg8MBWY zc5!ux{KRp-dny)7C*M|DYl6Ns&5y}_cr>`usk-&<<F*U0b3U$?f}q+&4r69#%yBZ_ zw7P;X^Gz=_gl|G8;O%?WQ(76bieE$Vma|LejMDBpF(qoWARz&{q{p`fL-T=$LV|lX z&0%ixTh!w-Fa?=UG8IuumCk49sOYG8#MHH4`6Uz2y`1E(<Y^iYTHd?Pn7@w?7v8`q zj79aSbj+R#^%1cEwZdb|i>O-trmJj_-*O6?fhY-eb*VZ#>-0Neq5)s85o@}?x|o=Z zkVjcHN=T{9oL#rKgR1Fd1mu?~bO3Cf?9MNyT|C4M(=z_ywp>Ng`Da@=bQSP*@#wv5 zzY2n^V%Hjh<&jgVismm6x<*U`;Oe3y%g@;`<9tAPsv0Tgpk?d7IrJ+6qJIg`hI*>% z6E|vC&Fbk&U3R45PSkAKR2T_3ZeDt$Z)t>pe6HEuevs|B<z=+GS;snfAH9itCeuMc zq<Sk3oSMUeKkrbIAi&1}iUWr_b+V_Unv?=D{>NU9j?^uRY5dLRw|E}z=UdS8_uda5 zxCN!3qugta1Gez_UH^d?knp+6Glfy5*sFK*h86k=R%UIYpScbi#Xim3CkH=wDuL|Z z#B`r9MYnACx7{TjEZqTKH1{^B_ki{07{6(meP)0*9=56lVGF#3tL#C3iT(r8K<23` zyFZAF!zuY;eaK*Y+?>_<&@IsTe?NjtAmRTrS>}KE7yh4r`QOz4s#VE*ynW3U_h)F3 z!9(tdr34sw5jMX32YS!5Wbjnb<A>7>N~u`DmzppVv4bL6xcmoFVF@ER#ty8M;X#+J zedyUJJ)uwxNT1(1MODAnjJIhW1W|4O-M1_=CO9h7Lm|fm7G`1cNFCO$chsv0M1CPO ztEwv4jx+fF7L$6{MulVFp8m4HB6^&`kRari0wB(PvzUK3Ucp`+-ToV;r%ImTRlj6_ zh^J%lH084<`aM+m_aZAZhrGM`QI?k0k5u(A_j5+XGl|WmNsSE}=d`)K{(EY>=4FjP z4#m}7y6iqElt}U4Il-+<Wruo^5S<1!eRBwFCdf2SH2egb3g9@E=kCuv!^zI%?ra*P zhBuQ`YbZ06rKDHco7E-5`*hngCo>)X3@~&3%-WTy_4!Rw9A6iuL53ep!sZQK#EsQ2 zeBvLhEOVE0o?vPj@OUb2pvqVJu}@%lmYB3t%d^}tH%zV_Kjo0<H>)Qjjm2<(Bgf>` zGl(wWbt=c*f~qe2n`Tm|SkH_L$odJzDysYKLDtQCa$!0?#=~6Xa-i4pY#<P%;vZ-q z`q<II)~qIwBXi@Atp=mlxIwY%SRQ`?Kvvnh$w*8ua~o*zU#59~=QqG!z&iwr_W!qc zp|Juyqj}~7-ImWSooosGt6wq_3%2vh5UweD2%#@(it+*<_-G_ma_0UG0LogCR#M%_ zJLzY;Bpsmdvk|)M82wL;qa}TCR6b)6XAObq(05W=uzoefi2l!-j<0~YG*ZytbQOU9 zsUQ<KT0S#ja#Zetq6?K8c8SE%_h7^u(;mMfN>%6Mno{>J{w%_XZgpgrHl69=95ZvG zh4L6+(}ZDFWhW24UtVO{ml7dLgJu*?qWD{7gx^V@Oi#pA82R7(DjG}=(Ez=UY4&xs zL|$-g?xrhGjaL5P``E<RTIy8#jtE;;&A>XR@>UC*(Iw82YG;5_wkC#f^q?t@zo>Bb zGLniuxV`ezkoaX>`iRA9{^jI|mKNJPBmYEW0ZiO{?GqqjYi8CzbtK9xyyHDP-ZI#L zg(OG?Ta|`GeCZSn&I*DO|9qsTO+@+zoTXRy4C+636XfVsBph2Wc?Yj?KEUtwvwAwi z`=nj7iY8{}aoZb}srR_%>L7Xvjo9ZGHkh~B!osm-cev676q4A$9mCW8b>&g(L%5S; zwrl;7XEy>`gP>~Pn;)W8ALN`L+)$Z19|a#}LH~iCxH0%Vya;bup;hXOdU<HO%+FX3 zc~lVn_(!12?K|5;A!K-hGye%w5=i#Q)-HmHuEVLh0rh}u<5a1fWK&PaADdt4CJcQn zr!)ODM}zV)qkT>4gL*7`G=UzLs5|_yt-4L=%XbhNbMoG!0D4)+9VFK!|EnX5mXCTi z$pl1yu>kLBx3W#3Y~=8kP}{`uLnaXgutL*>UX%Mjd5)0q<N>1R^R;xPdGSC66xpzE zHzIGb%5=MQei0Qu0r*ql%lT~AB)X~^aY@Ck-@%*5v+ON0Jmh^^OaP*8Qxm^Z=caXy zVLJPXT{D<YQlXz>e#)4FgtzxLPj|8_lVM_4`}Q%2-!ko-@kM&5f(*41|6=*tMyTGj zy&HqU{YunZ!ZpA%8U4_#CQ%&HSos5ff3%(94H6C)J}}oEwwS<t2-inhXG+)v`=!Ay zSm!uP?xm!>76X4Nqi9k;a_%WCw7g_2eW8@|M4vuUrAj|EmaLj%Y9NWsv$56SH6WO0 zi+4ez44K>q7hkrri*9ct;ZX}R1-%9aRCNtiVHsR%L}*idZ<yWNE%=7oQwmBd%7{7U zUmRq#kuB)`)u|o#E!695cHfaHiV$2L87`=!sh`l$8?v!0e{<&6sq5fd2b5Hv0lO~i zsrMctepXwPNZ7tdfGvW8n$2wT#qdhUsJ`4{?|V?W?XI)W<po|g(Zrf<B_x+GnOP(U zSOLYAj%#tJiD1qXSeD#qQSn?9FvAusU{y;S;UqU)LQ-Q=u5cyDOXqqECI8F7>+_ny z!(ukV{nft3TgBe3>D56@>m4Q?*-WC`q)UWz0g0v~T8A>?V(F{uofieP!0k(#dsGx3 zN4?LeG<<6nij%z%>i2ee?op6Ylp7}8XbC^E7q|D-C~GOS(zt)lfWfyG8gCQI(Ydd2 zzXFk^86?xOL4*4RKRn|9#3A84SiAaN&3bLxWa{$v#-b{6Gxq$o?_GcECZNMzjN-P* zv>C4*`q^El@(h?tE-fXn$=8mS2{{@`Rjd0SyW|8OzR|~8oI!J8fbuC=t<^-WQoh>O z=~IYgqva*Hx^l{gyueP3Hx@E?QKj&!xb=iZjQNBIF!zSdtB#3Rap-m$ly`qKOJbc8 z|3dd1b>c_VOJ#YnqoQD2T%9!&%A>FNS%PpKjBWs;DT3#o5+?&0cNGpDjsoQ!fk5Rd z^KGB=YmL0SN?p4g;sW8~rU*DlB|r41y1?}@kNyL3F_G|L!ib8`eh2s=+_*~MGXc{T z<df`f&+o=L>~?}f&e2T>NXU;x9Sm;5=1l^-U+Va_JQ|cC@b(s$795n<Yvgu7nxks! zf|fixL*cvFu(6j!RsI77IJk?`I9r3f&)L^1E^vibE^?q$LLKdzuvnndzaHgH)4L1e zpa-3JG3COnpnL0hA%JH`3|Mt1H;(KK4UWSFO(tgWO!#4hOMxey>N3?>Bs73afCd4! zRkBkuE1(1M-6;WU13ek&s?x|9DFrA51fHM-<o%RE*B*NmhUiU)s069YcF8Bg%Pfj0 zFA_E|3M+Kj>(6ID;5)?>$2%7x2TUNU_g5a=`&C(38XVAm1Gb-99%TyS7P|?Q=PF5d zA&|OkWs%kUkvojZ)sgCiSXBeF|Ha*RcNjLsA+oVs?Yx6=K>KItU#B$|uXdX@0@NZH zoJ<u%PGpIej%Io#9q?WmGuh}(>~)G-4}&G!+k_wUu@%Wk6t%ktppSj{_E|;)j#2n% zQwu$aP9yi~0)y70N2X!2LE@a24+xT2;m43Q4QhB+0Zg|YQaFTQYEvwa;i)116k!Cs zxi(B*RtCP>C?Us;=fE(oXer6rgDcf^`AeqQP@y`_?B0_A|6^y&SVw#~l=qoz5gt8H z+Le_(rD;Tcz`3VmJHx=!3*ZO%6BZbU_gH8N*{B+CCfUet9R3h%gt1$Kw`GXavhsW0 zumIV2$6VZhZc@9L6FBe4pPyW_(F9Eza^EwkZx{Yt%>fn#POvG*y@$D4@;;AI46JE= z1PC-PeE-n!$!McmI;xT~wsJvNhMr9tp!xv*_tiYZzqqWwTfEF{;?QNrNqRb{;#kVR zb7ydz4XKhlwTrUz_J~*32fF{zSuaZtci?xE2PK_=53xB_)HDU0pvT4X_;I<w+GzmX zn2+DhrdUSIi3+D+f?tCkVg>_qqPFKp-+9g;KQpI5y>TZ;R@nnAocyY053=;mZ3&M0 z&JJI&IizG8CbAH_&s^EjNyA1bG<fgNn#`K=sxF7kfoL8Ok_TXJd@H=p#MB8^3{udf z6w4O-Zw9dC*#xBhjxxe23T(tJJW28yzG{Ji*DD25{B0T1d3;r5fsV>-_?+5h^$ng! zHBT9TIeL9G;(EGZXC_;zzU4}UM7i<;poz@z3wem*{b2s~t3YDQiDi*Ddh$5Ry6*VB zvI<YM;a`!v=ExX|`m2jRsjSbgr{4--_&9-s?<=3wgp4Z<+aI|qYNB}-U5{({%DJ*$ zksin*lF$f_74|eW+utSmten)tvBqKA0L!bR>@xG&*XJ8dN$3bfrR*{n^UPz7b(%*L z{lLXlIi5~$qUXQ~9>z;Zy0JM-rfgshdU)|W3fNZcAvEG-ly1b3=@V#y417Aee;t^? z#h*%r(J29pWz0Vi@JUj+5390Am6PIaBH?||jxEc7pwKJC`G4QAo0zU-Ak2p@FX}6_ z$l+?2VR|;fz(I%0I7fZTt97{*-(Ni$)iw;IW$WzJ8yE*dfa3nM^&r#6uVn9h_sE|S z@Y?zKCR4PoDs6v!7E#Ie3UDNY7i3ZcfeASeNy96Ce47U(t)aM~CA+tIlxZTMsR}sI zxXq}s3E1<Jc@H!Q0#+0I3>TBQAl^1XDxf7SgycCWYr+P}rSJV{g3%q+iL>a_ag`@B zO4l3O!)BVr9wCtU%XF9Nf5(NFz8VZ1-YpPYhX_s|?w75te04;#o*W~E3y+>n{i^(F zIzlr8Zi>Dk*<VF_rmr;qjB$0rEZ$n(x*y*q!b5tg&1eMXppOIK$Mycl|Jz{C|4;k% zZ~A}LO6BT=9!%bjUX8LZr>rnhgDg#3I=lQVsK$AgBxvc+zKRgnn5H`MK7m2xXH!kr z?Q<N-9z5snXe_`RQeEv$8f|K?Q!o4W^@Z=tx`6nHBC{bxu#&hWnA~n1e=T3aRKPyV z>T|sKA_BSw^U)wc=w?+Z`&$dQ>Sz!cOHmhDGfQjJ2QH9ZO?apB@=M#dCjw?azUv|R zhqjzVUzyBM&wX2Z>qPi0Oe}**OH6S}<M9Met~FHpCP^=IC}2B6pnB1Q?sGE8-_+~i z&f@9>MbcZ-)IE!*%sf9AOfJG!_#=Pq@n(<W^Rfa%1c-W`VaT?1<Q??=A?|wGme~xv z5Q^F%F~gtQ{X*lyGR4~smBl22;(qb9hKi_yXLkJZo?6Eq0gyr`@+WFiOYD)^$WZg( zMf6r}7Ae0;k}@l)^2o%GPV%?5scDCs)FVc5wGlkt&}eb4?a9H~AUrT~jXB}%v!<f7 zW46aZPU4NTojaIUIGJw2hDrWOYZ_TDKy&B($8eMo8`bc_LF&NHU|N6LP)Ph`YH@lW z`y;InK62cBfVJq>KM-A6CiQ(o4$cn^>B;$4$yN8wb}*Ddxr>V{59kfs-YPgRDW>cz zCdLXscFQzGz74JInL`XyUmL@@1lqdQ$vdk`%bf4l-zbi2mX|vqWYto1Ut!f)e<Q-w zf0)^1Q}J4pP<~9{1ig$d53<^amiPW-*8j3NCYH$P1Dguf*BPBIn>wO@g3tI`7?H0O z+$%>^F8m}*lqZ$Mn@;?`!SEwfxtL=E+y0lol_MJIu4jn89ja6j-$w5DS%TSSeS(BZ z?)qL`u)bb$SFdY#WkPOPe~(%e+bnVRa9@XP-mi1-C#+XR(-IL4Bg-jd=r{H4lL#K9 zJSaA<D^XTxlX~#*%H#cIs$@weLpBp1Ybt|5)#uqFQT{qeeKhOLcVF5rVGa`!SA65h z;&JNuq(Y@EnjvrINCxus=j$>7j1qr{w4N_&p3n3?Zk>E;c+Dj;P9CHAu=Pb&qC!!h zkJ=9E5EFe>{?sW)JVCt9{&CI4)Na+-Yk(&<cfl8nndgmm6&?%&u(+`dmOGw<WuJ8S z9LzzGg`&!xiHBX!RxLF@h_#ANs<8;3jT;&Ih;85AZEA7#vVTXoEstPb<xYKa_#&;! zncPSNY}!HpjB{zxb*qzlIAzd<(hHU;#rx&cv)O1%9cE64_})rs<($o1IQQT57z$0= zeNM666UH1H1D?1M&4kT&uLaoSRLe}0RasA`OY-t)#3;ufi0%^~<+SWg{VEDY$u2Es zV%;6%7k+5u>A4W!1d1-d6L{}FW!D+^-M-3pw<t-kIuu)4$8?}&715X&Eo8)^#mL#L zJgl0@&8;xmL_<6yguW+RfSfdb$-&E(ttLkJ;QTuuw<6PH2@8U+lsfbmu8yNiTbCDF zgtJeloJK%~ldnxTe}&wi;pjg88dSdZWW_^7g;eUxQz;aP5l1svIgB?nT{14r{!o<e zC#w>Fs=Ar&OXKv~5q_u0odIj%;O@07hUA)_XjB#dj))k{yF`RqxIpw>N{VOA#U&qL zrqkJd40VkQw)pBK-qaAn`|wkQixR&SFeyd8NV)iBy)68=vt~{*x1VkH%<=)rO(4-< zD92m-cLXx59ApN`HL;ShDw=fzANjaHm(>uurdZ2%|8z9isI6{0#Me;$yF0wO<?bWz z6&v!jS}osPyqz;h=2tF{;IXcIvXXDh*q0z%4FyD~iJEx5fE7PF{1*G`!`F9XU*YJA zle=@x7HW*?;#~7HZ`VWjREu`=Z$ozzH-;TPw~Qm1ig)VQ9;7#tyu34QTT?s5mDym% znZRa0y=Bk6PfZyhyFVKIrnY2!yFCA4rQ09p>=w;(l@s+|TFAtcA_lOo%#JwXYRO%e zgs3kv;gxQR)i^LIc_nx=cjNh+yE46igCl8s?mAbSE6UB?FP@q?fLZX3QRqZ=6^H!z zMFT<4MO0*Ysh8z0F{8MbR?>7D`m!TiLu?zb2u*t?!<{)*)1zOyGnKnoP*87&X`eOp zfpF{vT%6n$z5Ax*p?f|a+kZn+?gV>VGNI9X!2BAhOQ#FpP0SGHzcwu*l9sys<~v1} z5)2q)QEl$IU*AGqOckprU${=E0Cl$6+5YNu>q~>ZCVVx42RwU3GfOAFFpnk0GCCcr zFQrv@$+eA?@WZB*Ka+3CPnSy?rBk3x!O1~$>kl*@3aDQ@miJ7g@+AV5!JLJa`^>CS z*;W7&$Xak?LcxZlf~uR_BT=nYQha~-Zo9ab-w|&ml+NlQW7>J<H?`UxnF_J*bLHaX zn*fxb`9_33v)1VO?nD03`W3lpq+Iy<CH+^&iJuSfG&N=GC`PuH5@n_=1e_Vv6Z6`> z+Kt}V{(6z0Yv1B{F((qxmH`wu)*kLdaN#8i`2nd9N5WnJ!gnn80@mx7x}MJglsJmm z0M=P0d)mO30^q@J&kGj-y+8-I-84$zN^t59tbSINo@fc7nQyuJo_5cJ1-hsr5Y7&a zx`rax@Adx!)jj7~d@dMmVzWJ2^7^xV@&4+>tZ9s~vMjgWl@JPpm6XQHWDN6#Q3Sqd zgA+JebO2B8>%kU7C%<q29AM-WETXJt@vpiry9u*H{5s(YM?&KhiNW8XNbMNf>E}QR z$7Qye`qbw~BQ#jDWApa?rAhDky3_9WzkEn|trNFBHPKzBLUf(hcU-|fajD6$A9Gji zv&V`^cwN;$P_Tq@dgvg)lEd?Od~n35TEkEhnUJ2MNeMrGR7Pfh>cYSPo*Sb&Z)aT= z84PLuT!jkRY=nB7_Rn-apdTA+QF`A_J@&d&+DdK{b7PONd}yl)SJ5oj%f21Ayuf~} z_&amNPE=yqwNg}gXm!|hAc(M<et1FuC*p~f+du#hc##m|w2So2(0@_R^KB|$<j;2w zR+UMg>=mnh8Tp8STijMH?(@#c;k}k7X+71=^YScgY}K7LD>ODoA-V07e(R0wYjz3B zX4IQ`om12a9&r&?p$f7eLe#ccb!(uRiBTKqq-WnmVP&cv$P_+c4C|N)nWeUmt7t`_ zDSP^*Dd?SjbUSi&CX$X>Y7H1GnEE#-p#>q3WOsmKa6E(B&EpL$0F;7G)n(WzRRHHp z0>qQ%xQSnN4VL+pr4Kwn@dPp}&{YyXRg3!Zt6<ntkULIxK^P?HDnDHkWJSHd`e*aE z6mhaMPs;cemg-y@y13k9ct_^|B|b7U8y^o05F~d8Lv8~@ISPkvRi+o6AH{6d<oW4O z=PNNqSzd0x7L@-?ssfblmGMgIlrnnrEMfru=dAFVV7xLH)(*7Yjjf*r9?SqP7)h$! zV!lZDm@~Pzs6HU~8|nPhmlW(U75|}2GuB*=uK-wDM<2L)ahdI$!LT7rP*CQc-CJuB z6^*-uN(50}fIwx`YTtT_oxdj4EG!Y$e`CM;*4B^$BAY31`&v1J6JSFcEi4A;HfL{5 zMQ&@?&f{HOl&s7>5w||KcW>3G)=T>hq<_EuG&VEx!a2qGi8vh`&JC=||2hTrx<5sm z#j8s#gDD}#@c_4M(at1hzQojbxE&ZK0~2E8A|YE2#xKAoomZ&mO(XRE;EaZ5e3wr3 zcLJbT`_2gj?e42pl=hV|a5|R*Z!;F^l4mTf;UGB9j8RXqn{_R$j|QFww4G0IviNSQ z_XPg?j?9Hm3sIF|OZdS#`qu+pJ%g_enf`Nb$gQ|UNIjzQi_oGxv+asx)%en+QknL2 zY2$AcT~}?XswJCt)kF+o+F&g;z{LSi&sQ@vyzFSMVk^AZG7S!jdhs!6je7T;J$d%s z_krhljso%bu1j6NEp{})WB6<-q6t^e2CDy%ukXf-L?>!35Z_q~_Z{zo-$|A%NZcS4 zRoR?yFRWGF<=ZH+wzG{fZw{W<eMR=j1B{Jo&+`-RDo=42Fry#5p!7>6W?ledi3Pu< zkg4cI1h!=OA8t-KclyvymObcnOt4NGH4e#LZFn%+3lQ{P0oHA^M+bu=Q7gGal>K{^ z%g^nMU>ek18Z#UK49%SLJTeml_Tipoho?^5a_HSK0nrrlEWJ;%2$Du?g_joiKMWF4 zWR&}I$tpfnGP$3@JC<fJxZcca)VT$uKj-O_l}f@}FtAxNi+x<vMY;N(v1cTlzKM-S z^8DnmlqbqWCFfH@-Pq{)v^^&f2%y_FUHZD~*doBQpXwBSY?Ib>n#|I{8AOHVz-}(F z8;Xx$^T|sUbOEZv``%0g-@!B=n6&Q@`qCyZkwr|DtuYOj1$2j#CG-O5FH{%IumPE+ z^6!1j0G!4TQKUKn{J)oIPFP-m2dT0wFMHX=_BcTx=D(gppdg*;rI^2o4cBigEiZFK z?nN<dCc=jHAgn8-x_dHWduFQpt7jUJawEbD?rkbiL^}ZAe!Ys@pS4j)Fa~hEzUurv z+N)&6lEuVY@RV$!#_H<)d)hCK<=az51w@U|B)lx`OpsMl6%KJMdn@nu`2Msnox@#V zl#@P*iZp?wdscE{EKaU3%ajvMTi@sSL;W2y#lihVf3&48yJT_x*Y>5TUoAaqyy|&{ zcC&M>5(M!N#^^LP_&9d&o+dv=Gv5sAz$w;3+=RG#3RV;h#O=KXyTiiiHj&1#uM(cn zhW9(KH%xBzWt!gbV01X`NZ)Tz=l~C)UNOua$AI`I#z~{PVfpn@9}9H#ap3;S{}oQ+ z%>Sx8%~=a~<G2nwX|`pFfcUvFMwwnEm&?A*aiGpkGT^7i4Xx$cb^LkY@hlh6fn;er z?<75uqB^sBsx-3vWlVZhOnt=OUTmc&#V{ed>aK~{jg<9rdc{|Y%p$Xr&{NZ{12=uc zFRfCeuXVVdd}51dNT(7yc>b&%xKj)L^#%&+X|G6Q`VTpK)iINJ61*3Ws`lizOrgW& zH`(Stn@<nBkJ(_f{CT`ns<XO&t-#2U`sPE|rjE#eNTjIeE4ZsWGr9ZWj&OzKue4<? zqP;NChLx26s}8DAro>c5VT*RQGW{IF7oP4}IXMJ|)Fu6`8J~x{CKW&1)4zqpf!#l` z{CrT#=%p(*CfKVvQ^v>L(;i2gs9qq~alw;y<a)SEW&;yU=#W1#Rxmil@=IJxp0jr0 z&c@#zd9c)Nr3gh4&MHH)lC(bt!T)+v#nGPXEG%s}L~eKg5R_ql>Y~sVU>Er<ti1yr z6jUZ4AtWEFt2gm74(D%MpU-;9TKPt69|2IQtG?Xi-Kf2hen;h1gNx8G!T#Y;hsnlN z;!Yn!A2Z`WRqZw(6_@>?6?=WoJ#}libWfi1sb}|Kop4RjjQvRhvy!JTxz+dce)HS; zGaIbTrhA_rU!u8~DxIDo|ID{Ob28BxYl=}=+XZ%L<;9|dBUwNTcSoWQW_nT!c^Ppt zo2*U=j`#-}k$8pb=5;gOq@F#Z*s$!%Zxj!H&MB3h%aBAbsp}?*fsPaE@iS`}#0-o| z@{Ai!s?O3>a>!4!^EQEXA0I3tkG>yfB-B`#zv3?%?=wdp#<ARc4GWN1Rfn7ewddtb zyZ+8RX(^zQV6lFhIio{eld+$5!x!YFwffah^)be>okku6IL_P?1s`}GX=YH*P8Y{9 z#KiZTaaI<jtI=t~A^=DvaNr7(<Wd)W^dY8wZOMcFCR!tUvGzLwtbAUaYPYbx=;!6y z&arrhicA@9Q!{$=xBg<*A8}E->+{2;tM-|62OER<5&PnT$|HyJ5xYNuYVP286(JeP z%J|Bikjf=_aq39Vjrj;hxTwJ!zA-((@6OWyE!)}&Rwf=XXO+eI?vqD}&!4gqm5Rzk zeb@a-!NL!aZ_M4aDys<-Zz%;$udjDqs4~1!Q|TW9QMxsy^%uj2x-}Hckv*2ZIM>3) z?q3=l%&MQ&RMc)_aSyy(LLs_R+0WWIgWm%YFBfTP?uQ92$xl;fxnrOH;ib<fIPN|v zIPrKl<zZ3SfxLt!hJ~5D58o{pi{&pex4zTC8l)Ls5*YSL{X25W9iLCXA|aocy`S#= z182zeVmL+qukSDZfihL3etC?_a6Koj5h^EU(1lgDY%cy%i?Xmd1bAs7P$hw#kKm16 zzMx}7S87;gt|!y41n%Ob6xzn|BvZ#-*6bP`787%Kk~Cd+9%^F7%nNCz6If-d!&A|e zlaU%12WOU6s@9m`sBqV16b7mw)4E-vT(yiE+V~alcSwGzFc`iVPfaEE>h9U_Kaj{i z$rXCa?F~=;vyumIM3pv%uI{`X(z8%v4}UAsxwF*kLH-Z4@-hXfriE{anM+f8A&RHm zyvWix)NZ}SsvhgbvqYdsjw0-24cL7SI~wn>==WcK-;BzRohs7B{mR?z(x`5l$O)7Q zZL|FdDD<y<R0W^kIc<pt>L0MJkg}D4kda6dPqN>M{H}OvsGqgQp22rdA2H_2;pi>% zLsBkrP-t1wtE4Qw!?hP@k}&$s?1sWKx?eI<dU^*eDe+TI&k9z{Cxc{}m1t6`oS$*u zM-$B#stCJi%D97<=YVxP#KD=7I#R{gNzWO_bQ8Rp`Iam|th}vBGg8RlI*f19FmJ^7 zLn8Nb!|eWmDOHPs{N~JLMBmixw24ye;D8MOkCxLuMJ@8=7(dsk6ZYKHaDX0r(G&T> zgMo=bbX6q6T#YnB_?KHw*m4&9@Zf$>`+`ypndu_0p^Z_@oldd(4|{=67bU1FyTa_A zJW)%~-paFH2T3MnDXn!siv$O6)`~7jE_Qz~q<c<oUiCLc&qz7FnKVUvQe1y9e6l(+ z*Zce;<-nu7d@N^g&r}u1X4eq2`Q^#n@cknhuBeLQ%pJ`AgB_dIk_g!xN6-xEb?E7B zm%*5HtzQEDejD}greEGVg=#>J{txu%^QT4Az$HIT@1<#{tbQZG{(2cL*(c($q=jPP zpX_75XKs;s=`;&`eXFeKsm1AkuBClaxQMJPtI=v?a&9Yn4!HD*LBE=ih=J@3lFn{) zlrpoXI19DBRMWlxGhc|)(W5Zb5Th9q)5bH2>8mEXRcX2VRD*f^e&%aw;o6Y3bx1F6 z!L_E<2v(@lJV4%5oy3XGT?#W)eZV4@!S<N~OEG*4_9w@L?+v(Wr-*m(kCy!f>iXZ+ zG{v|wCbG3cm%9<aQsH#)uN)Eh04}Y8Y+Rl#qfW3RYa(2HU1n3Uw*DLeC$xDIikdvE z{wp+==v=t14OHKaUoQOQ`V36&<!UsM@stHB*XF(o6FQjV)QLgr%BmF29d^M96DM>4 z7OIHuZ)a5DMR7uq-Y66csaYzZz;+)qyJFOdvg9}X&BULk^7S=DceJ-k#?s1J_N1DE zg0otO?gmk{m~SEF3kpuA<ocpsK~%2Ruu;=k1p4w$@f&EyXK>=<a|{;7I$QcA{(O+3 zg)y-++#BY7>qc2I<&HHB4#0I|O~6Stgq(l(5kYWi&O3$@b8{dI6b_DEuust$n6B!o z1?K-`2L1GlDqBHBJ4<n#Q73+X8I`2gEeC;K0oYLqQ1`llK6a5iNPTgTaOGc>KfiHn zW+SvVQ&Z{Y;oHdY<xtOR9O9(WsRrA`lZ|=;OeeRA2-gzb(exJuswKWv%;(4$c01Ai z!WOMy&OQ!-#)OdW)b@@}#_lYJ_7;QSTmtGDgM>g6Prt$FpGf%Up9<??O|1#h=ks9H z)oqf~0&}J{{9)amBm#r*w7i_s6QvL6#1vkD4<NFO6XTJzEbklSSn2{VzYV^iQf&fH zV=q~;rF{dgQ(kDGabK=sj4AagN^?!B-ttCYy1vg-V<W<^(T<b*PuZ_4|MUe2$yorB z!1#a=S4l4DFQg_VndZh(!1p#yG&5{ps~<83no<?b;`*^cdxN!t>#eRjr)V8v_@Qrf z45f0bSjZPQ_es(d%*aq#!qu&yN|Mv%)g<>)9#G7HPs;wAnjgw?n|p)TQAv`1N-r<q zXOgP*|3E}P^b<^gJXtFpn_I&lGwanIq=_Ki$F*8f$lD;yyi+^*6DKLb4%iHfEL0oW ze=@4yHe;+67~RxF-x_gsQSqBc#T2Di(FznyPui^~s{qy~VVTjR2Gc^V?#Ruv>x<zS z)QLg4Brv=1WO6<QS3{|>&v6JV4+`6m2AMzrG>=qm^|jVI#$DoAZ_kTrkhO%<JBje) zVo)36!OS;&pmm+Qh~1D^=rO3jDzM}RtasG!@9GM$k|4HDpiq9okA~OilAl}s0B#$Q zrNE!bpi6KBna_oqi{ZZx;tE)93X*}YiKpVDZE)N@03p~TsEgcvpsV2%(Z>GjO>#ze zjGUgU7ZK*x3P|<o-IKB}+W#()nOF(LQ3!eKHr0|PmEOPJDv&L>eK7uzSxS$h=}nz* z5HN5lKYIwIQ5hS5q6ppAir(A<VHR-<sT2ZL5Csj2j0(}Fh7Z+9c%=VQ_D|*<`}g8B z-|{&v!aXSdf%+Ed18MjYBPZNGE0$5O=m4l*q+q!mdHE;B@WEyzcw*U$YG;Kkl{9py zl%u_WK!J_t;ragRrt~r3J(|$-S=WQ=#^~zOP_S$!m2dW&D8W4{U=<A!R{cswm{I&3 zKi4~NZ#fVvkb#JlkOG4MSd1A9yRNw8gplj9_Zuio$K$WAT!R#`2h~REKw^OmbRlc+ zmLW&2qAKhlWr$JKB7v)wzI_V!^FS!GWP3pD`U57h{k_uicY5_Em+1x?5hX#bmSh+b zX!S2-e`#~$k<<2yx{H3C_N;Eb-Z?oaIEmBp8k?<dT$r3&a5EgxvkaqHFl@ZkXP9mP zd_Q_9q-o^;tj<gbJm<xmy=H^Kv5>~*cI#LY$z6Nq;D>~r=etCshJ!SxKVH7$@;v{H zST9h*R`XW%fQE~(LV)rZ18cgy;qUAaKlI43v-94iUNNisHegfLJ`q89Ipq&@tgLJF zWW4r9`bD`X=xH0zzkp}Vg)usfAi5g$${YWo%b$FPx+Aw)2h`nD%_DSEwzYy^7kjhD zLyml5PH@uD)lEZVvgDOBRPhmzKa%z+YG78|NX|-XNc<_h`{NI4;Nqe7^m*G;{&tqD z#*QGkr!4g1oN6^B(KN!f4q#*eUJM?uAL(uF3w>*UXRcaP{pN`mhOH)7o%&aD2nxI| z6Pj{La4i9{!rB}RQ<6<`{Pl=v8YaTrWbsSJBr_2z&-Sp@3e~^Cl3cWi>()zBR=cv% zgq!;x+t|ORbrlHdQb;We^%(@B$FQ;p9Vc7H<w`uusAp$SBVNCTE*Wn2FVf~(?4RFK zxwK5vZA|_BR3CWRp%RxGToh;VAI^IVy;SFTUM}5r+2Nynv`iKTX#^kFA682yrV|=H z1U8K1I=ujA;CBh;j3%&7k;Kj_knF5u{*+e|Af#UnlLpq0V^W5$fYy64^id}JqLb@v zxFJ9Z?@g#Mdh-@O&>a#4kuLN*+gtkx%n;FX0z4qlbJrloTdbQT>+XnwIl8q%et#A7 zTY+9g5nS~7&Sv5M5bHAGM@IiW1D;0ck%LOSoSGTQlqpDIZE8$5i(`NMEB<dCk4liJ zI1s3}GUmZKty?DXrxX7S4Oq9M+i~xptLjHT_l~-0O1U|RGM4F_XbBR|=q9HpzlhvT zJ)b#P+o(zkaJ<Z0M*93&N%BaDtg8Rc=9q+kBY^aTeO1itj#^4|`@@rj_7K(=7dx#e zElereU)`-yAn3u+^q8J_4kUEa%bujF-b#@o8!{RR8W{nyaZo!zqyPWMiRypkjr^Ov z{s+1N@<(7v|0h86r|}2?Yy4t!ve;k$HT;bTzk0H=M>ekfi?6366Ug`5p)Auq^CLR3 zb4O6pn4v$z%@W7Z!9P%^W)z3RR}Rq5cTdYMev;1HJF*qaKx*b;u*`9>SabcW_|##} zghg=&pKTMP;V-E|<K6YdJn*iii_V6M*dGK0#BcG*P9Ygm*AR@W&%{d`D@vtWQg2zr z$!l-wA`F?;p8TUE%mHO7Zf(_>C%(3pJ@w+QDHA62Ht0F7U`lzeU2m{?v3Eeyh#v8& zTxZ8J)?kmWlwIM@T;bz0T#(Py<6sX6FlUEbu1-Hxr!#(+C{8>lh|q-<{kerYXlSTH zOIi~&yh}pO{WzPKJyM9S`ogyrb^iLVb)2u#@pp^gS<`m+9ls$<J9efOZ2g;wutvT7 znsAeoJK~JXMjw0Js}r>}d{b7akg7U6j{dWJ6R<GPJLkqWbk1<QLifY$`)ua^>S}SV z;Jsww=L}@l!G#(Y*v*yF*g-_OGqGpt<LfTL9ImU7BOc(sQ|mY2Pb;IhWtx)B1aA!S ztZui48Iw;=Ol8+3&GSlm@Nq^_f5kiH1Ptcv%jA$o1a;J^2#)hmYxnUM2IDg7%8k?A z@1Do%5fb{&mB2B6+>+iSMmu4W{2o3;v<H*8=zsg&yn&x@T0CXs(bsBV7p(Gf#Z4?~ zjUe_s)TD92?XvR)4@&JPLJS>CMivce?ksEq>_LR!&=GBzQnm=DP!BkNl3Hk94OP7y zCN|IpZ=dbvhw_gF6u=Mkdr@@-bkf<G_ihX1J^yl?9Vj|-`ZCfoekoufvNtkQ#4B7v zMk9z8;(8Pxl#&}C;U($uoLersLxUm6%UUEX%EVKr_n-@<^Y{_YBge%wi#p9ubXff# zDEba?Y*{`&a4nAJqI;HNPh3YZe%Fg?4)<L`?0uOrYmmSRC;JnNjg3AQ{Mk6oh1hED zc<DU3{x5;W*6mu4!U`MbkQZ!I3eA(sf;tx})d=Za-=WXnHeOb;z}0hKk$JZ)4W$A} z+Xg{jJNGV-UJg_IqIXk%%!-bJO#j>;XBTXk=A}`!Q#==x11C8p9Dn(|aA0_1hr+*_ zQU<)e#7^>wW<D;PTAUrMxh>nPrE>ZfrG)pDP#nKeWE|FyAYn_ChRhC?+Mm>RpMG*{ zKHLkJ9=&wd)YlS}hRn<R2?`FODRt177B3nd^~(KUN!D!Fxx8OaMi@y&i*^#?R1f#s zS+}DP>p5R0^71_b<)sas?Xoz3jWfZz6+zbbMoNW8-@U$@W)D9=#HCzc(9i14>V1tQ z&~DTB4C16A0j!r;r=!!KrAjiw(u!Y7h-!;Re5V5!1nwN!(^<#*H0R69ZDRc)fFK?v zcVD&WZAhSlDL%^Hat~mxG)+a<+SLTVOpUTQ6^m-EgUumYVh_>ml`Q6`^iI^$K)sS+ zXpx%8QKm-oSt{GbS|b@hSUs#CR)4VJL9I)8%7S-%G4eV$f!`Si6fFw5T{)t4dpafb zXJvUocTh%%8?ZYU`YR)yIjnYP5^^*e`wukWf9#VLFr{kxNzEu?@#lEM?;!x9sY*QR z<N7o4q5DlGO|M~<?@&WD(f)DEN_w-HOUa(n_^l@(FWvasD$YBQ{#O=m9N?&Q@O=NX zP|wAVR$8px(^YCFST5<98La<RZmJB@!fSRa`f&c`Dde8iW#@<$lL+ZR6a!*2m#PPI zGuMVj;bkh>(lsIB<4=azRcTE_!~j|oV6%t034G?xhO7-ZEgZP*+O3jK!Cf9#t=L&4 z27uPEAwctSX{$ynBOLqNA)ytLH3|fO065`$*&S#7^4PdUy!Oo`|MmI(;e%On7pb#U zA~14E^QtB=_2+tgsw!s{H_)HfkMFF+$5I8(oUhYLMX@&Rd+y`@LVg!ax8C@%C`isy z<o;zGDl7qn(#xcYB9drNx<QRMb)~zIIo8RH4l^5Wz)J0J7x-r<XVOp$%>bKMaT7B0 zuL)5Dw!EU$&H-_?cJdFzA$!P)=jZZ}qm-J>x*WLc&-dt!@iB!Mo<$gi(THB!+_sP2 z(?s~;JVz%Xt>6zug3h}rv}gxJu6u<&tjMpAoj@EQss7>_)iV-|KLOdI>wbMtOqQb# z3`I!axctl=Oh=BvvdYFlw1Dv5)?<PDLmgi0J(1jk%%Sj`Tu1(OFQRP*Y<o-Ilxm!( zH8c*E4$Jnd^~9@iR{*pCJZe9G4Dj))=Yo$3*BCZ;eOlgOZsgw_@up#}*ue*WMgQf( zSM(5uWS)Z8cia38q^ch@USsrb241TL*4%`c4C_+;3@Z)A2JkG7*uSg&VpOd&S{Y7D zYXGc55$6MckqsSYtk5_J2wfNS_(kFiNSAw+Ig%_dg-E4PQ-aSS`v7AJjqR+juISh+ z;0*14EDJxzFPbTqV@SpaUI@;GR^xpR?6j3954`NYubM*0DA%-!{ft8Z3`}#$iE2xk z_w0^duD}y)S{9l0tm47KF4m9>0{2|xicsdo##2Bk)uy&z8gZr3em(=6UtFCTpm|~x zp`+Yn{-t;Z_(a^(?X8t2z^I?N!Wr=X@sx5^t5Gp(!?x@{--M5!n&TBm9x?}4JVJnV zg*@#C?e!In;ZnH{Ma0_hBLd5}#OQ~@a$r605sU@u5S_nCN3k?np1J(75s<tx+6i#h zrH^eXJ$y?%CHf@^&2fSvD)i6XQvneUElm9Sv0`KSu<`)t%|57a%xi%}<Juv5o~T1U z&YS`nVV_sC&A|VM#E6IA6NVJy3N-)%Z?uaV$>vmCV3ofn4VC5PV#{Y1GHtS_@0piR zz&mH2)t3z|lP<Lyg=gqv&lw>t{k=#A2MTO1iU`B%2@8Crt$MoKd}KvQ-#G9pC?7gs z+wwPq&gjkHM>1aW;38IG7z6$-a0aZbDHlrlfz~8=Cut3S0Nt(>6rXUWDSDXkR#gKQ z0@z<{9L%k^mY0-8b;0p2h6!en3}V&BMK@HK?$u9U#BEbE)xhU>;yfS*_?`oR#(wD5 z{@zqk<3!^YR*(vPLDkhs_m-%dX7Mqh7$mM_sH>eeRfC^IJA{+|ksQ{9K}wNEFsODH zHD=jNkOYy(weKu&Ug9OQ#@|isp;_1^S<Ve1ju%uj?25oxgt-OGs7P$x1nP=mA>b1o zr(EQC`ALGR+Gb)1bKUd|lH_I5Z`ExFeHAI&H-I?j@7(A{X?1#<lZ~_FW`qFjGW^&p z!Jxk~R7SY{h_Z46#g#Dh{>v3}eyfs!iJk^y?!8u`9`;~QABrn!XruO-S>;NC6-Sow zQo51%D=JOyuI^mb*2bGyghheOh=q!qL~`{(tQfz{!?IHFm>=06h5KHuN-XK%Us1JC z)b+gs7b(-aRNdKe9YeX_Do-<$abo*GSSs`mpHbt!1kp^<ys0_hw!1NF*n}Snr~wl9 z+lrNl6xKSA#*`3!Iuk*H0qd|Rxbk3`2`!4#wgC=Zyw61#nYot9vwx1*DRfWf2&|w| zSRzZ{4><vJ21edko%36uSnSS&MYVtDyb~~8OiunP7U;Yj8=(%+EMf@vD2{N8DtPz1 zp$PzXi;L4V4LuNg@U0&E{`OQ*Flb-(IY+&efPN`Z{;Q{8?+9I41j&Ab85}F!AM;D^ z6>rM`xF}cInO>H?eM!~rn;75gOB<!v0)Npkx-_6?_{~Qz?$dMc0*BD^5)4KZ1`k<a zioEdlS2w;95{g2&-rr)GUBR2@o`EQJRW994pk?lRTwd2*SUEwp3xik%@AWl8`?(YO zti>q;Shenh?tE1$%M{?M%h8HKAeS<?3ARhr1(?K%ul@FFrA^50VPCTsy2D11@IAWQ znfCtN#y`rKO0%Nsip8sZM60v8s2`b<T-v_tUk4#We{eAm5J|Fq>FAr~b@>>Ur=fSZ zfYj^o<z3ytlji{WygLQAorj<jxFH$!iu0;C5c@eKw21F5&DR$N=%VW@Kw+x5%A_J5 z3^bnQQ}&+txS19bzHzAy|G%N?oBQ8&A8Qe1RGBcA7yy3z+6-CsHS7U&IWM*coF!IL z#pryY1Vp#XEK0J#ADPa(e2PnNw?aF2UBb^k;cM}r=@MYqg2084mY#&hqJvk$&&bQQ zR3@h{48eb-uGNIAg_7UsOapg{<z=F4T|>Me1!AeX3k&Io3bDQ^snS8E0_PQQxBEy? zVDbn!ZiyyNhIUpm)ar?J`kgjqfYs%s{+w>>rKo9K65G~V+V~Vw7Og<RHyNk(kW>TU z@Y88NBh>XogCrpGp2fE}_D11%lm({A6$z)2@E`i}2`YP>CpfUh*-(2-B`eF5Z(!hH zT{V{51$=~3A@eD?R`?=c3BMCAh@SbrCHy!%&AP3F{w_>cPzZN&m>X#_T{f2=;9dP+ zlU;1W{r8^Wd(I#Lb9A{h9k9S3ZDha!il9`<mp;`brG@3+I|0^`4ss#r+L#5=8|a}@ zb5F<e;)W^X6D`(2yBI5*g#iZXI_#OMAuPT5^imo6I+d0eyzVt{aa}g)SE7KtE!hpW z8dNy{;mv^zVMD3CxIiNOVCZm2TqMF$O&C_mz<^o8K#hXCXzF7Vt36MW+A9Lr7P-x2 zM5D_Hx=VIc&O2|RBBr|&N=n*;qx^zyT0cL9fPx@H%f4^>H3UQ6pL4XBZUxkDxhOp) z;F-9lj0--g&H}6C{hZ><{6bT2pu<>TDXx^N!ocRFuv&^O9B-2?JIM32-jt|2chw}E zI)JZyj#ZL+*3@QS8Jk(0^JGAC;>Q7m^KnOI=KB&>r@{}SI!w`zx5KLB4-_$u;6eVN z0AyXt+BK8UmW8m@>uHaix!;)Y9tuCo$pma3y7u2Aq1`K%)i#*fdU5IcyGn1*+aj&9 z&E$=SmBv#sQ3v1i)_F|!PbJY*)int##vx5X&8bH(@2<_iTb5Z1ACQ0Dda>F4Myi^N z%jgw@n}_J4kS8;`Oi=vkUFjjFr;leskBsL9lV)pG%lJclAda`X<9bvgt1*X*#LM*D zk%d6xZA7JVvjH0t8vlZFe%FmDuOFP9)BB>g-mE{$Wm@?bsU@Q%)+5gGSz?k+4OSC+ z5T*CWg*~&P`G;MmMP~d0r#13DEe%BsB;~-_A$4h4OPaliG3^w+`_R9wyx+TDmg4yh zY<xDkpLDP|qJ!R2=FT$L5RvNUVlii*2Vjs;K~VfdMD}6kAE`(Qkeanhbtgo}k;K>j z>&UtWW!(A;o$-;TNm$ggMfunY5vRbBLxGHijO>M&bl%wSA7kFY$`KMf0AFYd%I9`d z)=70*jD<hwgL6=kB5F~9|CCR(_smcBrgu|H(d@(TnVIlME@o)T;@=q8g^P_-CO*5C zZS9q%?*=Xmb>^80rAdBgBYZSj991#cVr><d4%xeAK8FcURPpx_jRX3F>~{pZ*vD^i zz@)mV6QV6sno3|M{ZbP3Xwk+^0^p`ed8oI_Ho(f0&fCHJ(AuXpPfBJAvV&6Z1E=-7 z^5s-k*50*a2E_EOOgUIjCnRaS%!EluIfD+^2K7bDI!-^@%X-_SLzKQFWM`LFq#81R z2l5tr_?hT`0yv5HJU-z+BJc|#LSjR~I*niSI@oLO{D$AtY5hTB`;iiUsHfJo=}VkE ztX&EuL0-4tFhPyzFtjXv<D=NbWO&)Ae)R$%pL5}TIwMzT*p=`zI~P_5Te>k7ew5*q z3Pi9@%Q8&^bLQ3R=4xI>5a=%j4sjq`x)#Pzb*E*osVkB#=S_n!2{X*fx3orKQ9K;h z2tFLbUtQ$E2V5tL?ge=5vqs_yk2!Dx$9(>-%kyOO^H`#ZII^26!jS2D^t^G$<|%|@ zDM(AqxA?B>ESecmTqMlnU0<C0fPDKJDVuMU?VcT2@>v4LWqU|(6ePtJ&|Dz2tEIx2 zIO?BvO|SzWfxO`a@xzR0;rczP5KgSpz|gY`?Z2Fk-=@p?UL==l$-V|8A1;*<52+YK zbb7FsxywO9FxQffkB{^>O^p1kD}m0TtIk^+mpoV2PQNkEX_ZtBwsNcG^_d+AbOe4s z^~J~EuP#`*HtdEx2yBKJ7C)I1K{h!6oyUTNcyN7dUdlG@7RIqu=F5%CZI_xS7Y~7X zk5}q%v!rus2i}Tgpmx_(*J16;v&*Tf%1##2Qmq-2V@PEC$OMGp{N7oFS0$BbECQxw zZTK)@W>}W02Wm5^{T3?_G+v#0EPmB_r{8pO?KMv>Ea^*OWKA%;z_lOJ@?z9P^N~vC zYL<@L?fstv-6cbd^vZh15B3fBSLY}5{Vh(3Ij}H)W;_6@I?iZW$PMemMj+Ox0v(L# z-XyjVO9S@|X-7Di84D-{0i8=@^Zp2R6~040UptX;qcHri_5QG1QEowS={^n)&kS%2 zAou&+WH1CUiVR{X-r2ZIpLrHR4q&Wvf_&sJGmolJyPiLtC;&5_#XZy`b(7>2e<DSu zrMT7J9<2M}Xbg>MDRoZr)2Surt@o3>e!$p%-q}KT)fr^N!!bvN48^*iv;H9W)c4yg zMrRb^DZL21Y+Lw~5dJXx39G)qp2*s%JM_kAT?2Omz&<KuwkL1<A;6sE*lkwOexi6J zL5)|?gM^+FE950JdH6@H47LuSM#|LvG|wu5LFapCYX=|NS0QSfIEkOw364RruF0;K zr<{s#U}(@?anFR$om+qs@MD=qdF~$@#K-y>`F<mNM@C%(%+*xrXZG#}lJ1h?<h_(| zd9TvEWZFU^#aToo(jczPz&1O^k4UIgKB===z0e5h4;@Eipi3_k{b^kLO0<rB`CE*u zrg6dWgGsknEMkV|xR?B9KXdnef}`R9#zagVN5ZpS!z~$@4U6(S377>(DRVfIM5y@q zd;)+d%m;B}N3(<Y_4U@VDMQ0e^l0GfzPh3GWYBzo96M)Uv@uPWJg+?56<)jMsjP_P zh0C!QNUjHYLIJ6_$mF!3>c=2Al7FC}^9#}f9cmzo)|mK_Z(^(^lvBX6q`;1U>&t-X zmU0g+QTRPOZslGW2yEpv|Bf~DrM!`@?}R{B90!a4tJw@23VX`v7zP@t&v+)P?wssN z^-<pv*9GpqpOOREd~wjfjL|HD1`1L-x|4nBh3ZdQnxwuIS(C=CG*E<sg4_>ZWY;^3 zqWwn4(iB;qRN<!8Z$p9$m2OiGdVj1bZ39(w&J6NkcgIy(qwLcFJATsAEhb+nva+<Q z$k~+h>L61?GFl$es-+!jcOKRz#P%6;c7CDv;*5dWIigX*#4Xr_rvFY4sXU7tb(3!2 ztF$|y+okz^Tf;mLp+AP;w$@#aU!=h$b8;`p{DN@O4%#ZNKUox=s{P6uorZ0D`92|E zAE*clp%GV5K{3Nq^{pY=B=605_NQ3^J>;>pG-Citd?TjD;lkf)5hZssLSNB97%1Am zly>y)?`^6^k*?7vP?J1<mIanXmjravnD8Ko;A?l^l&Bavq|Isx$>NHIty0|@tnzhT zzS~Nv2=_E@@vzWP1N}eDy=7EXZ`e0Flr$)*v~-FzA~4b=-60*)jlehzA>Az?-Q6iE zF@y*X-6;(M!XPo@+5hLuIp3f6UF&=%*Jg9y*Y!);`Q1h30~dEV!_rrBni|U|n%fqa zdGV)WYRVu!@q&BU<U(sb5F6?bzR+}Ox<cN5Ci{#(MtAp-NH17DiV{0is^H~1_YKq3 zRVF(0U~eUxSEfi^M_T<K=*=~JJpZWgzn{8-V`%a_kR4xiW?X!h+RVr9Y;`5wi}%eP z0Avw+(lL#Ore9jZ`ZX1KdX3Z!#4N>1Y0)9a>qh@TbFgl)2i@hsKDYgg(&dlrvbmv@ zzJb<w)RIYEy?KY%V_3Ku3Nn9~V3DPd&kV?4$^?d2?<bzm)XB{C#(~%S)=y9Qxz%^{ z1eEEaL}j8xH7Z>Vt+T85x(vT;Jn$3FL^<|<erpmq^-x*{q|UsKI|)+eVdG0bLXD*h z9yPL{+@wzJ;!}r+S1g)=Yj-u>kGB(;N!uzc@i2EE!wTAWL;aceSC$dt-xFq4;{YTH z*PQC)xHj@Vkmxi&lVzD;@Tuh6fNck}EXB+AibU1qYh>H=x~NX~YldofUV+z4saU5# z?Bx72yy?qkb>gKf3p1OPpf3XLSA7f1j-zc)a>5G4F#Q>uOA%q@hrXpB69R8qGR`Ng zU(&+Fawg_b31H%w(cS7x^6yE*nm9N@`pWBu5|1fARdOUK^c1eSTd(+4Pa>3N=(Jm( z&Sx?7ko0sf)^}#+bUN6qob2WuZQo&^@4R~}Cq+zQI}L1_KjFI!_fmzcJ@I{c`bloC zQ%R%R>6V+*_>vS<zE?TA^1Zhq-^Dh%10ub9v7hdyB`IO$NkL`royX@_b53t~y^iJ_ zTH6TpLx@PpNCdKX>LEC>ginH8l=hELvO2Oo>g9l$gu3Vda0$u(P(S}b|NeIk@mYxx zuI0aAaKF2i_O@5;cT3}PCo<!annGUpf#&@%+Zfo`tp4vz)T<3|T-0;7zM~o6-;gs$ zLO%WBEB&6S>!12Oum+U*U9Cg>o#yQ>_<JhayjSxq@putd0?^O;wZHgL?(n;j_34m( z#8Ysy6|j(Rli@BHK7^L`t*jiL9W6V@T-^oi1DiFEnNcMIJg61~tM3u0RhRJaq=#&9 zltY<<jTRXTfgAlHDDU@HzIm<Qvju><_wlLI=V)LM7&qDd<35LGxcZsPm;WM5s4Dak z0J{B_d~cNUC?^ou7@=r}pK{L-OMj$T+yIhAKT(?)5?H6uiSAt!m?k&R;R^)-;0q&K zNAPhN0M+{69H>79a?B)}3AhUX(*fhL3zlD!USs8#H{B0$ieJ#Pt7!Vw07efJzEa)* zCP(nLDhf7_)SvODDM|*K4{BZtwx_&)xj-uq-uxzt6O7J|Y@Y1EVxlY~mCu|#`v)=v zQs5krJ~x*02m-!LL+SeK+ACzAH%5U@d9V~y)K9pUpe4;KH-7m2?+WSf5yI3?B&5Z| zT>n6iL88FHe;-$`$SCbl_+qt}Z!N|k?KAjf7`$}Q?taT$yaLIqD_55!xAN-Hue7NW zx12=KsE(Nn*wnJH$lO&@BQ-sHyz>rF@g1UyEMB{US(B)q4`P<??*>(=QaC2g#<SGz z*T3<4$X~Tk=%Jsp-dxTKw(|sGGR#11MPK7@=6LX?yhSLhV(1JdtBNSR<jH;rIdZ+( zUx!ZR)&<BeAt(}E8xqFZqMDyjIBn31vu3cDeImgM9;Yr9jo3J;J?02lIIU49;c&C$ z>>8}N@IpZ6KAKuME7zNLc_Vs1_X1nG$+n~q8%9BS{Aa2Z1541E)K|mcY_kA4wC1L~ z|3mV~A$I_4@=(oh;WPDYMYhE2_D(<N4=qnt1{LGzo`l_A)N6&e?OYR`3VryJe~l4x zd2leJIGR_PZTjcvnzC>66@!xY-#Kr0N5_H;J{6kBeWn)r(XF>seKV-Y*7;ag?eUuQ z7eM=>72QQwSdC$&^sG4By$W-MuQ#up7Ske`Mxk@!$;R^gR^xT3f|D{9B40YD{+%6J zxC?NWkR5y=^7`f|_*hAg)v|C&?!8*#05vYE>H+dt4hHB4%ehl#ceIf{bT9j&Z2vPx z^P(q;#&k4A#h-mDHV?TU5uHK57c?SFaDmmg54Q;XO&KcuQi~&Sd(|}KorhnXjKM_n z7`(FvS@Kz>wP1^~e_tf<qWKq5^4hCwu8uj6c0fyLyUzBVo7sO<fB0ips$cCMVV9DS zErBP1xG4;|+k3hXVvTidI-<-E5SUyV3E$s5@M$mf-2CdK>ZD78&9mIyyI1|SknTBC z%1Sfyfc5_aHSBD3%{@^k0HWc%4lg6{5U<#|m<!^d>OTsnI1=qH1?0-7O1zE#)*0IX zC;K{=x8Q3~Q{$7KFBKfh=&|kjzCQWgjJiM-(baIlG`KmkzI06WH=Zt2C(3{$3{?%$ zs{4z0FG0F`I*vi(xzJO|<&_e8T$9objHh>b&~={7m;%p(6w^16D(2$H>$4P{RpEF8 zUl#L3fU26-28-`;OP7}iMW#&eS~_s+fi{FG{lS;s&!6P-Ij8iej#2>`>hFjjnv^Mz z%V$as=`koLfdoDMM?tEk_N3n$shn36k*uEX_Ly5n;pc~DzQy{0_N}yRTye~y@$T|z z5HX-@B5i+o>934`l*a^!YIey1Czqkh_D~oa3`m|%FhB3i4>|#+H)9uDoul`Q{Hzyr zuBcR0(F>XOo^DLrAH))U=E&d-8YAV|nr<^PWz3D^jRR<MTg7K*v%u?Goi70BbEbf! z0M3@F))v0>QoaioMoX>PDb^{OBl`6-av{robsPa7@>avfs^1x?l6A+cX-8JaYk8x} z-0kis@Oj@_BJyCZePch%7Qto&o`?KjhmmM~cV{`Go4ZvdZ44fC=TBgwVSL}=a51B+ zNFkPJ#2tY9cGxHR`f3H$aq|_wa{MKy*)en9X(fU-`K}rX0CA;C?f9P%!Zm923NDv{ zUjp5W9*7D5KHLKgEI%!7t(MQi(n8OkEDcA3{_HcKdL|snyWTJN#x7XHn_V}vyXbeK zQzZ!CcMqz{+D_J^H)y<j0>^!T7H83L@TkUI{q@vf0F)=6d@8kK%tb-`M7#mp3&7tB zQCF;LSk`(l3D^wDtkc0BEFvMh3AoqTMnJtl<>?5c62?5bG7<wg;|YH91lSs#*kG(0 zf!u<^F8(4D4?l-7SqhlIQhJQcl!tIC>41m~$vTTCibTHpJanI&1VR9@P3^$Ly$Wx| z&o39{ibzwtUt@*|g4Dp)`KTEZVRBev5+JKPV$6(-yneK|iFS2&QN&V>94fkv@jTL6 z@pJYsh%YUV22wNLp9*TSRO1ryeZV!zSsnr$NZH`<3WP+$C$6iu@tXf=jFnPpoX^x> zof%7w$YD3h{R}Ho)EjP|aKWf%#+8MheKf94q+y&m-FX`~E-Ts{=3_sj$`)w#;d8L% zNct9#&d{MX&FdP#!t!n!v&6A|AGiXqg!{btNEI>-i)5Eg`hcsXs?_8?ifEH@W)v_M zhB>d~+ud-L$>d9i-LM-l0k06UpuC{0BEE()+TuWb7uMMKwZCvFEYL@nZM8vE22hp0 z7ud@-8cTfZ<m=SX`g;_p=BQ)gl;i9_#93QNd{%9rcDxRcFE!~E*AeiIiVzc(P>%+9 zmT5-&d!IbE!Z3B(_&_2hqhGFy<%YL`+sBpk?yjm6utN{PMZE|Sdad(5o(U2@D?^|@ zbq8!$RwsPj#eawK4qV%Mto6&^%s&{jqFQAQ*e;ImA~-IAOm_%5Q$>8b-V>-;D;&7+ z0+8g%^zXg~k|~|@D>w_p=5jM=R<I==GOyPc?jQeV)jjg!b2jr*`U^PHTnn9?IHYsI z<ex-XN_tI+csx@ilK0|kY6}`cd2br&9s@|rlw?f9xZ4XB8~1C#T)=Qm!}tVusu*v> z2%98Hr7iQ>=Rtj)%w|1a5s#epIkEED5hQo`Z#2uY6f573qeiL3E^E>&stXjXyt$!- zk9vD+y66Wx)fs%+SIjqRK}dv;oLH;dr=Xrec^AR`2J7kwWCp=_J;K<z;!kb=a)y3D zpG39<y{a{+jm1<Mp#GyH-3{*%5dLic)mxoz;h0wUY8I^rR8btg-n)%edtW7av9F8t z@xFVJrlXQpj$g!i{x=B>GB=a}FG6q<wwE9u)J3V?$I@yn7&C>^KRc#szBt#>ouEzR z-wb`40R23{+aN)VE5rEs@-|-xN#KL?o~?^WO<O4)JS!ofMy%LiQXuHWS{5qBjszV^ zCV&~Xc3-P`YRu5!#F7&QV^c)r2`(!UT#9EGrjfkq*#C3t9?sfOT-#~$pwGB@RM`S& z!$#;mgfJeVdK`dlAn_g!r2JCz^|Xgu^TO8gt&^VkINgR(_RKHiVJ%yf_vk9-YTfBh zTpsLA*{p24>J&@d83N4+a5VTg<3*q2UDevJ^o!^lic(!=wO#JfH+VCi&)zBE{)d<N zKq%8Hor`EGrxdI2Gp5sxqUku_z#k0FTKIv%4FbS9_o=_uFD6y3)~*l5dFV*hjFa@V z=0$x1b{_4LD7D)zeIBv}1O^=+qv84ahNih$NBJ6NjGwVT9&7Xz!piT*{`(daq|Sco znB6-2nd}X7D%d{_r*(E8`}auXRx>BJwDtzCESY(%;uEwkJ-Pjffwm0=zsQHC+I77D zQeBb;|BxOy$i}PFU_XmTb=61*WTEW*rg!k#<K8s-^V-{Px0q4BZbj=wj`<x9x$a#Q zo%i+4abiWgaBFLO(!#doaN_rP1!ts9De|5vCUjlt{b5|<HMGa-9<ScxG7OTa%>>f` zOP|-tRkgcZ{j`0#1dTNmkw|?PQt|4eeCq4>j@&P=+w<vy6K5z!Hm2`Y=;!8!2Sp~U zi%Ezy9dQ#thnMXcQOi+DSV%^sl97o-mnECUWfPtSritKkl+JRF7UoM8;GS@F$@^v% zH@4Tdr%JpQIwCkTKc}Ujixb3EwN_l{QYKm?QV}Y#RAd{SCYVxwc%tU`!JZ%@%RV7e z>#u6^rKD$G#`1=cYVw+*mF^y3E=OyWKsV(u^*BSFGW|EBOAKBd(khSRfh<(-Nldzw zE3J!i-fgDHmB|aJ9aByBXG|abw_;BD$KyGR%@5t$06Knwb7n^qqsz#d2XMZQjVCit zf(S?%hzNkSlt1WQoJq9&Jj|ndWcQVlr)ogPezxSUgW>4XjrP}CdcrBa-`}TCuaMI- z7mSiVR@jbSh^&&bJZv=g4x^p(t?$$t_#a4|H2DhZIn(k^s*T$w)x+<>ElW7TFs7Nh z<$WDbNw+PfOG$~m?;-^q1isO20nRZM7=JCl#amXTic7-@7)0T}61wbAtG%PE-eRH? zXH@qN96wLVA4Rt47~^<XoMVl?a4IWH1MGIytzI=L5pjZ?@sCoLgPF_@w_Mt#&*McL zG2$H)C+b!V3L(+Lla3?hGZTo;YhYWtZ#Ow!NBv%d{Qs}XGAloVoAFm9+}C*#c{ciJ z1ditS>B~nl4AE8aeE<6NA1L$l)QP2%wqY*|+VZ9||ElyW;6nyj$lrZ<WNL=hx%%m1 zjqTIC#b`};<oaE;t#i-OA5jq6*04Tx<(oWG?^2`ZcU6asD-UGRWH-OQvTwboMB@nS zr+#;BIS@n8wnJ2<JPTYi8mRzAJq%c@*QR@(GR!J6Kp`8?3G%p!@-<vu0L4I3$Ew%0 zuDigVcP@-Mel$5sM@t(7Ui=4YNB+`$D~L%f4S=uHcyDdFbNS#4s6=p1*l(rp4D{4a z8DdPp472{63_i-Jh0Bi4KSVQHxYj7QV)jVUHn;xg!H%|RbgxS?O1zHz3smAjW@9Lr zE~Q=xeqs&=<gLjR<Q%o*=^j3gjEVJk`yr>5NpjEp2Z;Yny%mm@E9&aH>68_G!|@<p z1cU<KAmyZnnnW|1!+!mQ`U77*cIPW(vY$USO9Ey<T<@$>YGME>TQ<Kg#xSrGnTw@9 z$EKq2P%O)T?fe&Uq^MSl+6q=Nj@Q4x7#M9<ArP`KwtIlGuxQ?GRHG7`BQZ|)_zu7m zKUHJYWCm*}1xgwv-MvbmCXuA}?<9{R-Op=zXj}=1^4QLgC9I+N&)j)}<*g`Ez;g^z zBRR>OjGC1PRt>WD-Ep-d<)oTZMX+_smcOXvoY2atSgxeflIGr|3w2^WFE}BYq`R`E zac=mSt~=m^N~NSyHI_`ieM(z7D;L_QpeTk2CUZSCSn_78aboeJxI+JNrKLTt1+C(T zV(sGagdG1l=2t%p>>{F?9Y)0ySUQx>E;(`cdI`x9g|qo;aI^R3TFgR&@&=$cO_hRr zeUR<=(x{P?AU-<h@&j=IWV@$yy=nX!wMTh%uW~^B0XykahM-(y;h)QTMN_QLfl*)d zC0fEysltum3A;?(CGiIB981t2E>rD~s&X;H^UxwTFls6$v`XXtVurkgN?c4rc8ufP z`~9ychX(EK;5^tO<jvgB)zBe^9+fB)83h!>y&mL!QfKO4LUcw_pIjDbj2+E&_P1|h zA!x;>!jt=@S4pG6^WG{9XH%4iR9eGNCIq~+mrSt1ZWsud%OiiKuSw%yLQu~|BI0PU zNtoWpq27u7;NWMCD2cn<FWQTC&H5D=RLb>b^P%ge)mMRp#q*n^(@s3Q`&xRdBk%lI z0nfWgIv|=3B2h!_tTJB^Xj5X)uDje@VOvbT52!QQR(o`GRGi;G0Rmn8sVG^Of-zbW z=gN;ynvUIODr-aVBohMZT_2{;iv~T$VJ~`4$i`0}BG`?wr$-5lTDM_S>;o7LNG&S8 z%)!rb=a&bLj-AHl+##Gww}86KWe?KX;$ou~fcre80la+8--^Z#iY&%*-qw;`E-7bZ z0SK=EA`d;xWi?0iw0mJ69ZIultSE6vpJ{)8j<DN(t8WDG!I*Hf7~7QbN)Ay?2VCV` z!WQp~SAuJ7D!}dK7;_f{2|cHXNHGy`@Y_F5MA{@xQYr)gp&{0STaeK?UBt5!9i3V8 z?*Z4poomc>UVU!PHx|9iu{d_d8tZFqyHH{iF}ytB=`>Oy)Y8?w1)h2>U4<-E*$r2$ zuho8?^-Rkdc}1DW5&t<E_(5t!&i+7imZ*+&QGY1z%(0ETlk2vsbGdIIn;>@zDT5Sb zIpSrwk39B!;*)Oj+q?6iu;s3x_0;ARb#aECGuf|v!PbBr;8JK5xjj1SR4!#$57E_8 z{V^W3o{QbkI_5rrdja2B`eG^J<R;}W0Bk9pvE-voLMB`Bee1nIXx?%-qW2xANgD1C zV1r@=zxCysZg-!v#5tY(dk2+DKYPqmDYLIpO%%V)zX;%?X`WqroFKlr`N2zGe%Rqj z&cS$BusSK-Rb!5h)*bLj>Usr+dqqv038p%Vjrj(JUCH5(`(^{bgrT6N8>xqF5kBad zVZtw)mG%3na){av#J;2e!r6!OBb9^Uc8saf$`zii=B*^axFVxME#5{wjHenniBdJq zO_U8DUNQV`1wC{<cMpEWvao%a`%T1HufF0?xg;GMrHl<YXxyF2**{l~vlsKqY#_jf zh@CsqbL4XaX7P;+kB2S=cpVBD1ZfYa2V+e(j%i8VSXzD2FRDgMW&xa!Po6ay;>5%c zHdm;HGc0EtKut!EW&xf3{*1o%071znmWPPhA}s@Kk<@j7TmD!6R;h$*`WJ0~pu>RM z>*-<<fkSwP`dg?BUK5HREs8w30VdIMgP3JwjSzJxWKpQIeNBp4UHxtNuGdf8sJu5@ zMo-3C(S7+=Uu&0ztDcWlpn8bf7)(;^NjdMm?*nfd0)ChZX|w#D5PEstgra@m|I50v zq;ur9rhlXq?IJp1A8l(uxdi}$6`aWYWZmDz`D4RNM&UHU-~vJtXhrDrZDQZrK$ea@ zY(qO8%*4$9(1w-AvI&ip?BHVY-6NVFRevOtG7I7*%w0i@w(ISQ8V&GHnOOab3mwP3 zhl96qo5r&J^U`U8e#9p1#npsFbJRn=bFK@LB=zT)PVL-nT%0y~3ZAndea1QswgYcE z$#i8eLap?ZXbH_ok4NulWAEGN7)LPNm*&F<NrRE-^`6u0z()Z!3bo^hU6(=mzk?93 zvxrTL2_gL~A4#48po{pCL%7zvke)==6;$8XZ^IV!Wa}DDDIj{tVpwERLdya7ZpdA0 zTY75^-orgO=BGm(axQ2iQzkacmb9!0*dP}=Tx@^p!D&R2)X3;1owTwiC;K?oY}%a- z+(`*YR}j9KRq*)00~9ejL;AboQi5JI<!Ap}fK+qi-LSAi&C5Fy<9?7+ufU8&GK3K? zu(~8rc+sm*v4+4d+V>ZeLDLI5ZdBiP^Q6Kzx?1TOO5$)Z4zMSR1ZTPQ8#!`}&`P*R z;O*Wu7AlNL?0eN!<wINOXvu}t^WDd~LqDr>lm|Ww@c9Ali<h9Cb(N$^qAz!grhl9T znmzx4aFC-swABn)#o{V)@lvcuZ1I8@G~th)IS_Mdm$sHyF@N&PNFrQ0zCz{)T0c4L zRJ21y<9DdDW)j0@O}00VAwtFqIx^?CiRV6Cjd?0@9-R!&81jD|ysmt1SGIak3r;Bf zRa@5C{aZwL;K?U1X5|cqH+QKg6(7(hZ*o3h($!q;KTocbl_DyA(5o-Gb5Sf(=tQ!w zun0c0jmV=*qvL-6NFivwxYtqtZc!`YXX2hjgZY<Gi5fP0YbpcPU&W!hnTa%m43$M5 z;tnx|qkZ$M5MD}+<s|;wiAdh@(qO9~@vC##e0%f3UbHenLBlmDaeq2!^+jgI>4Do# ztjvrTi2X(s1dJgVFuO}qvTa$jm;fx9dkN2F31X3Y^BXEvNWLZ=RA&DO#*rT=`eUzZ zC^4&ElWiCmKpj^(^=czJ#hDl_u9#q&hrfd`F)?bDrb;s1wKT$~CIElI*ExI~;igK< zpMyNlXIBMBPRdjN@}O5%5_yy5W$AoUh7Hs{|IEa`wW)_pwZGKTcQtuPtFF>DxjIql zuwgPb?zCfCMf0Du&P8mzmhY>x2L){BZyoyWL#0~x!zp~<a7xQf181P)<VrgtA|}s* zW?i{YBUJU=-<tnaR?gRkNMV|6Ts+Y{^8^w<)XrBYR#va8xdbp313fBlKgxq_u14~{ zItk+HF?lU`0)&G5$<m9EXzT7Kf^klK3Wh8OhnfA^xNpWZweF=oK-#fd`MqLu{Re%h zzB#9Z=j}&_^M@Z2j-NC*ZK|9;@0QP-FiMiCzWp$}48>eP(&e~(Vp|*Kt<{pjeOmvd z{|=hVv?e;&)yzHqDEqu(8~5`)uFB~^C|U^TSxl9a>Z+?x05`!WKMZbbr8w4c@Q?n( z=S1aUrvdA^R<V(^u_v`DG}Vr763Um!a_Z#K6+wq`<g6A(^Z0dfeQ)~jH(tC-xtfsK zV)G3azk70+0G=wLrDzvLv`}MHVrzZT$y53>3~)YXj{@H&*A`Vq&Fo%~`8zvaao<kB z_ac<vNQbAe8?=L~moz6GJWU<k&!on3vY6mj7rt2%%b~~gd?HlRpXc>dS--${p-<AW zF1I)G9VCapnhrLoaHS5ma{F`)FiM$emhaDoOnhmEPQFGDPuyDuu;=|^sUm0(C<Fc3 z4*HR9gKED+UwyuXZ9KC`THETrr>ZOb1=qKA7;%ldR@*6C5hmpo|C_=g@mz!;Pl@o_ zUivoo$HSl~s^?wFkg%ufvPl}N$5cH@Z?04<a(iVt(Y1|1zscdED9Znb0-^FaA?C+X z1`7Z@)6a^{$$>RcVdX56_U|uB+EhA>lFBUfT-==f-_S|^PlhM#-~X<z4e)Ax`v>Cf zmAHT)(Ibp^E2TS&VL(dPZeLT#1=I{S*%d1Vh-k~D&Kyu&FaCj!CIHe7`KDk}oPhsE zpU;`_(Ec4C8gS{I-^>&G{sVo8djp@Yztkc<VX04rAaI5gV7TTtp%oRD{s8NS0?G*6 zZ0hsn8y+D%0D;JixV3Ju_}Baa>W9T-nA<D~M=?8*jV8b$T!;{j^pjgz;$AW4Bc-ov z@exm*j=E?8V0A#KB_;&QSDkvb|KJMb+43a4dDO!dEhkY6;QiN>KHbkq@ekQ|LHy3Z zeg)9L!LM=ruL@6XxE)miEy#pe=CHN0Xd~`D`$qm`?eF2>SFaB8jN*8m6QzNW?x&%T z&4K5kZm<hg!i{BgnXgTaxia=-uQ~`s!+=jf_@@;)BA|@1Fqu(w-nI7{InG$UvW-11 zE3jd?b1wHVC8Lyp<k<@gTR!)fY;k;=&E*4CfI|79Ka?`0^9~DeZlX8VK8lvg`W}ff zzUcYQC!x%mu+@(ZGY3gRrNfpzMg&#<Rw(T$sjH4Djso<7#PuwqRb9H`iz59TE5c={ zY4s7h8zQlit=L#{q&I!>=jtFq-bS38Tl$@~pSG6~4&Dvx#g80|Wvtt>52AYqn!)t< zOJ6wzOxth#Hil3BBIlh#KZH7c>@okna-oDJD69ckC@;qBS>Y^<!#z9jZb-?!64#nA zO~&5dws=H}oOVg29aD_9Ol1Y!7<R+88(+Ta-p#>!S$FGVw^T~*-$LJVUdr-L1~Vd! z<9k9#Ot&wk`N49_wf0YSirbTuIq}uF$Fryty%qBs;g;EJd{%pL8voWLoY=%M<f{kj zTZd+8w~uDW`AHCSyYi~Z9Yez2mx6~P5W^tXQ*qQpR80y8%9k?s#^>97N=g~~-p)qP z*S(oZTAibjEtfrf_=QBcG&3NW2o>6M)}ZIQpS&0NaFXBIJu^<tU$-bY=CValywYo> zrHoPXY@w{I0I*YqM~%3xcbpK^Rem>jfzs>y?JVacQ2dm$XcBzol54+KUMo96PhvrC z%)5A)4B~C5flnVv#XzRGIU57Is(iBbN@X+*#OL4nifAOuJT~N(3^g=p=xpz7nvI&r z9PHjDmGq6w+|9DZX#iUS1(!-I7@9&lGtj}A{Ke?Z5o`WYY6pRoWuqX?{Y6I2PA)+n z{g|rfM_Zb#NPJq}3r>f4ZG0`F%m&=Wypg+CyH@0neCYe_^W7EDbQ7)yij*Rh07NOK zM){6KeXRW&nLV+|TvMiE6(XnNNAz^f_-wrpjnHIS{#q@^eDubPCn3K%*=Ed=)T}4G z*~Y2zR|+5Nq0NsRsCdVJ4k4>0-g09Vqs5)#ke7DYF(iQ6Oi~1IHI({6i5ffuROFN< zXmM)I_vly|jR6|uzMV^bVQZrv=_b|2-DGz}?KJ^_Uh|k{Ml1U=h5R%-=Q*?!md@V? zijY1-$3z6v(y45dHsr((8H4dVwb59KezZ9P7*)7$pUNxgiUvU;E3Ixq?rwn}n1(4$ z<9VYtJ8jSvMvuR3Xxe+KQLfrO3Wa)VU)F2DeO%XU=be86`zOsRN=cVyf{!FROc5p{ zE$e>e(YK7wd@x;d9S-|OI8eDVBSqAf=b1r_ss9|bvtY<L3u{Lb8EziGtg)#sv$BTD zm<*xcFs5KUprwj^e2P_`F}lksw=1`kIjHFB-Uig)cKu#~%raLrrSVyfwI6L7p#r4W zB18g@p)3>oh{fWxi03NwkX0)}p<{&6#^E(nEO`lIG2>0XJFSu8vwkTl3u33eFko!| zL)n_v)EKuCQi1|mU3c7>S*-sEF5%<3JVYc-+0poA&V9jrbNk;O{)5Fw5!++#6{%;* z2*$MsuRAJ<9}Qo2tloUf?fWEg<=3<<fPu}$r2xfga)2hEyrOv)!3|4ua$e-F%7NnJ zxDNtm(Dk2{6sqR&C5ER5_>iV<-f_L_-%hH$MJoP9b_Bn&a%&+8#tLULjB1d;^%I`7 zq}v&3D5VOaY>RIxt~s)#BtuLfr<hoFe+Dhqf%JSltC6g&kvxrP0hm_Q+uFCGYP8K_ zROm5DvAceV1!gWU?LsZURDUeb$lVcWY>6M$3h)T%XlI4g#N4%#MwW7&lQgzf=Qy?h zwYY;gPS{vigdv`pj!r_ns4pPiHfP~)i#1K3j?&vA=_WA;ba(j1JE$I(;j#Hg(BeEW zQ3bJXUhOInI`0ONRVZ1fHLuK4O8j7nI8h=LRpk5<yFQ@heUkXWme%!Hc~IRkf13sC z^y&}_$nR;|a6z7^UZ}u+uy{=a6XOl7*)HP&r;}n4xBvocY;U>N1G}b;&#XiV2B`Y7 znrc%vM*w*Oa)hQUrV56c$}TTmIZy&3<ty{Q6Dk3_&yOzxr`3$I3w%`xS*>I$&fg+- z^Hii~_2#&LehUsIeifg+dEllDn2S>XB4NaDpM(H<;?$yZ(sEs?siJa6t!gaacT71c zu0d`Ad%!*DToga#`+juFL=u21nsg5cN9tscSWqFm8##%7JPI}?I=OoD5nmh^dI|*p zNX$0u;@C^(iFW{)%%774KYiD?Z!|4l#JO5uP+)SQ3m19{dG@Mn>|=x>)UEws%`wJJ zXvUffE$B(ey#(3vU#hgE^m<@6hKD&mD^sb$ND@3^BAr(6;kfLEyfAh3vF;A~ks<c- zz@AGcA8@9fnkuw!muOMt+B`%s)I068`vv;jemtD^0_d}pGdU*s5&&u-xuFE(hR1HI z8;72^`-dk#=1B>S?8caZ(Sbw$@;ic31aS1NgInL6C~JEIj6Qt-?&4JCQ>u>g@rKqW zhAgm1GP<gBltvjy1}5%=-J*LJJN1%2^>dWHy{z8?>e;4Z&nrdmLIXf3+=U0Frd!u{ zZv-l>_BCI8NjP0<dI<ow2E}xZJ)MByJ#qoHzieh0T(2m*)mQ>d_PZ_5)kmG%cVibB z6r%4upi5+RRUY_9W#>unfx0|@fr@dGI(Kh0Ai2ILKQ(GqjZxYE`7zTA^L;tsZfN>8 z-A(h%S1`+$tR%fP&D+@%ez!hEb&(c_Q%TR1{yh$#Lil)nxJC^TiE&jen(XbSiQx(m zPUcHrNhn$c&{bEzDse79{sT?S2DHAEc;P~Pil&KdEXN@GOf}Cw6pMiZ_1(i8UTn{$ zC3#MdjK?^6NZ^Tix43dqqvR5TGWHXlm95z@ni)$`DX-&|n0xTI2#bRczW%xlv`f}+ zwJ>%TXb_Wpf`X3mQA#Qr`{I}$`LS|>-?2YHti#u1xHUDagcyc7P7WE^E5;0<r4%}B zwvk#3Uaf?d&k8LHwZQ929TyAg%tyf{f~2hGn`Rg2!IfH<*vK8kpG7aOdDRm-44|vl zp0kdC2mKd9PqnYryz=LaZccmhT9M6+%2FdmVKTgL2-4ApFcdyG8x_cSWO9grjUgml zgb>zM>&BEe%6mpu?2|NePgKv2NbhU8iNfw2S-q#4*w&sYUrI%1$Xk*V%gIH`0HF!W zwv}m&x1GC{2;kkfp)XVOui1)HEmD_HHrt)`W%MSmKI17p!gBbP2V^a2TK9drUSBJ- z<wVR(m<(j9nKuP+W3U`UU+|$B2BPa`c$oM{QIf0PaNvB>-&V+0?x85<+|2eoP`}x# zND<D%1#g{wi)5+FCITkO?d64gK4)l9*n`s7S`BKB>gOi5F#yuDZP{2&%eP0De+{R; zv5;U8K|}S^MpZehp8%r&<h1&W?b5VLGWD{QBtl|WZrl%82Lq0BVs|6x<*zt=rL!#+ z<$F+R=&RNxp|wivr0yU+K&y=`AHKf)B|6W68r9RD)7L3eEHzPjQZ#CMy2(}bO;>{= z87+pK%NTDSB$PZM7UcFvrK6z+`%pD0g!0}e6*tl(>^tDi#<>UE8<5kBih0<-U(PsK zm&h_!u^5e;#h^*OYG`l0boHCyh}bjWR&-Lnu4kZE{!%zeE4cbvbmdNdQ!r-cw49;m zq{a4SPikD1U-2qj>g{@`z-^$N<7W~d1>HR|GVtILH0!JIn-Q?K%Jv)Zk6d*RZ(R2E z71|Fg8QkdSN8$z*z+?|tj^-D?7motIAM3(eBLnX2m!;NW!K`^FXYcfb+4`EGr75CW zNm0=rcnI9b{@EVG;1?k)xkazPF4wy9go4w9R<D0Mr0z(c#QJP~;Lvh2PU+2OiKtwC z?d0HPLHV-W@P`(hg;tKvbF^7L5<w>wxv(hb87fum+2R~<RI;y`>mLUrq=BY0yu%Wk z?5XWmVTp?zR&e~K1rzv%WZ)?H8mI%qowWW?OHr-KMv>O>{e*4JkWD73l49|%frzwd z&9i6P`cbob;T*~g*WGB}jpy7FFxR;U&y{p*`k9`x`}k?A$^@H)B~Q)WVt@_EdY&(& zOYo{ISoZ<{Cb9S7570hT&$=vr4nLyOQJ_OKrf@pHJp^z%1Mt_#CDS0=0I9__OZPQo zbGN}(N_J^X)n2KW4SSrTB2FOL&+8<aj!xV>EYhlfAX&YO-v-gYNZRI=zrOEmL{w3b z<tx}rZd^)j3zta7^Jg8ml1=h<>Gd5{6fjCt-aGEkyL&)q$^!q-r+(b@{lBu(|7EjY z7U0CR{m+EQ54-*Hn&JZW$Snu8pKnc~4;OH8@h9v^Jn;yOK>-whIrz-#AE<<F17+;i z^BZQQ#fxaTt7(#lRdRMGqp?Y$wk;!9{kMw?$o+?&kJ%f1Zpf=Yw!m66Xe^fC1tzSZ zU56(g0#**bKI>1CfpTu2$^mDkOI5Ye#z^+l14HZLnd44;lrLW=a<?xc_o;#_d#+Ll z&=fMD6LI;fErlWh_XvdfBIpZ5BRHBckKTC8<2{a&`3;xTR@8{CmVvswk@?`&CAbt- zNk&BAGu@Wfsv8ARsh1M6M?X~!XR$-9neS8@cww5XD1O)n3Q1Dc%@}=avlv|<!HHf@ zSCG@rsMuO2HPszS&6wz_67kUGc7*Yqsgh5l=TwE#GEcm89uGF-j?BOA4ZDN57je0M z6ty%I0R+%0hAb3#-@Z*8S@!+rNDMm40|A1uUy3Is#V>gSx?bAdLGf{qPdhaoErbw; z1Ry&=0=Ux3t0G*%iX9KmA8Tat^gf$E=NY0%*zDUs=vfO9cI!EjUvZ9^L8c1}P|J}E z@6ETBcF^>gVNJF3@tqQU$_CGtGrnMKFN!Yl<~|ZXE~CbVFt*L%(7%k5!IqY?nvuPf zyTsc`EN@wW%eyoZkkrzJmf*ZPtm=#kZ)tePG6$O+ZS4T6@G2AkYK{DfbomE*!CSWE zG*_C__%*Qxc@VRyrY{#$G3usz71ao;BC@pu;;7cS?k~DlR$T6b7GANP?S80xZSXie zG;#ZRL_YU?dEFP7ul*SPKhV1Au{$%LopZ~os`|_L6`Dp#Fn!R#F@p9PrHJ22@fX1T z^38_FfqS7#Ewd^jIaitLS_hHj?xy@;u_grj76ks>XZTTnbuIH^rtbsmYZk!2^(ubN z^flCO0DX2kiIS1+Fo_FtD~M)%rqFRdiEolYNWrfmvhr?7?9~0WkyrZsmgp~*7Yh;v zewbaxPjS3eQ%H*IzChIRuoL28{+erFKldE{$}`{3dmFdz4k1^;Q_xwL^s<XNvMNj| zSHZ|p5x*k+I%Vx}>E(KQ({fNW;YMGnmQz|g@PtM0jJPLF*qEl<UI1H@BI~{Lg#e9y zRtle>stBgS4NdiT*V;d!E@$vHQ|AE7lIoZ?qLKNjZ80havrl@VCdB$3F;x|5&*;@l z5IFIuy&GQl9L3A$*+0%+?L4h|X~~n?pG{-`MXB6EGHBZiSJcaOwe2-}N~Cm)l8ag5 zCFPYJB}3fHK%MLP`(cMkHcA8XS1lhn&WD`7Q|%AwZFva{Q~Jz0GQ~LglxK781$Vm= zHB~BNuPh1I3GR4nY)%jo{AP`DBgjZ=ean_e(lv&VVG27@poyhjNY?vnceEjJIPHXE z>{9eXPBh!**I;1<?ZyLfPpr=?@uLiU#9m&(t6<Z3&zuftGh#mY6;e=TD@maM*$U>q zL*+qn%W5?GD{BLPuZZrA$VWL<1`Bp9F!M7H(JqpnVx96`sc*G3k{r#P9=@yn(Zpiy zA#u~te&$4({R+xyl!7M#95MH#R&tUdpPy(XK8=h5+SJz{nj6w`8sp5^&?PWipcfB) zz0;R<n2#?UlW^8^I!+KuA=*H22sZWLhgc*Q-W2rjlq6D>vqb<Z!nD@8WQj5L=f+r> zcfoufF0QxIvnvz4ijCr>-gD;b?80`(>qFfPr>eDDwvQiy%-WLhfr41g9aMYsg!9ms zsO3U9`LCHrWD&uBXsC@XYbw0`V+0m%2Uiwb$)_XY7p+<*-|@7%+<d8k=C3}fZe8LR za?f`(q&U^T?XBx8M{5P{sfVM2ArqCv<j)$<RX91D)-br9>Q;RRbpU{un^dZ1>*xHT zmb5gEklR4rLi@Gve-w3|jWJzs4)kX3EHWBv8p`qJd(yH4xml(M#NlT+y?Iwe`3F%l z*HWaeFZkRE=4A>JzTOaU7|ROL^RX9EocD+NyZ3k8K{ixN;}is9IA}mSyFqgGkkn18 zxnBfFB1-mm;2kZjL-#;V3sA^>u&&lB7f?_e#$~AF`a)AoGSBpn`}xiqk$gZx?EXsa zUR?puaWwx_eFo?RDw8e6cf)JMUek|e0%|CKj!TMGN<*Kx(RC%HtLo%~{{Y?OZDQWK zQ3HxmV&KxTZJ^Z&s6lO_ADG7ZQEwhhoumFuSzpN=ATC>3A=!TB#`G|%cIpQ&XpW30 zmyXfJmPRb`m;PxkART|W^Q45BS^G}2JOtgpzP|#jQ!=IT((5%=DIz9-BYLHk#6|zP zVan+@^eff`B{3@L`DJzT7rDkq>$C)^fcNx|fNJZ-Xw>(({^qK-WM`AT!5l`r4<oX7 zMdFn8hVjI-K-40goobx}wIJhP(vFviC!^dy_YTZbVWpdV=#@pAfTZ3Di1z9Wn|~nC z!MAUGVxUfdcNV+7vz_Eok;#as$oR+>SpT<qkUN}l2j=}jo|$i<t8$E+>xMm$3^rs} zrPzql+DM#g{c3#S_X<QYC+>1b<gh;@rYzsD=Xoqmqn}A0>x=yeuGP@fo2W($a?xZt zfWcSvbd}yFQ>N@GdMRD$S+_lSX*Mqc_tK}wD)n#uT}O(`HJt+AhBoEX(l#JSHy`tA zF63#pHou#bWn!ThVB(&Y24pM73{Iop-y#vP#-KN%bSp<nEk~EiHA1SsZ+(Be=s#`! z+$8DMfie%D-H$a2xdQY9?Z^yeNqquE2}8PV5s*O-WYx&;{t+LM2wrAq01MWF`^7bs zv<>W+F&^^p4>UQKcai0@*8^Fp@wa-W^V=_3Bou=#Apr0dw?`GlXMd4~GC4<cD|iAi zbpA)%Oq^Q0-Lw|@7Yqmb)X#|n+s$vDCZoamz~hX8REYQ#<pGfUmap}*Z;jgSaInl@ zpwHOA*&Mg_`u9f~YWrVCj~Ejxm^{&!QkiDM>;;Cne~z8ruxP=}Z^DBP37>VRaDW!c z^kGA^P^FfQ9f2mSbPk}@Z7%WC?9VWO&DqO-&#GM-5dm66x!*FXL7~;6jiL&;r$D+( zb+`+FYOOBXM9V2qFk2OUA5@1^OSmcRtbNkP6qt|ZNHo96CEIAH!Le_!^Khg32hvjL z7sFc17Lo9=T&NN}0s;K~JL~uJ;x<~(V)NYpT33(mA6lItz=)U{CH1HDO?c(+gm9?E z{*%9It}Oayyg@Ksb+u0Z17R~>Y!5C`{R-4K!Wv?T7d^&e!BoORd=N=9;q<L!YH@Le zV^=ed(Iwiv5RA9cE->;6xC?>bW4K$IR<0QODak?9=t!`N4!drirx=>H^YVMOq00|| zfLQ;OiNB0$|3G!n^wd?&dc|YVsr=Q!)A6|>qlT~4M5`uy`zY8saxIxyk1E=T(h>8e zMeP|rpQrm&tLP?sr%`~9t21U%S$AI6^E&Z=AW?G#e+7D$*`Y~G6MmC$4VZZWP%~mE z6BUz@Yx$;4QmB0sC}yme9y7mGVV&2&RXwZusJ-~+HZNMAdi1VCOv9k8#+DsEXy6ET zV*B##jNnlUcG6cKAyNcB_UgU78;!%zfN`o;wTg_Uwh^GF1z@?iCUX7L`pW2bmZiDy z@l`dP=1ShZ^s7st75oIZim$)rjnM<sd@k*J=l4yqYjgIL?7*X#hg#pq6uJoYw?r5d zdZyp2YxPw()K8>)k7=JLrShkY*>gV~dtu&H&B9`uGbbc2gGK}ggXthN6Yi8P7}H^F z%J2!SiV2VWk*^t=`L}^B1j^QBWuX~3dJ);2hdc5p&ZL5PJrBMT{<eCuhDHe)^fbVT zUm!D8m>kMJz9DwZD}hXs|DCN(pIs##41=tuS}L_h3swco)awTwkpTFLRxJmJV<CBr z8Mz-D%jo2~|IVlIuvlJa2zbkZW_Ibd7-pljlT|WaS1T{~D`e~MMIQdpF3`gvI;fsJ zc(gxz0i_T4=Psw!Q@*>db*j#%G?mlFQ4|DS<nY2)Yd^okC@d@miJUz9i_E@|uOu>8 zjJJ^4zuhlP=ud)Mb*>3vF%kKDmXk=ATpzC-pDfEfD<2UW#>T{x^hrL7R^tfGeUwt8 zA1q_(!mBL(z4%E<vOIoWK7V^fE=y-;SDsF*?B9EdO<S!2_c7}K;ylq4{MNQkIA)iM zPlt9#5DFUK#QTWaX5fqn^jf;Hz13SM;Ix$c@vbqiBu&a2NyzKd+MLK+r<Y6fO!#%? z7Co#0NX2zt7iNmbEo^R`YkEwTIMQnb7>Ui1bG;s)6gyRwKM86N(@R-?G8a%gly>0e zRn}+g$#CPlO2Xfc?|##7GktNO*5F3=t{}nkt&0E>fV!~y&@x*=yr*Kn->%lQ&<B35 z>6Hy7h;pp<_IUPP%XOTK4OPz`#I;OCQbVKw@Y*NO&e-<m#=m0UUtLCa65ze@auTF! z0q(6MJ!Ix`?!tz2K+`Y0+Rw`Z+&>B{3Sdt_h)<qZhmo&ip2fMDKC5($0Fub%ZXCa# zOQ}mH59>eWu@YguV-WJT=D%+5;qSG5C|>P*DJ_HKhn{^AAz2-B4?LJ;SRHOjvGGHa zgd5~Pi7w7C6q1<HW{%|ZK#)QI$bDR|?~MGg`XTq5x}l+<le8rt2xQRB1)t)WsxSWS zfNEHg%GxE)SwW?2wx(D3*&{Fl>?BHAgSiRmtV=M=yZWtYal*fCEw_EFw)UKTiX1hR zI;x5%dc;<pscbnXMD8cXFrLMVf~Nbj`}_m_t`GXZQ3LV+?TP<i))i)HMhazHq=Ol% zC?37P53@F6&^wya;p)I<oVrFb|9Z)o6%ZtJ-g?4<I_#HH?7i&IWSF^+CQPAhgA_Q9 z<PrJ~C#17N-`reb%(=5BC%#RY;;1kSH7*9LnGr&ciIeAgUP&9+!PwZ8E*abFQ{Igj zsI_|FSe;sYX6OSq<@(Z6E7&+E1L`Y4m!`5Hgn!*YeAM{}+0h94gMy~EA9l@@^M+0n z_)aYcm8ahOk^DH*+{p=q)g`-0{cUuUqE}oF$D;uL|EXM6$HRL9)u3RV1b`k0hj-0X zA5zu=bE>!A4=O3TZccgY1=iHb-`CO)hKBC-no%qj|3#=OI%x-oj4IJ&x%nPH<mAJ< zBxRWhI>-JTPtj0LIsrAe9ff}CLS%R0^T<w{>rQkg!vUNA;Fl}2%PJl+K_Gd68TUQ5 zw)sSrIsa8B^WYs&&a;s)ga}PPq$_kd16HfRm;%|@=U*T9b25psKP2@hy7?D+J%A?= z)LQR~OL2G>db)H~|GZ`O)$iN{W}V|;>D~jX50%_pzkaHJ<7UADG;t{jdP0xiu?Tn& z(qcI?z!v&ir{{%ne`KMd6l%0pJR$IUh;P|wRvO|PzqW`<1bf0J<8#8}-4v<^2p6Q_ zN3v5<3XT#(s#L)|Yf9If%6o~!Ivn`Uf71V{U5FUO^y#ed6?2(4%YclAO5)qD{9}P9 zPNdJ3;TQpZU2fiO8KLVT)g@Hdx92+=l>{Y0eEm4JVHJ!+iWz*jq0|%Kud*km(DqL$ zOH+#EG;ppeDjNJ*{P5O=bL5Ea2mJeb6kuCw7Tla-<?h#v!^EvgPT36UBL=?{#>yfJ zv!6mp19cDzG2gzkw79b`uXs3r*uUY4c}(ypbRFqUpK$%%V8yNo-_%`24!p^G=ZlfY zqFj$j>h`fW9M-k4Y?`2jgl9tsH$)Q&{We|{Q+?n0dRP?O11flKqMJhd99KE7T{qM0 z$B7ZW<+z>Gg@2_Two=kcN7td_p<p%ji$wEL1V7Vzo<oM~0LpUX*>(d<b3wR_7N+1I zKk9fF;xll4hr$~z$%Utv!q=q|M<2Z$&!3D`JgF|H5E&5OGGP!tFcJu>&bS73(>h|E z_W^`=KEx6B%~~wFV(jDavY_q0kW7oHv^RZJUQ<<91h3j|)8cBpy0nCGcaUG6vUZu) zGhdK;?V0C&pT(g2j}KaAue2pq`!V&Xm1;2)eunAD&IV%$A1CFn4>Tz-a$k}H)rXke zJ*(5B0~pVVg4{1swUlRv6_w3HrS9L5937QrG<D@gNTSVOIT^rIs>Qi>3(6}4C<h5D zxjthIuN?li?uD-?W)u~P%aa+*L=nq4riFj*Ptj94AhvpJDcud3s{E)v=$7DLOtaiF zKu3&m94e~aO;TpSEUM*rsBIGTj=<<Bt+phliO-z7a6i`Y=%VkCHAn;lcAeO;L7ksZ z$`w;>mR7cG{pjbn8pUSvCx>J~n$qL0ZmbP_JB_ywy+1O{Oq6_DKRMjyBLj0!WJU~V z26@+6*n~ih<3^Hl`cX<^*rRXIVX^m8ZG9=$$^<qn-(k%^$%oGSMuAS{n)#<HtgQmv z6}v_#END<LApUFd+o;V`1O=0ITpx@tUy2rS-3P7robrCYT^Dm65jLO41<HLo?-n8Q zU(LehXs$XYpSl74y>@x8QL>5s9g_bI=(Aq~@b%eyI2EHOK~q4l5y3+EED5NCtrMJB z8-@Hlf*f}ega*AcSA#tv#7I6|==pKqr)_7A{}3DMe}{s$vHNMMW8C^=*;s;p0%l83 z+NyRcCV{@RLp<zEdmh-yU2sQ7AH#z2J4sIcgworq*^IkrFD{KP=I!+%7sc(jar`%g z+j+)cROupA9w(|+<rxBw5eSm0Jxx`Fz|tppYZN!qjI{Xh7TvdnUIE;stERa0*`*)K z6LQ&;=}#~t*rfBr25y|w)zW`5m4CM>rZrv3@uFuHxxJomMl=2fUs%|+gB!}>W5OA8 z_ff*1nAxb>XF0jcR`ZWfst-BhEr#g_Bc~93ndNfo%~>oNY|)2d@Tot?OuJZUhG}nW zZmaZ9+Rr_&o@Ck#Co+WqOwf#^rp5zlrmR0dpPQ~=I9Nz#A!uv1y5P-4Q){pldRDqT z8AaO1eLztgs7?0Dm3tuV77(5i7LY~8H8`&v|Hbxjk?y`%_y8}hX-X&fuaL^!=hZW9 zWe1+ZU@B2bv~+obR8u+;se+iP$Cu3WejDJqTCS3F>CLL5jn#t0_a+D0TdRXEKqdSN zIwu!HgcM`P>l=<<wS1%AR}U%5t?`Lq3&%4zh6B(@a3HMWO3k;u5_B*H#CEBCu821= zimmpL!}J$>U~q+Zv0lPIxfm81yXYE3`~^1Xk=GvjdT?yr*DoFP?HgFlw*tU3OKUy5 zDa;Z2g|`k5Qj}^0-z*dWCCu+!JBrycU86|TlD(qsg$0dDZA}m9;1*lPBk;<-TBi}d z3;%)yA<xd-oAO^X1`Dj?6>s4AE9M<d5x^1LxakGvb|aoeLo39F1lf^mtl<)ugnDWo zHA{GoO*DBMN8y?+uUYH-ZbHLk_xIuDzYkJabWwVTui9R`6i@GDmUy2=e$AvJDQ9?n zRo^m*pnsCo*mzx={_y3sGixlN_m%XNvJIV`qgJ3iyNz}XNm}o(pSrUO!XFebnu9PJ z`*zKDRw|m6uPnuVW+Qd=08D5>SXTT;#wVlvStZfFBXI2f>jbuV5S?RQPC3x)W#T7@ z8&4XSr+R0U0OnR=xY;T{zsgq0GONJMg{-z9q6>B8f?W1>0GPK9y2zb?Mek~FEeI%| zNSqwmoe%Uulv)^6j62u+>m@)5=R&z~s7T&_+%MR29PGu$acHmW%y{1V4<tR)5*SYD zJ@-)&kh%NA!ow=FDd|E%K!97jWPVV>s$M3&^N>5BDI3bjvL=e)scap^N-UpFg0ZZ` zQIIRpyZ_Gzck(!Ak}_LQaA(hfZOaW>68LKNO}(ixz9k+7Fz#1136$t?gq`!17@^#! z7M6S#*hDzv-^1pPX9m<>0^+N3yE>*d8_saUSfh<grN4}Jf~#Gxu)-w__>#^6dGOXF z*@}rOie!ZopCd6qlHG<(f8i#nz=V}ogB-1b{8{L!ZMe{U&D*5FH;Pif5@Sa09K&Z8 zGw@0?*pf4)q0Grf7+bpl0r%9y=+;gEy`EIt-!6&0humenNrd!dJ$VU_RIQmez!oHt zlBumq$QCsFW9f><)nZJ(W?7t!EWE(Jx|BWIUPZgYcNV$P9=kUIS1*=Nqp)Wt3QYa? z@yIxRapVgP=PXroS*{5aot<1?R^a9j^z?y`p{^$pCY|-NpPvwrzKG3mbIR^~8kz8l z^#7slEyJSx!g$@GOGFV6>Fy8(1Ox_@?q=wY0qKwsh7hEOPU#qiZlpyTL?nj>1*D`q zW<2kIpYvgVIp<vG>}!ALdVyiRi?yETe(qnzQKd|CG}WxT|He}GFdGfksOBYn8Xetp zahaBiMD9-0RN~{JO4uU(mb=uFhRa<g4X5yK9uU3aSPqQW#@59W@_kYGqJx>{Sx8%5 z^Its^G%PSi=T|Ypj{9uau7gT~3~SoEO-NtQqYOuc-<w3$lSt0Ydi)t<sSg8i_tgTp zurBu3RC?(}Tz2t1Bs?!QaE9<p@jfns=W#AJ7GAz(b@EH40HOXez#J4xgcGH>Ylq1x zo^0~C6Rk^W%KZrn)r22(e)mo)$q7KbC^9U4%QmTqE0Fc!BR6$tQ<DDtn-C{(NwQ`g zZ&$;`IU1H+i1^I6aI;gd-Ko~!M9HC0nVcxc?o|HKsaVGe`yQ>>7!(;O&Jni$Bc2*R zQk1lOhM3rv@~j+Cw8*UN`TH&zF&t7U^S(jQDAJ@?6H1#(G}OX70$l`7%GLBgJTq9l zR_2wyIWK8aO?Uh~_zW7`+2l2T*|@Jryw<{hE<FO7XU^tHUA<rNOgV)-_+BZOoT zC3~r``FuN#s;AyoW<WC9!@Zi)i8!gZQBx8Q&X?KU5K_rw3n^x4srbA{d45_g@me)8 z+~iG~u#PINV<JteA~}f@DYd~A3XEE=FmGKwtBv=~TMr7(b)d5*bn$}mg{s4^;m(Wu z&Y;?T{0n|kuzfvzVcM$LW^rL3F{;~NbvpBNh{lUG=%<9t{Vs{uM62U1{DaE!fyzWd zU2;HxC7%m2TxAaYH>gDbm?jp_IhzFD*+y7MP#1@DJl|Kkhjnxq-kr2#C0GgZSsmSJ zFV+;e|IR(O>b<MjDEm^=e#LXwmwMz9;elj0FQjY4fgFmrB5m$g0l*RB*n8mnWK%#V zK%wQTB8GKmhBhO<M<Y|T6=G*v*r4vTu?fL>OC<|wQ>==5*B^|zn`)D);acV&;)^oX z%en4@UEG{XT5{8xm9ohSL=b<VcL7fG$#3^Wo^8x5t@sGeR1e(T#=;S(C3Y8lf_HY$ zvjWsoFS4?qo&KCD($-auiAjE8Q&L$HRt70cuaT0gbjF1jw03<md9PfSrGontB>qEP z-Y-U{;18mB$r)+z!`b%QH0CpE8gN_=?^Bip(?u|v6fmu!ec4R%o%qMQ_E-y)xf)Wd zX7wiZs*)`&jc~h#7_xpxHcxRhpvgG*4>n&glTnnj{l?MTxI|SRoGL!4QXrM_kMWr- z)9D=>bLW#%b-R7Kpu8qx(!tA^obO?|J2q@v{^VCBcOlfxovl0pN-iqOC@<Qxy^yT# zr_W&I(sv>TK7XqFluxsvsJyK?l^n>s1#gIB%p9=4K2-0iMJ?JmZ8qM}PsSEaSpP0U zRK;oceJ)CCn9&ze_PO&=#~w1&m@>IgitX*{%8Wf@EeRm-VElW|_J_aLRlUv>vQpVd z1C@kY#7>`%?DyDcSTGW)WIM#Mkt%B|O0nYXWoUJ_SXB=0A;O0lW}A4WhNhqu>5ML1 z%3wEyOnm!p6{MeuuU8{GAQ{-1Q$Cp*#VoGn+|5qkjNQjnRE)U5<L9COK*KHf#U5@d znGQ+ht%B70-!|CdI1g!v-_HSefmhS}?+EVt&-6NS&uOKaHcb|XN7p^X2Vk>XC;-vv zKrLL~xMjRz_|VagkGTm^iVK=dAYE4Q@u2uhAWrX1@v3}v`xf?zVTZ<^Z!^aft$D}j zH-J*LP3$1oGpq?d3A|FjIQst+`|v;C&;JLT{Qvk6KsWTi7V4Pk7M(D8pS`1Kz-8Fd z!i!I!tn>0*x)Q@jR{xX>Haa^7J6m%D1UZJ~+bd_9PH3PqT&h2G7T-R^xgJb<ggH6d zVb*<is#!Q%m>q-#!NnQ9oV0PK#YkNQf9vUN#TR42#=8GOaMOOnxKjT<K<3~{&%z0v z&j?;B*4J(H-#fJ}=IISHV|cXktG3yJ3u8>Joi6e4r!fTokMx55jHczHDW++AnjR`= zlQSUJ8ljMwq96g(enZ151rm#sD(2|sfr;+FLHmdcAT{<-PT})&%T{+~9mSvUiRsWI z1$m9%ee{0Ad%RVEDuo=+7fPwG-o^vs%-<7l1#5ak`wO^?b)~djpN$yjbsfs^2_reD z{P6EdFyOvLWrM1R`2tDHb`yYB<43}yTS&Q0V3_<@&XkD@wmTm^Et?V!1znZwhg}DQ zJnr}{ElKDwv99a${KAdZ(Z~W`^Au%K1QW5(k^f@nbed)PS5hqF!J*p_7~<k`@B3st z8@WVz>j}ik6iAO?WZBxX_U9sGT6VhMZN<e%E&7wl#YKKhpyv`*HU@D6atmvi`Nn3| z@Q`Gvn)k4Siwx8uiMaNfykzZOD*eC!rtRS+*|Sg!@(b2K$C%+?1yrmIhU_0NhImvz z2W{y1J(RLZP0YZB>nx+iW&~l!t@X7XcN6ya_B-Z-H&|VJ%%8C4Oca^(g$HIC1+(LE zY2ENNz@};9ahrADC}k~6v*!qwVTTA2*%8UcHrMHRLy53A;1~xyt)Q%lF{I%9#Jg6; z_fp?BEnIz@C;fx>*x0PBbtLUYs1u2?jA;xDP+mjHl}ND<Z;RBkhjlibIQT{#>j`Uh zb1FiU2YE`0)d^jtJh^U@j{fMc1R*=_Dx0oQZf;I(6<3Bfhu?o3j{o?+Xwdl5gk8Hd zDiJ)g^V>F3=vwLPec-mu7+<5Ax^P-s+REllQ=Hk!4kYC*3Pr%>IXFGsSv>Y3wxJ(s z(`;PA1;Y8IMa1N8FYc!MGg^`2(ezt9NnYGXh)6DNVK(id9X-8<2(70(cwHZdO(2_V z`Gwm%ZkVAJB??5w8%o?dV#k2A!zozGn)jul=7;N@7?*G}WwqSW4i7V<c8@lttqjt< z#hko$;o+&xUtvn6K4r<wk~+osk+ud;Hv_3WDQD6@Y}A>4?SC=;hN_t<E?n~>*v!UZ zbt9<-?%ox>of9-m5NqYYcAQPBJshPjA50VG{55-{1X$svedEoiB3WPZR6fwT<Rkp_ z<JF~HdBi!(@^aoZRu!zc^6z>Q0ri7BduJ}Y<!3w!89y2|4RspTMtGHy$5=m}np_bQ zC|;bKoE#x@%rw@V_D#!&K+lCV1siLVk1A$eY1L);2sV`_{_Y-;&?Q-&xYx564r%p6 zunS|RWPMvv$*@(aNb!~S`ie2)c;oRj8JhGfeJV`xl1ai`296Ha5a0n2NKt&xW*-XS zzJs}G;{2C>Fg1hV{8Xup>)FtVpUn<qN)ixgSLT7kih+jEBNJtNS=>}<i!Fp|R$*II zWC6^aagYA}fqV0p?`gun%7o9T^zMxN=wC;Zb_op+;D(>TYU}B%PAbTsY2}H7&JFn0 zaEFU3PXT*S(?zgAcQW#q()nA~r!wOD3atpO^@X5PKZHMhk?*ncfSB(63YSR)FS3}q z`$yM7jp@6;u!bNNqTA455N8fP;{kd~)nUpj=EA$P*moTmLkJbcR!D0m$sxx^vgWxB zu`Vi$o{8`{5vUd|t8nWO0qo51_2T?Uq>vVhW5C9Mcl^L>kMXa|0f~!imA;J&m?_|S z!X<~kJuzTjT@xO(WvS2RsdjShX}kki_GoPnU?X)qpqP6vM6(lwvkAQcq**aCdeB1p zz@;w9i=V09xRfsHf#I;r|LF37NY%^|g9pE4hsVj}UF-F)A9h&=7h(E8i*95?b@G&S zA#vFKv}vrNuw6InoWQhp<`3l3-mE3YO>xb5n)#+d-~;dx!)&uy`@1*>V9>IQuLu}t z+*yP`H}3r|tIHq|MK-Ge4{h^YCE`Eg!l>^IUrhIIbDS`p#+bhQ^ufp?MILtiA<e%E zpjY>PA+N5G{4U!xk!d&Hg@S*gKiXv>Z!R+C9WwUYSowM#f&x;>2;Hl_d@o|d&o7Mx zmaPral&#Ehz+;8%aN*{J#5FP+cZ^x!e2{)4M_E#u{-)>9dFg!4yb}{8KCqcfC7SV) zYeufsZ4^>`SkAB_p7isd0$Q4Mcb?sxgiROrKZpqHP0{{t$Cr5@gP%;!cHfXx6KT!d zf&2bzn3I~a*T*={Ja2DD%YR~(nYKE}!qM~1OIkj9oaoGw@?SGTSw6b_NtLYLGewRy z1i()*<-fQ1_%Q15m7GvA2%xl?t=t2qzwW4VSq@~Ri*D=*C`sPEY^p!$cxZBSPTsN7 z%N7ni#W{8<EKkY0oV9P0l$&Xv%|iau+?vJlrH5OqpZZ&4Zv{8wt-Le}pgCrKxi9h{ zjIZl8_r<sAZ+m5<NxbEaSk9@EKsClt>dUS3WNlL@=3!2J)aUIGK-hJqjyuuz;4GDs zJ!bJn)V@CGh0Cb1s>ls^ORppcLsmD}=Ni|_Q$=<?&P5vNWpUXvR!l#Wjig)$f0fWU zn98@Ib2RulXV1h{^7v^uzd`*sy<qqQI~m<u`L2ymgglmUlsY54%e%PY*6qrS)3?D= zF`Zo2<rU{fnCA9WT76C$F>NjTP_cy|*_2=d=879UPs`Zn@F=6G9qAm#>MwWn! zp$kBB+-6~)K5Yw*H9<#o&5t^oNO!&C)zep(*h#V(zGa+fJJG?5gYdwQanN2vEe|$7 zhfjy$ddjLW)SCjiXPD)?U8HsWtNL3r;2Z~NU@QV}BdH4!CeK-DZ~@l328R6a;=OVF zjsJZzurT~vX8={yQNZUb;)`k~V%^&R>meI_T8uvz;f#s$Jz#j?n~U)PTf{jy!!$J~ zHpT4(fK`R_UVxZlO-WhzU|<<H48shwQ?^e*<=66BBLOgQ8*`QF6GE<Y6kb1Q1)~z> z(H&ttQ1fgbiFk_XT2q)dW;xKgqFDJe_3u2vYfeCoJhlAzpe3I8aY&G#_tqm@-5PXj zZcD>I(CCuW`YQFrZU28$*$JwUE`y#76H0;9{|*qFv>Y^Hl9lF=`M0Ik^48K}^0=Td z;6pf|NH<`c(~SM`nfu1u9w&ft0fob7IX@f%V2uFwyVkl#pR2B33doZY`o9TgqziIP z!ZCetH&{$LMT^QqYR0`<^=ooNZEHmYL|w)7He(;Hh^9YKm5qi}Khw5ck6<Dp>%}>i zI#kW&mIDDI<RQaH`%HKkGwbx~H{^sp*Nn#iYes(DBrDt;(ip@i-C_cR*9~0sN_<jm zF|dN_gHR=+zelNmhFviOT-4i{WuU0n|IX*t)Zh?&VsmjhSSq3h0dn8bcBYn*Q6ZJi zeab(b_2QXP<{;Bck%ja9ldyi%BzK^rRexEm!ftNd4?w-OH*NPhcRYbb3KF6%+A_p9 z-WH7=dB#~ZVr)sI_lUv4U%Gn_Q@))|v|*>gNQV9Zma3RQ$K@wOr2!O}$L2}yhqLKM zRwn9&pVA)pXSDv2*Xe%*267dK*xB3=KMqs(KAfh_5QRN~QE=XAzI2yqV_h8>!1`I- z|LZ^HlJ_Pd-d`Mb*6DoBi7J1y#d_;M8E#FUhrZpMq{`J6w6aE-I`HgKi4@|W8}(4$ z5)GM{N>%s%KmkVnK<4(f4NaIYR=E?=B}~&7|5stEaFU{|>2r}URS^5Qr-+kTy+R#6 z`5JI--O%ghnz513z24M}KW(d_W{}<sB;8B%(luLuh!>(m5;^KcLv%&0tb<il?93{A z>cS4)Hum0XX(=zAIFDC<@r-rsQsc)`bJ~=GaZLrB(kSr<N>jMR+{rZ721jjhmm^WR z?y4Mo`t&Ag^~w9y^S!PEi;j->E?W{)9pcf$M^Y21-=};uOz;fpU4*|3S61v5P@_lF z;F>&T8GK1O5Jo@XrK;I}xBzp2I$w~p^2O+Wsc4D}O?BW@H;R-IFF=@yV9t*0(XHVN zX0bV!uT50>F+Xm(W?5sz((RIDecYQKAWO7FxSY7nt2No94XTQ=(@=~gh6)Q1B$7Mr z#m}fMBC0noyxa*d&tFOA2MXC<wFK0ZImJ{BYBcI~j%XXQn4QCwVqPf~5|#+0fk*;> zIGeci?7GY;aC3xEKOi-?y&oC5ka8!D7+BUWZhN5TE_M$`AjS{@#5^jHn77L68slsY z?F0Mlf0A-bpwHyurc%6M6P-f7RPSyLT8eDzNH>!UtMDRfYj=kl+4G%oxpfhr$Sx(2 zOMkSSUxr^^aPbRqjA6Nrf3yXp>p&S|Q~G{PDcgxHtC+5UOd@0`%&lSw{!VyF*$b&` z(MZoJ`f#ww+T>uLrNkypj`3Ju8*?tzqVI~_j*8cbCSqzI;i$y=%CVV9bfxg6G0Le? z27#KmM;nPQkZ3Qbo}cbFMEP@-0INA1s8t=1SR_>@8*sCQde;ZJu4DbEOlNxXw|W(- z8t<ZTlP~>I)K{xQD`NJ>XPCrY3@3zOW6|u?E8YDEoAqtz`?lNWc`DA+pbp9cNU!b8 z*PHF)`Kscd!ligp;0*IC63(>rA}o5f*1)jrlBZn7O4`F;7-gioWuUAl2{Hw>iM1JH zr3UG5#6=WREhK<D47YI7D4S7ot~KCZqAz-2uvYRq$&L*^(I^Zl873Y-op@s+mx5*I zG>xh?zfVwXp^LDSm#o*9XNzT3rpWUsr!JMjbujg9YR6BzrI()Zt0ozQB(Caj+;|6C zXMc@ejKKoVJPtLX_?JxcfB~QE9N2bfP8N7f4@JYdwsa2-<u#|+O7bZDJRkFjG_$f2 z;Ghks=Tr7^seWI>XFzB~)A5;<-cDm@lNO$Bnw~EDdF|hQihZB%E~@*z=?g%TwSV;X z9|))<yB(!>9ev@>&8J(s6N(z_NTxk_o)YKnQj)U4SI>w*t#SF+dS<}>bX#U+#Yin@ ziVUXns-bg(<nNv|3-lc@a+(Yl__b&c6XL|$K2jV$+5gGQr$k+ERk2m0+(9ngiIz&) zZUqA*PbWGk9I28}D%}{^_rJ#n^*`lF{-0#||6;0SW>3`5pox*yk71}&-<Iznn9fT* zVA;#vJ@QP!{ZnJ?_0<9(`os-#6;@B4TLAZc@Oex=Ml4{6a{~>(SL`V;9o^*%**&RB zsI>74WCd~@)&8?KRUcK=b3bj3@r#uDkb1~l?)tq`x)6TvcrgM7oM1MX%b>E;k84_( zNjBJs%o(O{L9K*`+|PRk%xKzG-x^;shw4tbzD@DCz7N!(hvZ5<>+hAVHNSdhrL%2B z#g<35o+q7t&*-~5K^X+}&6ng0a+&HdA>>17D*#&e9ySWc3Z2FIR6tN0=v`t?<jju= zt)bn&VTAj_qwgeC#r;C;&ZGjDPjJ<I#6dvXYUsr6_R<PVV6<~zZQ$8!g%%AK(xKmf z$xvwLyQ^!7*2o_sLO?_4B>Sij>47N`X~VQK{4L?GHCMI6B?kti`;$xQ;BT2F7jJ98 z)j&umrnP;9C0Xov7x-`KxDJ2=GuqE=^+Y8yD}rTMhGx+OO$xrwNv;KZu8a;#s7yz# zZQwk+kU!r<s){F&uRFd$$SOm>8K3GD5ubjQNRo$lsL0SCRc8YEF{On4k!|K!iwDNz zL;tlk=g%1*)o6bPu5J`7Q&5jU!6s)yc7kzM$?*Uo`H0}tXo|3jk}$e*%y>L{Z|wfA zCzGDc_K;5S+hN0ZcKgA7no;$>Me)zJO=nt%(t}Nr`L4q=Kj?RT)!Dj<Qgq3>X5<fs zq^0iuT>de4Y-wJqCHb2^O3nV7E#*48_7~;pJ)~6PmKJjW6v(JA_tOd5?}v!Yt(Oy{ z(jE8Nlzf6K!g$&%W#1+ev3^pPs+2X<);Igoz~X3JV-QX%%WA4^%?ibR**2p(v)P<Q zmqRZhdmA_xQ(?rWbm;QT!LlG_#1nE`6O1@@4f>{lSNMh0h@k|9swrq7wcm(~5Bo4e zjl9X$J8#pr!nCmg=ZRNWL||jU@U^z0_;_w+GBppB&~a(mEq>{Bq4?n(X4gjx8<hNe zkk(dVL_NehW%FKy($1D=JrcVphV!jkAER@NdhZR)r)70PHesdjasMuzqZHKQf+4gM z-OvKm4>23>cF8-|4qAyrvb8cZ1rMwx+(xuzlZufA6`JfKKK$zRa_{T?oLDEhEcU!D z)}~Z_wsv^;ch0_8QGEF@$cmRaI437-RDLEl@98}y@4WY^&w!ftRQzhTMth}bMcmn8 zIaOPL*@$4ikMdl)c&s-5;L0{5{eApK#<s!JM`kM2kH2P!;hs<rY5SrrR;{5WGjZ0Z zligttv$*U6l=Z96?UJ7WVTaPMSxN20V_>=;QPl%Y+)r^CG)fuzRau6eK25NqioA>; zPop|XqaqEG{ZgAVl$;>|07f=3y}}mx;ufB-)>8Lc?Bd9y=i{_W<69P1;pp#?&$p<B z&<|v})`Q5}gD76a%NdSUDop>J`V6o(fB{d0!8kN#qhg^vi<(cILuGh`rB_}$Ul6S7 zbeUUKqt*J!`%MD&7X(5x5*LSQA{U%L_MdM`m*r#}YuJi8(l%bdVu#Y291ln9onA|5 z?$BhjS#a^9wn)D#)w=KWFiJ|Rildv-$KP-o&=)V`W~x=Ibc*b1ay1B3j!;SHa~_LC z{R#}lROMe{))G!B4NgeO43+YQf#1sfviy_F<hO1J_eL(iFH+|jxW6eH=?CQ2t_-d> z^@otPR{E*$Nc2zh)a%e}+>*dc0U1)a_Ae+0!Tsk{Aoj=Y?HXL8p=twdb$DtfaamXW zp%TN8k1#d&Ay!$vH-VopoRAj7*J*tC5GKZ=4Uv*Up6QI5`$H|=^{_4amq4A4)VA+o z9l9^SEdNpxHV|~C-Xs5@UlunYELNpm6SXIT?Qh}AKg>g91{SE~PsTqy-|yj)tOwi; zv9SJNsg6E)o-|23+o?#N!P*;sw|8R_LJykDceR=lf6_n^wUF#m5R>`+Qi336Z7Jp_ zd*ZyQ5FhwkzsL3MorbH4_>d-h8qvDM*I7IHiK$EUf1xws*2U>E&-4b~bLv?(&tnZ; zKR$vP43~tjXw>-5B+g9N5HMAgMvD^Uuut4M-#KPLbtFq8_~ey%V2;=@ty=)jy}0rS zjtDHmO*-98vBc2=y&Rg}p0gX~mg&=v36r7{ZN_BW-nryhYl-tYZfLmFhM)gl(*#6Y z=D<~DqY&(%pFX8;Ts=M4<ppP@70J<}0xvM05G>ie;{mM`KAI<&|Jn?{U0u7)QJ{2| zt6i8<Xv#W8Y2HaBT_rLWy4`TDMgAy>pZy)fr1wZR&h#-x^up`N+F9^MP6eH7xJ!l; z0PMk4MSi1uxbtV@pY8_GTVm`8tW^ADlw}g0M93GW6F?H<cX_KxWL1WzyFcw|%u*;3 zaH^I}f&>~+kKO!m?da9tw6_$r@?Lb{nJ`Wu+phedL7Z^4yffvV{-U>?;Zp|^e0ro; zogCA+_aLDj%0{%1l>OtRAxXGhhF9&B*-0tyeS3q<ZMGVewJO;5*6X5mdMWSkprjFL zy&(_tU3^+=TGiJS5k4XuCzIfuB%2$CK04%*m;at>?cK=4wY*e-@JgX4sLUIF>K(I? z5^eJ%?N-<IU{Y0T0G^2|iAA8broL_8-VMl0K2nt0Xvs#JF8BnPl8SDpOw^lv+p1Gg zl;_$jmaQqA`ZhE?e$8Y8uqD^C*i*5OAOSLr;a}T&a6>0L2(!bQdUw@4YcDOcfx)5% zJkBIH4-v@9k-IF_J!9?f)fuhbg7aNPQ%$<S?JLbdQe(iRkNUVK&<vO#TS=MP>rK## ztU0UqfpyH!`=s1}a>a3pfbGoTqP|ky{c<p@{Hl><8b|7dR(fq3(3{_XJUP&Vz4i<E z1H6;k8O1{WnuafzJe2YV1~9gaO`c1*!bG>v#>df3(<u?ZrlBAQosCcCGs5YAzJ_8X z?~+q_4*ftm!Nuq6uuk9x_`D{-;6#Bsl77YJjPv@xuK6!20dgsM!G9q9jm7uv0gqFr zLam5|DfUPVzUb(izS@{{n*dme>E-zyZms1`o7-GNOg_96jd(fg$GrFZn^=~*y!2{a zvx_d0nDig0AM4@^2C@9j(FVk=VDps|m|m4$;P?f95Y)ww4Fc6_jT%s%K364Id>}Z4 zy^iR9P#5+_Vim;%R3hR-?(>4Ko6ctp>BU&1))_k4GO>dB@G$x34z1aIuV1s({??Ko zL@?##fMkGMf9pA(*EABFigLH8=SAxADD_AcH%Xb)n@z^y*5*O!NU`(NDmQeP$qb0` z4S{sum7&_ND4<sxHj8Dzrdy!veki`Ftm-fJ#X^(GG`|*TNI-s0KO|J3k~D>&!IC*9 z3zXl#hFVC+Q(%=mq<^|Hn**i1PZ=PFCN}D|Md8-Z7#fFRaxgi7ne2qw9^}yr<b|2u zz4=bKYb%p+-wry>inDFJw#=%u<23G7zIr^YAKdV=QyWeWb9^#!ezl}ZCo19dyel&Y z?;nWt)rHs1<-CDcX@CyJ9>H|EAigSl5w#Q3;NYCXyElpqDvl->vQoO-a`A2Li4e5x zWnbnPkM6K)G>5e}M-B@K6r3-63AM5RyU`NjF`-GU@|UVJz|4gnF0U$_#Lu|&Q-ry^ zT;9RQ)>=PI_+ztN&6PdDZN7KP{ZaRupXcz?y~B5;L7H&y_kGp@DbU_$*_@CUH-w%` z2{t>gj3ou6WsI`tQetxg<D{<JV<u8vs#tz`dAx;-NPJpI1Unc_UOl<IX+s^6Hh4+m zb*I?k8(4EOp0DWj%=hbVPp0k#eVOeFZ}MHCc+By%7TFLuCM2Sl?;)Veo;IZUsdswP zOmk^!Qd$M-$|pqwaF4qGKp$rh%-Xu&1Mgw6w7~<{Kh*S<X$5vM&t5#STF+s|S0J`L z9nzZVHjcW;JUZ7={PJlb{>))fJb88X_P21Codev$(XwfVGJ!WyiA~S_*PITJb37z? z^cunCN#_e?bxAT!;mDP9!ml!tVUD1N@+kxbyy$9Opa{}!W%S$fXxKbSEiBF>e3tIG z2jL7KdNCmSF=K{0b2OeQxhHp+OM<t?qXMZN^C4HyPWpFQyOB|}{>B*HyRpaGy8t|^ zj^6svak15>?C~qYKnzFZe0m>6Ub0|GPHisl5F`B?1j6mbCH|SfP;#=9sr#XQEhj8- z`}sAxS~UV$TV7h0Ot|-=Q71DriQAbiLC#By)r9;JzAHgJ)u&otZ)mCSDjTyF|7Cqn zIh|=*&-8-#UH9+5yv&G2aIZ!jpQw6Ab6h}1yi|a|O6A#;mU>|dAIQpxnY^~}-^je2 zXENGd_M<jQ+I)iNUgx)G2Veh6<>|4yRJ|>~#V?Ks>G|`cg)Y$Y@cr7+#(mHDWZlAL z5|GnjSn!hbjCJ$f;~~9{JK>7^CJQ%B@{#~oVm`4mT9NT8`YHgpibKC|5QH7JJR%{h zk@%c*h6XX9?}pqZ*$99WZ;_^6P>O?>E`EL@z4(T-&3$s7u+mke!uymfoKE4g(KU5- zHX+L9?)279G88RE;&`(=zctO!GYuF5|9Agg-Eu4Vlkqg#VsbaU(Z%wY7_1Ui`s39W z+3;v$wRYEwVlCV#ZZ2Cw2+lzAk+mPD`y{V(zPFZPa;bPv3KGNGyS-bqx@@vEnP1_A zMwBi7;5vE4z^L-sZJ6QAen4k4kU&sM|BZpvJMd=e2wnSR`REVkSMpKMt(<Y(YQLOa ztPYgN|H^SVh-UHO*}GODhcH$kA5)pYhgnz5FYg=ba`eGb`KRFtnbVKTPj{i}D=Q=5 zh2fl|*D7xMdTdZfXKn@wJW^pw?*k1EelCU(nu)(W%YY&zpw?ZMP0Px5=}gj&C541% zh~k}_o$ypRZB6lib~$@dlktP*XHEIWHkVoeoDp3NDT5ZZCE3~-d;I6$aVREhl2$m{ z?Z*Mfe08uCd9EX3C!wB@uXQ4vhA)hwNsCdJ3lfl;)AaGFwlLkQ;AV#MLcND4XZ0tq zn^j4oM*W~}gc(ED1bg=(ifx3~8RHGAS_2ccSlbqFHQ+a1o~n0kOc`29w94nm@?J^# z54W%9+iyRV(sQV!!6RG1ERX<OormeS`9Q`vPj26-;=6mF9gV3qlZu+MG!H+XiNoI& zQ?nELoehb^4@3nbBi%=vDaj0LblE>@@d`05EiiW8&pMBM?tn?^CcRKp`WBP_xE!B{ zOG-1tNb%|e5F&HMx>BRO@a|x?WvCPOEK7t-FFrFnDFh^ZA-3nDr_N`r)4y-9IS*mn zx0&doeI;%E;nGX+b7i8ke%9FBRt4m#+e%G&dYMVawpOx^bP4Q@Vz+&e3GMD-=0A|5 zPwTDy;k%%ci}SRWdBx__!^!48bbJ#x&F|cD><7fUI8_@V1lNJ8Lb%JY?#(;-KLLR? z=>MH2wf~t``1ijSL7Scb@ewgR!-C$%ItJ0=8~cR2$w5Iu9Aq<?r~=&jWMK}=4L0h) z63SCxW@(Rkd#UiN$EuX*e2@RCMVFu{Z+-M}c8+aPcyz@yPSr~)BBYFjKhw$Xef}lR zDtdPNKwYU^F+r~~-_$|b&LIguUArWf=RfB0cEXC!P$?&+JMD|iIURnXw#v<i+Z*{5 zm-KDl<rW_wgW_RT>&TK_#x&_5&*&+o*$vAb6i?rd{4j9Fl%B8bSBp>u*|_itz6bxN zQ1k>?T$jJuP*$N=@8#S@5eAF|Mp9*2#ST7@MV*$`Jq;qsT<rwS>p)(U;n_@b?#b@V zS^ds8Sa(1bVJEfHJ~MM}t;ETm;A{e}+05$?EqR*$sVtgpk_ea&h~+oct7^9;$oV9s zXC&aqsb)({R!gw(Zt0_vcNl4aY0ihZvBF-juX+w!$pmNgfoo5*b@XRCh6gv~rL+|o zWAll3308|crc{+59}o9F6WNt*NM_hiCC)3UWgGz@i_{S_W}07*n?v+;4t(y#t%-2g zcI2-wub26*MeEial$YmmEGQToYuLl`4Y5&en9gNb%at^-<gAalF%QpZt74;~`jhXJ zvh_3CJ~&orsb+y)>+ClfeClHU=jqCqJM+A1ctOzn_TRRe<;mbnK7u=#VKXJi!0H*F zZeR0hGV%iF@^qo+Ygmk8>%feHl#|m6b5Nfv(t;+}Iv^zMw$N3kE{h6vzcS1)6Ft2W zZ>|HmY*Bf_M4*<$rO%ig(w+=PU4Az6%g$_aeyf)4ZN`CaeX~gOCirVW!VQn(@Doi< z5(a%^{ltcoy-wTE3A#J&Depnx5ZEN5FJ^xnVoS=bbUxyW6E2WO?0=glbt~RvDlb7@ z>JiVSQF}Q%5d7Uc-Q{!+!k%UzQ{gDo?-TEN&Z%F?oukAS!Cx5~h=neK6<iv@GF$Er zj^K=VcSp^a0|sC~pejv3+v?~^nps*}S%_TC3ZU=4@H1jPV{03w2{@AU-WlNtEq^56 zHIR4YcDIVEGdD9Dj;1(KrPWSorcoD$OX)G8B9->JFdH8t8qc;@!O1t8z8_mqo}~Nr zTJrp}TgTLNSOQ^bzX6?{=!~sUTGj)7+@czz7u@Xn!V1Z5O$NnrGXxchMJ4Ydf_MhM zFPp4Q<On8K49_TAm>6oCnOI2ao4J_Ty`3@8*VogSo-owsi`ewvvrTMleECvj<keNk z_d_|4rU)Pt#DZt@;zE$v#6*GrfWPy#k5&Z;3p<Xd0AwZeJ3xCdQiK@NcAtXQoa@`E zM1$7JT<}JS$u40-Xn1?F5{c=<8sqY(Re9He$HF3eU4hG^K32%dD6YZ_?5DB<<<ug* zI?q0_E@cS8@gSQ|`l<`fWKFT-2*Fka^w#Mwnal)iY<=w9u~?R0%wy=43D$nn-Z)*R zJ9h>Xc{BW77I{-FBHwDi;5nntm97%y%dfT+jPtU#oVvbA8yLJe<v%Ks?weo-Abt~< z`c&b$l_JD3y@c~_7*x%sHr=h~6UV}-+p1xHi;38Gg9b&)sh6O_kHxw?)3`OoXJ2cz zcDRrp-;AA<(z&*f6*Y;1G>Gc*<RbglpJvN<5;Hn8Iuq6UIjRwj_*33FPh4HWsEy^U z2M45miabCVAFmqSRFBpE10x&q7X;D|i+HzH)~Dxk#qLaPDh=OSOdZ`uvvKP>hmbw( z0dnishZ}y92_y~wvgOIrGzZjCpC?x$&R-&%L#C8Ba?nJrEld53pdrHq`6-`AWTr+< zIlvzIH|TtRfnB>WHPltm2J>}okAJ8K(6-^@+??-TD>LMJoWefcP24*#`y)9dtg{p& z6wAvEQkTx-U5RA9fC<;&DvjF^%3WH&D(<5E)|$TM#Wi3Wpw`7}1G@bDq(kuXLod5y z?`=gBl6|&EGH{_KEWocGThS!mmZqZLXJX0P>RxcO$SJA}|CeR96J!SVhFnpx>*!Xd zr^WckxH9%~@bpx3Zql2nri=YbvK{bJ;Gg##JB4PCH|?vURZ?TwRW(iZ)^cCG7+}7~ zC^kT=N)psn{`A;^duK67G8&k49gVwuGvj67(iQ`HSZR!Iev~JR8ZWD$JUdQ-;ExT< z*aPFXX-ikPBadEILyqFJ-T4|_w&VNTUiQb%(6l*JSIG9R+eGW;su+Nczm6M`slf8z zxC^+pu@>iRDib#%x_#8z+Lh_5H<`(}g$Yg%diRb;%dm<gMs_IqEe3qbae0KlxAYR9 zC<9^mASzm$(vI5t+ob~%0omA&O>e!vQwuI?I)<y@%|ImDU7UF0)h*}RFkdk}`*&WE zJ@mY+Z<7a=tCrrNruMK=?U-wU{#XdvMAu6-89c)KXutxd!9Js=PG+jk{^raYhBAS5 zkq3<?J3wa(V~iea@z04u#;}8vDJ@+IH$4tVG)dFOX}W{_|FB&$thNiErgH7ca@>;* z^bD?^M*nF$m1Q~gZ;W94{lmr;JlMBn&*wX#yPHtMX-xcix(YvC)}4B7M+*FxU}L!i zX-4ID0deXsV%@;FQUD)LLGwh5nrDq{rH^g4sIa6&Kh%Lyrw?uCc8$E+$<7WGiz<fH zB)>J^-YenSSiMk@W}O~eZ-LHnQa(xq%@FpKKXx0U3miNL)pI{%%ELRr%m;cnOo0UR zTF*~jy{|QkLCH`?J&-N}Cd)jW%5A{NclnFXdE!)?o&rHVlq2O>9qlFQ^^U?DGmB0W znsoWQmFS-7vxCZO4N)UX<G?3uGLbY<ow1{9yD%r3J<IBat^Fxj=_l2f;snSy4bIP% z`pi-6nQ55xI?22E45fkugN8Jua=M6y8?t-x4VY7g34U@p)+kasEUlXV-8;1*x-e9W z5(XB!d{{R1a^ykWm_yXDK#*Za?6N6?%fVd(_hxavJ-(`ivvwb|rL3}Ld1-Wd-@fJ5 z_5vf1iuvWCgr(%#Izh39m0Z}|sy?Ma(;awLBdn%iOfmISu&^r|<nD>sKq#lGUgwK; zz?CYLM{v<9Jy}FMJ)3zrV6Pg27vk;;oE@;c2lM#Z<=>G?;eN!O(-(IWh?E9%^xfXA z*Ns<|$7-*`KFf=4qKUrrbYvyxKQ&M)F+?Gx-fzimXo2&0|A8nmd8gS><)?5ZT+<sH zCX>B);;iWHCF@^!UQ^j)n7jSZY=)o@X9Gj1wQPKInTG^(i#6cYL#b=E>g)D8!RzGk z8U}rENf@kj=w2SZskHd^8_7eOD?Tz*2$szSCU0-5=)|>iG5Wyx;=3~AWi$bhWq&0g z0UbY|?x<Za8XnTT;3+qf0j^;D2a#ViMR6)HwReAmSKH@^>gb;I_G}yn=UppnGP$sb z671g^&pqYbt2}48+E9=e@lgl-Q>Ia0T07mIhtTWDGj7EyKbm&izl*3oNYBLr0l0q) z&b<^wvM7#~-uvm40gyfelyK&*04@fb%s`c999CEz#x2uB^D#Nc0}$1@NPWXrNTfwe z`1oc!SPwqF(}Z;5ptb+2Ont)~YjudJ)d)jxjoY6DyPWeCpo6NO7{}EQ>HF7UXDhzF zx!~*nXrgpQqc-?5c`2NxO0blsorUZISAjZws3XXw2&JSu&3w4pTn(14Z?fXwh!X;v zti{5r57nN1=JoToM^-SJkkJe4``G^jy>*Bbd<Ilazuezht_-lnm4}C%0Yy0qEp{gn zSO#Rl)@RCE8^dxBGY3o0%=o{1G0gUIR-JvIC?>{Ind&?o90wGfY7e3f_GF8=B2)TP z`VMyG-W!_{I{iGpxba@$=a)+wq3OqY(NJj`QgTdt3zDiqzQ>b{rxQM>blIIzo!i*u z6D%=fc=Hcra7&VJ`&9J(WAYy(CHK)gtKub@+<JvK3`w(@#NX_pbRdH{C5{T#9k-2$ z*%^~NxuyJ*%Z{N5R!V6?n~l8-a<%m!m|=p5D0ph)J}7B|THi&(*+Lc*40pLVEK0gL zf9M)RqawCx1dh_Hr)hpGPygZ}#F1O?0C;USX-&}Z=4O(_kPfsATdc|Mb<b~rDrq2! zEMxNb_IG1wXQk147?XEh8D(${b?kdYqst+i!_4=t)wvHhJu+!BVut@4;P@MV*<ayZ z51?{M2=t%Tg~zXHbaJUnWJ!G(!WK>`^~pADbc`nUgqU4rx+__LaUHP5JD;igxKyyN zjm<g1ErV1|Q|w#%ATGTJM71g_jgb`kq`LZrHtDliZ}F4Pv5BUZNf>tzgbcBw8T{W2 z7YzO~-F+Z_x*VG?(|Si{KiTH{^UF;_hVfuo!7W2N<?HxUT`M86%)BQ8ifYxw=_&o6 z(H+<zp;wz|dGy}IKM--y@;T8|59OsdoO)zCkkR+B{6K2YzVBB{zu4X<#|)Kf@;TdI zN}-HM&CS3e%}(}(gg(cmxEM1#K6y_f{4W^Wn~mujB+SPZ6-0Aa*rB8<_l?asRCYy6 zb|ah4eP`XrjvjogvwA`CR*u>%tG;FBoqJ8Y;>f6HmVJ62Z03}Wzevjuq!3(vq?ubd z|GKi)EA=kJ#+B}Ha#=V`uJp%2=MPoVv`}NYqz59Ii!sm*zpSU4<+Y$h^{(HaFHg)B zSlVPpsYS?$6R7XJP?oQ95zs&U?NS{<OU!MJ0?#&*-d66eKZ87u+qCtzu7D<2h%+a$ zD)PPPW%HD$n;fJ2$~z?N8L4dL^Tn3sJ6YEvIMMA)`v9Jj=q8w8!SagK{9^U)DW3e7 z7DFpr_5}5bFTOKbtO>)`ulUBx-c=jCaEi+1S<=0JS0y%k#ESm>#|-7(xzge?_*W`b z^tY0v0$X8Ca*QVBFBv~~YI+ZoyI$#o4{ZmXIgA|)%LidoLmr*Zfn$zLcs6cCB`0_f z6XPnJ^{)3npR05%ZCp+j_01frXN4g8^^3FPx@id=-7_WHI1v=w87f58vBg!Tsg-Fl z&)M1>+BOd;mBsv)hDNlA(-k2r;8}uASbtz{X_XtDn+|})c6pKaru|OW`)a|_%X$yg z+bgD9eFt?Pe=aEeVeN!;EWESfcM|qFKapD%aYB-ZiQCvQ?bRjWO{{wyx+j{<_BLN{ z0=N`<Hm!%SR)P40f^R}2r2Sqa@U_9DrCH^ULuGa>Rax4)VnVtHU9((chrguF?pH?C zDn~9EF3_9@*wGFa<^Ecw&CU=J5pQmn)t;646ut#Q?fSE!OTCo-Ze<3?ul&1gOo>l7 z$voInJ@u0Dj?jLX{QbKR0xyS(b`L4ih1ys}ZoH)r=tDD0%Zm}!S(bNE(9KAdRC%mT zrSHKoq#XWY_3+axx(WAXFtN$(*Nu}SyN6s7@*8^(up}jnTlyl&gd4`?O-%Srg;nZr z$bFKbf;G6(8KL^56Tdf>Ax<RAv9!t5PtF4aWVTO(IxVNm5G9iuZz-*e^FH!ne)Mnf zuoZ|z!=o3&jQtsS$JnJPAEezF=?Nl~g`(E-rzcQLK0c7MYrT-K3vF!#v9U1KJEI4< z1C{EsyZi@zypJ9|k}$ropD#*zXVdU<%kfV_`RACyosH3EwJqq8bG-H=3UpAqD`r2X z)7@EDV^AW--xk^AR;cdYFs=WZ43b%J5_!vW^4DU_Fv5u69tT3HyCd@ro+SC<Y}&+k zeU1x-h3EL;I$!Yk{GG&-r*5yIQ47xO3msXy`D^<qLT;=27vGW-R4OpZ!QJP<7~kC3 zB*dCj&`e$DIb_t-SJ!vzWS95N?2U0}xb~!;t#wp{KT9#<3u)kjCc}y95q&rN;nlh5 zQpyX&U}jkr6#<%?`r>TA2fWfen#kF@H1uNCglv^mEQXVboGKzK^)u>Nr)esHjv^B4 z)6dwuLjTXN-3h5Ikeqj{w<&H{g-0~hAQ@6^S<J(B1GD8uq2J1gvVfyi#(;eQRsMgU zIr;xKq4Iz8)&I*m*_EOSI<+M2zL&-^HARud^q&U|1`L&Xdv$T94X0IWo+VZeURsp1 zF6JoOelg>d5ysE6Owyk8J%dvHs`tW+o8%qhj~msGki{b`WR(7*B=rEc2#_k4!QGhT zB^dZKaw*WDp$JZvH@GQ!NsJgvA3%>gN7sG10ideS#mXf;dsbe~Iq0AZ#tcU9{50RY z2s8J$xXweIIAdFVwTTHITB$xP{7B}sO-D_?9ZWT|k5MeZCDWcEv`XMgV=ec4<TOoA z!INRFgqN0+&Uto}IOb9-*}Om+8&;|3Mpq_LRr(03S@H#2AvOua6ln6Exix-ZprHcA z2GSf$Pzb>5<2$ZcypGoq+Kt<|^kFZ`M3RW>4#a<=`v>X`I?WKOn{wQww~NggaYsJX z<>bYHHRDUR^LPKom{b}#&hav{KIu<jtT(yOH5PBhw$=4rBtU|EVh{FuwxB&rg6L;Q zYzQY6k*N#%%zy>Nh=Uv-ZPM<WSW7ShwX)foVxsG+q80EDlnDz*1efhP()|4gVhPwJ zFO-k_zV=%W*3Wgd!y{BbIxasw{}l4o^P-I|+;6yf;lqVTef>WW&Oy&gVB7?CP?3nr zNjbs6#xx$=ms#-9$Vbwp5<0_8cSWNIo{&|0?HgA0p<9%W;K9}1l^L$N$i7r05_`E+ zF7iF06w4?tjR7WK(@oN(t6>-Nt-o8J{uO;HQB}+COI~8TtWWn)_`yDjoaR)E58x;p z_y;maw@mv6+ov0*=Q#RY5CvY(|KgEMdUdC&K~D_t<y=r}T`H-9;!)Tz{`Ik2`kMx& z-v_kF_lPPN4Tw?Y9w8>b1-x2(m4Oz$<kMwrt{v1G1iu@I8MJ}^touFCJ*{7R15haz zuI#B-d5kk^md4?6s7n-l3_<JE;!(aT^`U20qn$d=AJl|3^3=g0`A~B<{xeDa_Np4f z$M>O*t+5(fpLl@IV}g#Kf<;+4c>>8ykmF_J=CXVM(~?UMOi@V6eqOYR0R{*JvdxNy z^~PR5Qg{IyK?x7o6uYP@Qy#b(AgI9Wf4^aVO}SQ|Wh0)<xc-fMEbb@0jM5+B%{gP~ zPvJHGvmB`ZaLC!Oy=1jS(bPKYtd?x7MBTZ`=xO{2Qlv&x#~?imi%63=%+-aQ1V}GJ z+y?HcY1o_dG&VOoeWz6mL#{v)eN>utlnXP?0G7IzTW$P_tltDffqtceun6wdCwQ&- z?Y<DiQbz0ZhlvbCzG?sSa1RRL4xmjFJ~W*OX4qxKP!O$Tkyf6-DsLgSADhhzyZ(%+ zztuR=d8^tYsxL*am1)DoY=dFXt(ZE^RnR2c$Ol-}tMpFMl!NrGgu-$$4F8*tNjqNo z3mWZR=;;sfXsfHIdFZ|*{fDz|=X<H;Mr`;^98J<e@(CiwWj+LY;d835O+&9Ad`AMf z|I%u%CzXT`ZH@)n83Qi34eXnK(_db&-?ac@KK;)z^uE&^jcUK@o-qx9t$F+pv@Z** zk@9>YwWbUdfk8dJuQP$wUm>66^gSGs5kVLW)BokE{0E|txavZ{et~;<>K#bCq}h*% z|C9~SZaS<5S+hCf7*-uuxW2=~ZM0BVvn_RxBcVvp+;{U={mc1f(&cr+LDM1Z@WHNY zQgZN<3i)@$HX2EnF*goz`KyIj-aUWB{ok*eRj~6yomtGWq46T|BM<O2l7_CK(>`V@ zlM^^y3r=)zK#V1^IcxWBM-GlVzneY{(I2kAFi{_)g}!2f11SWb;V+gKi-*{4esXyv zX<Fo)3bkNw+ofA&O?rOVA$W2s={z&8s9!r+84Rv%)6wDyv5AI6%vyQ4?YVdiUoK-| z<Aa=b+J$Z{r>nY;`)gVKR#fJfAHw=_&L*U13k%rts^mYk@05eJ4+hkZMN5-vlv=?D zUzpHAC^=2@$=TUW8huD;Uk$s#5WM$3-&IRlI1eeynbTIky^Cp{X!2xp+UTR+nc{sv znSQuWc`$x(=(Z%we*t-Xej7f4Iy*>Dm%oODPLDw?qHs`8KUep>!lYjfU26fVDIT() zCi~6+4F(UqEh2D7UhKs?D6rNrdcz8Vy8M6qxLXbp``>qegEMgQ_t9dl9XU}Yl(qzA zCQeM&UB1n4)@cEt69y2Z6qgGEYe_ZUY_^`n*j1VrbLq|==%5>0IGPk#!-9IWUW$Wl z`<%<1K>y00;YR+WHe!rUZ9V6Ue00>klh8*&Oozo%m=bci*)><`F@Cz~#e8Bb5AY*c z25pJy#fd2sl6&^TlYyS1=7)5gdoaVEdYb|EBeGiM@zl+1A%xjJYqB3*KBDDzrIYlY zpL|s3R|Lfq+O<;}0id3Zii98k^f@YP5API5<S(F-<x_WCe_HHg`c5!CEe1<z8RY$i zcKSbiZSr!4wbEZulx0&NeqFrmX<3{%jM{hhd6|H+#FyAEB%4+2*8XYFJ;J2VyxFK@ z>p7W(R>i9ZoS8YXfpv{<Wo31@QStrDs(D|onY-?t16P6Q+>4%W*!%oaVAkQ$m=f(w z%nOl+`ffKI;>KTt+%RjaORjb_(FD@DP`7B7?p$B1_=p(n|M<{;#^4n1Lt}pS2FeDW z+ESe|oC2KD`hcK-FP8~{j=z{!nGCWRy0}#r;ZYY6<w*0zUka^LSKt%r69ja7XT+f# znM*dsjW2#K_%q&xczErv|D}=VWf`2l3rkvm*QIObuhMOJ@vu*5z1SbVaaD%0&3vux z+xh1VtI$9%_UFfapAs4je#y<8l>T)ac|*u#dN-x$##%AdY94;d(k1N%vC1ijekz8a zt26xr(ZYA#hNai)monV9oT1imEO=SAzl?H8Oh8njoIzRz2O_#p(&KvQ)p~y6<$V$~ zzP$Bu`NnU`i4mps;5F?jqgpb@i+u^L>7hWqQ4v_oe~j58Y-p8WTn}aEalGX*t9MMi zQ!aV!r|v7EP+e8X{d#72gq^}Q{uV<2w6&#{O?1g;9(e*C+S;1?>pc#0R42yiI}iup zuZj!ppD1-z-f<>Gp1vUu$PdaxFJ&wP>ICAM>SH@N3kq^v(KGrDqxG9bQCdG|Q9{vd zC4Q4d#SzKR>2Q3(a4Tue&SN)$x7bFZOt(*lE)ya;C+*cs8$zBd{pQw)Y3*Mw$>KA( zegApbc12j<i`xoWQRzryypRRD`*}86(3+(!ASD@yyW_cFpIlhjH$f#nY3>x)$NwO{ zG$Cu}<frZg<ajAojB$T$Ib@0m;+qYZ4u#Unl?bQZ=ziM9IGwwd$klyev={+S0T}5e z=T`}DvX-YNwHmQqw9eajat%b8f93Iga9;#!IfvlBB^_rZh$cA5R4G>7(hzO2_iIiN z<SPH^H_&<>2x^r+U-sd4+><1S*4>#JFx$*2;;s6YTekPGAzNEMDh`WiBBk8+67?QQ ztaGu<P*>#ZPw-E?0_RV33vIR9E~;gj=9iz=>shnL1qIjjDtrfF+Pm7*H4ggs_VTsZ z4k&<3tO!H|>6{4u+&vl$fYG16r0Fk}yyW^mmnNB6RD(otp6n&lIMN#44zBKtd>)nI z-`W9>&#Gaby49LxTDz)nOHZHu1JOh2OOO>vdz%XJ27j6<#O?FsQYFT@`#YOmTgE6| zw1q&Yo8F1oefuV>tTPwYEv)Y9@ky5WXLS)Z?V>e3-`qrCc!{NSqz$lD!rb1rDCwDD zzabNBrQ=2Q_?a~9KHj`ojQ{{OPJs5~9_SpcrIxj_G^Bs5i`GPvyt<F@I+$)5dDUBj zwTJZ&L@4FF|NJHDe<XDK18yI`o{kHB0%d#7+aX!`r@#M!7RtzRtZt;ITLze|m4Gfn zsl}O3&#HF96u$J@*~I|~8oa7`Z@4CSc;JWYKTO)&{6Of*FlIgm*n(EpZ1QD+?~$*L zIf;l6e%Kc1SG<&#+9x;ZWKT>Lq=RE~4#Fn^1VAgX_DbnBI8T5N$DvkaY7`Wu1=r|0 zfGRfYlM`x<+uJAZ-W7=qyWUrgViDX+9qsZLah}gS-p)U}sC{S@SZRC_@kdWo5`cgi zP>}Ja(H#>LN4r-^e-dMZ__yBk0#M(P*Wu%#$MMez5h@4oE*|S6cPnbSl6c)^uxkGg z-rh2(?f&idg+fb_mI`j6NL#EFFBGSEX>gYoEAF&-3KVyDLa`9sg9q1Aio07O1Sd#H zx^v&poH_rQJ?G3b^X%C#c#{_pW`6R$*0t7Woo|6uS5KvIG3*x`Co+NpEX~y#1A8}* zvAFxgzO`Caqvm(!#P1S1MVZEprI+#V32Ke$@fLu4Qb7wbV>M3_b2fD*>1w}`b4oS> z5CIaQyYX-7V6whS=e74J|3IA|biMC(7qT3Ov#R{uoE&N<S=CfVT~G)p-)P~OuSVZM zc{ro0)xkLAX>Kf`QeLtOAA=9CsN-aGQO@Vwik+7;Chw@mOIP^SA~YXVde>a;6#APB zC!}fNyC}}y;7%PHF88GOEU6Ag^n?WWn0)V1rWRMyJkgF+58`32iWtqEG1|lPcRpe? zR+naahOpXBUrnu<2xx3#S`N}nDKqpK;p*+R+ngxCPc9qP=COx<Q;{A2=FitDzb!49 z%6rj{34J>&oZcp{V;TxYt1z_t)t_((j8WcwIwd5;Gq#wqZTT1P%VXSTv(y#`>0E(v zX39K}=Lo$6cOIeqv+5Y63MEtW#hJ%%x6E;_8g0Gpo;bN9GbEBeGP2b5DYbQ~uuDPz zn9YjDjkJ!b@{PXl^#n-B3~T@bOKe6hxgv*`v#M`eiA-uy3QL2!Jtk%>&kgg{dNPNU zffSDhG?Io`t~NB#uUViw*kkf6?<}{SiVepR4>D3e(yfkq9IRk>c0zCY>zoAS@N$sq zODYVY;oH(%x5Rh*O1e-r(W3}RryaNOMeN8@%fZ1@{*L87|Fi*jzbcPWQmIZ66k5V@ zmrOA31MF1mxB7%4gY_*TQKGWYJ7tDgQK=IEvOP4+fO-D#RD#&F&nX`%*}d1k9eiL~ zz3knd)z-MI?K!uE+Zp6%=0A_j7o|(gn*;)P2WAR=I9cM;x8i~%${Uto^hBR)*o`Z5 zRD+9K!X=GCk`bc8X~Ma7^Sq58KFuSK8f`;p+2rGk?@v*7nHY5a*ypi&K&3#;VZUdK z;3$sSNF_;6^eT*%=U@?ZyQf)MJg_nfp!tipu9lMnvoEs^(7lMJM>xvF4QL_ve-#*i znHw3`9q-X$w9FXx8=3A&h!S8`P?cj55|#NlE0WY`<|Uft7+$$HZu+>Jk4|9>^YnSX z_Vxbnr8l1|U46?i^SLYTlr>BK4J(z6IlZaeiL_QQ^L|JV_rAFun0fym1->+?<i^!W zSnM4%iw~KQ!=#wy)32JAuo*EGgAscbG450i12WFl#wh5MPQ#JID-`cpC)RB7(#oM? zfZS(O7`3$$6-j|HO3W`w^51Yfn^IJiTo{%iB$cC>&{6xOJz4IiAQa^<F>GYkwv^$? znBEb4x^O+pBtYva>lKi9SBYpx>e=jD(Sk$y%cSk7(VgXM4kuKWiTvk?Uup7ec66HU z!$Ou|6}>zl<B4z@ZQEW!Vb2&U{np>Z-AO;c7$@~jZIjf^bVbEh58K=Br>{I>eW$Ku z@lj&F2x#H>ho8of<LmR4BJdY(KBS>t`3fg(T?TQy(o4Qaqv|QN3XccdWEs*3@A!|T z!sz`_GcZ!0=vpNkjbNM${vpwnH;HFdEBN`DeA!a3x6k(H@A2!(&y+5vDHRu7Y=b96 ztUd|*D?GMms%&LduDJ}|EM4~c<048j6~(9+?_(xm&V2?{Dt|e*_xa=-jKTDbWkv93 z14W0v`EXZk9J^LjMQ&;u>s_)_nu5XU-+~>64ptX+YV6;Lab}jEmi2M3=+F1P0K69# zsMz2F`&Fd5-F*K-$h`tBtt};KA)nva;q#`7#kVhb)AP$9V>;UA+rN6Ei^t@jf{0V? z=scflDjJdXKQq$P{C-8~qng7SE3sAlt8QFpD&RJd>M8m6J}pXf)%%`?bm>)uQR*9m zoz><Jk{zp=eKIhw$UWwy4^rtbV#E|2z7mp&>(3wp)l>s_cVm-j%Z2BmOY|R9tb=Sy zETBU665>Gy#zTw}e2Fxa>;@L8F2lWdH^JT|h@#vKPAFS!KJSJ?26nvnXX5x^!u0qw z=l+SVCJ<^Bw;z4~*4HL{=Zj`1CGFVR9(N-;r6#WTr4Yjt=_>NG;Op2NJN5-piFt#D z{ZG)y7O?#ee|>Q=*x>aRM;V979xofu`s=<8aiO>^fDNu0DZij%aKy|+Ek>Vf2Vz^c zqvExka?nNA&E`h5sfT`if%-CIdN-LLR~j2lH$`HBQ7o0KJEKCd(rZWj&VJOHDA~1p z`bV}~9a*k`6GQi);}`3H=vXPAC5&rk@X+38=Xz1x`Lq-}JUdjC1z?mEb&%+4d#hOA z*vLySC&!{10~uOBZT?wg%w7AO-g)ii{=Pl`Vo;Gs`o{f)qU-p@IWea%+oP4FSw%|( z%SIelKcLpdL}_3MKxV2330sa@srn)^TJU-M(6}kKUl}i9ONP#tg4>|gMw(`ja&sby zRNQb2OBpiKl{m<$<v(rBQkj|y4lJ&%H4IOcg8g9CdMO6}^U5D*uKjwO{TI)AcPs4E zrH(LNmD9(w48r1tCc0U@(tgLAcaMJi_lMe8lt5$r;^<tWXPO%@>*V}L78zgt2YgZ` zsGkg~KDQ}ubKhC97|!t*z|};FOOE({tvxlTv>%KYuau&QzE_ZFzlVJo(q=eZUs5|I zVN|LRr#@Vo^uElt@Ed2zBLOUU&l$DVUR9EGAr3~dV0#FlUXgSUv)fNDI~r!&|3Bo# z|KlJ3{f#rQqc2Snpt%3ExE=jB8NDOLt&9YEukP3B-&6N!#<q}4uELxj+I!0sMCgL| z(LQc$dgkRWJxaKZWYJ6Mm_tH{bi<La=@Nkx5j_90PqMRU2~I63sn#5wEV{5#z3{vN z!IDFm)E5XTHekz!M7&R!BpA8@_{*buJQcSW*>l1Z?z5>+WXn{UaRmIuJ1u<d+kQm& zTD`k<yYGQw2I1N+VeXG|r*CY}-Ya_o^%?Fs_YI(`4s4*E>?Ul9T0(3Itr3Lr+Sl|0 ztamjjz!hUszkMGc3g6o>nm6avfqqyM9~c9G^%r?^5w^l1+^F%`CD*EvxDWV&Yk*6* zU$vlFmzAb?F>N076H6Y|V?2hk+EhYZ<sA_wtg*bnm-vYStJ(t_85{>qzBL+_IRC3w zRy*iTq^p!<>*d2zXk{N~X}SK1_%tu>rS=#-%Yc5ByCKKb{#JQFx!~aljn(1A8yxe| zcBqzwxcScPmhq_+%Oyr}-qv%7!ACf<uZShI0LkdP5KzXYRk9CM7seyK;puAn$9CbE z-Gq1iXN9ryb&CuoVr4PbusSF&1<&r2xtCN{Ebl=3UGvvsx~<02)+vY;bWo#ru#Z6% zsEV5EpA9z1hlZuY#CAROvFsGY6!k|b^JTnZQI{%iFhD!+9iGx;b2j(+NFNZ?%{GQr z`I>GLX!Se{irl*QYQzm5MQRr1%OClSv{$iP-iLVQtBNv?G29@*a0X<H9~DcF36>I5 zeZ=jtu&zXP<9cvrk6h}=i<{JMczP;C0FcZQUJ?BAgJ_>EWO1uP`a!_z`-|}#z(riU z;9tDlS=YXKGXc9sPe$Xj4IlT<=E5=DPM1OA#qU%zehG=Irv}Q6>CmNO93fyxhYNbF z!_?Ui*|r4q-B^qubA6>4n$XSxPKTJi2Q612Z`?jFH#yBd>U93<KLmLgt}{<5tE<4; zw13wf746y0&iU<i4Iq{rJ=-D+!ud6Dg0Al+<wFWY9?5+u;zbA3<@7(w*<8^7qnz!S z-MC`{;3xkAt>-ePFj;LazJ88QzYt3ort>SUtU{_ei`S*R+Vwis0#Skwi#gLdS-wQ- z=kBp-^23;xf{a>?!e3Q9=VAfRpWR|ja~_=mxemk;BUuBSXF`$74%Fi`kPtXsE?L-d ztt(-!_#4ky{ZvbvB}Sf$nwz-2qc3LL$lDp8f*oPN(;AVsx!CG!TDeGyGh}AOX8r>D zVvS!&!N_V>TSb?oFFe%Goq$!T_Zh5h8|A)I#<=Qa5|!~ha;~e8i?3?}Z`zQA<T{tA zDyORba_MPqdpdl{J8f~%_A9x~E{NjRZAsRyH&p$5>B}|bt4OkvHF~)Q%25No?-CQA ze^ccONv4_ui!8K;E*&G1(7F^Cu7k0H2+-um?zL2LPNQ7Mu{N+$UWQQrv>B@$SXM%D zEX??qhd$)E2k)+n65%5o8Okn3QM)HBwQltB0cl1q@<<nF<h-4&2UOa`*v!Cu9o}c) zdFRbPaG8|Iwfz&;A4IeaV*0Oi_8tkvW}tmGi{KFSY@3?Fee7u3vGNW1zSl<2Lc-HM zN@J~xarN;4{GJ2E0{soBE$*tS<Ao4|>ZWqRCzm%OQpm99*gw_Xw%O+h6+AIqV34Vj z#ueL%({|7q_YA2f%5_HO0>{t*oRPXPb>CKQW_*5M-vvQ0D__rhgR|@6GQiD+Q@tv3 zDvX0-8s%{5H^S9xzAB&M86K{HHS_RIo30A~VS=2(Pt4pcX4deZ=cyeyNde?Q7wxsS zvbN*jpRhb5v+;=#S2EHxUc?kuP2n0*Ah4H?gIpzMxrzkX=h%6lwcrn60}FBbl0;V5 zfs5NXT<bExEjqqmYE7IQzMt9GeCy_Hk5(H0h16+c_VKh>Yc%Nd={3)2HUvMqWlvwk zRI`HpQVlt}v=k`gm{hU3j-@@vJV(}V)Bmih>yz`EmY27LeG!Y<tkhJ7^1p=Vcisa` zX89|2%;d)O%?A3jefx`tjHAA+qWaVa>Y6O4(onfr7kVF1K?P@cjav?!xL;ikZa~=> zlwLV@wfx1armHyWKDF$RO;04O^cOqWvfEs@U}N&e^-_=z0VY>oESC0YDnX(GU7WF# zSY~wXVH*K~X~tUzpq6oNEA5mK(JMv=1{-q%@~Kync>tHfMNNs$NYbZcIO|V@6xdGB zL#8ha<q2FS$xE8?SbF^QCSvo9;T!08#~LH#rh-B5`Oe6|Z;Zs;;ZW=*np~-WP}!V9 zv!kGVFEjAaQ2o2B{cHFY7VJ_uukjw3n#<zs=8+3TlRGxtlBrS&Nxf<mR$zFfhh$VT zZWjtWBESku&Elj1IqSmrOky^Nh4f`KqhT|>PZXJ!ROeCx^@Pym+HVy@h4yF=@grJH zgasHFh821FAk6FZyim4Bj&D5|SO|1e)qe>%lAMFLDf2K;jVfx#j>z{qDcTT~>s*YJ zXugj^SWa4${aIRiiA(QUAQLw36yORihR64-tv_M6_8dDe3|AIcn6kt-u~axB&R6Ha zA4@z!Bo7p?GcUq;HL6IHx!fV7CLC)^E0K9G|Ki!P1s}L0n^Z@`ZAOTnq2pD`7`Z1W zZ872rMZjvkm#+;f89VN9{GQDW`=dy6HC-AocI=@hNjoKSoU?D4sP<&Xc1K=N(3UQL zW1J>(E}}A{q2HfB(0QO50+73Wvc3G6b`&pGF~PB9@bHoT=__>{fej_nlC5|S88JI0 z$aAbE*d)R-_&5C7p4DbkRk;-QPQQb{i#ozcK^lju*L4qmc(eVX0%$o&!X$q_L7NEH z>@iN4W|q+I^>OLchcvU1_f=tg;~c9ZLVp^WF(*p+ttLvQqE46E73Ft+1rxNMB3@?1 z(E-IlpU)0C3PSEB5CbMQ&#;nH<%j)YVJbR+9ufun8Es}J5Tq$Z{m~os{MAsRKR|ID zN!Gdl;?`g<Be_&a(Ljc1@9=(JtSXE>D_1g2YTX%ul|;!BXbMcQDR35&;V?MoQ+Nu) zv(%e=uA}8|Zqbus2?G*Faz36%Zhu?@V#L%&2tGMOgm1C_)xRj706)kR46b{iypIvV zg9%a1)w{>QNz6xo_&D;Eq56xi)I6Nvq^mt<pkC9Avkw0l13vJu{L+NWn{iADS4QqC zvjsd%!btTwU!XAe?oric;Tq^5aX4{lT)BCuFr-R>tKs>Nz}prFzcSv!mE98yT#<PK za)Utv&+-Z5d-6g&pt#vB5za(Nk#_bwo^P_pNk0Rau+OOQA+H-tIc+6YDGg)rS3;E+ z45-TzFfzybzrWkn-Xs;}%bt6>KEfFUgsAqqB&hj6E44q>Ox0)i5UGysB_G0=eF*4S zd1goxPNbIoO>J!QRB8qls@wEQJ)1}&Ud;(wuxB&XRdu$<m6^ZfNn$Eq9wemp!2#qb z-Aory0ofa9pp+0N;eB$7y0f}K7(Chm23pmsAay_=qM5aHojE;dwh;D)?`RdQQ$<y{ z3+;HQ67lh6SWiH9oCb>F3cUy|b3xc|d?!a|tJuOiku(KeFx@%6bIbEc!^rS&cHX$Y zBF+6NgDQ_E=R9oguA%_JM{*qA3HQXYHBi%^WCZZyqdbgH_5bjb^=lT%@ii7vYkL31 zW73QA9pDa(n08V7)&iS7mJCLKe1PGeJI{2|Z<#$a!|0_GNNiYQg)&l~4DR=GN3%{) zDm!v35#9KHs<2hLy7<oBN2z+pBqTNkpeufIu<-ydw8-zq(NL65bh@JlczPY+Pe%VG z5yudaXr?u5g&1fm1AZsx3TtpF3j|Dbmv0>TGsp9ewRmA2ab{f?5K)JO&X&!`!k;LZ z?c}a7$atMISB8P|>89k^vzF4|jH=2(K~3rD2J#k=Ep)X1+U{Sx{oG0Cy2V9A>(X>` zJE$&(zdzfr^8!+3;D7lB#p`-mw@h<MS~kQwFRWZqSty9~WDn}Z6Juk}ubExp>JBal zoI&%sCy=pn74Z<Bk2Sl!@|dNIHdWyc6(D{$FwH>w+S#yiC)q~$mBj5HqKuOhl3SVo zIkmLXCo(hWr`mRD3M9Mc<|*4R{l9qOmm%HO89mJvVmJIYOcQS$)6n-fbO4TSVsA_r zm@aV@EMMFna7;@0t_sDMhCCc5N8CZ({IlZOn+#IWz`?6exPc7hC%Ds>|5*D4E}SbN zp8uTND23XHT2r4M+kl^pvl#}Ew+C&suybkTwt1v*?m>YmuIT&Bm>iGpwJ1BnM~WAO zhD#@7HMFhvwVY)xqIN62t2ze$CALQPR(2vzYiZ9KI|j=%{^EVzAN`v3n1{8r-jP^6 z(}4&^IH`DS+)>aUy*ZpA7Iu2}r|j}x_sScS%x?CSJ43}2<kNUl-1Qn23IUwix%nhs z^=%+Ww7?U#h-hnif7SGhN2*w_TVLa`)2J;8-QCe6y4(w71Wb6R6WA#7@QcZHGk_~| zvEdP&_pvAmyp4Nq{s+4sv)At&xhdt#BWMR-(AT$d6c%V@d!U-(zT#k_qzSXB>NZV9 zNV#!E!+o8G(x|sCkxO=E>*@^T!@=M71@%kqyyOKb>iIOvyDbYsL+?YUdvj&@^B1P| z@NxF$GHTSW)CfFD=PlswOrl@tp+4^A?4XN5EiSmC?){-zVwhzTh~wKb&lQ9;bI`g| zIJ_z)MpZKY!m#qGHZRo&<gz6bO+D~f@1bDCQBjPX9dET<rC5>tCU1`1tsInJL<mJ& z-dyKOcrrbY;CJ7i1;{;=EzK44J|!k0i>^_Gyn8ZL8tSE|biT4gUe!@8^*R9Pm`Nfo zxpMtrDLq?<Y7@Fb;Xz>%(!#N~2U9cbPw~Fc9$a}JV<<})Qpw=&d^pougemOcrce3l zz4knv9QMTZ;$klgZjY(%;Cv-YVEd6Oqpsl%4W6!WF-L;z8#2#KKTp9iwkkC>?jx1+ zT-$sFcOb`e6%bDw;%O?|w*}xiya`ix+Oy^CZN>Z(RrLj>a51Pksi>&-%E)6oH8VF; zXaWk1ErtdKQGEijWs)U6b@Olcg*z<<!)Kh*`1`gh*vg5vEG6F7qdf=dIlnJli`WOH z;8he=SM5BTzP$<i-nl7^xnto<v+rQSdDBKA9sW3q%9+NZ9Lk0PK~Z<~tlWh9aKzKu znYWKTXQrp724_U5ZHtojKvu3}F45&y&x1qMq`<OS_HW<5`S~Ta<{e~MP{(X5@BA8B zKDcy~QH3o@`iuAU@cUG%I8a6Gvg6+D=H8KsZ<Q}%wdK<E^1nO1{}0s!(BJX@k$5or z<K$Xz=sJ;|2=f8@h)Qbe<<9ClQa<AjYBXC{C_(vp%M-$#P+#gB9}219ic8Zk6t)!Z zh#$ocOn}eL;6={b8?Hh*tWzGz8k4w*JFTqotrkA;iQFT{8oB+D17)K@XN=LRezSm6 zL*w=$VQjQz;KMwQUcIng;O^}3*EZ@|H}w4Ba(j2YnP*dq`#=*3sP4Aq!)9^vCX;5r z?Z?upz^P;XzQoe+!exxirFSHbD-aA0V`_qrra4RANquIk=eKPl)7U7R_!f68?utZX zN`*a-FF2=2^meC!zVK1HIypL`+i=`>Q=!1fVdBu^m;6x=)anguGc)sxz2HF-dZD$o z`9(i?B~x_->Pq?G>;a9`sHnXp=fsUCTPQN{Sf1%#lgS}s?HBb%7aoaAC6Pfp1i`Pd zpTOp*R9P?)&@v@-<Xi%;g%*-8yz$6<d(VVT7*`D6QzH07=eMy4b$bvff=V3c@uiLZ zmSe&hNjrJ>>f*@7GB;FH+ikz&2X57Get&Hh<Ls$3rF6!~8T&TnS3c-f`=OZl<Fb^A zg~~=LBm9eYxCrVLdM8H$E3gmxY<ihW6wTA0^%oCHkSGZ@>tbDg?<rKJ40*zbhbuyC z=}&-14HRYt&3<fgyr8aWhPRh9vF6(q`|z4?8g|^pX#=+7WRDHyty%xTs&{6ZZsN>o z9UrFKOe4?#WNK8D;hID`KK42aQ0oeFu{6YHn6UC}NE20RT$^>GF5+`GSGYf_EYW)w zFx=ggmee_`0xFhuXLbKP$QLrp{h?4*<kxbt_7o%W=k9ISX<1W%onO(2JhK<e;e%Nf za+IZ^N_tIAIYZ(T0R;ys(xX)IS&Oe$KU;HRH-*dUTwnP$UFE_jAd9oJX3Jo^v`^?( zwA4sqwFrT2%$B8T_hG)da^*{94a7j@<Bo<0_GTx(xJi-R(iXmw0pE~_*Zdw^d`NSS z0C!0KW3++)2j?p3>0ac!hLMyDvpop7s2Sc`_FFRtchCCt?jVlmNw`y=m(~GN@}aTC zQS8ckY?K~4e3UkJHjwDT<Y_*Yz*K1`ZXP6ejaSM3p`So6nYIr=V^S}EVRga(F0yn8 z296Za)gvM!$qb_ZsTo$u{Uaot2;)MI?pj07`(ES3GVzuJe@OrYYdvBD?$6iQK*8KK z<<*{lCcpE&I5x|BJmVRbRnedHk}r<U4#wHNyCRSNdem_?0^^%Ig7Ggo5^@rgyUUiz zY&CGnBp}}75LoSLmv#=f^UnUGOe1}tKLx0c@wDZ;S-L|bVhT=3wkGA6%_KbYxZNz+ zigQ)=71IWXH&kefIMiOVNhMe$w^XQ;e%jZr+47x-IXh)LZ7g2pOg9R6`H$4QpIoC* z>x(H7sr}m@UzYciWU7C(Wwx-@-QRPbJQ5RTvR!Nrc2et}z|!V_BuN%xx8d&&R=)Q6 zT*>Ip%X&u6YGQJ_%8%q<becJ>eKNQ}Q}yWko$2$bJfGZRP0Mv?^5o-<(%9FhldV5e zpWhaQJU<{3F;T?@rW&$;v7JmQa&=TOD3=6%H_~daC$GY>>kdWPTVjfWLZ5Ix@Pm*2 zrnL?U*O`%;eDLCy4J29Ie>hBwOANZ`WHjBn3`Ucq$sOt@bo1EQq)815MA)!+M8`PF za9-lukm%Z{rYTpS2KCLfZd?$&P1R!w;W0LA*t_t7tT})3dM?i|3_rfXe!&ks5Hf;v zp3iBtKUews7W|x6d}c5oPd)@ZCPFw{A1n1~zi$DKNjRjuBkK>tVSTkGxA)C7`6k+I zKUtJbV+rEyG+*X;22YqVWD^U_-ieg2z@GZID<*gy3fZmEudL24)6LVR?;`{%+<?Yp z_d!+YRrG3GS%Nn)847?{A|GhUveF2F?&JC=S0$S;Eg-YYz$4xhrHM{~CvR>}26hqW zsQCtqJ|Ax<8@*Nu58H_WaMvyQ-jSezl|~UeT|_e+*J$E=f|^`-`eA$$;azyEvoBhp zEFsd+jKO(P!q(ythd!Sf72?Mm4m?F(<l@x6ih)MD&ZnAU9%dzs-H>wf7*%Wy?)el8 z+5jzJPH7_^C7N!>T-oD;el&ChVfP&<j!jFQBPwuo+{dwD-^{fzhA8-jJt*${a^z}h z^<)avft#Wa;jLIXl&Ah-;q+2}G9t;sHhf)LYGwc%%XIeK$>V~Uf*eX{rUA%87wB_z zF>)G}GacrSgcW?bC(kWi4J+<U0zsYq87{Ph^H7A?`wj3#^xWb`-ui3|k{qx*YyW*< z573A+1|Y!*EUd#mqx2^9-s+T#|8%)#I>r820HezHq3esIwJBEc)3_>0)j13i6}Dsb zc)r;pyRJW#PmHSRoI0k{*9eU(({aSERx+1fSAaeNB-_dMmKZ@`wT3yjVX9osA6Xhx zq((AE<kQ#9s0?Ujesudg{sOOca-PoVT}8dvPmI^!+x)->U<7iWB~z&o7O$g1RW;-a z9J|f&W(2s_GynG31p-+o#5GAvSU9w!BFuffIp!tX%tp)3&S5o*wP!(bxbDT>m&LGq zc1~(tIi)|M7Xt%iq&5#;47<>V9D=*RBR^gUi^(~Y_1@aul^QXP<n86QrFk`wD2lI- ze$p7kKJLsYLHQJFwBJx@7ueSX+nuw{DA_Y@j<49CVB;yl6L;jeare!TT9JrVi~8{F zMbJ4}=sJbnrfJOW6c2xQVc@#k8phd!*trBYdF$Uz8_eEi#k%LVnuHS;Ab-xc9yYM; zvD&Rqg%sC?b!6cQ27(|vm$$ep=aAxJT@JwJcTadT%M`n+>ZY=jV_O_0L+1`OExdot zfK}YQ1<j&{UKz!|Y6D*egN6w9kmg3;+^+VR)3#?ofUAEza{ZX=qFALUjv5g{Go8^Y z*K-)o(4oUQwIOD`-tdgsb5tS0xk_>!l*~S-LMd4)7!`PNvkHc^#@`=P+Dwd$f{avR z#vqk?(S?EJbPcAf5b#(d9j8Xa=OR39+&!Aya6I`vUZs>+#Ar1IHePjw8Y#|OsFv`l z*3~7entlHxB(B-@F!LkmSAWIz1;IH0;eL(^tXN;)AeabtXYN#qgOz}-!Ta#7aa{iG zQjF|$csSpKWHrVZ3!DwSU_#As=hZ}eY#9I2CwO7}JU^mxU7Ga|9_-5BzeO&YPq@)d zdokr=Vd<0?DcJZU<j!spmpk)D?*)kp`<iXF2wbEIn{5kg6GE42*-X5!7PlL}31bFy ziCiiP&rLSR`m9dBL5!R4;m?edf(LF^v(Anr1zs4v?N@Dx;=$RFsc#-%v944e<i&El zb(fo*nRtK7<psIdZc+K8AeJs}?txc!u5^<y73Reanuf0-36yj;9Ae>1U(v~W;#^g2 zg;UoPdVYii^Od}y_|taSCdk8M+!Yi|OUx8CKh#lS*o+g!$nE!i!c`tv+FO*62Hd|h z%di^vso5f4S$=(dy45z`B0s6kRn#ZxP2BVcnA+zUHUS*O1FU?>!C=?)ftZl~#2({l zzj!Ors6CM~rs%3vYu?OAeH;6{H1o%%t$r_7hBf?vm5osYTj2Ie{^%%qq6p(~XP+qK zHn6k(3f{tIofj+dQhz&MDWMM<nXQ|CCY7fCGBqeyKbkV4lw)0wUox3crDC&v@e-gG zR=pT-h-s}Bp4nb~J0hS66ONtj+7eid5;fDg%g*%Tadb>+sr?odYWePU{-ejhgb~gX zHuN%T)2hKxwP|Je3B9&0P3E?2W~ARJ#~Iqk-FEKlFPE0ZYmt<ST>nGV`m=Y2Poe3I zmU_A&G;DVyKb~9OpopO`POIDmuC1jM$I!m@g?Q~5%Y8T3G%x^2uN1-l3=(4H6K0bS zc`U%UMQ~<}wE?}c$GZ9zE}KNCY3PdeI4JAX`?%lbA5vy|Yo#$g!X_$O5+xGH+Qb=N znx4gXM@R~?LgLOVaL6#-B!u0VmOlMMB&bXen@CpYLd;GAm&@iC0aoo*bKWj}cq=Q< z*cbchTh<~U?hHLXF1lwYR+HN@PvJ}NFk#NIUtC${Q<`0A^g{DU@S0)Tmv3;YZpiw0 zWxN~yE>ehL0gJFrKOIQI!mrTGnq{=o8It}$ex(WFd>=D(_h{*o;R<3JH~ai==d8mm zf8xtST*31h`8gFf=n_Y;`u7xucK<YRf%D7$v+Z2DA$)wLh3)=}cf$0qA==`Pl+-51 zA6<nj)*4wd{5KB%?+%pwzqtwhAI?($`||bg#Q$ix8v17JqW~o>&D+fiBHq05t#>Te zpNk!pg*UbvQ1?#LP+I2!Yy}Yvr6x+61bfe%Ku|5LRLbSK0@if7Cj8Jj6aWy>p_HYI zJ7f~D-+F18%II-zQgFX}HV85mxK%Xmd~!KiWpy2C`e_SoU>Pr%+N;ou_nOEi?^;XT zb@nHGWh9|!Ypjh|q95@t?LsS#)_7lZ(~R@@bnlr8=gEXs$<KT8LWz34vn>sjauLNE z0m|rRHj!Hb=RBrg+~abb?~5okIcQ$R%czN09LQf|JGS|_-TMt{d&-46@a41sV3qW_ zG<Dzw7W^Ci@M|>X_X3Kq$|a*LgM}fF>>iGM#A~*9zd+G0PxzY(?)<10Z}L!ee65_` z@Fs41s>+7u^`ue<!Tx7w7ULYlxQ*MSj{&(u=HFYi8MM!>FHcz2qj6SbHzViS`=hU) z>#;2bo8#}y;2u9;?A>L#CZ#%f?2j*wa0Wx6yvLirR~J5Ah_Cc6VI;<RVt`d22HK6Z zH%F>`dxe)Z*`iNvtAz>8viu$V=aI|m)uMYRZ3_uFJnLZ+v97*ofHa;kSPedHICJCc zXzoS@iF<GCq(05ppX0lTdP^W61-%6~5u4@RU@Jw}d|%hq@wnt|dFXgH_5Rc*VQNQ$ zbV5CY;=04j8Msr8mK{r$9?Ad38$cYGI#O_3{CpS{S-X9ED*3A&$v!U;_<ipXw$h%} zY_~SxxE$PYh6c0iY0*T|EJRW3h)X$&ClZ!GGd5!hBZyL!i}73}f-r+SXi3>^<;&Ke z92%604C^x2Q=79;#ra<{ZI{$b9SYm6_}E=1b!o*)yG<0?<hT6??`zGFYyLt{C|{~T z@4OAVr4QaCG+X^e1Ue$Y9JO6Go(+nx_KKt;emRL<lt?Q3HJ9gnQ1WJ)VfVZ7s=geF zS#)t6`P#Nb`gB9o>P4(87zMAZs>%to`d~QU=pQt}Bb-WA)6Jot*Z<><-xL`Wk+@HK zDaBolk5Y4J^0=vgO32f)Gq2BWXL<eqWA^$l#&C77KRMj<u>C0Bx@q@RiU7-T)T#Z= z%k!vu7x#8fs!f9Wg-xk_yNCSbFFV?K1Cl%Z8HmTJbh?jV`AQsU#X2jMaRzvWD=BOk zA*G*tLf|5wPCnI!k*cZ-JAv=U4Oc}@0!7}V`q#V6OAQ6|8r+^Ub~SKGE~mabTjM_& zlfO^By?!Ek3pW3yyX(K3>=e^KFPs_qxJX57Qijg{tjH&<eF#N5f8^=-F)FwD`LBKb z<xCF2K$cb#du+xzH(hS}K3mhia`vz}vDEj@6uzgF?=^&mls>B}Kblfri&eh#Y=40Z z;c_lDMbf?38+`b^*J%@N=GpAC`Qp`LRZVrGR?yIhNsKKqd;I{r*waoDiP1&!6{&RL z-*=bW3*_Ewzp?X=nm4PqMdaWNAs$DZ?(|nnfxqe;hsVm~e_BoB3r&zo^FdM+d~6{7 zGruEJ)3|uP2U>`VI6yi28k&6!rsK9Vmf^kV`okiuhZNr1_rE7*oiRPr-gXs$LxEb< zsI|oVo!tSd&rg~e1bSmGK4%ca@X*=9@HUZ`OY;juO-oVU*`GCW;ufuj@l9ml44FIZ z($s%Llpo;&w)ZtTa+JGqbm-=UWVo_kMRr=^)jLr#y7}C0Jt;Kn-m86_Ygt-2D5nNF zpO<IXg?uCykp!Zc4&tm(W*R6DVv||Mf?1=()O_|-qJ_mC=N-;^K_%5G^F4dIj>MPq zosKrexJ{;oM8Wb0jpb$dm(@O2yE|p&6Y%FTHvVrg1!4OBaJ2FvoMY7(xd>r)QWk%= z@4e|6D19E!JOzxO5lhd|S4#icpZ!k9(wikyFeWmUS()3OVGaK#zBUfEGOMEx8TWo! zO6g^8NUl$mV#5t9iwR>c>WjPbWiWDTYz_8$XUF<eTkrZ|K~|Q;1jlwH+R;--a7rQa z29?40M?%5xW<)I^zf=Qtas1QCUujUz@DfI~7KO7_<L^b=ls)B}yixCZc^g+ILPc<c z7Sd0B?Loe4nv}&KY{(Exi<{;423BPdLa*{1mIAWKFPG3W#rr=@0t9B;n-m$!%T<Ir zo3`N__iub%gztXvSv9>JqWmt?rpF#`pM--zMJ63C#Ml~N03X;J&mwUGzA2TndU)JL zMj#ffE^!5e?)Um!{8pC?UJXwnzrQN&42|jNIA~ECVsv<Uj_8xYmE~OwJAQ#pY@SN> z;6gj?T-l!0ctU50R#)eN$yNR%Y{?1EKd<jwnXosVGh`~^`Wrrp+4{#3e};yzbPjy# znz;d@Ndb;m_5;Pev3Kzi1$2NCTv$cw#LRmw#qvBYbmQ`lYCLGmblwv`R;<Z<ify;D z`{f$>CGL`<ykxg~0zj70)8Atzhv0tgw@Y0$KLs(r<nOc`J(YO-=P+iB;^f}gCb+6i zIkm*DM3bB%ro1G?vc_dtMug!;2DK<dD`z-4Ta0bMF)y0E;z6G=Tj0yO1MyLysmNVM zlJ<!L_85pI#1V+vsT4`g_|`n3vjjkZtj>r-)|CbyW4M?A;<?~@5Y=wqnY!(%Al4f} z%k?OwBQn+9`A;cv^*{eTee2|lyC0mbbS-YdpI@;wPz)BCP|=0w@>7P1ncZL|zBj+3 z))%1#_IiO+d>*+AEw^v<Q0N?|C<4sH&rJiw9iky-8?e|HuPuZc7H6tn?j&dAvp!QM zE7=Ygql_nA=mcC~LOiFF1{eXA%!&4FWvR(2+88^K$~Q0Ez;^rIJObBZ`YPxXrSzTp zKmBNnEJ1S%Z(LGo3sk)s=!%3o*Z;6$fqyog5dSw3briY%Jw!;Qw^_?kE^!qBI6)1s zOhE^XCU@m#4fSw_3$S;H0}r#}DLwrSG0&%g$s2mK_N$bWS?DaUx1JEghZ7bhZTg#- z@HJ!K*%^6i+k24W?JN2!-;w9mw4dCq^<*7d;_l57T!731L>izBlE2bpg(kk^dTKh} z7u0ilG|c-OI>B~ti2!D{t1e;z&nDWLt#NNpk87%vy+?zqr7MlX8@rgeMy8V$4xDWj ztS3#|DK2)|NX!o2k9v_R=MnwZhi9(=E~ScQo7t$hm>9heQNs1zHdD;2MB>Q0Tu;>c z>&6IAFg(cjA_8nBJGz|Yi!<1iKtdci9fxIhpXM*L6yu5~pW*{d;Ie@BXydQEno=M4 ztLPQg<l;=Lr8GcxOJ8JmIH2jZr+JB9O)P)$W)Vn}dbpN?6!E|TTFjZKQcUm0_wjKB zgPqCD4)%jGHQPRXW)*BJ^1y+H;}oJ=ewCZaZx!uUBBj4&206d8ij1o<qv!r$;o^av z0~p1Xo{XVE9~TP~RQ1ASN1n*w`FPwr3@>6b;S)!W{qU-uSekRcLbVh<4vW6VHzDo9 zi@U_7e8YAuifZ1UgC3ExXkiTg)E-_8()k23vV|9DFx%b3t$VWk$zlFJaYf~M*!)*` z08PiyZ`j<C5nIiHLA?Td=Q~uk&sA!KDA`D^34oj@e_%U#!^+}<E|XW#Vi|V1>W8@0 z#V-4kmQ;D1Q|23r;!^7C`O&~~R{X)kg9D!C8MGArmaRZm!LtX95)R`<i-4$N^$YJz zusVyFj&~sQ;{(2*#>@4m<s6>P%X{2>G@2%1=ZYtw;{2OjMy2dD<XBVg_A*Zx%js6y ztfmlwyc~t2x?r8Qg}8F1m>ps?jmhHcK>cz0Qs~K&=a`m85~NQ{cY3|?+=dcLFO=aU z-@6IPmiRrwrhIWhVE?D)cJ(h_cB<aavN48rTysY_uHTPIX1g-8h2x|(80JplmIO@r zsYUB)@ue4EPV;y5*@(Xuc|N-T(ChLZI+IY6?uy=gs<#27NK+L{WX1VbUHg%p`>eYp zZb9pgDVXOX7;Qu0Kyo{ybeT3K^>=;qW3KM(a?dkI160-aVl%_4{uB-F+Odo{l{RH& z6vqvpzfqx{n6P-=@4EdiyEHT6)%)l?f;{{qv&d%8;h%TrD2dUZxLfX4w&eC=oq$b+ zR4f&}H=fzAMp^4)=n4Sc@cTG6Pk%f4H(NZ?cqu3wjt3?Vc8Pl0C3R$aIn#2q`fnKY zKQ-$AIw!6CztT0D8#wh}1Jl1F)#=pgEN1QBgVoi)A7lP6)kFNpSNg9%`FHYvHXcE& z)f|dPA(Q88p$bj#Eu+P7Aw`$^TOq6kg!M~w4U>5=@)WUh8H&|1(&bh>z4<f$&QoJ% zmwkC24lmB0oRZ~)Nqh}|_o2wcX5tbXyeiWk9^8$BIV6roQ@gFIs1BsMvEC_$TXAfw zTwk0eU7~hG(vJ9kerdS(*7v)tzpWL(7ESyDS*39&>Fx3TCZfIb1|-H2%XAQ%AM7UR z8va#RC0E!}vu?b|I5YiQ71Y2<qZyZyI<(1fF2KBK)rmi*fanWQoir9&0v)6wV9jzV zT>QonsTI~~eU=RVtdSEXkf9P9HJS#2_P?`DT`-T#d)q}ibdC{UTp)hdlFDJK?5D+K zBVKlTeNNHdT-((8e1;|BR0#d%_O@n*cIVNnMy7AX)-}%@oht%wRu@oNkjoS8gfG+V z@71Gv#}5r@0*TO^2HH6<qH=+?u5H{>#ph%aIP0s(ds2mXd&+<EDic2bv#?J%Y}ue} zU5Vz9?G@Q8xDs;ka=~*3Q*0f-R_$RkI}J=+q4`|$-E8_r3Of@Yb_or;2+XSM<c9pF z1J^P5s0sfn?`mj})W)&8!mh-km=3y!Nwu`CtT~nN`BEl0m>0SNraf`dMmHSjg@gLt z9I}jU;Qaf_T(|QWN;Up&dzr~0OMas!w&yRN^f~xaUxAJj^!fT`P((<;|DFXVmZU=J zk5@9lVg5m2lX=WSxudwBVSK71GScXqlddL#(j$-t&V9a~)5g0zVFRIijFp*KEzlwH zRL2D0QX&MsuXY^yiggw-O1&!d;A2P0&Gj!-7JM4IuAW9jFCM<B{mHr_boZlP6Q`BF z4vhl-1!)wz)R~mX@^z{yp{}g-IV4PFazsbcW9$QlP3=ZOYs0oeFmiJ0bLD>B)Oi4) z`N!zsL@g(ctEU4&4W9k)9l;v$m7XQk+&rhZhotkz3I4}ktV=qcZsdE`k)gV7Qh5xx zQ>?u?yeXS^(vd&7zn($LX76!BV@$+JXcbt~{AXL?{TkW;?`a`At>UbxcqB*jZlII@ zP!?VM)kKxohK2rguwq=#*1mO`Vnt&1w0|4fO+c)Cdpi5!4*h$LKmB%!$PL$N<%pO0 zYVW;3r8ysB4UDN&41TcPq{`wyU)A``x_5*#f?TPK_&SpjE=x`LD!P8=YbEts6KI9G z{Z`gNA)Cu;Xa`SzYi|E2GLLx|m{I`wizisj9{^dQ#%znUqDMCtZPLDEI}Tab3CawF zvBmJIYuK7DvqfG*2TWkteR8Mk&wlgXV_*@J(1~6DMVS6ZV@AG0(|39w@I$%dpocPa zbpz!ma{K&Eb+EO$gsX;Y*1#iODso4Mn<(s$0Qruz%jj(t*`n0UCcn;eM16FBamA8P z(xEsw4kdNMG`FAQyCU`fC;hMqi-Y^%EB|6eyoGpi!jhNdze>{2G?6LK0Qy8lG$7y< zl4b;j3Jq<ZA<7<lF<`uKufZyeKk9RO437pY?@g*TE2i#=#I-h#$p89GLY=e1qXh7g z(esDmxHhO7Z~}B*^!50R8a3Z)+*un%9u&Ugre$E#gg-SRNu)j3-rVO{V&xB#zh?F| zW7-Dc>%^igHLz$$W=wq%5Tbl^z%61giu-xjTob^j1ZDe-RMaIpMf@!7{#n{{^O_LU zDDL~_E!TDQ?FwRWt#D!WCE6`DGD|r!vC3`QYEL$A0=n^&;hQ?qZw^?^y~SU*<Bhc# z5q>99nzat`AaE^w@Z#YiE;Cp4@#{_kjNGn~|0q!{Q@5i{tP54WvY!CC;<mx<M)bdt z*U2}6a;455FFw|k6LI?Z)qw&x#vCGJz7Kyu(jyG)AhDT=%KPcxXnUe!CBiG(r!PtR z8e%^+rIWw-`cd&m1=p0oPqSsjrTz}B>EVYDsl&=|Dw5$q$botc00|f?dGQqJe68{q zFP`gEY4w0+M&I{!s}`2R;Ea_~(7Da$FWwJW_T7w9!nX==#~BrU12RvV+p<EkunR^) zKqrbT>WKXCZmmMm$N0ESJ=W8Sp0f%+=!XKvF?2=1p$G@7W0JG_&n2N{#n)yTKw?l& zE>Sr!LY)VwkyK>ebV{;U+6;h<YjCgOu6i|*+oXd*$G9uaViS^qc1fO(CW0?BP#vft zEx|ZkV)nZ?x8F$w>n-^Yl)L?9_RdxxcFguAHqcYbiVnWM57^;7Oxaq@-?iRd49~tP zP{2Dy$`yAMr@Ro|f7HLbaW3EV*|M2=BnlTOOY-$L^%{cRQpZ^<+Wf`qj_Tmm>j7ep z|6rFFd2g(eC^Hw|#!^~~cw2@VI(W($C3;kq-TFuKV9)B_|ERpSqO^^NF>Yxc2PIeK zJ<pLt?_Mgn_k}`5?Uy844oBrRxxx+fMMm2!;xYhYo}BmHY9>dwQ%M%$X?iva9aQRq z_XI>!s|`8|zsX~R=aUvz$IPx>oeVf;+1q`&ECmzq9$Vj_1+++fc(-z^=*}j%efpYk zN06`@rsd0|xok@K{9(S~$56$wA9uc!c+aE^Xgo(^=2pG!sVo`|9cCXLA0*yo1C0(z zrEr}go_1_ZmGe!>%YW0zOfXz?Q#-CK1mHH6XntSk>jE4&q<s~&CwE#`<$57RNV9jQ zn%4D;VNpf(GFdi<nCb>LJ6Acj+9BVXA2q*e5yM0O6s`UGw#?W+N5N?966C-8CA?6; z_v_Q@UqxQawy5r(9j4wFr~BO3#_f2voX4VuaZo^4DaYx%fHHi-o#a%i-IH_FoAm(Q zTa~3_MXlg#zVQ@?i7~p>y2c{$N_;M8BK#SC$bpl5pw<<<hP|rQI8BuF`6UT7k8Vio zI+6sS?&W>))M?cd<R`3!MT$Q4`%1JPy0RV*c*jM0Pvc7Ic|nNJrgBqpskGkRW?Mig zmsz-16l_~LvUxKm9ivt*Rn#6nheXW24P;5WJCCu=P(BYgqo1RfwM=gx=A;qeZ+I7D zAH%YX8!AAi-rBvdsNG<~SjUaxgzNo*-QS$(d4NZGbds^J@6Q0b$an{5ef)jJ$%jl0 z10T!=IB;ffj!-*q5T96%H2H~9@F!<GA7hafavmndrzQ-MvdW4X`Q?fq9b_BCYwyTl zb7zgtU>wW&5*<jpeJ|UT`%+f)d7gjqp1ZS02XLKk-(<SHR(U8PV;FqO%+&N?{9ANN z>G0;sp|^Hb1*E`R+r^X}nkbw=ajeut6z8{Uvon#aJQFKx(M-sec+V)QY!Lb*tnSUF zW8>BgY142n#YgE|#5jS0^EiumW1>9msn^^iNl!`uSo6mc4QCdK-wdZOM%;iznZRtX zu)I(NO^TMDp8WhVRZdWCb8@SNZT7e5Lwp8a23$u3YO$yVNO9P8|IN6pG6UY}P}g4G zF)h`-wpbCVUIbQace4FxaLczVsHz0aM@T6nPw{R@bJS}~!&hN&C6~908Wk!ETP<VR zA(U5_VU1vt55qr&j1}ymP<5~;%VSiDQW37#y3MBp`b_VpqROUbt81iGO}9vS(e%FL zZ!cS_bMDU4m$iSDBy~TGfz7(AW|ed!Iqkg9GDz+l*p9rL;Nm7#6H0F-)ek<vKcwb! zVt@~#QRj`dBVTRZlJ&P93EaCARgoNY|I;?mG%R2gZBO2seOZn6Z&<CUJZU!Jj7np- zh|T7=;k4a0K4G*-*t{QUpzJ1FA~I|K3ZI%VZme2vL*$Hd4)BxS=}>%TVE!$n%B-ZU zq$a~-$$;BQfjfPvX4FiEhsTQI6G!eFTR-P=BR!_gRm=8~74L~Nw?!wEl4GTYP)&HM zqKb>sACkv0AC#dLcOUZ8ANgJ?@0hg|uMeBGeH}5Gb!h({@<@K2Qqn4UOw?yKMscp0 zk#GG3_vG4lO9bjS@Ne7a)&vE0u#(xt(5Q>*&#GavI6B4|-CSlVV{J^LpWv4b;@{O& zcxDNoy3642zvb}7HgDN}Y>Yut@pk!Ba!nVF4B-=!%&9U^+wq2&mYorS3;vB+6mIN6 z#XBUw+9T6T+5R#k#M<)6|J_^;7ZzSSQql9-T6@XaIz0Q!-3i-U1Y)}fV!pWHqvm!) zL^kEQ3KR1u^M3%+`~8NBdd=6$S=2qqDXow<f7r`ET`E;VtYX+ep|fJjg9vjvfXduO zo+0w33Bh^PQV4SA<n(zv{M3wy1c`UV)O!KiaRZXG&la`|WG@rruFO@ak#r$kw^!#z z?9<h5bnhll0|&7>h;OL^m@-gTpaypN8#QVxT`xXRz5Enj_TEw;AH(zK7T1d2dcNof zFGd|wTxZ4|?-sK)!zfthv$Z1o2PM170kBg<ycKi&u=Gv546dE_@SU$;FZ(^Zz_07H zF@v`;lKhH*sMW=wMaKwnp^(UuE+Q$LUqHxM6;(*Ll0y-$z)^0NTpVz+@XWblOUC)) zdUI8{`whX^m+*L29ogb%pn@fWvB|T;no<4`rB49G_ZctQtI~#3Hc$75la#}WpZFvI zVR0rA`OcHv&eyg4&9Do6<rhoWU$(gvDHFP^q=N97jbq;3CBy^XtYBEL>J>?-w}HoG z=cheiiLUxaI7nQV3gQM63~$S3nL9DOfAQF0w_1zdi}rgymY-hY^+Dxm@x@<L-?(2l zV8*!}4LrHqap2@`u{Id>#QPg#sNg^(Rr>jaEzY5J&r(xn))m8&9EV!JFS#*RXU`eI zasLSqd=zS)jq%z{7^t5>esR;t_2=F02k-0+>LR5+Y;2KSGw`9gW?%0sl+A&3?}Gk( zl-LQj-da0p)ts8*OezdX5KORN*nb~(izkfwQ`Z<F9eP0s7o$73GgsQsJ(+sCrI8IA z($|x@&Xr2jU&+6Ztkk9!W?0=D+raMjg^odEn0s~kujzi~gP`I;D_nTw7pRa`x9El^ zqn&483jl*mOgXPpMW`K^R)*KSwFa0>K@agynM`EUUc@SLvHMee;8s!9A^d>P@FY>J zn&{YZtih8&?{@@KqaA~en7nu4uoZ_*fsQ-|x8C$+BcYVuS|WEaK3p_uB5rniL<@{} z4c_Z-dUG+LnB~lMc&b$6xKz<>GWIM_e?eJ}@3c^()%EmFM{sc_vL{&gcSg3>ht9&+ zm?eR>#9S(&AS!Qmb5Lzw5|X$z!2DKs|1Li&Zpbug!8*lIArE?BOmR(p|333(jgDtS z?e`k?JEog^hf=G}h@{5Ol~+4~O&_WM;^7IReX^QAAC6y!?Kl^wrD32;e#A<6qYs9Q z{mBxTt9A|bMhYcA^F5HUvl0Gs@Jj%cZS&fkTYG%2@_SinZb|{@8B<a7WPn8C!%-~g zFWyLS`S<fH)4o*+cyiQW2TyRFo9kww$8-TwKs0&a_n$9xs>x2G@Fw?k3FI7iemt@< zae3(dON@}XS2HD`HpeOscQE<-Vui~qlW2$3=t0Z9A?sv5Q<~#cb!D{s_V#ZbFhX)* zY2yvwqP>-4<cl<I9oqLOy=OXd#B|>a$Q5I4*vsh3J#_+I&F@y)eck$c%duNqr%jm- zh{OOw5G9;X>^xT)(^A!Dn<t1V^Oaw|_OMlrC`h1efVy+B242AUUTq(N1e}z{=aG#l zP8mFe$V09`Lr%QEcmgGQabv>%<=6vL{RL~2#bu^XY7_4~twQnFIMii0Z*oc>yzCCm z%f5KZcp^n<lmHoV_Fr#yA0>Hr6x210<|WXJkLZtF*n2$<%q@i^;%_GA?59#|%Js{z zt~9}!1At%0hLBT>i|uHmuPAFT<8k5bpFo4??gE_Pzm?7)=)|Y*r71jVldoV!pdq<| zz)X5BK}1>$L+xYfR(XK6^+Q7g2Z$eSaY#6u@28JwXV%l$<%apB?unPbVZZcr9v<zM z)>kQn;lobo-4eM2o<5KXDG0Z*d1;|PV;)H*%D7oK_2?CW+E<iGuD40$q5e`<>i=r* zx`Ue9*KGs^#DYjuL^?=S1OcfL1tApa9f@=SkuE|A!2(#2(3L|8D7_d#LWfWVMT|6q zbc6!}fk+?((s*0X`{T~sci-HZbLYPMX3oER_U!Dk_AmQeYkli;za6Ovo3UhKp<W|G z(YPz>p<*Ll&2(@e+86|)9zs+R9|hkw2j>A|o@*;n;FqNLcLdSjQCa_IRMkH?lf98Y z#bA9be$<zygt;F#Vs<}M)Max^crwW#(ddM|(=ttdG3L6neiFuV9OI4{dh&DHAmuyL z>4b3ubAL26>h6+U_Q7RV10Gcs<6T0LmviNB#;UK<WkY~EG)8ADa7=}T=Fz|1(dE($ zll9tfmniGR({9y<J=^A~3%ISUMW*dh^^I!_T5fG$F1)VW7cl=+G^oP%?M@6_Sug9| zKw8DSE~^^6Ra2FjCWv_G0z~v^>Bsr)1@`2Nx;T0|)x&MhO~LqDvd5i;<^82F`Ez)P zI_qRl<BQiudE-^fg?{KU-qfg$s)0aP1Eovfcz%pT+iZbS!RLqTv$!~Ia~KjM1v}Iq z=Cd%a8V#3RDZ|t!k#l#?I=2_{BUUC;0+*S(9T99B((D}#cI-PRShuNLb?@*FmXdDJ z(P!YUYmJk63g>R?Ej<X*%?S+*k|qR9S&B^9c)zvi5?Q$ivFMzga&%tlqB7xYMQs@^ z=C9@%$G|CoYw_$u5C0IYv!Zj0fG_ckWy}Xw2`#Ik<A_>!tHnLgb^WsW@vCT!)J#KF z9=w@6Ygnn|1!J?Wx4SK#d`l=Vo1@H=j^*Yw0&>aPyuReir^$QH85+g%sN_}kr1scP z0!*h<fSTny{}dMQFCXw)JxjQoS@@W6fLPLJB1=V}*G6mJ4<9DP=!Z8DW+S}z--w)z z8D7GRoOHEvbYLi4cSf%I@KeSiP*;hTw1IFlh^+cT_8Y;eCrEPZWB~G9a9!h}F3=lB z1@ma#cJ-xk<7d$@qg(L&&+we6#g~(qr_4j&Te19dy0IvCVQI~AA?G6S<Vo+K1cGdd zVT!;r)4?MO$Kc_~&QzG1^Yyl2zf{uVvRG2FrjdRK|D46cOo`7x0sdVcp&(6jY)Vpk z1v&f;n=5ZjUfP%0Ey3Ew2DXUFhXbQ+N-%bs_v@Y<tXOd@JI;DCue{Ckr9r$U+++Qr zwCxtjZVsW_zea&)2e>aXOCWqBwYVkHvg93siNySXOl6mq&Yde39^bjLbT4PECN&gT zTztmPLcsvhTbl$REHX{x6gBlA>GZ1^xIZRlJky7!`D`!&-S=GmI`{krIU|Fp#oc)~ zopyMg`8&yO1gEyzU@D@=DRFrRecUGbNOV#(H78PJy_ty9aT&i-3-f7wC%xl(SNa(T z89%2y{0XxP46ckl@iMcRz`LXE16+^dLMk|)Qf9dMc-L|WC99QPrOY_hBwuegHr5Jv zuCIyot5o$Ce!kNabLZAmPOj$Kj9OC130(t=dNukfw}d8oe|@OE%9Y5v=G?~0%!~a` z)Nj>5>|7;|7z^RGIaYzV<EdRBOc5GEyxoYsNfGWGp9r2*=WTfG{=(SQ|9I~t$K^`n zydk-L>}7uA8BX1Pps{g4dU@aum_f#9fT}HnX`~{c8x%nknN+PX(C9w2<9H{!qW`t) z##=TFx`$Eyh5A2Oss6#r^*5W_j5`8hn}NTWGOvLhZA7KcPY+czd1ZbSM+S1c)-Id= z-tLhE*s->{tEz$#5n1iPjLj88R($IYq*wh6BcVyLKYysab~mXOqHzXLmBDgpHy(Sm zZ^?k~y%(Z+He;>`313*r7qXd0!xlI}SJOf}z?OmJ$CP}D&2#joNJpz8t3sr1D-rL? zmZ7HJ7RS!Gt=qO8c7Wh>WH-DhMA;I;1OkAg(ot4a#7jpr9Ddo2=nk;~Ne0#wqjXQ= zoV?lZ-*W|@dxLm@hK0Wzu=kjFZ{P@pNP0OPl=WLG&Q!DRfXwE}ii^hBqhcZ@Q;hk= zj&hXA8IFyL^cUv=Q~v7BPafs$(=ysDWk;4gccA-C_N8Pl<%%*8c;PP%>+@?0=dX6% zfqJ^<DLbk65?gWl9`1TgoVQ*&qXv2yD*`t`+!)ZT42Q^#9!*hH>hAI1i~REp#tRkh zVp=4bp6~p>NI>?fD|Lgv=!3VE^WaZm(8Zbl)7jd5ry>?gBIbRHlg)%*_e}A<$zm6U z9EiDAbJCv92^bw85hCA4zp+hHeCH<k1XOj2z0Y3c`NU_Bux;nBxyvUb&iShuhW5l2 z@~M(_Pk>Rv4U8PQYma6H_MaMl2WhBo6ohOCs|9YhG2Llue8p)^zV%hg-&u6qzh*;2 zjLIudoSQV!JE9*eLN+JZ*3+6lHQ%&-0HnGdyOe65mQZyGy?Zq3rva}4H3iG^Nrm@L z9CG-YjvVw(j5ScQVc{T2B$z4(2wkY@Q5j56<#*&AwqID5I4;2kCEfIEo2=>;dk{>R zCWGlRCD<TUx8?^$P0erSGScnrfv}>1gzQYIkPeF>ofy?4Sn*@vuCbvBq6BWxMStl1 zzB-mT;{_(%qy8cy99(@W@rZy_VD{_J-D%Vz%nx>6=IBVWih*=&*t{*WZ^Wi@(A)df z!$X1&H>*jnnDCHM>w)vtR9|_5&pOwRMKAP@;WgrPQ#al5(+!no+|4DmT1SQWotGa2 zR%}06*nIA{P3g)pHBb4G^~Iptx(tEF!jQzg`A?JCi2gxzLc?To<%r;ZPs{it;k#ug zgyGyY1Y=BzB%(tq(JX1E2$W%56dtpDReW(n`D(*!5y{<@yfd#MSFGejBTADT3K~Uw z3&!dtm(3>tYTw+`W7m2Qr0iO}BcV6x48y3M-JZVK_6c>%#>)0qS<qnrlQcHW<(E#S zp(+x%MU~QLHe)jC^}<~-gRm6#`uex7W`XjODe3Mk)vWAu7xr_JgH|GGYm8wEu4Mo# zudW!F>@XehC|GMiu+RMY?N_U)nis2a0erB2GhN-Nn}zXCCBYV{nnrSbwl)t%sbRFH zO8d?3oqhJEqnK=D17KkYI;6%{Q8Cx&^?l^XWxfv{B-T?h-dp!M>PKj~UbN1TrysoQ zweAa?{@`W|e|3dQC4ZbiuA?ctXbGDswanTp2~>Om(7bNo56qfN)L=dS{DO*2On7BK z%Le%ps}7O5K@j=`kOr~3Qf@UoU3HpV4eDOvCU;30yU$wdipo7biUyS5L*>NyCAC6< zG&i$G#(Z6~R2>6X12;)1^X-(6^ngIL44>Z#y?3o}pn_JaK@6&Urj86zP&iYDepnTD ziB$5)mg}o&R9?eSN9xs4i!Gxj<eHU2rxpS_x#TQ@o}X|=bRsqFQd|b3Au}}?q$>P* zf-U>TJhyw+E0Li|WB{^NE18{qO>hr%_0m4DTm1!^{O9;#TD~b_J)%Vkl7o8bgtFEP zI$)lkk3JlpJ69Q?=V#062da$lWZM>}^IJ0RZsUN*%xN<=2;K4NK0HJ<FE7F58+rqG z=6rzfDMPYg<92*tt1wkk`ltiOp`}6$P50sr^XH-%1e||gN^2YB3^r@v*^an=IGk^T zOi*ZioIXvtP&>p$u5C(LleY&B?pUE19oVg{$=!*GxalP&u}XFdXa<3aF@sa^?;6!} z-gRS@8Q8*4GYh;lO*JH~SmTyvHabEo>k{2L#GW8BAFcT=IO}*JLMR8yxCTBMxGlDv z2Lweh3P~bAijw-h<r8<R@fHU>fW*4#<@D^B<d~V$rZdp0_uIfnq(@$s)^=;daqSG; zqMYw`7|fS3%$(_+0yjqV4jYtO==M<WAGmnWdgMy%YyY$cl7#(;6hEIuu)kc6KcC24 z=$csa9w<(l{v`KtZ*=5xB0%$ddH3CRdF;Fga+FFvX@<qKehow2MFl%noE~T@DM^5m zfVO#E408o{Gw$fpCs^(<U|WxuKv0U(nRokoulL4Q=@r%Un+=oppJ;$+J3N*xb;}3{ z%=Z@==&iN0XYvMbiWf_}K(BAz7IatSeO6b~Xf)rr(5mQd(P0txLv^1&%{S&p-dtv7 zy%OK<RbF6HfhY1q?qLORGtc_c(T9`omLdaoQi^4v)#;hg{>uK?E1}jYA8bVk{8gDp zSZSBwjb=_rGSztu3Ut@S?MEn0)`na$$XzkzRX%3FV`wuwd$)a7!M)h8Zv+G$A;Di_ zDR@+1Io7GVB!9%5HmV1Hdz!Nf7-Jv0j=J?ICB%CcL}JW`8E%S_rIga;3XDoaz!<3j z|Heg%H!bd`piB_PA+*jdLb8>c=yB-I$GoyGN5{q;CJrHo2Iov<zu`rYZjtc!i_4Ek zn<o$GruGAoCt=E8W4<B68Bf^i>bD8z+~3Yhr-sCQapxke9175uOjQ`Nkj+#H1H+PK zLxaNg{qzGm=nrsjw$ZzQ8g$+?QMt>KRPmP2i*Yp*EpT;(3*W1#c2+|i{H;*Lw{U6N zCsjR>sDA&|qlR>mg)VyQq3u0TlloM10D>Ps9#9F{L|XK6#!Cw4$SRGks7P23^>Sy} zx>OkVWI%3kWg4<{GZhJnID``TH|O639;lO0t>qri>EF({EtRDJ^G;uUU-4~crN#r? zxLm>u4W!ZWP`*K@v?+z88KNn<B%eh3?(a(g*JR$>uR9v@;}I8C-6;*KovveYp&CG# zSpB?<909x_t|u`jkiGtCYBF)4fo5J!;YzbOqtZ%IyM#KUaIpMCznA}J;H|dZhBuy( zR~|T}s%(lraH6k%Zoai}_VQTPwVpnjK>=HSm+yW_?sn#Hhv_R^t{1`xSG1KHt%FR@ zPjWMkW+UC^M+@70N}m}2_88)3sbe~%<gRzN^YZUAbjZ?Im(A<fc&;xzWOd@P(Csvf z;wioP;n97C8$rXC^iN>1kvNMMx$;u#0scKu6zk+wPbm&kR6yzR{PXPDzL<O4Ulj|> z8w<$GyF!qwgAfoM+b&CIPB+4qE_4jE53#F>x^R8x+JW7cJpabse{{cz+0`qr0mm7o zVet}T6j80d4g5nDCu>vlM_qCN*F3x$XyEu?y(Ir>1^C~5#{O~M!0?9AKUI@2J~0DT zrN>LGXb5o!-4&mu)#Ur}xCqys%uK)dNxMeIe>a@y-CLv^Krh}Q-ApUfs)!6ogmYgp zzo!9FJ{2x*$G(QR@A(CNYCFkLrnJ75h;R>R&W=RhgxhJ=e(qT)?dGcGa((3*vJIVp z2F<5vWM|%5$}|wmzLxM_GS~mIS@Zy0Gq9>>o+RT@S)Al0y+}<#Z?!dKNzxI}JIETu zoE}uMjw~$qARLfqAjI!CUmhEMZ=KNpE##{mL&XL^Ixqj5NK2-gn5R)^g~PI1eWP$M z@!&e!)R7jJEt9}wCDFkn8{aN-{cfD6dfJVs35dUeHA3%0;qbtg$+a4j4hflMVSnlk z3v|J{Dq9|0AIWNBej@%!ant?0*%z{Ypv#5}ErSC})R+^n(|~Lj$ElxQB^A+K9w|S4 z*~RdyhG2BGemUy&Mdo3jbbjqj>Y-o)2}cz8ICLRt@PQah(@E=(8~J=d@okYlGo{{_ zW;!j>g#WDP;){7=Wr*ueeQK_#9ZhcvH%c@rO&SQS_z>QVFf((5!MuT0G48O9YTTKw zsW*<d^f9^58?KMjGr|)NA1h}n4z#sRr4bJ{tMCNh5QHjs2+^C9j7qq!oxX^jI@2fW zGhMX+&%Bc#(;>4e)7q-f!(^ErBG3HDhtJCyjo}_od6?UZXQ+H31`N6!<LMaYy+FO3 zq*L^qcnF&wd|1l$g_OtImd3MtB?auSO0i9_WZSu6B0qJO`+=8dJ~JGkLpMR~Wvxg+ z)yNM4<N6mA%49mjd<^Hp**5wWoL?zQM)33deQb*t&1NR+q?iz~4H4&xCqj)r3*tK- z;x~T)&KUkRpiB&2Q!PkbZy`5L>vOeta@+%NddhoxNasaIC2f_@?o-$po8EX|n5XE` z(IDIzP2B9+?Rsv0T4>5V?$K3`NAsG@_{bY}ip<-lIhejw8MQCRz^rM6n$2mEkh3t6 z7>ioc6EWri*4=O^K<bV-H}!f@0D5N9Tv7SmH&<e<LaaSnQBI~h)n}0ssVYK7yY;>? z584B%VNqW{K$g=M*sNJ0D(80jxsG5;y%I|?vw7M)w)$1fbBG&j!#NtyBU?=vxah{z z4T;Yj?y)kwMG`}`q`r&upXa@4>G1J88t!-DZ=X0OCl2U*j8!U`Q(o>6;*falzJLEI zEMQlxV5$<z0>SAb;?07NXJ&Fozh=MqSo6vtZ91~Ja8RSATJeaECvO?gL<WZdUFW41 zbA984eMnZqzy$rFa7pgd3Gi)=+mZm)uahT(PuQtcFr>LRVdt-hoEMvmFJK6EL#Yhi zw)SO`ZN~B1QB@Ul6}VSvrsIP60@4O2Hk(FhGI?5mz`HRSKl)-1^t>QP_H?d6jx1-C zc%0?-KIAypbq;a1Ie}$=UuSqAqKL=@veU~`?$;M4pM(s%Q|}!>F{~ZNnL`<9eh*+B zI`_83DI$dqVX4il1u;D>P@h^u4LAzyV_S{_#PD6vzIBo2Eu720CM$CL$TP;z${n7m zNT$OmOFySX{4%s)k81p3{ZlY!`-5a<leqWa1^@ebBxQ#1t7|IySKa?d8xy5;?9dq3 za_=u<HYMclJc9z5VOFPLGsnJQSy3zJ);mf$hPWnv{suYkq7cFa(CF&aDBngcnhfC4 zRI+&*H@@|E2+DqT(qR(O!$Gke*JDp`9+|#nK7snNv>i^2Y#DMk^53^rFNo;o#x<o| znQ&W0ZI=j1-q4Xpe#N)WMLOT>V9hfaz>TCvs@FGjvh*7DmC$W9HzXH}vA%mCuq&nO z!yae^XdWHFj&a4TvHZrq8&$<F$Vn!`Po7{z?txr^?l5Rn39iB>Pr4O0<AkJrLwsx7 z)}6)xk>13Lv|aET?$;%ve_b1P49dXL^@M-9fb{xi_c1qrn$CZAqU|(_PyUT*l7`*k zqM&xrSRXua$79&=Kn9~B4e{$Ju7Bgb1a^yBwE?<^;jkI!)#?BE|IdHo1pnmhY})is z=BSC%^3raIH&YIK$zd!shW#@fzLV-}om5fecP#}_1h6VWxA8)(l|R5h4%7@ap7kxJ z)zEdQR>G(RfChQdgRFqShajdIZ$7$=4w&OAERM3GVc$I61IrS2{B-FDSz<qjH=%$d zoh`tXhLV?7nb>CeH=?OVKPO2YZ-*cGNvk!RI)EBhC$#|(U$!z-F#I--tiz~p2xIiV zYf{>t!mqtH5~nO|1V-g+8#?E2txj(gET!!L{P`QotWlrN?Gij1wwawW(1S(qpkR`D zXQ}|+?A#6=h6nl+|A|}q$IJK&R{z$$+tv|fd!XCD-g-FjjzE9mw)N-Y@%LSfe{DwW z;s4Y(<-feK^sh(q{}85sGN<`35dFJCvHp{>^(XVX{sPfoAcD>O$y|cJK=c=g01$os EFEaw=ng9R* diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/srgb_image.jpg b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/srgb_image.jpg deleted file mode 100644 index 3526efa91f55a94f0fa8807609fdd84f759882a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1692 zcmex=<NpH&0WUXCHwH!~28I+MWcYuZp@o5!1q6U3D?2L?bF#6rvT<^8aBy;PaPe|+ z0|6H=4-XFr^YaS|^7D&}iHV6z!T}>28ygoVmoP7{u$Um9pcq_&SjPWD41yetY>aHo zjDieIf{e_9jQ@`?NHQ>j0RsZCFfuc7{J+J(!N>>_{Ed)dw)_g@uv>n<Uf00Dz;9#O z?q$NjAbGrl_w5Y^hT?}$N?#wJ@zs3;gIq<i+=_1?f%gj-kM~L6-XLR{xOX>$P^GNK z1)#qEQU)d4@)tJ0Y<|i7sLXBP=yq<`mQ>F>tPEoN)!4l4>3!9o)4Pp9Nmt42m|JcS z4VfyRwshQ<l-y=yzN<m<c<&Oo8z;_goK*IPL9SL%I$7E5un7aB;5H-U9H7_v)Z_M4 zZr`53z+iL!fmQhzU~oJ=4D<<*mOQ-<7$``<9vHkLkT66NWdMgPlK`^N0JH1yIFxy? zIaE=Ak%5t!6_jY9!3NA{3WkCP4grAzLJ5x0gv)5p_|N-W@y$%{^#{FFY_@WxY!|xJ zQpxk@mil|q2|;U@*see66Lb+MCCZhut?8n`#*Ry>OTL`t+kNKGj`rO=Haq6;?%Fmn zis6>yGR>Tjd#sBTvs#<XHeQ?TzpNtX&-!nd>TW%LIgeXmX>6mHin-(qpD$`v!nU0A zTFyJAFEqX|^M%jbWpfh33)|;7$-P(j&|vK*7vb-7{lzlxME*(+lcRb{TNqC#K23h0 zD|mVCvaLV<T?WR23fObZEX)Yc88Rpq3J5AVI0jBMXk3_>0QaB3e+H!mdAmi6^X_H1 zJt;k&SpI2Z-YzHm(43qfubxK#Se+tgY1?~>x$xx`-rJ(LzOVhh`vaHtwCt_#gNv7J z6VK$m-fh0&?`rGZ@AuZXo-^3^Xsy7H7cRTMh8;RC5%D?xM$LzPVwt>?yCVyKJDAH) zb$Z+Nw))cQ-8PJ|-COT{U9s^b%gvp!@1tBc+px+WPwU*;KYvo-ug5hx0n+S8wco9u zRQYHD?=-s%=XdLS?-}*pyS2@Hfw`P|Vfv?uIkOH<e#Wpdxv6n?((bq~)~%mc^JxU$ z*&MsRYW5t4weQzRT1h9CAJr^>?d0?7aBpJr(R#&?tNBcu)jd4tMid^HY_;KWgi%S$ z%&YA(oW``2sL9yo-vY9(QLQxb*b;#acYj>)om{m0-qvl`Dkr6f$rPxZTVt^xJn3!u z-VCNAHJg^-Hv&4jk7ef7y(@T|mw{v-c+Q*KeW&8VwV1lgoA>=^2+h&S;s$B3>b&_m z{-({ReP%XS6u)1cTrDrVQgl<!i?)OA#pUZ9Zkn&%eRfX(<A$92&7ce_2uts<(qaLm VBw=K<XW#&rH8@lN6BAg)O#pwM{<i=C From 8b251e467c663d689f2a475007188cb6987cc17c Mon Sep 17 00:00:00 2001 From: Timon de Groot <timon@mooore.nl> Date: Fri, 18 Sep 2020 16:31:20 +0200 Subject: [PATCH 0530/1013] Remove reference to undefined member --- lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 5810b62304dc9..96f9706ec54b8 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -587,7 +587,6 @@ private function getColorspace(): int { if ($this->colorspace === -1) { $this->colorspace = $this->_imageHandler->getImageColorspace(); - $this->originalColorspace = $this->colorspace; } return $this->colorspace; From 25111f0dcd1ecf050112647930accd057b2d901a Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Fri, 18 Sep 2020 21:19:28 +0300 Subject: [PATCH 0531/1013] refactoring --- .../Catalog/Model/CategoryRepository.php | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 6865798052e58..0d0ceec6221f7 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -77,14 +77,7 @@ public function __construct( public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) { $storeId = (int)$this->storeManager->getStore()->getId(); - $existingData = $this->getExtensibleDataObjectConverter() - ->toNestedArray($category, [], \Magento\Catalog\Api\Data\CategoryInterface::class); - $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); - $existingData['store_id'] = $storeId; - - if (is_array($category->getData())) { - $existingData = array_replace($existingData, $category->getData()); - } + $existingData = $this->getExistingData($category, $storeId); if ($category->getId()) { $metadata = $this->getMetadataPool()->getMetadata( @@ -243,4 +236,25 @@ private function getMetadataPool() } return $this->metadataPool; } + + /** + * Get existing data category + * + * @param CategoryInterface $category + * @param int $storeId + * @return array + */ + private function getExistingData(CategoryInterface $category, int $storeId) + { + $existingData = $this->getExtensibleDataObjectConverter() + ->toNestedArray($category, [], \Magento\Catalog\Api\Data\CategoryInterface::class); + $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); + $existingData['store_id'] = $storeId; + + if (is_array($category->getData())) { + $existingData = array_replace($existingData, $category->getData()); + } + + return $existingData; + } } From f7816e10e141f1253665295504f8636204819778 Mon Sep 17 00:00:00 2001 From: TuNa <ladiesman9x@gmail.com> Date: Fri, 18 Sep 2020 22:12:35 +0700 Subject: [PATCH 0532/1013] improve visual counter review with parentheses in blank theme --- .../Magento_Review/web/css/source/_module.less | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less index 69ec01d71e104..bf77ab46712b2 100644 --- a/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Review/web/css/source/_module.less @@ -15,9 +15,21 @@ // _____________________________________________ & when (@media-common = true) { + .data.switch .counter { + .lib-css(color, @text__color__muted); + + &:before { + content: '('; + } + + &:after { + content: ')'; + } + } + .rating-summary { .lib-rating-summary(); - + .rating-result { margin-left: -5px; } @@ -359,3 +371,4 @@ } } } + From b351d50d61bd4a0836fdbf1ccee89666c7f0f2b5 Mon Sep 17 00:00:00 2001 From: Tu <ladiesman9x@gmail.com> Date: Mon, 3 Aug 2020 16:00:34 +0700 Subject: [PATCH 0533/1013] Add collapsible nav in customer account update --- .../Magento_Customer/layout/customer_account.xml | 0 .../Magento/blank/Magento_Theme/web/css/source/_module.less | 2 ++ .../web/css/source/module/_collapsible_navigation.less | 0 3 files changed, 2 insertions(+) rename app/design/frontend/Magento/{luma => blank}/Magento_Customer/layout/customer_account.xml (100%) rename app/design/frontend/Magento/{luma => blank}/Magento_Theme/web/css/source/module/_collapsible_navigation.less (100%) diff --git a/app/design/frontend/Magento/luma/Magento_Customer/layout/customer_account.xml b/app/design/frontend/Magento/blank/Magento_Customer/layout/customer_account.xml similarity index 100% rename from app/design/frontend/Magento/luma/Magento_Customer/layout/customer_account.xml rename to app/design/frontend/Magento/blank/Magento_Customer/layout/customer_account.xml diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less index 8518b5bf76735..8f99550271967 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/_module.less @@ -3,6 +3,8 @@ // * See COPYING.txt for license details. // */ +@import 'module/_collapsible_navigation.less'; + // // Theme variables // _____________________________________________ diff --git a/app/design/frontend/Magento/luma/Magento_Theme/web/css/source/module/_collapsible_navigation.less b/app/design/frontend/Magento/blank/Magento_Theme/web/css/source/module/_collapsible_navigation.less similarity index 100% rename from app/design/frontend/Magento/luma/Magento_Theme/web/css/source/module/_collapsible_navigation.less rename to app/design/frontend/Magento/blank/Magento_Theme/web/css/source/module/_collapsible_navigation.less From 327c603c67022147a961e4a1a6a0aa5d77815727 Mon Sep 17 00:00:00 2001 From: Marjan <petkovski.marjan@gmail.com> Date: Sun, 20 Sep 2020 13:04:47 +0200 Subject: [PATCH 0534/1013] magento/magento2#29927:Search should be disabled from products query when general configuration chooses to disabled it Initial draft. --- app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 1a244b8a10546..7032262629ffa 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -57,6 +57,9 @@ public function resolve( array $value = null, array $args = null ) { + if (isset($args['searchAllowed']) && $args['searchAllowed'] === false) { + throw new GraphQlInputException(__('Product search has been disabled.')); + } if ($args['currentPage'] < 1) { throw new GraphQlInputException(__('currentPage value must be greater than 0.')); } From 7983d4b2306cfa2151dd0786c6ade250322bedc4 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Mon, 21 Sep 2020 12:01:35 +0300 Subject: [PATCH 0535/1013] Fixing a failing test --- .../Mftf/Test/CartPriceRuleForBundleProductTest.xml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForBundleProductTest.xml index 101c72b78078a..ed5f3de9b13b0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForBundleProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForBundleProductTest.xml @@ -73,7 +73,9 @@ </actionGroup> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> @@ -100,7 +102,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <magentoCron groups="index" stepKey="reindexInvalidatedIndices2" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterDeleted"> + <argument name="indices" value=""/> + </actionGroup> </after> <!-- Create the rule --> @@ -135,8 +139,8 @@ <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> <!-- Select two products --> - <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.productCheckbox('1','1')}}"/> - <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.productCheckbox('2','1')}}"/> + <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.checkboxOptionLabel('$$createBundleOption1_1.sku$$','$$simpleProduct1.name$$')}}"/> + <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.checkboxOptionLabel('$$createBundleOption1_2.sku$$','$$simpleProduct3.name$$')}}"/> <!--Click "Add to Cart" button--> <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickAddBundleProductToCart"/> From 2f11cf4ca2b86899e83087c6faa3e5a2d15c9785 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Mon, 21 Sep 2020 12:41:08 +0300 Subject: [PATCH 0536/1013] MC-37083: Create automated test for "[Timezone] Sales Exported Dates" --- .../Adminhtml/Order/Creditmemo/ExportTest.php | 119 +++++++++++++++ .../Controller/Adminhtml/Order/ExportBase.php | 143 ++++++++++++++++++ .../Controller/Adminhtml/Order/ExportTest.php | 64 ++++++++ .../Adminhtml/Order/Invoice/ExportTest.php | 114 ++++++++++++++ .../Adminhtml/Order/Shipment/ExportTest.php | 114 ++++++++++++++ ..._shipment_creditmemo_on_second_website.php | 129 ++++++++++++++++ ..._creditmemo_on_second_website_rollback.php | 51 +++++++ 7 files changed, 734 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php new file mode 100644 index 0000000000000..67f275dae5a8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Creditmemo; + +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Model\ResourceModel\Order\Creditmemo\CollectionFactory; +use Magento\Sales\Controller\Adminhtml\Order\ExportBase; + +/** + * Tests for creditmemo export via admin grids. + */ +class ExportTest extends ExportBase +{ + /** + * @var CollectionFactory + */ + private $creditmemoCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->creditmemoCollectionFactory = $this->_objectManager->get(CollectionFactory::class); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture general/locale/timezone America/Chicago + * @magentoConfigFixture test_website general/locale/timezone America/Adak + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php + * @dataProvider exportCreditmemoDataProvider + * @param string $format + * @param bool $addIdToUrl + * @param string $namespace + * @return void + */ + public function testExportCreditmemo( + string $format, + bool $addIdToUrl, + string $namespace + ): void { + $order = $this->getOrder('200000001'); + $url = $this->getExportUrl($format, $addIdToUrl ? (int)$order->getId() : null); + $response = $this->dispatchExport( + $url, + ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] + ); + $creditmemos = []; + if ($format === ExportBase::CSV_FORMAT) { + $creditmemos = $this->parseCsvResponse($response); + } elseif ($format === ExportBase::XML_FORMAT) { + $creditmemos = $this->parseXmlResponse($response); + } + $creditmemo = $this->getCreditmemo('200000001'); + $exportedCreditmemo = reset($creditmemos); + $this->assertNotFalse($exportedCreditmemo); + $this->assertEquals( + $this->prepareDate($creditmemo->getCreatedAt(), 'America/Chicago'), + $exportedCreditmemo['Created'] + ); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedCreditmemo['Order Date'] + ); + } + + /** + * @return array + */ + public function exportCreditmemoDataProvider(): array + { + return [ + 'creditmemo_grid_in_csv' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_creditmemo_grid', + ], + 'creditmemo_grid_in_csv_from_order_view' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_creditmemo_grid', + ], + 'creditmemo_grid_in_xml' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_creditmemo_grid', + ], + 'creditmemo_grid_in_xml_from_order_view' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_creditmemo_grid', + ], + ]; + } + + /** + * Returns creditmemo by increment id. + * + * @param string $incrementId + * @return CreditmemoInterface + */ + private function getCreditmemo(string $incrementId): CreditmemoInterface + { + /** @var CreditmemoInterface $creditmemo */ + $creditmemo = $this->creditmemoCollectionFactory->create() + ->addAttributeToFilter(CreditmemoInterface::INCREMENT_ID, $incrementId) + ->getFirstItem(); + + return $creditmemo; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php new file mode 100644 index 0000000000000..a830c2bcad5a5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php @@ -0,0 +1,143 @@ +<?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; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Tests for order/invoice/shipment/credit memo export via admin grids. + * + * @magentoDbIsolation disabled + */ +class ExportBase extends AbstractBackendController +{ + const CSV_FORMAT = 'csv'; + const XML_FORMAT = 'xml'; + + /** + * @var OrderInterfaceFactory + */ + private $orderFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + } + + /** + * Dispatches export request. + * + * @param string $url + * @param array $params + * @return string + */ + protected function dispatchExport(string $url, array $params): string + { + $this->_auth->getAuthStorage()->setIsFirstPageAfterLogin(false); + $this->getRequest()->setParams($params); + $this->getRequest()->setMethod(Http::METHOD_POST); + ob_start(); + $this->dispatch($url); + + return ob_get_clean(); + } + + /** + * Converts string in scv format to assoc array. + * + * @param string $data + * @return array + */ + protected function parseCsvResponse(string $data): array + { + $result = []; + $data = str_getcsv($data, PHP_EOL); + $headers = str_getcsv(array_shift($data), ',', '"'); + foreach ($data as $row) { + $result[] = array_combine($headers, str_getcsv($row, ',', '"')); + } + + return $result; + } + + /** + * Converts string in xml format to assoc array. + * + * @param string $data + * @return array + */ + protected function parseXmlResponse(string $data): array + { + $xml = simplexml_load_string($data); + $xmlAsArray = []; + foreach ($xml->Worksheet->Table->Row as $item) { + $row = []; + foreach ($item->Cell as $cell) { + $data = (array)$cell->Data; + $row[] = reset($data); + } + $xmlAsArray[] = $row; + } + $result = []; + $headers = array_shift($xmlAsArray); + foreach ($xmlAsArray as $row) { + $result[] = array_combine($headers, $row); + } + + return $result; + } + + /** + * Returns order purchase date in timezone. + * + * @param string $date + * @param string $timezone + * @return string + */ + protected function prepareDate(string $date, string $timezone): string + { + $date = new \DateTime($date, new \DateTimeZone('UTC')); + $date->setTimezone(new \DateTimeZone($timezone)); + + return $date->format('M j, Y h:i:s A'); + } + + /** + * Returns order by increment id. + * + * @param string $incrementId + * @return OrderInterface + */ + protected function getOrder(string $incrementId): OrderInterface + { + return $this->orderFactory->create()->loadByIncrementId($incrementId); + } + + /** + * Returns export url. + * + * @param string $format + * @param int|null $orderId + * @return string + */ + protected function getExportUrl(string $format, ?int $orderId = null): string + { + $url = $format === self::CSV_FORMAT + ? 'backend/mui/export/gridToCsv/' + : 'backend/mui/export/gridToXml/'; + + return $orderId ? $url . 'order_id/' . $orderId : $url; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php new file mode 100644 index 0000000000000..0172d6886f982 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order; + +/** + * Tests for order export via admin grid. + */ +class ExportTest extends ExportBase +{ + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture general/locale/timezone America/Chicago + * @magentoConfigFixture test_website general/locale/timezone America/Adak + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php + * @dataProvider exportOrderDataProvider + * @param string $format + * @param string $namespace + * @return void + */ + public function testExportOrder(string $format, string $namespace): void + { + $order = $this->getOrder('200000001'); + $url = $this->getExportUrl($format, null); + $response = $this->dispatchExport( + $url, + ['namespace' => $namespace, 'filters' => ['increment_id' => '200000001']] + ); + $orders = []; + if ($format === ExportBase::CSV_FORMAT) { + $orders = $this->parseCsvResponse($response); + } elseif ($format === ExportBase::XML_FORMAT) { + $orders = $this->parseXmlResponse($response); + } + $exportedOrder = reset($orders); + $this->assertNotFalse($exportedOrder); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedOrder['Purchase Date'] + ); + } + + /** + * @return array + */ + public function exportOrderDataProvider(): array + { + return [ + 'order_grid_in_csv' => [ + 'format' => ExportBase::CSV_FORMAT, + 'namespace' => 'sales_order_grid', + ], + 'order_grid_in_xml' => [ + 'format' => ExportBase::XML_FORMAT, + 'namespace' => 'sales_order_grid', + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php new file mode 100644 index 0000000000000..f23241e65ae9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Invoice; + +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceInterfaceFactory; +use Magento\Sales\Controller\Adminhtml\Order\ExportBase; + +/** + * Tests for invoice export via admin grids. + */ +class ExportTest extends ExportBase +{ + /** + * @var InvoiceInterfaceFactory + */ + private $invoiceFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->invoiceFactory = $this->_objectManager->get(InvoiceInterfaceFactory::class); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture general/locale/timezone America/Chicago + * @magentoConfigFixture test_website general/locale/timezone America/Adak + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php + * @dataProvider exportInvoiceDataProvider + * @param string $format + * @param bool $addIdToUrl + * @param string $namespace + * @return void + */ + public function testExportInvoice( + string $format, + bool $addIdToUrl, + string $namespace + ): void { + $order = $this->getOrder('200000001'); + $url = $this->getExportUrl($format, $addIdToUrl ? (int)$order->getId() : null); + $response = $this->dispatchExport( + $url, + ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] + ); + $invoices = []; + if ($format === ExportBase::CSV_FORMAT) { + $invoices = $this->parseCsvResponse($response); + } elseif ($format === ExportBase::XML_FORMAT) { + $invoices = $this->parseXmlResponse($response); + } + $invoice = $this->getInvoice('200000001'); + $exportedInvoice = reset($invoices); + $this->assertNotFalse($exportedInvoice); + $this->assertEquals( + $this->prepareDate($invoice->getCreatedAt(), 'America/Chicago'), + $exportedInvoice['Invoice Date'] + ); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedInvoice['Order Date'] + ); + } + + /** + * @return array + */ + public function exportInvoiceDataProvider(): array + { + return [ + 'invoice_grid_in_csv' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_invoice_grid', + ], + 'invoice_grid_in_csv_from_order_view' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_invoice_grid', + ], + 'invoice_grid_in_xml' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_invoice_grid', + ], + 'invoice_grid_in_xml_from_order_view' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_invoice_grid', + ], + ]; + } + + /** + * Returns invoice by increment id. + * + * @param string $incrementId + * @return InvoiceInterface + */ + private function getInvoice(string $incrementId): InvoiceInterface + { + return $this->invoiceFactory->create()->loadByIncrementId($incrementId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php new file mode 100644 index 0000000000000..c8783be6e6758 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Shipment; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentInterfaceFactory; +use Magento\Sales\Controller\Adminhtml\Order\ExportBase; + +/** + * Tests for shipment export via admin grids. + */ +class ExportTest extends ExportBase +{ + /** + * @var ShipmentInterfaceFactory + */ + private $shipmentFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->shipmentFactory = $this->_objectManager->get(ShipmentInterfaceFactory::class); + } + + /** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + * @magentoConfigFixture general/locale/timezone America/Chicago + * @magentoConfigFixture test_website general/locale/timezone America/Adak + * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php + * @dataProvider exportShipmentDataProvider + * @param string $format + * @param bool $addIdToUrl + * @param string $namespace + * @return void + */ + public function testExportShipment( + string $format, + bool $addIdToUrl, + string $namespace + ): void { + $order = $this->getOrder('200000001'); + $url = $this->getExportUrl($format, $addIdToUrl ? (int)$order->getId() : null); + $response = $this->dispatchExport( + $url, + ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] + ); + $shipments = []; + if ($format === ExportBase::CSV_FORMAT) { + $shipments = $this->parseCsvResponse($response); + } elseif ($format === ExportBase::XML_FORMAT) { + $shipments = $this->parseXmlResponse($response); + } + $shipment = $this->getShipment('200000001'); + $exportedShipment = reset($shipments); + $this->assertNotFalse($exportedShipment); + $this->assertEquals( + $this->prepareDate($shipment->getCreatedAt(), 'America/Chicago'), + $exportedShipment['Ship Date'] + ); + $this->assertEquals( + $this->prepareDate($order->getCreatedAt(), 'America/Chicago'), + $exportedShipment['Order Date'] + ); + } + + /** + * @return array + */ + public function exportShipmentDataProvider(): array + { + return [ + 'shipment_grid_in_csv' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_shipment_grid', + ], + 'shipment_grid_in_csv_from_order_view' => [ + 'format' => ExportBase::CSV_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_shipment_grid', + ], + 'shipment_grid_in_xml' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => false, + 'namespace' => 'sales_order_shipment_grid', + ], + 'shipment_grid_in_xml_from_order_view' => [ + 'format' => ExportBase::XML_FORMAT, + 'add_id_to_url' => true, + 'namespace' => 'sales_order_view_shipment_grid', + ], + ]; + } + + /** + * Returns shipment by increment id. + * + * @param string $incrementId + * @return ShipmentInterface + */ + private function getShipment(string $incrementId): ShipmentInterface + { + return $this->shipmentFactory->create()->loadByIncrementId($incrementId); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php new file mode 100644 index 0000000000000..4aa5e0f421cf1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php @@ -0,0 +1,129 @@ +<?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\DB\Transaction; +use Magento\Sales\Api\CreditmemoItemRepositoryInterface; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\Data\CreditmemoItemInterfaceFactory; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\InvoiceManagementInterface; +use Magento\Sales\Api\Data\OrderItemInterfaceFactory; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Creditmemo\Item; +use Magento\Sales\Model\Order\CreditmemoFactory; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +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/Store/_files/second_website_with_store_group_and_store.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var InvoiceManagementInterface $invoiceService */ +$invoiceService = $objectManager->get(InvoiceManagementInterface::class); +/** @var ShipmentFactory $shipmentFactory */ +$shipmentFactory = $objectManager->get(ShipmentFactory::class); +/** @var CreditmemoFactory $creditmemoFactory */ +$creditmemoFactory = $objectManager->get(CreditmemoFactory::class); +/** @var CreditmemoItemInterfaceFactory $creditmemoItemFactory */ +$creditmemoItemFactory = $objectManager->get(CreditmemoItemInterfaceFactory::class); +/** @var CreditmemoRepositoryInterface $creditmemoRepository */ +$creditmemoRepository = $objectManager->get(CreditmemoRepositoryInterface::class); +/** @var CreditmemoItemRepositoryInterface $creditmemoItemRepository */ +$creditmemoItemRepository = $objectManager->get(CreditmemoItemRepositoryInterface::class); +$addressData = include __DIR__ . '/address_data.php'; +$product = $productRepository->get('simple'); +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType(OrderAddress::TYPE_BILLING); +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType(OrderAddress::TYPE_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->get(OrderItemInterfaceFactory::class)->create(); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()) + ->setName('Test item'); +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create(); +$order->setIncrementId('200000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(100) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setCustomerIsGuest(true) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($storeManager->getStore('fixture_second_store')->getId()) + ->addItem($orderItem) + ->setPayment($payment); +$orderRepository->save($order); +//Create invoice +$invoice = $invoiceService->prepareInvoice($order); +$invoice->register(); +$invoice->setIncrementId($order->getIncrementId()); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager->create(Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); +//Create shipment +$items = []; +foreach ($order->getItems() as $item) { + $items[$item->getId()] = $item->getQtyOrdered(); +} +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); +$shipment->register(); +$shipment->setIncrementId($order->getIncrementId()); +$transactionSave = $objectManager->create(Transaction::class); +$transactionSave->addObject($shipment)->addObject($order)->save(); +//Create credit memo +/** @var CreditmemoFactory $creditmemoFactory */ +$creditmemoFactory = $objectManager->get(CreditmemoFactory::class); +$creditmemo = $creditmemoFactory->createByOrder($order, $order->getData()); +$creditmemo->setOrder($order); +$creditmemo->setState(Magento\Sales\Model\Order\Creditmemo::STATE_OPEN); +$creditmemo->setIncrementId($order->getIncrementId()); +$creditmemoRepository->save($creditmemo); +$orderItem->setName('Test item') + ->setQtyRefunded(2) + ->setQtyInvoiced(2) + ->setOriginalPrice($product->getPrice()); +/** @var Item $creditItem */ +$creditItem = $objectManager->get(Item::class); +$creditItem->setCreditmemo($creditmemo) + ->setName('Creditmemo item') + ->setOrderItemId($orderItem->getId()) + ->setQty(2) + ->setPrice($product->getPrice()); +$creditmemoItemRepository->save($creditItem); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website_rollback.php new file mode 100644 index 0000000000000..9a5b889fc7143 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website_rollback.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Api\ShipmentRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var InvoiceRepositoryInterface $invoiceRepository */ +$invoiceRepository = $objectManager->get(InvoiceRepositoryInterface::class); +/** @var ShipmentRepositoryInterface $shipmentRepository */ +$shipmentRepository = $objectManager->get(ShipmentRepositoryInterface::class); +/** @var CreditmemoRepositoryInterface $creditmemoRepository */ +$creditmemoRepository = $objectManager->get(CreditmemoRepositoryInterface::class); +/** @var OrderInterface $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('200000001'); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +foreach ($order->getInvoiceCollection() as $invoice) { + $invoiceRepository->delete($invoice); +} +foreach ($order->getShipmentsCollection() as $shipment) { + $shipmentRepository->delete($shipment); +} +foreach ($order->getCreditmemosCollection() as $creditMemo) { + $creditmemoRepository->delete($creditMemo); +} +$orderRepository->delete($order); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); From fba8df9ffbc855d36f724c5b02514b0296eb5dc0 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Mon, 21 Sep 2020 16:40:46 +0300 Subject: [PATCH 0537/1013] MC-37083: Create automated test for "[Timezone] Sales Exported Dates" --- .../Adminhtml/Order/Creditmemo/ExportTest.php | 7 +----- .../Controller/Adminhtml/Order/ExportBase.php | 19 ++++++++++++++++ .../Controller/Adminhtml/Order/ExportTest.php | 22 +++++-------------- .../Adminhtml/Order/Invoice/ExportTest.php | 7 +----- .../Adminhtml/Order/Shipment/ExportTest.php | 7 +----- ..._shipment_creditmemo_on_second_website.php | 20 ++++++++--------- 6 files changed, 37 insertions(+), 45 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php index 67f275dae5a8b..60313c4f45a9f 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/ExportTest.php @@ -53,12 +53,7 @@ public function testExportCreditmemo( $url, ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] ); - $creditmemos = []; - if ($format === ExportBase::CSV_FORMAT) { - $creditmemos = $this->parseCsvResponse($response); - } elseif ($format === ExportBase::XML_FORMAT) { - $creditmemos = $this->parseXmlResponse($response); - } + $creditmemos = $this->parseResponse($format, $response); $creditmemo = $this->getCreditmemo('200000001'); $exportedCreditmemo = reset($creditmemos); $this->assertNotFalse($exportedCreditmemo); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php index a830c2bcad5a5..271a99a8037ca 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportBase.php @@ -54,6 +54,25 @@ protected function dispatchExport(string $url, array $params): string return ob_get_clean(); } + /** + * Parses string response depends of format. + * + * @param string $format + * @param string $response + * @return array + */ + protected function parseResponse(string $format, string $response): array + { + $result = []; + if ($format === ExportBase::CSV_FORMAT) { + $result = $this->parseCsvResponse($response); + } elseif ($format === ExportBase::XML_FORMAT) { + $result = $this->parseXmlResponse($response); + } + + return $result; + } + /** * Converts string in scv format to assoc array. * diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php index 0172d6886f982..c447568c4daf4 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/ExportTest.php @@ -20,23 +20,17 @@ class ExportTest extends ExportBase * @magentoDataFixture Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php * @dataProvider exportOrderDataProvider * @param string $format - * @param string $namespace * @return void */ - public function testExportOrder(string $format, string $namespace): void + public function testExportOrder(string $format): void { $order = $this->getOrder('200000001'); $url = $this->getExportUrl($format, null); $response = $this->dispatchExport( $url, - ['namespace' => $namespace, 'filters' => ['increment_id' => '200000001']] + ['namespace' => 'sales_order_grid', 'filters' => ['increment_id' => '200000001']] ); - $orders = []; - if ($format === ExportBase::CSV_FORMAT) { - $orders = $this->parseCsvResponse($response); - } elseif ($format === ExportBase::XML_FORMAT) { - $orders = $this->parseXmlResponse($response); - } + $orders = $this->parseResponse($format, $response); $exportedOrder = reset($orders); $this->assertNotFalse($exportedOrder); $this->assertEquals( @@ -51,14 +45,8 @@ public function testExportOrder(string $format, string $namespace): void public function exportOrderDataProvider(): array { return [ - 'order_grid_in_csv' => [ - 'format' => ExportBase::CSV_FORMAT, - 'namespace' => 'sales_order_grid', - ], - 'order_grid_in_xml' => [ - 'format' => ExportBase::XML_FORMAT, - 'namespace' => 'sales_order_grid', - ], + 'order_grid_in_csv' => ['format' => ExportBase::CSV_FORMAT], + 'order_grid_in_xml' => ['format' => ExportBase::XML_FORMAT], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php index f23241e65ae9f..eb1cd59dde632 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/ExportTest.php @@ -53,12 +53,7 @@ public function testExportInvoice( $url, ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] ); - $invoices = []; - if ($format === ExportBase::CSV_FORMAT) { - $invoices = $this->parseCsvResponse($response); - } elseif ($format === ExportBase::XML_FORMAT) { - $invoices = $this->parseXmlResponse($response); - } + $invoices = $this->parseResponse($format, $response); $invoice = $this->getInvoice('200000001'); $exportedInvoice = reset($invoices); $this->assertNotFalse($exportedInvoice); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php index c8783be6e6758..d27fe0821c047 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Shipment/ExportTest.php @@ -53,12 +53,7 @@ public function testExportShipment( $url, ['namespace' => $namespace, 'filters' => ['order_increment_id' => '200000001']] ); - $shipments = []; - if ($format === ExportBase::CSV_FORMAT) { - $shipments = $this->parseCsvResponse($response); - } elseif ($format === ExportBase::XML_FORMAT) { - $shipments = $this->parseXmlResponse($response); - } + $shipments = $this->parseResponse($format, $response); $shipment = $this->getShipment('200000001'); $exportedShipment = reset($shipments); $this->assertNotFalse($exportedShipment); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php index 4aa5e0f421cf1..06f8954456471 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_invoice_shipment_creditmemo_on_second_website.php @@ -10,16 +10,17 @@ use Magento\Sales\Api\CreditmemoItemRepositoryInterface; use Magento\Sales\Api\CreditmemoRepositoryInterface; use Magento\Sales\Api\Data\CreditmemoItemInterfaceFactory; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Api\InvoiceManagementInterface; use Magento\Sales\Api\Data\OrderItemInterfaceFactory; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Address as OrderAddress; -use Magento\Sales\Model\Order\Creditmemo\Item; +use Magento\Sales\Model\Order\Creditmemo; use Magento\Sales\Model\Order\CreditmemoFactory; -use Magento\Sales\Model\Order\Item as OrderItem; -use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\Order\ShipmentFactory; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -54,12 +55,12 @@ $billingAddress->setAddressType(OrderAddress::TYPE_BILLING); $shippingAddress = clone $billingAddress; $shippingAddress->setId(null)->setAddressType(OrderAddress::TYPE_SHIPPING); -/** @var Payment $payment */ -$payment = $objectManager->create(Payment::class); +/** @var OrderPaymentInterface $payment */ +$payment = $objectManager->create(OrderPaymentInterface::class); $payment->setMethod('checkmo') ->setAdditionalInformation('last_trans_id', '11122') ->setAdditionalInformation('metadata', ['type' => 'free', 'fraudulent' => false]); -/** @var OrderItem $orderItem */ +/** @var OrderItemInterface $orderItem */ $orderItem = $objectManager->get(OrderItemInterfaceFactory::class)->create(); $orderItem->setProductId($product->getId()) ->setQtyOrdered(2) @@ -70,7 +71,7 @@ ->setName($product->getName()) ->setSku($product->getSku()) ->setName('Test item'); -/** @var Order $order */ +/** @var OrderInterface $order */ $order = $objectManager->get(OrderInterfaceFactory::class)->create(); $order->setIncrementId('200000001') ->setState(Order::STATE_PROCESSING) @@ -112,15 +113,14 @@ $creditmemoFactory = $objectManager->get(CreditmemoFactory::class); $creditmemo = $creditmemoFactory->createByOrder($order, $order->getData()); $creditmemo->setOrder($order); -$creditmemo->setState(Magento\Sales\Model\Order\Creditmemo::STATE_OPEN); +$creditmemo->setState(Creditmemo::STATE_OPEN); $creditmemo->setIncrementId($order->getIncrementId()); $creditmemoRepository->save($creditmemo); $orderItem->setName('Test item') ->setQtyRefunded(2) ->setQtyInvoiced(2) ->setOriginalPrice($product->getPrice()); -/** @var Item $creditItem */ -$creditItem = $objectManager->get(Item::class); +$creditItem = $creditmemoItemFactory->create(); $creditItem->setCreditmemo($creditmemo) ->setName('Creditmemo item') ->setOrderItemId($orderItem->getId()) From b7f2ba1fa13cb3976290a170906e2e6394d72f9f Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Mon, 21 Sep 2020 17:00:20 +0300 Subject: [PATCH 0538/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- app/code/Magento/CatalogRule/Model/Rule.php | 8 ++++++++ ...leForSimpleProductWithSelectFixedMethodTest.xml | 14 +++++++++----- .../Bundle/Model/Product/BundlePriceAbstract.php | 8 ++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index f2e8e54d34665..bcd01dae96e81 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -895,4 +895,12 @@ public function getIdentities() { return ['price']; } + + /** + * Clear price rules cache. + */ + public function clearPriceRulesData(): void + { + self::$_priceRulesData = []; + } } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index ff144c1686e5d..c863ebb55f41a 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -19,9 +19,6 @@ <group value="mtf_migrated"/> </annotations> <before> - <!-- Login as Admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!-- Create category --> <createData entity="_defaultCategory" stepKey="createCategory"/> @@ -33,7 +30,12 @@ <!-- Update all products to have custom options --> <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateProductWithOptions1"/> - <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> <!-- Delete products and category --> @@ -60,7 +62,9 @@ <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> <!-- Save and apply the new catalog price rule --> - <actionGroup ref="AdminEnableCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> + <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForSave"/> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToStorefrontCategoryPage"/> 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 bf369ed28167b..a18f1e0799dfa 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php @@ -30,6 +30,11 @@ abstract class BundlePriceAbstract extends \PHPUnit\Framework\TestCase */ protected $productCollectionFactory; + /** + * @var \Magento\CatalogRule\Model\RuleFactory + */ + private $ruleFactory; + protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -43,6 +48,7 @@ protected function setUp(): void true, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); + $this->ruleFactory = $this->objectManager->get(\Magento\CatalogRule\Model\RuleFactory::class); } /** @@ -62,6 +68,8 @@ abstract public function getTestCases(); */ protected function prepareFixture($strategyModifiers, $productSku) { + $this->ruleFactory->create()->clearPriceRulesData(); + $bundleProduct = $this->productRepository->get($productSku); foreach ($strategyModifiers as $modifier) { From 3460921a4748e4f202d4204f2c81b77433cf1078 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Mon, 21 Sep 2020 16:57:58 -0500 Subject: [PATCH 0539/1013] MC-37745: Missing products from categories, indexing related --- .../CatalogSearch/Model/Indexer/Fulltext.php | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index e226bdc6900e6..a38d671bab100 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -12,6 +12,7 @@ use Magento\CatalogSearch\Model\ResourceModel\Fulltext as FulltextResource; use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Framework\Indexer\SaveHandler\IndexerInterface; use Magento\Store\Model\StoreDimensionProvider; use Magento\Indexer\Model\ProcessManager; @@ -33,6 +34,11 @@ class Fulltext implements */ const INDEXER_ID = 'catalogsearch_fulltext'; + /** + * Default batch size + */ + private const BATCH_SIZE = 100; + /** * @var array index structure */ @@ -77,6 +83,11 @@ class Fulltext implements */ private $processManager; + /** + * @var int + */ + private $batchSize; + /** * @param FullFactory $fullActionFactory * @param IndexerHandlerFactory $indexerHandlerFactory @@ -85,7 +96,8 @@ class Fulltext implements * @param StateFactory $indexScopeStateFactory * @param DimensionProviderInterface $dimensionProvider * @param array $data - * @param ProcessManager $processManager + * @param ProcessManager|null $processManager + * @param int|null $batchSize * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -96,7 +108,8 @@ public function __construct( StateFactory $indexScopeStateFactory, DimensionProviderInterface $dimensionProvider, array $data, - ProcessManager $processManager = null + ?ProcessManager $processManager = null, + ?int $batchSize = null ) { $this->fullAction = $fullActionFactory->create(['data' => $data]); $this->indexerHandlerFactory = $indexerHandlerFactory; @@ -106,6 +119,7 @@ public function __construct( $this->indexScopeState = ObjectManager::getInstance()->get(State::class); $this->dimensionProvider = $dimensionProvider; $this->processManager = $processManager ?: ObjectManager::getInstance()->get(ProcessManager::class); + $this->batchSize = $batchSize ?? self::BATCH_SIZE; } /** @@ -148,13 +162,42 @@ public function executeByDimensions(array $dimensions, \Traversable $entityIds = } else { // internal implementation works only with array $entityIds = iterator_to_array($entityIds); - $productIds = array_unique( - array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds)) - ); - if ($saveHandler->isAvailable($dimensions)) { - $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); - $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); + $currentBatch = []; + $i = 0; + + foreach ($entityIds as $entityId) { + $currentBatch[] = $entityId; + if (++$i === $this->batchSize) { + $this->processBatch($saveHandler, $dimensions, $currentBatch); + $i = 0; + $currentBatch = []; + } } + if (!empty($currentBatch)) { + $this->processBatch($saveHandler, $dimensions, $currentBatch); + } + } + } + + /** + * Process batch + * + * @param IndexerInterface $saveHandler + * @param array $dimensions + * @param array $entityIds + */ + private function processBatch( + IndexerInterface $saveHandler, + array $dimensions, + array $entityIds + ) : void { + $storeId = $dimensions[StoreDimensionProvider::DIMENSION_NAME]->getValue(); + $productIds = array_unique( + array_merge($entityIds, $this->fulltextResource->getRelationsByChild($entityIds)) + ); + if ($saveHandler->isAvailable($dimensions)) { + $saveHandler->deleteIndex($dimensions, new \ArrayIterator($productIds)); + $saveHandler->saveIndex($dimensions, $this->fullAction->rebuildStoreIndex($storeId, $productIds)); } } From 90205dcf305a4de6365a9520a51918a362bab7de Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Mon, 21 Sep 2020 17:55:29 -0500 Subject: [PATCH 0540/1013] MC-37649: Paypal Duplicate Orders - Fixes the issue when duplicated orders were created after unexpected http request termination during order submit --- .../Express/AbstractExpress/PlaceOrder.php | 1 + app/code/Magento/Paypal/Model/Api/Nvp.php | 9 --------- .../Paypal/Model/Api/ProcessableException.php | 6 ++++++ app/code/Magento/Paypal/Model/Express.php | 1 + .../Paypal/Test/Unit/Model/Api/NvpTest.php | 16 +++++----------- .../Paypal/Test/Unit/Model/ExpressTest.php | 1 + app/code/Magento/Paypal/i18n/en_US.csv | 1 + 7 files changed, 15 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php index 29d4a5bd1f25c..95dc8ee487edf 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php @@ -174,6 +174,7 @@ protected function _processPaypalApiError($exception) $this->_redirectSameToken(); break; case ApiProcessableException::API_ADDRESS_MATCH_FAIL: + case ApiProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED: $this->redirectToOrderReviewPageAndShowError($exception->getUserMessage()); break; case ApiProcessableException::API_UNABLE_TRANSACTION_COMPLETE: diff --git a/app/code/Magento/Paypal/Model/Api/Nvp.php b/app/code/Magento/Paypal/Model/Api/Nvp.php index b35f783482e06..30bfb660aa6f1 100644 --- a/app/code/Magento/Paypal/Model/Api/Nvp.php +++ b/app/code/Magento/Paypal/Model/Api/Nvp.php @@ -1286,15 +1286,6 @@ protected function _handleCallErrors($response) ); $this->_logger->critical($exceptionLogMessage); - /** - * The response code 10415 'Transaction has already been completed for this token' - * must not fails place order. The old Paypal interface does not lock 'Send' button - * it may result to re-send data. - */ - if (in_array((string)ProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED, $this->_callErrors)) { - return; - } - $exceptionPhrase = __('PayPal gateway has rejected request. %1', $errorMessages); /** @var \Magento\Framework\Exception\LocalizedException $exception */ diff --git a/app/code/Magento/Paypal/Model/Api/ProcessableException.php b/app/code/Magento/Paypal/Model/Api/ProcessableException.php index 40ee6d98c4381..12a11ff442418 100644 --- a/app/code/Magento/Paypal/Model/Api/ProcessableException.php +++ b/app/code/Magento/Paypal/Model/Api/ProcessableException.php @@ -67,6 +67,12 @@ public function getUserMessage() . ' Please contact us so we can assist you.' ); break; + case self::API_TRANSACTION_HAS_BEEN_COMPLETED: + $message = __( + 'A successful payment transaction has already been completed.' + . ' Please, check if the order has been placed.' + ); + break; case self::API_ADDRESS_MATCH_FAIL: $message = __( 'A match of the Shipping Address City, State, and Postal Code failed.' diff --git a/app/code/Magento/Paypal/Model/Express.php b/app/code/Magento/Paypal/Model/Express.php index 946c0fd4c66ca..39b1c6f7e3c28 100644 --- a/app/code/Magento/Paypal/Model/Express.php +++ b/app/code/Magento/Paypal/Model/Express.php @@ -276,6 +276,7 @@ protected function _setApiProcessableErrors() ApiProcessableException::API_MAXIMUM_AMOUNT_FILTER_DECLINE, ApiProcessableException::API_OTHER_FILTER_DECLINE, ApiProcessableException::API_ADDRESS_MATCH_FAIL, + ApiProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED, self::$authorizationExpiredCode ] ); diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php index 42b99ae8e7459..69aa9b99bc9e7 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Api/NvpTest.php @@ -305,8 +305,7 @@ public function testGetDebugReplacePrivateDataKeys() /** * Tests case if obtained response with code 10415 'Transaction has already - * been completed for this token'. It must does not throws the exception and - * must returns response array. + * been completed for this token'. It must throw the ProcessableException. */ public function testCallTransactionHasBeenCompleted() { @@ -317,15 +316,10 @@ public function testCallTransactionHasBeenCompleted() ->method('read') ->willReturn($response); $this->model->setProcessableErrors($processableErrors); - $this->customLoggerMock->expects($this->once()) - ->method('debug'); - $expectedResponse = [ - 'ACK' => 'Failure', - 'L_ERRORCODE0' => '10415', - 'L_SHORTMESSAGE0' => 'Message.', - 'L_LONGMESSAGE0' => 'Long Message.' - ]; - $this->assertEquals($expectedResponse, $this->model->call('some method', ['data' => 'some data'])); + $this->expectExceptionMessageMatches('/PayPal gateway has rejected request/'); + $this->expectException(ProcessableException::class); + + $this->model->call('DoExpressCheckout', ['data' => 'some data']); } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php index 8cf2fb91a8452..14dcc4fc4229d 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/ExpressTest.php @@ -53,6 +53,7 @@ class ExpressTest extends TestCase ApiProcessableException::API_MAXIMUM_AMOUNT_FILTER_DECLINE, ApiProcessableException::API_OTHER_FILTER_DECLINE, ApiProcessableException::API_ADDRESS_MATCH_FAIL, + ApiProcessableException::API_TRANSACTION_HAS_BEEN_COMPLETED ]; /** diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index 8db6285dc157e..a8f26b422dc7c 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -737,3 +737,4 @@ User,User "Please enter at least 0 and at most 65535","Please enter at least 0 and at most 65535" "Order is suspended as an account verification transaction is suspected to be fraudulent.","Order is suspended as an account verification transaction is suspected to be fraudulent." "Payment can't be accepted since transaction was rejected by merchant.","Payment can't be accepted since transaction was rejected by merchant." +"A successful payment transaction has already been completed. Please, check if the order has been placed.","A successful payment transaction has already been completed. Please, check if the order has been placed." From 0aa181c520c7ce62293d3798b2be277fc7ec4523 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Tue, 22 Sep 2020 09:54:38 +0300 Subject: [PATCH 0541/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Catalog/Test/Mftf/Data/ProductOptionValueData.xml | 6 ------ ...CatalogRuleForSimpleProductWithSelectFixedMethodTest.xml | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml index 04c24ff7e36dd..e738994357366 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductOptionValueData.xml @@ -26,12 +26,6 @@ <data key="price">99.99</data> <data key="price_type">fixed</data> </entity> - <entity name="ProductOptionValueRadioButtonsWithDiscountedPrice" type="product_option_value"> - <data key="title">OptionValueRadioButtons1</data> - <data key="sort_order">1</data> - <data key="price">78.33</data> - <data key="price_type">fixed</data> - </entity> <entity name="ProductOptionValueRadioButtons2" type="product_option_value"> <data key="title">OptionValueRadioButtons2</data> <data key="sort_order">2</data> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index c863ebb55f41a..0a6f5c5f104b4 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -93,9 +93,9 @@ <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> <actionGroup ref="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> <argument name="customOption" value="ProductOptionRadioButton2"/> - <argument name="customOptionValue" value="ProductOptionValueRadioButtonsWithDiscountedPrice"/> + <argument name="customOptionValue" value="ProductOptionValueRadioButtons1"/> <argument name="productPrice" value="$156.77"/> - <argument name="productFinalPrice" value="$122.81"/> + <argument name="productFinalPrice" value="$144.47"/> </actionGroup> <!-- Add product 1 to cart --> @@ -105,7 +105,7 @@ <!-- Assert sub total on mini shopping cart --> <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> - <argument name="subTotal" value="$122.81"/> + <argument name="subTotal" value="$144.47"/> </actionGroup> </test> </tests> From e59de414e184be5eee0608b94509193dcfe37963 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Tue, 22 Sep 2020 12:34:25 +0300 Subject: [PATCH 0542/1013] magento/magento2#13440 - Adding custom attribute to category doesn't show store specific value - integration test fix. --- .../Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php index e2f8a2d5e4c21..3f9ab815038cc 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php @@ -9,6 +9,9 @@ use Magento\Eav\Model\Config as EavConfig; use Magento\TestFramework\Helper\Bootstrap; +/** + * @magentoAppArea adminhtml + */ class AttributesTest extends \PHPUnit\Framework\TestCase { /** From 2e88be578d77718412e7cc4cc5f94b27fdb6d922 Mon Sep 17 00:00:00 2001 From: "taras.gamanov" <engcom-vendorworker-hotel@adobe.com> Date: Tue, 22 Sep 2020 14:29:02 +0300 Subject: [PATCH 0543/1013] testCaseId has been added. --- .../CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml index 4c473f06a8884..b4ee0144657af 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdminCategorySearchTest.xml @@ -16,6 +16,7 @@ <description value="Global search in backend can search into Categories."/> <severity value="MINOR"/> <group value="Search"/> + <testCaseId value="MC-37809"/> </annotations> <before> <!-- Login as admin --> From fee7c0cf8b18a1fc2d4ef6b8f127189efff77565 Mon Sep 17 00:00:00 2001 From: "taras.gamanov" <engcom-vendorworker-hotel@adobe.com> Date: Tue, 22 Sep 2020 15:44:30 +0300 Subject: [PATCH 0544/1013] Code refactoring, fixture has been updated. --- .../Adminhtml/Order/AddToPackageTest.php | 39 ++++++++++++--- .../_files/shipping_with_carrier_data.php | 49 +++++++++++++++++++ .../shipping_with_carrier_data_rollback.php | 9 ++++ 3 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data.php create mode 100644 dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php index 0455181d42b00..fbbc6ef25cc09 100644 --- a/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php +++ b/dev/tests/integration/testsuite/Magento/Shipping/Block/Adminhtml/Order/AddToPackageTest.php @@ -6,12 +6,15 @@ namespace Magento\Shipping\Block\Adminhtml\Order; use Magento\Backend\Block\Template; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; -use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\ShipmentTrackInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; +use Magento\Sales\Api\OrderRepositoryInterface; /** * Class verifies packaging popup. @@ -20,34 +23,54 @@ */ class AddToPackageTest extends TestCase { + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + /** @var ObjectManagerInterface */ private $objectManager; /** @var Registry */ private $registry; - /** - * @var OrderInterfaceFactory|mixed - */ - private $orderFactory; protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->registry = $this->objectManager->get(Registry::class); - $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + } + + /** + * Loads order entity by provided order increment ID. + * + * @param string $incrementId + * @return OrderInterface + */ + private function getOrderByIncrementId(string $incrementId) : OrderInterface + { + /** @var SearchCriteria $searchCriteria */ + $searchCriteria = $this->objectManager->get(SearchCriteriaBuilder::class) + ->addFilter('increment_id', $incrementId) + ->create(); + + $items = $this->orderRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); } /** * Test that Packaging popup renders * - * @magentoDataFixture Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php + * @magentoDataFixture Magento/Shipping/_files/shipping_with_carrier_data.php */ public function testGetCommentsHtml() { /** @var Template $block */ $block = $this->objectManager->get(Packaging::class); - $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $order = $this->getOrderByIncrementId('100000001'); /** @var ShipmentTrackInterface $track */ $shipment = $order->getShipmentsCollection()->getFirstItem(); diff --git a/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data.php b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data.php new file mode 100644 index 0000000000000..736487ac5c006 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data.php @@ -0,0 +1,49 @@ +<?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\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Framework\DB\Transaction; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_with_customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->get(Transaction::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); +$order->setShippingDescription('UPS Next Day Air') + ->setShippingMethod('ups_11') + ->setShippingAmount(0) + ->setCouponCode('1234567890') + ->setDiscountDescription('1234567890'); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); + +$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/Shipping/_files/shipping_with_carrier_data_rollback.php b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data_rollback.php new file mode 100644 index 0000000000000..bbb90e0326aec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Shipping/_files/shipping_with_carrier_data_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'); From c6220b70013b3fe105f6d2d3d3b2e65dcaf4222b Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Tue, 22 Sep 2020 16:48:43 +0300 Subject: [PATCH 0545/1013] MC-23908: Tax estimation fails on CI --- ...ontCartShippingMethodSelectActionGroup.xml | 3 +- .../Section/CheckoutCartSummarySection.xml | 2 +- ...tCheckoutUsingFreeShippingAndTaxesTest.xml | 157 ++++++------------ 3 files changed, 55 insertions(+), 107 deletions(-) diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml index d9f8c17a81545..f8f24331d04ff 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml @@ -8,8 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <!-- Select Shipping Method on Cart --> - <actionGroup name="StorefrontCartShippingMethodSelectActionGroup"> + <actionGroup name="StorefrontCartPageSelectShippingMethodActionGroup"> <annotations> <description>Select a shipping method in the Estimate Shipping and Tax block on the Storefront Shopping Cart page.</description> </annotations> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index 4ec45e7b26759..960b070b08f3d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -35,7 +35,7 @@ <element name="methodName" type="text" selector="#co-shipping-method-form label"/> <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> - <element name="shippingMethodChecked" type="radio" parameterized="true" selector="#s_method_{{carrierCode}}_{{methodCode}}:checked"/> + <element name="shippingMethodChecked" type="radio" parameterized="true" selector="#co-shipping-method-form #s_method_{{carrierCode}}_{{methodCode}}:checked"/> <element name="estimateShippingAndTaxForm" type="block" selector="#shipping-zip-form"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml index 24460738e1c20..e731b31d33abc 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -14,82 +14,73 @@ <title value="Verify guest checkout using free shipping and tax variations"/> <description value="Verify guest checkout using free shipping and tax variations"/> <severity value="CRITICAL"/> - <testCaseId value="MC-14709"/> + <testCaseId value="MC-28285"/> <group value="mtf_migrated"/> + <group value="checkout"/> </annotations> <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> - <createData entity="MinimumOrderAmount100" stepKey="minimumOrderAmount100"/> + <createData entity="MinimumOrderAmount100" stepKey="minimumOrderAmount"/> <createData entity="taxRate_US_NY_8_1" stepKey="createTaxRateUSNY"/> <createData entity="DefaultTaxRuleWithCustomTaxRate" stepKey="createTaxRuleUSNY"> <requiredEntity createDataKey="createTaxRateUSNY" /> </createData> - - <!--Create Simple Product --> <createData entity="defaultSimpleProduct" stepKey="simpleProduct"> <field key="price">10.00</field> </createData> - - <!-- Create the configurable product with product Attribute options--> <createData entity="ApiCategory" stepKey="createCategory"/> - <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> + <createData entity="ApiConfigurableProduct" stepKey="configurableProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> - <createData entity="productAttributeOption1" stepKey="createConfigProductAttributeOption1"> - <requiredEntity createDataKey="createConfigProductAttribute"/> + <createData entity="productAttributeWithTwoOptions" stepKey="createProductAttribute"/> + <createData entity="productAttributeOption1" stepKey="createProductAttributeOption"> + <requiredEntity createDataKey="createProductAttribute"/> </createData> - <createData entity="AddToDefaultSet" stepKey="addToDefaultSet"> - <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="createProductAttribute"/> </createData> - <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> - <requiredEntity createDataKey="createConfigProductAttribute"/> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getProductAttributeOption"> + <requiredEntity createDataKey="createProductAttribute"/> </getData> - - <createData entity="ApiSimpleOne" stepKey="createConfigChildProduct1"> - <requiredEntity createDataKey="createConfigProductAttribute"/> - <requiredEntity createDataKey="getConfigAttributeOption1"/> + <createData entity="ApiSimpleOne" stepKey="configurableChildProduct"> + <requiredEntity createDataKey="createProductAttribute"/> + <requiredEntity createDataKey="getProductAttributeOption"/> <field key="price">10.00</field> </createData> - <createData entity="ConfigurableProductTwoOptions" stepKey="createConfigProductOption"> - <requiredEntity createDataKey="createConfigProduct"/> - <requiredEntity createDataKey="createConfigProductAttribute"/> - <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="configurableProduct"/> + <requiredEntity createDataKey="createProductAttribute"/> + <requiredEntity createDataKey="getProductAttributeOption"/> </createData> - <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> - <requiredEntity createDataKey="createConfigProduct"/> - <requiredEntity createDataKey="createConfigChildProduct1"/> + <createData entity="ConfigurableProductAddChild" stepKey="configurableProductAddChild"> + <requiredEntity createDataKey="configurableProduct"/> + <requiredEntity createDataKey="configurableChildProduct"/> </createData> - - <!-- Create Bundle Product --> - <createData entity="SimpleProduct2" stepKey="simpleProduct1"> + <createData entity="SimpleProduct2" stepKey="firstBundleChildProduct"> <field key="price">100.00</field> </createData> - <createData entity="SimpleProduct2" stepKey="simpleProduct2"> + <createData entity="SimpleProduct2" stepKey="secondBundleChildProduct"> <field key="price">200.00</field> </createData> - <!--Create Bundle product with multi select option--> - <createData entity="BundleProductPriceViewRange" stepKey="createBundleProduct"> + <createData entity="BundleProductPriceViewRange" stepKey="bundleProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <createData entity="MultipleSelectOption" stepKey="createBundleOption1_1"> - <requiredEntity createDataKey="createBundleProduct"/> + <createData entity="MultipleSelectOption" stepKey="bundleOption"> + <requiredEntity createDataKey="bundleProduct"/> <field key="required">True</field> </createData> - <createData entity="ApiBundleLink" stepKey="linkOptionToProduct"> - <requiredEntity createDataKey="createBundleProduct"/> - <requiredEntity createDataKey="createBundleOption1_1"/> - <requiredEntity createDataKey="simpleProduct1"/> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToProduct"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="firstBundleChildProduct"/> </createData> - <createData entity="ApiBundleLink" stepKey="linkOptionToProduct2"> - <requiredEntity createDataKey="createBundleProduct"/> - <requiredEntity createDataKey="createBundleOption1_1"/> - <requiredEntity createDataKey="simpleProduct2"/> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToProduct"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="bundleOption"/> + <requiredEntity createDataKey="secondBundleChildProduct"/> </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> @@ -100,64 +91,44 @@ <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> - <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigProduct1"/> - <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> - <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteProductAttribute"/> - <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> - <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> - <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="configurableChildProduct" stepKey="deleteConfigurableChildProduct"/> + <deleteData createDataKey="configurableProduct" stepKey="deleteConfigurableProduct"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="firstBundleChildProduct" stepKey="deleteFirstBundleChild"/> + <deleteData createDataKey="secondBundleChildProduct" stepKey="deleteSecondBundleChild"/> + <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createTaxRuleUSNY" stepKey="deleteTaxRuleUSNY"/> <deleteData createDataKey="createTaxRateUSNY" stepKey="deleteTaxRateUSNY"/> <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - - <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - - <!--Open Product page in StoreFront and assert product and price range --> <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> - <argument name="product" value="$$simpleProduct$$"/> + <argument name="product" value="$simpleProduct$"/> </actionGroup> - - <!--Add product to the cart --> <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="addProductToTheCart"> <argument name="productQty" value="1"/> </actionGroup> - - <!-- Add Configurable Product to the cart --> <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> - <argument name="urlKey" value="$$createConfigProduct.custom_attributes[url_key]$$" /> - <argument name="productAttribute" value="$$createConfigProductAttribute.default_value$$"/> - <argument name="productOption" value="$$getConfigAttributeOption1.label$$"/> + <argument name="urlKey" value="$configurableProduct.custom_attributes[url_key]$" /> + <argument name="productAttribute" value="$createProductAttribute.default_value$"/> + <argument name="productOption" value="$getProductAttributeOption.label$"/> <argument name="qty" value="1"/> </actionGroup> - - <!--Open Product page in StoreFront --> - <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openBundleProduct"> - <argument name="product" value="$$createBundleProduct$$"/> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyBundleProduct"> + <argument name="product" value="$bundleProduct$"/> </actionGroup> - - <!-- Click on customize And Add To Cart Button --> - <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddtoCartButton"/> - - <!-- Select Two Products, enter the quantity and add product to the cart --> - <selectOption selector="{{StorefrontBundledSection.multiSelectOption}}" userInput="$$simpleProduct1.name$$ +$100.00" stepKey="selectOption"/> - <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> - <argument name="quantity" value="1"/> + <actionGroup ref="StorefrontAddBundleProductFromProductToCartWithMultiOptionActionGroup" stepKey="addBundleProductToCart"> + <argument name="productName" value="$bundleProduct.name$"/> + <argument name="optionName" value="$bundleOption.name$"/> + <argument name="value" value="$firstBundleChildProduct.name$ +$100.00"/> </actionGroup> - - <!--Open View and edit --> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="clickMiniCart"/> - - <!-- Fill the Estimate Shipping and Tax section --> <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"> <argument name="address" value="US_Address_NY_Default_Shipping"/> </actionGroup> - - <!-- Select Free Shipping Method on Cart --> - <actionGroup ref="StorefrontCartShippingMethodSelectActionGroup" stepKey="selectFreeShippingShippingMethod"> + <actionGroup ref="StorefrontCartPageSelectShippingMethodActionGroup" stepKey="selectFreeShippingShippingMethod"> <argument name="carrierCode" value="freeshipping"/> <argument name="methodCode" value="freeshipping"/> </actionGroup> @@ -165,47 +136,25 @@ <reloadPage stepKey="reloadThePage"/> <waitForPageLoad stepKey="waitForPageToReload"/> <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.72" stepKey="seeTaxAmountAfterLoadPage"/> - - <!-- Proceed to checkout --> <scrollTo selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="scrollToProceedToCheckout" /> <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForPageToLoad"/> - - <!-- Fill Guest form --> <actionGroup ref="FillGuestCheckoutShippingAddressFormActionGroup" stepKey="fillTheSignInForm"> <argument name="customer" value="Simple_US_Customer"/> <argument name="customerAddress" value="US_Address_NY_Default_Shipping"/> </actionGroup> <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickOnNextButton"/> - - <!-- Place order and Assert success message --> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickOnPlaceOrder"/> - - <!-- Assert empty Mini Cart --> <seeElement selector="{{StorefrontMinicartSection.emptyMiniCart}}" stepKey="assertEmptyCart" /> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> - - <!-- Open Order Index Page --> <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> - - <!-- Filter Order using orderId and assert order--> - <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrderById"> <argument name="orderId" value="$orderId"/> </actionGroup> - <click selector="{{AdminOrdersGridSection.viewLink('$orderId')}}" stepKey="clickOnViewLink"/> - <waitForPageLoad stepKey="waitForOrderPageToLoad"/> - - <!-- Assert order buttons --> <actionGroup ref="AdminAssertOrderAvailableButtonsActionGroup" stepKey="assertOrderButtons"/> - - <!-- Assert Grand Total --> <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$129.72" stepKey="seeGrandTotal"/> - <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Pending" stepKey="seeOrderStatus"/> - - <!-- Ship the order and assert the status --> + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeOrderPendingStatus"/> <actionGroup ref="AdminShipThePendingOrderActionGroup" stepKey="shipTheOrder"/> - - <!-- Assert customer order address --> <actionGroup ref="AssertOrderAddressInformationActionGroup" stepKey="assertCustomerInformation"> <argument name="customer" value=""/> <argument name="shippingAddress" value="US_Address_NY_Default_Shipping"/> From 819fc7cfef122559826edbfd17ce397527461ae1 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Tue, 22 Sep 2020 16:56:13 +0300 Subject: [PATCH 0546/1013] refactor --- .../Model/Import/Product.php | 126 +++++++++--------- 1 file changed, 65 insertions(+), 61 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 4384cec88bc46..e249e5b3722e7 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -45,9 +45,10 @@ * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @since 100.0.2 */ -class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity +class Product extends AbstractEntity { - const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types'; + public const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types'; + private const HASH_ALGORITHM = 'sha256'; /** * Size of bunch - part of products to save in one step. @@ -1749,43 +1750,12 @@ protected function _saveProducts() $position = 0; foreach ($rowImages as $column => $columnImages) { foreach ($columnImages as $columnImageKey => $columnImage) { - $hash = ''; - if (filter_var($columnImage, FILTER_VALIDATE_URL) === false) { - $filename = $importDir . DIRECTORY_SEPARATOR . $columnImage; - if ($this->fileDriver->isExists($filename)) { - $hash = hash_file( - 'sha256', - $importDir . DIRECTORY_SEPARATOR . $columnImage - ); - } - } else { - $hash = hash_file('sha256', $columnImage); - } + $filePath = filter_var($columnImage, FILTER_VALIDATE_URL) + ? $columnImage + : $importDir . DS . $columnImage; - // Add new images - if (empty($rowExistingImages)) { - $imageAlreadyExists = false; - } else { - $imageAlreadyExists = array_reduce( - $rowExistingImages, - function ($exists, $file) use ($hash) { - if ($exists) { - return $exists; - } - - if (isset($file['hash']) && $file['hash'] === $hash) { - return $file['value']; - } - - return $exists; - }, - '' - ); - } - - if ($imageAlreadyExists) { - $uploadedFile = $imageAlreadyExists; - } elseif (!isset($uploadedImages[$columnImage])) { + $uploadedFile = $this->getAlreadyExistedImage($rowExistingImages, $filePath); + if (!$uploadedFile && !isset($uploadedImages[$columnImage])) { $uploadedFile = $this->uploadMediaFiles($columnImage); $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); if ($uploadedFile) { @@ -1800,7 +1770,7 @@ function ($exists, $file) use ($hash) { ProcessingError::ERROR_LEVEL_NOT_CRITICAL ); } - } else { + } elseif (isset($uploadedImages[$columnImage])) { $uploadedFile = $uploadedImages[$columnImage]; } @@ -1954,24 +1924,14 @@ function ($exists, $file) use ($hash) { } } - $this->saveProductEntity( - $entityRowsIn, - $entityRowsUp - )->_saveProductWebsites( - $this->websitesCache - )->_saveProductCategories( - $this->categoriesCache - )->_saveProductTierPrices( - $tierPrices - )->_saveMediaGallery( - $mediaGallery - )->_saveProductAttributes( - $attributes - )->updateMediaGalleryVisibility( - $imagesForChangeVisibility - )->updateMediaGalleryLabels( - $labelsForUpdate - ); + $this->saveProductEntity($entityRowsIn, $entityRowsUp) + ->_saveProductWebsites($this->websitesCache) + ->_saveProductCategories($this->categoriesCache) + ->_saveProductTierPrices($tierPrices) + ->_saveMediaGallery($mediaGallery) + ->_saveProductAttributes($attributes) + ->updateMediaGalleryVisibility($imagesForChangeVisibility) + ->updateMediaGalleryLabels($labelsForUpdate); $this->_eventManager->dispatch( 'catalog_product_import_bunch_save_after', @@ -1985,6 +1945,51 @@ function ($exists, $file) use ($hash) { // phpcs:enable + /** + * Returns image hash by path + * + * @param string $filePath + * @return string + */ + private function getFileHash(string $filePath): string + { + try { + $fileExists = $this->fileDriver->isExists($filePath); + } catch (\Exception $exception) { + $fileExists = false; + } + + return $fileExists ? hash_file(self::HASH_ALGORITHM, $filePath) : ''; + } + + /** + * Returns existed image + * + * @param array $imageRow + * @param string $filePath + * @return string + */ + private function getAlreadyExistedImage(array $imageRow, string $filePath): string + { + $hash = $this->getFileHash($filePath); + + return array_reduce( + $imageRow, + function ($exists, $file) use ($hash) { + if ($exists) { + return $exists; + } + + if (isset($file['hash']) && $file['hash'] === $hash) { + return $file['value']; + } + + return $exists; + }, + '' + ); + } + /** * Generate hashes for existing images for comparison with newly uploaded images. * @@ -2001,7 +2006,7 @@ private function addImageHashes(array &$images): void foreach ($files as $path => $file) { if ($this->fileDriver->isExists($productMediaPath . $file['value'])) { $fileName = $productMediaPath . $file['value']; - $images[$storeId][$sku][$path]['hash'] = hash_file('sha256', $fileName); + $images[$storeId][$sku][$path]['hash'] = $this->getFileHash($fileName); } } } @@ -2019,9 +2024,8 @@ private function clearNoSelectionImages($rowImages, $rowData) { foreach ($rowImages as $column => $columnImages) { foreach ($columnImages as $key => $image) { - if ($image == 'no_selection') { - unset($rowImages[$column][$key]); - unset($rowData[$column]); + if ($image === 'no_selection') { + unset($rowImages[$column][$key], $rowData[$column]); } } } From a1673bb085020222124db45db08421b49e67f5cd Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Tue, 22 Sep 2020 16:59:03 +0300 Subject: [PATCH 0547/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index d9b62ef8fc913..8efc5b1c5769c 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -67,7 +67,9 @@ <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> <!-- Save and apply the new catalog price rule --> - <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> + <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForSave"/> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> From cebb5becc68549a26f7c404b471e0f6b95f66a84 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Tue, 22 Sep 2020 17:25:34 +0300 Subject: [PATCH 0548/1013] magento/magento2#13440 - Adding custom attribute to category doesn't show store specific value - SVC test fix. --- app/code/Magento/Catalog/Model/Category/DataProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index 6a500c326b358..f3e3caf309059 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -157,7 +157,7 @@ class DataProvider extends ModifierPoolDataProvider /** * @var DataInterfaceFactory */ - protected $uiConfigFactory; + private $uiConfigFactory; /** * @var ScopeOverriddenValue @@ -183,6 +183,7 @@ class DataProvider extends ModifierPoolDataProvider * @var AuthorizationInterface */ private $auth; + /** * @var Image */ From ef3e4656372ae61b183b0a5f3feed022ea7e2233 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Tue, 22 Sep 2020 09:37:55 -0500 Subject: [PATCH 0549/1013] MC-37582: Fix Failing WebAPI GraphQL tests when DB table prefixes are enabled - fix test --- .../CategoriesQuery/CategoryTreeTest.php | 14 +++++++++----- .../Magento/GraphQl/Catalog/CategoryTest.php | 17 +++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php index c2e82e734cd9b..641253cc34c2c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php @@ -11,9 +11,11 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -564,10 +566,12 @@ public function testCategoryImage(?string $imagePrefix) ->addAttributeToFilter('name', ['eq' => 'Parent Image Category']) ->getFirstItem(); $categoryId = $categoryModel->getId(); + /** @var ResourceConnection $resourceConnection */ + $resourceConnection = Bootstrap::getObjectManager()->create(ResourceConnection::class); + $connection = $resourceConnection->getConnection(); if ($imagePrefix !== null) { // update image to account for different stored image formats - $connection = $categoryCollection->getConnection(); $productLinkField = $this->metadataPool ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) ->getLinkField(); @@ -577,20 +581,20 @@ public function testCategoryImage(?string $imagePrefix) $imageAttributeValue = $imagePrefix . basename($categoryModel->getImage()); if (!empty($imageAttributeValue)) { - $query = sprintf( + $sqlQuery = sprintf( 'UPDATE %s SET `value` = "%s" ' . 'WHERE `%s` = %d ' . 'AND `store_id`= %d ' . 'AND `attribute_id` = ' . '(SELECT `ea`.`attribute_id` FROM %s ea WHERE `ea`.`attribute_code` = "image" LIMIT 1)', - $connection->getTableName('catalog_category_entity_varchar'), + $resourceConnection->getTableName('catalog_category_entity_varchar'), $imageAttributeValue, $productLinkField, $categoryModel->getData($productLinkField), $defaultStoreId, - $connection->getTableName('eav_attribute') + $resourceConnection->getTableName('eav_attribute') ); - $connection->query($query); + $connection->query($sqlQuery); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index 463b07c7261a6..f0c12ac83f4b1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -12,11 +12,13 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -587,9 +589,12 @@ public function testCategoryImage(?string $imagePrefix) ->getFirstItem(); $categoryId = $categoryModel->getId(); + /** @var ResourceConnection $resourceConnection */ + $resourceConnection = Bootstrap::getObjectManager()->create(ResourceConnection::class); + $connection = $resourceConnection->getConnection(); + if ($imagePrefix !== null) { - // update image to account for different stored image formats - $connection = $categoryCollection->getConnection(); + // update image to account for different stored image format $productLinkField = $this->metadataPool ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) ->getLinkField(); @@ -599,20 +604,20 @@ public function testCategoryImage(?string $imagePrefix) $imageAttributeValue = $imagePrefix . basename($categoryModel->getImage()); if (!empty($imageAttributeValue)) { - $query = sprintf( + $sqlQuery = sprintf( 'UPDATE %s SET `value` = "%s" ' . 'WHERE `%s` = %d ' . 'AND `store_id`= %d ' . 'AND `attribute_id` = ' . '(SELECT `ea`.`attribute_id` FROM %s ea WHERE `ea`.`attribute_code` = "image" LIMIT 1)', - $connection->getTableName('catalog_category_entity_varchar'), + $resourceConnection->getTableName('catalog_category_entity_varchar'), $imageAttributeValue, $productLinkField, $categoryModel->getData($productLinkField), $defaultStoreId, - $connection->getTableName('eav_attribute') + $resourceConnection->getTableName('eav_attribute') ); - $connection->query($query); + $connection->query($sqlQuery); } } From 79fe484078dcff193cd66fa4ed9d690a368774ae Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 22 Sep 2020 10:20:58 -0500 Subject: [PATCH 0550/1013] MC-37745: Missing products from categories, indexing related --- app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index a38d671bab100..f72516d28c46f 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -96,7 +96,7 @@ class Fulltext implements * @param StateFactory $indexScopeStateFactory * @param DimensionProviderInterface $dimensionProvider * @param array $data - * @param ProcessManager|null $processManager + * @param ProcessManager $processManager * @param int|null $batchSize * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -108,7 +108,7 @@ public function __construct( StateFactory $indexScopeStateFactory, DimensionProviderInterface $dimensionProvider, array $data, - ?ProcessManager $processManager = null, + ProcessManager $processManager = null, ?int $batchSize = null ) { $this->fullAction = $fullActionFactory->create(['data' => $data]); From a9c369113d78b3a9ba5e283b81631fb3c213c1db Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 22 Sep 2020 10:26:48 -0500 Subject: [PATCH 0551/1013] MC-37745: Missing products from categories, indexing related --- .../CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php index d07b15dbfd5d9..241f00de825d9 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/FulltextTest.php @@ -144,7 +144,7 @@ private function setupDataProvider($stores) $dimension = $this->getMockBuilder(Dimension::class) ->disableOriginalConstructor() ->getMock(); - $dimension->expects($this->once()) + $dimension->expects($this->any()) ->method('getValue') ->willReturn($storeId); From bcaa4561c5935b183452c0434d0340fa96b13265 Mon Sep 17 00:00:00 2001 From: Deepty Thampy <dthampy@adobe.com> Date: Tue, 22 Sep 2020 13:44:33 -0500 Subject: [PATCH 0552/1013] MC-37582: Fix Failing WebAPI GraphQL tests when DB table prefixes are enabled - fix static failure --- .../testsuite/Magento/GraphQl/Catalog/CategoryTest.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index f0c12ac83f4b1..f086a2211b51d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -17,13 +17,15 @@ use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; -use Magento\TestFramework\ObjectManager; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQl\ResponseContainsErrorsException; use Magento\TestFramework\TestCase\GraphQlAbstract; /** * Test loading of category tree + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryTest extends GraphQlAbstract { @@ -49,7 +51,7 @@ class CategoryTest extends GraphQlAbstract protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->categoryRepository = $this->objectManager->get(CategoryRepository::class); $this->store = $this->objectManager->get(Store::class); $this->metadataPool = $this->objectManager->get(MetadataPool::class); From 5ca23336e4955110bd5ec0d2c5408681ad3a6b98 Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Tue, 22 Sep 2020 14:55:46 -0500 Subject: [PATCH 0553/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Model/Indexer/Design/IndexerHandler.php | 59 +++++++++++++++++++ app/code/Magento/Theme/etc/indexer.xml | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php diff --git a/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php new file mode 100644 index 0000000000000..f1acf01b4fc81 --- /dev/null +++ b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Theme\Model\Indexer\Design; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Indexer\IndexStructureInterface; +use Magento\Framework\Indexer\SaveHandler\Batch; +use Magento\Framework\Indexer\SaveHandler\Grid; +use Magento\Framework\Indexer\ScopeResolver\FlatScopeResolver; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; + +class IndexerHandler extends Grid +{ + /** + * @var FlatScopeResolver + */ + private $flatScopeResolver; + + public function __construct( + IndexStructureInterface $indexStructure, + ResourceConnection $resource, + Batch $batch, + IndexScopeResolver $indexScopeResolver, + FlatScopeResolver $flatScopeResolver, + array $data, + $batchSize = 100) + { + parent::__construct( + $indexStructure, + $resource, + $batch, + $indexScopeResolver, + $flatScopeResolver, + $data, + $batchSize); + + $this->flatScopeResolver = $flatScopeResolver; + } + + /** + * Clean index table by truncation + * + * @inheritdoc + */ + public function cleanIndex($dimensions) + { + $adapter = $this->resource->getConnection('write'); + $tableName = $this->flatScopeResolver->resolve($this->getIndexName(), $dimensions); + if ($adapter->isTableExists($tableName)) { + $adapter->truncateTable($tableName); + } + } +} diff --git a/app/code/Magento/Theme/etc/indexer.xml b/app/code/Magento/Theme/etc/indexer.xml index 7ed25878e383c..8cc8971024e48 100644 --- a/app/code/Magento/Theme/etc/indexer.xml +++ b/app/code/Magento/Theme/etc/indexer.xml @@ -17,7 +17,7 @@ <field name="store_group_id" xsi:type="filterable" dataType="int"/> <field name="store_id" xsi:type="filterable" dataType="int"/> </fieldset> - <saveHandler class="Magento\Framework\Indexer\SaveHandler\Grid"/> + <saveHandler class="Magento\Theme\Model\Indexer\Design\IndexerHandler"/> <structure class="Magento\Framework\Indexer\GridStructure"/> </indexer> </config> From df8592f4453133c8878f27adf9c9eb5a60c74d69 Mon Sep 17 00:00:00 2001 From: Maksym Aposov <maposov@magento.com> Date: Tue, 22 Sep 2020 16:02:10 -0500 Subject: [PATCH 0554/1013] MC-37307: Code generation failed for *ExtensionInterfaceFactory - Fix static tests - Refactor integration tests --- .../Magento/Framework/Code/GeneratorTest.php | 149 ++++++++---------- ...ionAttributesInterfaceFactoryGenerator.php | 2 +- .../ObjectManager/Code/Generator/Factory.php | 5 +- 3 files changed, 69 insertions(+), 87 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php index d2c4b1dd70d75..e19d0a7364b27 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/GeneratorTest.php @@ -27,6 +27,9 @@ class GeneratorTest extends TestCase { const CLASS_NAME_WITH_NAMESPACE = GeneratorTest\SourceClassWithNamespace::class; const CLASS_NAME_WITH_NESTED_NAMESPACE = GeneratorTest\NestedNamespace\SourceClassWithNestedNamespace::class; + const EXTENSION_CLASS_NAME_WITH_NAMESPACE = GeneratorTest\SourceClassWithNamespaceExtension::class; + const EXTENSION_CLASS_NAME_WITH_NESTED_NAMESPACE = + GeneratorTest\NestedNamespace\SourceClassWithNestedNamespaceExtension::class; /** * @var Generator @@ -62,6 +65,7 @@ protected function setUp(): void /** @var Filesystem $filesystem */ $filesystem = $objectManager->get(Filesystem::class); $this->generatedDirectory = $filesystem->getDirectoryWrite(DirectoryList::GENERATED_CODE); + $this->generatedDirectory->create($this->testRelativePath); $this->logDirectory = $filesystem->getDirectoryRead(DirectoryList::LOG); $generatedDirectoryAbsolutePath = $this->generatedDirectory->getAbsolutePath(); $this->_ioObject = new Generator\Io(new Filesystem\Driver\File(), $generatedDirectoryAbsolutePath); @@ -101,117 +105,99 @@ protected function _clearDocBlock($classBody) } /** - * Generates a new file with Factory class and compares with the sample from the - * SourceClassWithNamespaceFactory.php.sample file. + * Generates a new class Factory file and compares with the sample. + * + * @param $className + * @param $generateType + * @param $expectedDataPath + * @dataProvider generateClassFactoryDataProvider */ - public function testGenerateClassFactoryWithNamespace() + public function testGenerateClassFactory($className, $generateType, $expectedDataPath) { - $factoryClassName = self::CLASS_NAME_WITH_NAMESPACE . 'Factory'; + $factoryClassName = $className . $generateType; $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); $factory = Bootstrap::getObjectManager()->create($factoryClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE, $factory->create()); + $this->assertInstanceOf($className, $factory->create()); $content = $this->_clearDocBlock( file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) ); $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceFactory.php.sample') + file_get_contents(__DIR__ . $expectedDataPath) ); $this->assertEquals($expectedContent, $content); } /** - * Generates a new file with Factory class and compares with the sample from the - * SourceClassWithNestedNamespaceFactory.php.sample file. + * DataProvider for testGenerateClassFactory + * + * @return array */ - public function testGenerateClassFactoryWithNestedNamespace() + public function generateClassFactoryDataProvider() { - $factoryClassName = self::CLASS_NAME_WITH_NESTED_NAMESPACE . 'Factory'; - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); - $factory = Bootstrap::getObjectManager()->create($factoryClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NESTED_NAMESPACE, $factory->create()); - $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNestedNamespaceFactory.php.sample') - ); - $this->assertEquals($expectedContent, $content); + return [ + 'factory_with_namespace' => [ + 'className' => self::CLASS_NAME_WITH_NAMESPACE, + 'generateType' => 'Factory', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceFactory.php.sample' + ], + 'factory_with_nested_namespace' => [ + 'classToGenerate' => self::CLASS_NAME_WITH_NESTED_NAMESPACE, + 'generateType' => 'Factory', + 'expectedDataPath' => '/_expected/SourceClassWithNestedNamespaceFactory.php.sample' + ], + 'ext_interface_factory_with_namespace' => [ + 'classToGenerate' => self::EXTENSION_CLASS_NAME_WITH_NAMESPACE, + 'generateType' => 'InterfaceFactory', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceExtensionInterfaceFactory.php.sample' + ], + 'ext_interface_factory_with_nested_namespace' => [ + 'classToGenerate' => self::EXTENSION_CLASS_NAME_WITH_NESTED_NAMESPACE, + 'generateType' => 'InterfaceFactory', + 'expectedDataPath' => '/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample' + ], + ]; } /** - * Generates a new file with ExtensionInterfaceFactory class and compares with the sample from the - * SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample file. + * @param $className + * @param $generateType + * @param $expectedDataPath + * @dataProvider generateClassDataProvider */ - public function testGenerateClassExtensionAttributesInterfaceFactoryWithNestedNamespace() + public function testGenerateClass($className, $generateType, $expectedDataPath) { - $factoryClassName = self::CLASS_NAME_WITH_NESTED_NAMESPACE . 'ExtensionInterfaceFactory'; - $this->generatedDirectory->create($this->testRelativePath); - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); - $factory = Bootstrap::getObjectManager()->create($factoryClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NESTED_NAMESPACE . 'Extension', $factory->create()); + $generateClassName = $className . $generateType; + $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($generateClassName)); + $instance = Bootstrap::getObjectManager()->create($generateClassName); + $this->assertInstanceOf($className, $instance); $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) + file_get_contents($this->_ioObject->generateResultFileName($generateClassName)) ); $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNestedNamespaceExtensionInterfaceFactory.php.sample') + file_get_contents(__DIR__ . $expectedDataPath) ); $this->assertEquals($expectedContent, $content); } /** - * Generates a new file with Proxy class and compares with the sample from the - * SourceClassWithNamespaceProxy.php.sample file. + * DataProvider for testGenerateClass + * + * @return array */ - public function testGenerateClassProxyWithNamespace() + public function generateClassDataProvider() { - $proxyClassName = self::CLASS_NAME_WITH_NAMESPACE . '\Proxy'; - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($proxyClassName)); - $proxy = Bootstrap::getObjectManager()->create($proxyClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE, $proxy); - $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($proxyClassName)) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceProxy.php.sample') - ); - $this->assertEquals($expectedContent, $content); - } - - /** - * Generates a new file with Interceptor class and compares with the sample from the - * SourceClassWithNamespaceInterceptor.php.sample file. - */ - public function testGenerateClassInterceptorWithNamespace() - { - $interceptorClassName = self::CLASS_NAME_WITH_NAMESPACE . '\Interceptor'; - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($interceptorClassName)); - $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($interceptorClassName)) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceInterceptor.php.sample') - ); - $this->assertEquals($expectedContent, $content); - } - - /** - * Generates a new file with ExtensionInterfaceFactory class and compares with the sample from the - * SourceClassWithNamespaceExtensionInterfaceFactory.php.sample file. - */ - public function testGenerateClassExtensionAttributesInterfaceFactoryWithNamespace() - { - $factoryClassName = self::CLASS_NAME_WITH_NAMESPACE . 'ExtensionInterfaceFactory'; - $this->generatedDirectory->create($this->testRelativePath); - $this->assertEquals(Generator::GENERATION_SUCCESS, $this->_generator->generateClass($factoryClassName)); - $factory = Bootstrap::getObjectManager()->create($factoryClassName); - $this->assertInstanceOf(self::CLASS_NAME_WITH_NAMESPACE . 'Extension', $factory->create()); - $content = $this->_clearDocBlock( - file_get_contents($this->_ioObject->generateResultFileName($factoryClassName)) - ); - $expectedContent = $this->_clearDocBlock( - file_get_contents(__DIR__ . '/_expected/SourceClassWithNamespaceExtensionInterfaceFactory.php.sample') - ); - $this->assertEquals($expectedContent, $content); + return [ + 'proxy' => [ + 'className' => self::CLASS_NAME_WITH_NAMESPACE, + 'generateType' => '\Proxy', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceProxy.php.sample' + ], + 'interceptor' => [ + 'className' => self::CLASS_NAME_WITH_NAMESPACE, + 'generateType' => '\Interceptor', + 'expectedDataPath' => '/_expected/SourceClassWithNamespaceInterceptor.php.sample' + ] + ]; } /** @@ -225,7 +211,6 @@ public function testGeneratorClassWithErrorSaveClassFile() $regexpMsgPart = preg_quote($msgPart); $this->expectException(\RuntimeException::class); $this->expectExceptionMessageMatches("/.*$regexpMsgPart.*/"); - $this->generatedDirectory->create($this->testRelativePath); $this->generatedDirectory->changePermissionsRecursively($this->testRelativePath, 0555, 0444); $generatorResult = $this->_generator->generateClass($factoryClassName); $this->assertFalse($generatorResult); diff --git a/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php b/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php index 017f76e780a45..531fa6763fc6e 100644 --- a/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php +++ b/lib/internal/Magento/Framework/Api/Code/Generator/ExtensionAttributesInterfaceFactoryGenerator.php @@ -45,7 +45,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ protected function getResultClassSuffix() { diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php index 2f9ff0533f88d..9e23d0ca86ca9 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Factory.php @@ -7,9 +7,6 @@ class Factory extends \Magento\Framework\Code\Generator\EntityAbstract { - /** - * Entity type - */ const ENTITY_TYPE = 'factory'; /** @@ -90,7 +87,7 @@ protected function _getClassMethods() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _validateData() { From 14e84126d62c157ac54951303322f6e307ee05bc Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Tue, 22 Sep 2020 16:52:57 -0500 Subject: [PATCH 0555/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Theme/Model/Indexer/Design/Config.php | 20 ++++++++++++++++++- app/code/Magento/Theme/etc/indexer.xml | 2 +- .../Framework/Indexer/SaveHandler/Grid.php | 14 +++++++++---- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Theme/Model/Indexer/Design/Config.php b/app/code/Magento/Theme/Model/Indexer/Design/Config.php index 43b58257b343c..86a72ebc64391 100644 --- a/app/code/Magento/Theme/Model/Indexer/Design/Config.php +++ b/app/code/Magento/Theme/Model/Indexer/Design/Config.php @@ -5,6 +5,7 @@ */ namespace Magento\Theme\Model\Indexer\Design; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\ActionInterface; use Magento\Framework\Indexer\FieldsetPool; use Magento\Framework\Indexer\HandlerPool; @@ -93,13 +94,30 @@ protected function execute(array $ids = []) /** @var \Magento\Theme\Model\ResourceModel\Design\Config\Scope\Collection $collection */ $collection = $this->collectionFactory->create(); $this->prepareFields(); - if (!count($ids)) { + + $tmp = $this->isFlatTableExists(); + + if (!$this->isFlatTableExists()) { + // instead of clean index check if table exists and create it if not $this->getSaveHandler()->cleanIndex([]); } $this->getSaveHandler()->deleteIndex([], new \ArrayObject($ids)); $this->getSaveHandler()->saveIndex([], $collection); } + private function isFlatTableExists() + { + /** @var \Magento\Framework\App\ResourceConnection $resource */ + $resource = ObjectManager::getInstance()->get(\Magento\Framework\App\ResourceConnection::class); + + /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ + $connection = ObjectManager::getInstance()->get(\Magento\Framework\App\ResourceConnection::class)->getConnection(); + + $tableName = $resource->getTableName('design_config_grid_flat'); + + return $connection->isTableExists($tableName); + } + /** * Execute full indexation * diff --git a/app/code/Magento/Theme/etc/indexer.xml b/app/code/Magento/Theme/etc/indexer.xml index 8cc8971024e48..7ed25878e383c 100644 --- a/app/code/Magento/Theme/etc/indexer.xml +++ b/app/code/Magento/Theme/etc/indexer.xml @@ -17,7 +17,7 @@ <field name="store_group_id" xsi:type="filterable" dataType="int"/> <field name="store_id" xsi:type="filterable" dataType="int"/> </fieldset> - <saveHandler class="Magento\Theme\Model\Indexer\Design\IndexerHandler"/> + <saveHandler class="Magento\Framework\Indexer\SaveHandler\Grid"/> <structure class="Magento\Framework\Indexer\GridStructure"/> </indexer> </config> diff --git a/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php b/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php index 71d937d016e9c..c5307b8823bce 100644 --- a/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php +++ b/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php @@ -69,10 +69,16 @@ protected function prepareFilterableFields(array $documents) */ public function deleteIndex($dimensions, \Traversable $ids) { - foreach ($this->batch->getItems($ids, $this->batchSize) as $batchIds) { - $this->connection->delete( - $this->getTableName('filterable', $dimensions), - ['entity_id IN(?)' => $batchIds] + if (!empty(iterator_to_array($ids))) { + foreach ($this->batch->getItems($ids, $this->batchSize) as $batchIds) { + $this->connection->delete( + $this->getTableName('filterable', $dimensions), + ['entity_id IN(?)' => $batchIds] + ); + } + } else { + $this->connection->truncateTable( + $this->getTableName('filterable', $dimensions) ); } } From fab6a16ab94189d640b6d0009f5cb76b95672852 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 22 Sep 2020 22:29:25 -0500 Subject: [PATCH 0556/1013] MC-36785: Unable to set YouTube API key by CLI --- .../Config/Source/ConfigStructureSource.php | 68 +++++++++++++++++++ .../App/Config/Source/ModularConfigSource.php | 58 +++------------- .../Config/Source/ModularConfigSourceTest.php | 24 +++---- app/code/Magento/Config/etc/di.xml | 4 ++ 4 files changed, 88 insertions(+), 66 deletions(-) create mode 100644 app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php diff --git a/app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php b/app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php new file mode 100644 index 0000000000000..b2831aee0144c --- /dev/null +++ b/app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\App\Config\Source; + +use Magento\Config\Model\Config\Structure\Reader; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ConfigSourceInterface; +use Magento\Framework\DataObject; + +/** + * Class for retrieving configuration structure + */ +class ConfigStructureSource implements ConfigSourceInterface +{ + /** + * @var Reader + */ + private $reader; + + /** + * @param Reader $reader + */ + public function __construct(Reader $reader) + { + $this->reader = $reader; + } + + /** + * @inheritdoc + */ + public function get($path = '') + { + $configStructure = $this->reader->read(Area::AREA_ADMINHTML); + $sections = $configStructure['config']['system']['sections'] ?? []; + $defaultConfig = $this->merge([], $sections); + $data = new DataObject(['default' => $defaultConfig]); + + return $data->getData($path); + } + + /** + * Merge existed config with config structure + * + * @param array $config + * @param array $sections + * @return array + */ + private function merge(array $config, array $sections): array + { + foreach ($sections as $section) { + if (isset($section['children'])) { + $config[$section['id']] = $this->merge( + $config[$section['id']] ?? [], + $section['children'] + ); + } elseif ($section['_elementType'] === 'field') { + $config += [$section['id'] => null]; + } + } + + return $config; + } +} diff --git a/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php b/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php index 17ea535951bd6..01cea0a8ee4e7 100644 --- a/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/ModularConfigSource.php @@ -5,12 +5,9 @@ */ namespace Magento\Config\App\Config\Source; -use Magento\Config\Model\Config\Structure\Reader as ConfigStructureReader; -use Magento\Framework\App\Area; use Magento\Framework\App\Config\ConfigSourceInterface; -use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; -use Magento\Framework\App\Config\Initial\Reader as InitialConfigReader; +use Magento\Framework\App\Config\Initial\Reader; /** * Class for retrieving initial configuration from modules @@ -21,26 +18,16 @@ class ModularConfigSource implements ConfigSourceInterface { /** - * @var InitialConfigReader + * @var Reader */ - private $initialConfigReader; + private $reader; /** - * @var ConfigStructureReader + * @param Reader $reader */ - private $configStructureReader; - - /** - * @param InitialConfigReader $initialConfigReader - * @param ConfigStructureReader|null $configStructureReader - */ - public function __construct( - InitialConfigReader $initialConfigReader, - ?ConfigStructureReader $configStructureReader = null - ) { - $this->initialConfigReader = $initialConfigReader; - $this->configStructureReader = $configStructureReader - ?? ObjectManager::getInstance()->get(ConfigStructureReader::class); + public function __construct(Reader $reader) + { + $this->reader = $reader; } /** @@ -52,39 +39,10 @@ public function __construct( */ public function get($path = '') { - $initialConfig = $this->initialConfigReader->read(); - $configStructure = $this->configStructureReader->read(Area::AREA_ADMINHTML); - $sections = $configStructure['config']['system']['sections'] ?? []; - $defaultConfig = $initialConfig['data']['default'] ?? []; - $initialConfig['data']['default'] = $this->merge($defaultConfig, $sections); - - $data = new DataObject($initialConfig); + $data = new DataObject($this->reader->read()); if ($path !== '') { $path = '/' . $path; } return $data->getData('data' . $path) ?: []; } - - /** - * Merge initial config with config structure - * - * @param array $config - * @param array $sections - * @return array - */ - private function merge(array $config, array $sections): array - { - foreach ($sections as $section) { - if (isset($section['children'])) { - $config[$section['id']] = $this->merge( - $config[$section['id']] ?? [], - $section['children'] - ); - } elseif ($section['_elementType'] === 'field') { - $config += [$section['id'] => null]; - } - } - - return $config; - } } diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php index 6cfce1959d0b1..d9ff44c2e05d0 100644 --- a/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php +++ b/app/code/Magento/Config/Test/Unit/App/Config/Source/ModularConfigSourceTest.php @@ -8,8 +8,7 @@ namespace Magento\Config\Test\Unit\App\Config\Source; use Magento\Config\App\Config\Source\ModularConfigSource; -use Magento\Config\Model\Config\Structure\Reader as ConfigStructureReader; -use Magento\Framework\App\Config\Initial\Reader as InitialConfigReader; +use Magento\Framework\App\Config\Initial\Reader; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -19,14 +18,9 @@ class ModularConfigSourceTest extends TestCase { /** - * @var InitialConfigReader|MockObject + * @var Reader|MockObject */ - private $initialConfigReader; - - /** - * @var ConfigStructureReader|MockObject - */ - private $configStructureReader; + private $reader; /** * @var ModularConfigSource @@ -35,17 +29,15 @@ class ModularConfigSourceTest extends TestCase protected function setUp(): void { - $this->initialConfigReader = $this->createMock(InitialConfigReader::class); - $this->configStructureReader = $this->createMock(ConfigStructureReader::class); - $this->source = new ModularConfigSource( - $this->initialConfigReader, - $this->configStructureReader - ); + $this->reader = $this->getMockBuilder(Reader::class) + ->disableOriginalConstructor() + ->getMock(); + $this->source = new ModularConfigSource($this->reader); } public function testGet() { - $this->initialConfigReader->expects($this->once()) + $this->reader->expects($this->once()) ->method('read') ->willReturn(['data' => ['path' => 'value']]); $this->assertEquals('value', $this->source->get('path')); diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index 85304e9c20f27..1fa1b2d1cfd5b 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -207,6 +207,10 @@ <virtualType name="appDumpSystemSource" type="Magento\Config\App\Config\Source\DumpConfigSourceAggregated"> <arguments> <argument name="sources" xsi:type="array"> + <item name="structure" xsi:type="array"> + <item name="source" xsi:type="object">Magento\Config\App\Config\Source\ConfigStructureSource</item> + <item name="sortOrder" xsi:type="string">1</item> + </item> <item name="modular" xsi:type="array"> <item name="source" xsi:type="object">Magento\Config\App\Config\Source\ModularConfigSource</item> <item name="sortOrder" xsi:type="string">10</item> From a2b93809864fa6dd645d44a5c818266459f53b95 Mon Sep 17 00:00:00 2001 From: Ihor Sviziev <svizev.igor@gmail.com> Date: Fri, 11 Sep 2020 16:02:15 +0300 Subject: [PATCH 0557/1013] Use one format in all places for array_merge Fix potential issues when no items in array Convert all array_merge into single format --- .../Catalog/Block/Product/ListProduct.php | 2 +- .../Block/Product/ProductList/Related.php | 4 ++-- .../Catalog/Block/Product/ProductList/Upsell.php | 10 +++++----- .../Indexer/Product/Category/Action/Rows.php | 4 ++-- .../Indexer/Product/Flat/FlatTableBuilder.php | 4 ++-- app/code/Magento/Catalog/Model/Product.php | 5 +---- .../Model/Product/Price/TierPriceStorage.php | 2 +- .../Model/ProductLink/ProductLinkQuery.php | 2 +- .../LinkedProductSelectBuilderComposite.php | 2 +- .../LayeredNavigation/Builder/Attribute.php | 6 +++++- .../Product/LayeredNavigation/LayerBuilder.php | 2 +- .../CatalogGraphQl/Model/AttributesJoiner.php | 2 +- .../FilterProcessor/CategoryFilter.php | 2 +- .../CatalogInventory/Model/StockIndex.php | 8 ++++---- .../Model/Layer/Filter/Attribute.php | 2 +- .../Model/Category/Plugin/Store/Group.php | 8 ++++---- .../Observer/AfterImportDataObserver.php | 3 +-- .../CatalogWidget/Block/Product/ProductsList.php | 4 ++-- .../Model/AgreementsValidator.php | 4 ++-- .../templates/browser/content/uploader.phtml | 4 ++-- app/code/Magento/Csp/Helper/InlineUtil.php | 4 ++-- .../Model/System/Currencysymbol.php | 4 ++-- .../Model/Address/CompositeValidator.php | 4 ++-- .../Magento/Customer/Model/Metadata/Form.php | 4 ++-- .../Attribute/Source/CountryWithWebsites.php | 4 ++-- .../Model/Import/Address.php | 4 ++-- .../Model/Import/Customer.php | 8 ++++---- app/code/Magento/Deploy/Package/Package.php | 8 ++++---- .../Command/XmlCatalogGenerateCommand.php | 4 ++-- app/code/Magento/Dhl/Model/Carrier.php | 7 +++---- .../Magento/Directory/Model/AllowedCountries.php | 4 ++-- .../Magento/Directory/Model/CurrencyConfig.php | 4 ++-- .../Adminhtml/Attribute/Edit/Options/Options.php | 4 ++-- app/code/Magento/Eav/Model/Form.php | 4 ++-- .../Magento/Eav/Model/ResourceModel/Helper.php | 2 +- .../Product/CompositeFieldProvider.php | 4 ++-- .../Block/Cart/Item/Renderer/Grouped.php | 2 +- .../Block/Stockqty/Type/Grouped.php | 4 ++-- .../ProcessingErrorAggregator.php | 6 +++--- .../Console/Command/IndexerReindexCommand.php | 16 ++++++++-------- .../Activate/Permissions/Tab/Webapi.php | 4 ++-- .../PageCache/Model/Layout/LayoutPlugin.php | 4 ++-- .../Gateway/Validator/ValidatorComposite.php | 8 ++++---- .../Config/Structure/PaymentSectionModifier.php | 4 ++-- .../Structure/PaymentSectionModifierTest.php | 4 ++-- .../Model/Cart/BuyRequest/BuyRequestBuilder.php | 2 +- .../Model/Cart/BuyRequest/BuyRequestBuilder.php | 4 ++-- .../Resolver/Batch/AbstractLikedProducts.php | 5 ++--- .../Magento/Reports/Block/Product/Viewed.php | 4 ++-- .../Adminhtml/Items/Column/DefaultColumn.php | 4 ++-- .../Block/Order/Email/Items/DefaultItems.php | 4 ++-- .../Order/Email/Items/Order/DefaultOrder.php | 4 ++-- .../Order/Item/Renderer/DefaultRenderer.php | 4 ++-- .../Model/Order/Pdf/Items/AbstractItems.php | 4 ++-- .../Provider/NotSyncedDataProvider.php | 4 ++-- app/code/Magento/Search/Model/Autocomplete.php | 4 ++-- .../Search/Model/SynonymGroupRepository.php | 4 ++-- .../Calculation/Rate/Collection.php | 4 ++-- .../Theme/Model/PageLayout/Config/Builder.php | 2 +- app/code/Magento/Ups/Model/Carrier.php | 3 ++- .../Webapi/Model/Rest/Swagger/Generator.php | 2 +- app/code/Magento/Weee/Model/Total/Quote/Weee.php | 4 ++-- .../Model/Import/ProductTest.php | 4 ++-- .../Magento/Framework/App/Utility/Files.php | 4 ++-- .../Model/EntitySnapshot/AttributeProvider.php | 2 +- .../Framework/Module/ModuleList/Loader.php | 2 +- .../ObjectManager/Factory/AbstractFactory.php | 2 +- .../Magento/Setup/Console/Style/MagentoStyle.php | 4 ++-- .../Setup/Module/Di/Code/Scanner/PhpScanner.php | 4 ++-- 69 files changed, 144 insertions(+), 145 deletions(-) diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index 6cec9bf3ef88a..b181a5392905b 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -367,7 +367,7 @@ public function getIdentities() $identities[] = $item->getIdentities(); } } - $identities = array_merge(...$identities); + $identities = array_merge([], ...$identities); return $identities; } diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php index 387fac770c5bc..42f610f89768d 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Related.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Related.php @@ -143,11 +143,11 @@ public function getItems() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getItems() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } /** diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php index ac66392efe5dc..adcb1b5666560 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Upsell.php @@ -267,10 +267,10 @@ public function getItemLimit($type = '') */ public function getIdentities() { - $identities = array_map(function (DataObject $item) { - return $item->getIdentities(); - }, $this->getItems()) ?: [[]]; - - return array_merge(...$identities); + $identities = []; + foreach ($this->getItems() as $item) { + $identities[] = $item->getIdentities(); + } + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index edd68422ec4ac..861f7c9c1c50e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -270,14 +270,14 @@ private function getCategoryIdsFromIndex(array $productIds): array ); $categoryIds[] = $storeCategories; } - $categoryIds = array_merge(...$categoryIds); + $categoryIds = array_merge([], ...$categoryIds); $parentCategories = [$categoryIds]; foreach ($categoryIds as $categoryId) { $parentIds = explode('/', $this->getPathFromCategoryId($categoryId)); $parentCategories[] = $parentIds; } - $categoryIds = array_unique(array_merge(...$parentCategories)); + $categoryIds = array_unique(array_merge([], ...$parentCategories)); return $categoryIds; } diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php index 99d75186eca8c..a0af09edf14c5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/FlatTableBuilder.php @@ -261,7 +261,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $select->from( ['et' => $entityTemporaryTableName], - array_merge(...$allColumns) + array_merge([], ...$allColumns) )->joinInner( ['e' => $this->resource->getTableName('catalog_product_entity')], 'e.entity_id = et.entity_id', @@ -306,7 +306,7 @@ protected function _fillTemporaryFlatTable(array $tables, $storeId, $valueFieldS $allColumns[] = $columnValueNames; } } - $sql = $select->insertFromSelect($temporaryFlatTableName, array_merge(...$allColumns), false); + $sql = $select->insertFromSelect($temporaryFlatTableName, array_merge([], ...$allColumns), false); $this->_connection->query($sql); } diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 7c463267e5a58..5e6f3ca5fe970 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -835,10 +835,7 @@ public function getStoreIds() $storeIds[] = $websiteStores; } } - if ($storeIds) { - $storeIds = array_merge(...$storeIds); - } - $this->setStoreIds($storeIds); + $this->setStoreIds(array_merge([], ...$storeIds)); } return $this->getData('store_ids'); } diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 36ef1826462b0..7e410f0e5feb3 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -220,7 +220,7 @@ private function retrieveAffectedIds(array $skus): array $affectedIds[] = array_keys($productId); } - return $affectedIds ? array_unique(array_merge(...$affectedIds)) : []; + return array_unique(array_merge([], ...$affectedIds)); } /** diff --git a/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php index 4bc400605a429..1d5ef722db8b1 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php +++ b/app/code/Magento/Catalog/Model/ProductLink/ProductLinkQuery.php @@ -103,7 +103,7 @@ private function extractRequestedLinkTypes(array $criteria): array if (count($linkTypesToLoad) === 1) { $linkTypesToLoad = $linkTypesToLoad[0]; } else { - $linkTypesToLoad = array_merge(...$linkTypesToLoad); + $linkTypesToLoad = array_merge([], ...$linkTypesToLoad); } $linkTypesToLoad = array_flip($linkTypesToLoad); $linkTypes = array_filter( diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php index 17ca389777c5b..c7c08bc805a1d 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/LinkedProductSelectBuilderComposite.php @@ -33,7 +33,7 @@ public function build(int $productId, int $storeId) : array foreach ($this->linkedProductSelectBuilder as $productSelectBuilder) { $selects[] = $productSelectBuilder->build($productId, $storeId); } - $selects = array_merge(...$selects); + $selects = array_merge([], ...$selects); return $selects; } 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 105e91320de49..5fce0fcdf3ca2 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -155,6 +155,10 @@ function (AggregationValueInterface $value) { return []; } - return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes); + return $this->attributeOptionProvider->getOptions( + \array_merge([], ...$attributeOptionIds), + $storeId, + $attributes + ); } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php index ff661236be62f..ac3f396b45ef8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/LayerBuilder.php @@ -36,7 +36,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array foreach ($this->builders as $builder) { $layers[] = $builder->build($aggregation, $storeId); } - $layers = \array_merge(...$layers); + $layers = \array_merge([], ...$layers); return \array_filter($layers); } diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 0bfd9d58ec969..34f5dd831686c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -88,7 +88,7 @@ public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): } } if ($fragmentFields) { - $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields)); + $selectedFields = array_merge([], $selectedFields, ...$fragmentFields); } $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php index f709f8cd6eb72..8d584d15fff0e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php @@ -64,7 +64,7 @@ public function apply(Filter $filter, AbstractDb $collection) $collection->addCategoryFilter($category); } - $categoryProductIds = array_unique(array_merge(...$categoryProducts)); + $categoryProductIds = array_unique(array_merge([], ...$categoryProducts)); $collection->addIdFilter($categoryProductIds); return true; } diff --git a/app/code/Magento/CatalogInventory/Model/StockIndex.php b/app/code/Magento/CatalogInventory/Model/StockIndex.php index ad0cff43c6ac9..6b659073485ad 100644 --- a/app/code/Magento/CatalogInventory/Model/StockIndex.php +++ b/app/code/Magento/CatalogInventory/Model/StockIndex.php @@ -169,11 +169,11 @@ protected function processChildren( $requiredChildrenIds = $typeInstance->getChildrenIds($productId, true); if ($requiredChildrenIds) { - $childrenIds = [[]]; + $childrenIds = []; foreach ($requiredChildrenIds as $groupedChildrenIds) { $childrenIds[] = $groupedChildrenIds; } - $childrenIds = array_merge(...$childrenIds); + $childrenIds = array_merge([], ...$childrenIds); $childrenWebsites = $this->productWebsite->getWebsites($childrenIds); foreach ($websitesWithStores as $websiteId => $storeId) { @@ -232,13 +232,13 @@ protected function getWebsitesWithDefaultStores($websiteId = null) */ protected function processParents($productId, $websiteId) { - $parentIds = [[]]; + $parentIds = []; foreach ($this->getProductTypeInstances() as $typeInstance) { /* @var ProductType\AbstractType $typeInstance */ $parentIds[] = $typeInstance->getParentIdsByChild($productId); } - $parentIds = array_merge(...$parentIds); + $parentIds = array_merge([], ...$parentIds); if (empty($parentIds)) { return; diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php index b1aecc6885bf0..080af5daa0322 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Attribute.php @@ -70,7 +70,7 @@ public function apply(\Magento\Framework\App\RequestInterface $request) $label = $this->getOptionText($value); $labels[] = is_array($label) ? $label : [$label]; } - $label = implode(',', array_unique(array_merge(...$labels))); + $label = implode(',', array_unique(array_merge([], ...$labels))); $this->getLayer() ->getState() ->addFilter($this->_createItem($label, $attributeValue)); diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php index 308b82e38c43a..50875b1a418d0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/Plugin/Store/Group.php @@ -121,7 +121,7 @@ public function afterSave( */ protected function generateProductUrls($websiteId, $originWebsiteId) { - $urls = [[]]; + $urls = []; $websiteIds = $websiteId != $originWebsiteId ? [$websiteId, $originWebsiteId] : [$websiteId]; @@ -136,7 +136,7 @@ protected function generateProductUrls($websiteId, $originWebsiteId) $urls[] = $this->productUrlRewriteGenerator->generate($product); } - return array_merge(...$urls); + return array_merge([], ...$urls); } /** @@ -148,7 +148,7 @@ protected function generateProductUrls($websiteId, $originWebsiteId) */ protected function generateCategoryUrls($rootCategoryId, $storeIds) { - $urls = [[]]; + $urls = []; $categories = $this->categoryFactory->create()->getCategories($rootCategoryId, 1, false, true); foreach ($categories as $category) { /** @var \Magento\Catalog\Model\Category $category */ @@ -157,6 +157,6 @@ protected function generateCategoryUrls($rootCategoryId, $storeIds) $urls[] = $this->categoryUrlRewriteGenerator->generate($category); } - return array_merge(...$urls); + return array_merge([], ...$urls); } } diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index b1dfa79373a05..e8e6f55f80051 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -459,8 +459,7 @@ private function categoriesUrlRewriteGenerate(): array } } } - $result = !empty($urls) ? array_merge(...$urls) : []; - return $result; + return array_merge([], ...$urls); } /** diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index 9934cc9ad106a..0cd2a3137d39a 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -506,7 +506,7 @@ public function getPagerHtml() */ public function getIdentities() { - $identities = [[]]; + $identities = []; if ($this->getProductCollection()) { foreach ($this->getProductCollection() as $product) { if ($product instanceof IdentityInterface) { @@ -514,7 +514,7 @@ public function getIdentities() } } } - $identities = array_merge(...$identities); + $identities = array_merge([], ...$identities); return $identities ?: [Product::CACHE_TAG]; } diff --git a/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php b/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php index 2643e69ba1efd..c78c807f9ea20 100644 --- a/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php +++ b/app/code/Magento/CheckoutAgreements/Model/AgreementsValidator.php @@ -35,12 +35,12 @@ public function __construct($list = null) public function isValid($agreementIds = []) { $agreementIds = $agreementIds === null ? [] : $agreementIds; - $requiredAgreements = [[]]; + $requiredAgreements = []; foreach ($this->agreementsProviders as $agreementsProvider) { $requiredAgreements[] = $agreementsProvider->getRequiredAgreementIds(); } - $agreementsDiff = array_diff(array_merge(...$requiredAgreements), $agreementIds); + $agreementsDiff = array_diff(array_merge([], ...$requiredAgreements), $agreementIds); return empty($agreementsDiff); } 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 d1c204c01ad1c..154e76bd93e41 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 @@ -11,14 +11,14 @@ $filters = $block->getConfig()->getFilters() ?? []; $allowedExtensions = []; $blockHtmlId = $block->getHtmlId(); -$listExtensions = [[]]; +$listExtensions = []; foreach ($filters as $media_type) { $listExtensions[] = array_map(function ($fileExt) { return ltrim($fileExt, '.*'); }, $media_type['files']); } -$allowedExtensions = array_merge(...$listExtensions); +$allowedExtensions = array_merge([], ...$listExtensions); $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() ? "{action: 'resize', maxWidth: " diff --git a/app/code/Magento/Csp/Helper/InlineUtil.php b/app/code/Magento/Csp/Helper/InlineUtil.php index f9dd9aafa459e..648ba51e34f7d 100644 --- a/app/code/Magento/Csp/Helper/InlineUtil.php +++ b/app/code/Magento/Csp/Helper/InlineUtil.php @@ -110,14 +110,14 @@ private function extractHost(string $url): ?string */ private function extractRemoteFonts(string $styleContent): array { - $urlsFound = [[]]; + $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)); + return array_map([$this, 'extractHost'], array_merge([], ...$urlsFound)); } /** diff --git a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php index d48df02d9de27..400aa56bc68e9 100644 --- a/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php +++ b/app/code/Magento/CurrencySymbol/Model/System/Currencysymbol.php @@ -292,7 +292,7 @@ protected function _unserializeStoreConfig($configPath, $storeId = null) */ protected function getAllowedCurrencies() { - $allowedCurrencies = [[]]; + $allowedCurrencies = []; $allowedCurrencies[] = explode( self::ALLOWED_CURRENCIES_CONFIG_SEPARATOR, $this->_scopeConfig->getValue( @@ -330,6 +330,6 @@ protected function getAllowedCurrencies() } } } - return array_unique(array_merge(...$allowedCurrencies)); + return array_unique(array_merge([], ...$allowedCurrencies)); } } diff --git a/app/code/Magento/Customer/Model/Address/CompositeValidator.php b/app/code/Magento/Customer/Model/Address/CompositeValidator.php index 4c77f10c11de4..62308ba329d03 100644 --- a/app/code/Magento/Customer/Model/Address/CompositeValidator.php +++ b/app/code/Magento/Customer/Model/Address/CompositeValidator.php @@ -30,11 +30,11 @@ public function __construct( */ public function validate(AbstractAddress $address) { - $errors = [[]]; + $errors = []; foreach ($this->validators as $validator) { $errors[] = $validator->validate($address); } - return array_merge(...$errors); + return array_merge([], ...$errors); } } diff --git a/app/code/Magento/Customer/Model/Metadata/Form.php b/app/code/Magento/Customer/Model/Metadata/Form.php index 85637ebf508b8..81ded6dec071a 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form.php +++ b/app/code/Magento/Customer/Model/Metadata/Form.php @@ -363,11 +363,11 @@ public function validateData(array $data) { $validator = $this->_getValidator($data); if (!$validator->isValid(false)) { - $messages = [[]]; + $messages = []; foreach ($validator->getMessages() as $errorMessages) { $messages[] = (array)$errorMessages; } - return array_merge(...$messages); + return array_merge([], ...$messages); } return true; } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php index 020067570efb4..1ca1c5622803f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/CountryWithWebsites.php @@ -84,7 +84,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) $websiteIds = []; if (!$this->shareConfig->isGlobalScope()) { - $allowedCountries = [[]]; + $allowedCountries = []; foreach ($this->storeManager->getWebsites() as $website) { $countries = $this->allowedCountriesReader @@ -96,7 +96,7 @@ public function getAllOptions($withEmpty = true, $defaultValues = false) } } - $allowedCountries = array_unique(array_merge(...$allowedCountries)); + $allowedCountries = array_unique(array_merge([], ...$allowedCountries)); } else { $allowedCountries = $this->allowedCountriesReader->getAllowedCountries(); } diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index f15f920fe95f4..0c3be73ec5047 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -525,12 +525,12 @@ public function validateData() protected function _importData() { //Preparing data for mass validation/import. - $rows = [[]]; + $rows = []; while ($bunch = $this->_dataSourceModel->getNextBunch()) { $rows[] = $bunch; } - $this->prepareCustomerData(array_merge(...$rows)); + $this->prepareCustomerData(array_merge([], ...$rows)); unset($bunch, $rows); $this->_dataSourceModel->getIterator()->rewind(); diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 5ebf242bd6ac4..2a02205bdc7e5 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -514,8 +514,8 @@ protected function _importData() { while ($bunch = $this->_dataSourceModel->getNextBunch()) { $this->prepareCustomerData($bunch); - $entitiesToCreate = [[]]; - $entitiesToUpdate = [[]]; + $entitiesToCreate = []; + $entitiesToUpdate = []; $entitiesToDelete = []; $attributesToSave = []; @@ -549,8 +549,8 @@ protected function _importData() } } - $entitiesToCreate = array_merge(...$entitiesToCreate); - $entitiesToUpdate = array_merge(...$entitiesToUpdate); + $entitiesToCreate = array_merge([], ...$entitiesToCreate); + $entitiesToUpdate = array_merge([], ...$entitiesToUpdate); $this->updateItemsCounterStats($entitiesToCreate, $entitiesToUpdate, $entitiesToDelete); /** diff --git a/app/code/Magento/Deploy/Package/Package.php b/app/code/Magento/Deploy/Package/Package.php index 2a83d0d4c56ec..3c4af7039b4a8 100644 --- a/app/code/Magento/Deploy/Package/Package.php +++ b/app/code/Magento/Deploy/Package/Package.php @@ -443,11 +443,11 @@ public function getResultMap() */ public function getParentMap() { - $map = [[]]; + $map = []; foreach ($this->getParentPackages() as $parentPackage) { $map[] = $parentPackage->getMap(); } - return array_merge(...$map); + return array_merge([], ...$map); } /** @@ -458,7 +458,7 @@ public function getParentMap() */ public function getParentFiles($type = null) { - $files = [[]]; + $files = []; foreach ($this->getParentPackages() as $parentPackage) { if ($type === null) { $files[] = $parentPackage->getFiles(); @@ -466,7 +466,7 @@ public function getParentFiles($type = null) $files[] = $parentPackage->getFilesByType($type); } } - return array_merge(...$files); + return array_merge([], ...$files); } /** diff --git a/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php b/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php index 9e473ccaa2d92..8bd827958df15 100644 --- a/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php +++ b/app/code/Magento/Developer/Console/Command/XmlCatalogGenerateCommand.php @@ -116,7 +116,7 @@ private function getUrnDictionary(OutputInterface $output) $files = $this->filesUtility->getXmlCatalogFiles('*.xml'); $files = array_merge($files, $this->filesUtility->getXmlCatalogFiles('*.xsd')); - $urns = [[]]; + $urns = []; foreach ($files as $file) { // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileDir = dirname($file[0]); @@ -130,7 +130,7 @@ private function getUrnDictionary(OutputInterface $output) $urns[] = $matches[1]; } } - $urns = array_unique(array_merge(...$urns)); + $urns = array_unique(array_merge([], ...$urns)); $paths = []; foreach ($urns as $urn) { try { diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 204094571ba3b..c5eb27b21e58b 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -826,10 +826,9 @@ protected function _getAllItems() $fullItems[] = array_fill(0, $qty, $this->_getWeight($itemWeight)); } } - if ($fullItems) { - $fullItems = array_merge(...$fullItems); - sort($fullItems); - } + + $fullItems = array_merge([], ...$fullItems); + sort($fullItems); return $fullItems; } diff --git a/app/code/Magento/Directory/Model/AllowedCountries.php b/app/code/Magento/Directory/Model/AllowedCountries.php index 2ceeb70ba5b01..69326439edc03 100644 --- a/app/code/Magento/Directory/Model/AllowedCountries.php +++ b/app/code/Magento/Directory/Model/AllowedCountries.php @@ -62,11 +62,11 @@ public function getAllowedCountries( switch ($scope) { case ScopeInterface::SCOPE_WEBSITES: case ScopeInterface::SCOPE_STORES: - $allowedCountries = [[]]; + $allowedCountries = []; foreach ($scopeCode as $singleFilter) { $allowedCountries[] = $this->getCountriesFromConfig($this->getSingleScope($scope), $singleFilter); } - $allowedCountries = array_merge(...$allowedCountries); + $allowedCountries = array_merge([], ...$allowedCountries); break; default: $allowedCountries = $this->getCountriesFromConfig($scope, $scopeCode); diff --git a/app/code/Magento/Directory/Model/CurrencyConfig.php b/app/code/Magento/Directory/Model/CurrencyConfig.php index f7230df6e86ea..b574170ac5d3c 100644 --- a/app/code/Magento/Directory/Model/CurrencyConfig.php +++ b/app/code/Magento/Directory/Model/CurrencyConfig.php @@ -73,7 +73,7 @@ public function getConfigCurrencies(string $path) */ private function getConfigForAllStores(string $path) { - $storesResult = [[]]; + $storesResult = []; foreach ($this->storeManager->getStores() as $store) { $storesResult[] = explode( ',', @@ -81,7 +81,7 @@ private function getConfigForAllStores(string $path) ); } - return array_merge(...$storesResult); + return array_merge([], ...$storesResult); } /** diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php index 69f417e1ea732..f53f1e97a872d 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/Options.php @@ -152,7 +152,7 @@ protected function _prepareOptionValues( $inputType = ''; } - $values = [[]]; + $values = []; $isSystemAttribute = is_array($optionCollection); if ($isSystemAttribute) { $values[] = $this->getPreparedValues($optionCollection, $isSystemAttribute, $inputType, $defaultValues); @@ -168,7 +168,7 @@ protected function _prepareOptionValues( } } - return array_merge(...$values); + return array_merge([], ...$values); } /** diff --git a/app/code/Magento/Eav/Model/Form.php b/app/code/Magento/Eav/Model/Form.php index 074c6cf46a2f4..b06c084cf6675 100644 --- a/app/code/Magento/Eav/Model/Form.php +++ b/app/code/Magento/Eav/Model/Form.php @@ -487,11 +487,11 @@ public function validateData(array $data) { $validator = $this->_getValidator($data); if (!$validator->isValid($this->getEntity())) { - $messages = [[]]; + $messages = []; foreach ($validator->getMessages() as $errorMessages) { $messages[] = (array)$errorMessages; } - return array_merge(...$messages); + return array_merge([], ...$messages); } return true; } diff --git a/app/code/Magento/Eav/Model/ResourceModel/Helper.php b/app/code/Magento/Eav/Model/ResourceModel/Helper.php index fc8a47994a6aa..569da0ac2bb4f 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Helper.php @@ -117,7 +117,7 @@ public function getLoadAttributesSelectGroups($selects) if (array_key_exists('all', $mainGroup)) { // it is better to call array_merge once after loop instead of calling it on each loop - $mainGroup['all'] = array_merge(...$mainGroup['all']); + $mainGroup['all'] = array_merge([], ...$mainGroup['all']); } return array_values($mainGroup); diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php index b276b67ff7fba..980842d6233b1 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/CompositeFieldProvider.php @@ -40,12 +40,12 @@ public function __construct(array $providers) */ public function getFields(array $context = []): array { - $allAttributes = [[]]; + $allAttributes = []; foreach ($this->providers as $provider) { $allAttributes[] = $provider->getFields($context); } - return array_merge(...$allAttributes); + return array_merge([], ...$allAttributes); } } diff --git a/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php b/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php index 197be38fb7f5f..8dc153f28c162 100644 --- a/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Cart/Item/Renderer/Grouped.php @@ -49,6 +49,6 @@ public function getIdentities() if ($this->getItem()) { $identities[] = $this->getGroupedProduct()->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php b/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php index 97dc90ec93493..78ae4047c0aad 100644 --- a/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Stockqty/Type/Grouped.php @@ -32,10 +32,10 @@ protected function _getChildProducts() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getChildProducts() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php index 5ea6227231543..2f8bfdcf70a5e 100644 --- a/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php +++ b/app/code/Magento/ImportExport/Model/Import/ErrorProcessing/ProcessingErrorAggregator.php @@ -242,7 +242,7 @@ public function getAllErrors() } $errors = array_values($this->items['rows']); - return array_merge(...$errors); + return array_merge([], ...$errors); } /** @@ -253,14 +253,14 @@ public function getAllErrors() */ public function getErrorsByCode(array $codes) { - $result = [[]]; + $result = []; foreach ($codes as $code) { if (isset($this->items['codes'][$code])) { $result[] = $this->items['codes'][$code]; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index 775f585519947..872ece7aaabb5 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -124,16 +124,16 @@ protected function getIndexers(InputInterface $input) return $indexers; } - $relatedIndexers = [[]]; - $dependentIndexers = [[]]; + $relatedIndexers = []; + $dependentIndexers = []; foreach ($indexers as $indexer) { $relatedIndexers[] = $this->getRelatedIndexerIds($indexer->getId()); $dependentIndexers[] = $this->getDependentIndexerIds($indexer->getId()); } - $relatedIndexers = $relatedIndexers ? array_unique(array_merge(...$relatedIndexers)) : []; - $dependentIndexers = $dependentIndexers ? array_merge(...$dependentIndexers) : []; + $relatedIndexers = array_unique(array_merge([], ...$relatedIndexers)); + $dependentIndexers = array_merge([], ...$dependentIndexers); $invalidRelatedIndexers = []; foreach ($relatedIndexers as $relatedIndexer) { @@ -164,12 +164,12 @@ protected function getIndexers(InputInterface $input) */ private function getRelatedIndexerIds(string $indexerId): array { - $relatedIndexerIds = [[]]; + $relatedIndexerIds = []; foreach ($this->getDependencyInfoProvider()->getIndexerIdsToRunBefore($indexerId) as $relatedIndexerId) { $relatedIndexerIds[] = [$relatedIndexerId]; $relatedIndexerIds[] = $this->getRelatedIndexerIds($relatedIndexerId); } - $relatedIndexerIds = $relatedIndexerIds ? array_unique(array_merge(...$relatedIndexerIds)) : []; + $relatedIndexerIds = array_unique(array_merge([], ...$relatedIndexerIds)); return $relatedIndexerIds; } @@ -182,7 +182,7 @@ private function getRelatedIndexerIds(string $indexerId): array */ private function getDependentIndexerIds(string $indexerId): array { - $dependentIndexerIds = [[]]; + $dependentIndexerIds = []; foreach (array_keys($this->getConfig()->getIndexers()) as $id) { $dependencies = $this->getDependencyInfoProvider()->getIndexerIdsToRunBefore($id); if (array_search($indexerId, $dependencies) !== false) { @@ -190,7 +190,7 @@ private function getDependentIndexerIds(string $indexerId): array $dependentIndexerIds[] = $this->getDependentIndexerIds($id); } } - $dependentIndexerIds = $dependentIndexerIds ? array_unique(array_merge(...$dependentIndexerIds)) : []; + $dependentIndexerIds = array_unique(array_merge([], ...$dependentIndexerIds)); return $dependentIndexerIds; } diff --git a/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php b/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php index 2d323fea34e7d..b6ea810666b9b 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Integration/Activate/Permissions/Tab/Webapi.php @@ -222,13 +222,13 @@ public function isTreeEmpty() */ protected function _getAllResourceIds(array $resources) { - $resourceIds = [[]]; + $resourceIds = []; foreach ($resources as $resource) { $resourceIds[] = [$resource['id']]; if (isset($resource['children'])) { $resourceIds[] = $this->_getAllResourceIds($resource['children']); } } - return array_merge(...$resourceIds); + return array_merge([], ...$resourceIds); } } diff --git a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php index 1b64f3b635c03..6aff8aef2c2d9 100644 --- a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php +++ b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php @@ -84,7 +84,7 @@ public function afterGenerateElements(Layout $subject) public function afterGetOutput(Layout $subject, $result) { if ($subject->isCacheable() && $this->config->isEnabled()) { - $tags = [[]]; + $tags = []; $isVarnish = $this->config->getType() === Config::VARNISH; foreach ($subject->getAllBlocks() as $block) { @@ -96,7 +96,7 @@ public function afterGetOutput(Layout $subject, $result) $tags[] = $block->getIdentities(); } } - $tags = array_unique(array_merge(...$tags)); + $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/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index 8c8d13300849e..af42554484117 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -59,8 +59,8 @@ public function __construct( public function validate(array $validationSubject) { $isValid = true; - $failsDescriptionAggregate = [[]]; - $errorCodesAggregate = [[]]; + $failsDescriptionAggregate = []; + $errorCodesAggregate = []; foreach ($this->validators as $key => $validator) { $result = $validator->validate($validationSubject); if (!$result->isValid()) { @@ -76,8 +76,8 @@ public function validate(array $validationSubject) return $this->createResult( $isValid, - array_merge(...$failsDescriptionAggregate), - array_merge(...$errorCodesAggregate) + array_merge([], ...$failsDescriptionAggregate), + array_merge([], ...$errorCodesAggregate) ); } } diff --git a/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php b/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php index 61410499e956e..a3cef539dc17b 100644 --- a/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php +++ b/app/code/Magento/Paypal/Model/Config/Structure/PaymentSectionModifier.php @@ -84,7 +84,7 @@ public function modify(array $initialStructure) */ private function getMoveInstructions($section, $data) { - $moved = [[]]; + $moved = []; if (array_key_exists('children', $data)) { foreach ($data['children'] as $childSection => $childData) { @@ -106,6 +106,6 @@ private function getMoveInstructions($section, $data) ]; } - return array_merge(...$moved); + return array_merge([], ...$moved); } } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php index dc54b71324a9b..a6a18418e92ac 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/Config/Structure/PaymentSectionModifierTest.php @@ -162,14 +162,14 @@ public function testMovedToTargetSpecialGroup() */ private function fetchAllAvailableGroups($structure) { - $availableGroups = [[]]; + $availableGroups = []; foreach ($structure as $group => $data) { $availableGroups[] = [$group]; if (isset($data['children'])) { $availableGroups[] = $this->fetchAllAvailableGroups($data['children']); } } - $availableGroups = array_merge(...$availableGroups); + $availableGroups = array_merge([], ...$availableGroups); $availableGroups = array_values(array_unique($availableGroups)); sort($availableGroups); return $availableGroups; diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php index 13b19e4f79c9a..7e8b4d916334f 100644 --- a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -56,6 +56,6 @@ public function build(CartItem $cartItem): DataObject $requestData[] = $provider->execute($cartItem); } - return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php index c14cc1324732c..c4909eef31287 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -45,11 +45,11 @@ public function __construct( */ public function build(array $cartItemData): DataObject { - $requestData = [[]]; + $requestData = []; foreach ($this->providers as $provider) { $requestData[] = $provider->execute($cartItemData); } - return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + return $this->dataObjectFactory->create(['data' => array_merge([], ...$requestData)]); } } diff --git a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php index e14d8bde6be74..fac7b23d408e3 100644 --- a/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php +++ b/app/code/Magento/RelatedProductGraphQl/Model/Resolver/Batch/AbstractLikedProducts.php @@ -89,8 +89,7 @@ private function findRelations(array $products, array $loadAttributes, int $link if (!$relations) { return []; } - $relatedIds = array_values($relations); - $relatedIds = array_unique(array_merge(...$relatedIds)); + $relatedIds = array_unique(array_merge([], ...array_values($relations))); //Loading products data. $this->searchCriteriaBuilder->addFilter('entity_id', $relatedIds, 'in'); $relatedSearchResult = $this->productDataProvider->getList( @@ -142,7 +141,7 @@ public function resolve(ContextInterface $context, Field $field, array $requests $products[] = $request->getValue()['model']; $fields[] = $this->productFieldsSelector->getProductFieldsFromInfo($request->getInfo(), $this->getNode()); } - $fields = array_unique(array_merge(...$fields)); + $fields = array_unique(array_merge([], ...$fields)); //Finding relations. $related = $this->findRelations($products, $fields, $this->getLinkType()); diff --git a/app/code/Magento/Reports/Block/Product/Viewed.php b/app/code/Magento/Reports/Block/Product/Viewed.php index ba4d03182213a..09d59e475905b 100644 --- a/app/code/Magento/Reports/Block/Product/Viewed.php +++ b/app/code/Magento/Reports/Block/Product/Viewed.php @@ -76,10 +76,10 @@ protected function _toHtml() */ public function getIdentities() { - $identities = [[]]; + $identities = []; foreach ($this->getItemsCollection() as $item) { $identities[] = $item->getIdentities(); } - return array_merge(...$identities); + return array_merge([], ...$identities); } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php index efef617acf900..81f670de91805 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/DefaultColumn.php @@ -68,7 +68,7 @@ public function getItem() */ public function getOrderOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -80,7 +80,7 @@ public function getOrderOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** 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 cbb79f188f231..57fc0441fe830 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php @@ -39,7 +39,7 @@ public function getOrder() */ public function getItemOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getOrderItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -52,7 +52,7 @@ public function getItemOptions() } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php index 0291a1275c350..cb9c7315244ac 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/Order/DefaultOrder.php @@ -34,7 +34,7 @@ public function getOrder() */ public function getItemOptions() { - $result = [[]]; + $result = []; if ($options = $this->getItem()->getProductOptions()) { if (isset($options['options'])) { $result[] = $options['options']; @@ -47,7 +47,7 @@ public function getItemOptions() } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php index bca6d49760d9a..010878559c2f0 100644 --- a/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php +++ b/app/code/Magento/Sales/Block/Order/Item/Renderer/DefaultRenderer.php @@ -105,7 +105,7 @@ public function getOrderItem() */ public function getItemOptions() { - $result = [[]]; + $result = []; $options = $this->getOrderItem()->getProductOptions(); if ($options) { if (isset($options['options'])) { @@ -118,7 +118,7 @@ public function getItemOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php index 29e011217ef20..a7315aeb9e3be 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/AbstractItems.php @@ -326,7 +326,7 @@ public function getItemPricesForDisplay() */ public function getItemOptions() { - $result = [[]]; + $result = []; $options = $this->getItem()->getOrderItem()->getProductOptions(); if ($options) { if (isset($options['options'])) { @@ -339,7 +339,7 @@ public function getItemOptions() $result[] = $options['attributes_info']; } } - return array_merge(...$result); + return array_merge([], ...$result); } /** diff --git a/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php b/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php index 645e411b80b67..10b3ca1bde996 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Provider/NotSyncedDataProvider.php @@ -37,11 +37,11 @@ public function __construct(TMapFactory $tmapFactory, array $providers = []) */ public function getIds($mainTableName, $gridTableName) { - $result = [[]]; + $result = []; foreach ($this->providers as $provider) { $result[] = $provider->getIds($mainTableName, $gridTableName); } - return array_unique(array_merge(...$result)); + return array_unique(array_merge([], ...$result)); } } diff --git a/app/code/Magento/Search/Model/Autocomplete.php b/app/code/Magento/Search/Model/Autocomplete.php index 45957e8795744..57364e4c36bde 100644 --- a/app/code/Magento/Search/Model/Autocomplete.php +++ b/app/code/Magento/Search/Model/Autocomplete.php @@ -30,11 +30,11 @@ public function __construct( */ public function getItems() { - $data = [[]]; + $data = []; foreach ($this->dataProviders as $dataProvider) { $data[] = $dataProvider->getItems(); } - return array_merge(...$data); + return array_merge([], ...$data); } } diff --git a/app/code/Magento/Search/Model/SynonymGroupRepository.php b/app/code/Magento/Search/Model/SynonymGroupRepository.php index dbc2b66b1f047..c670235d67adb 100644 --- a/app/code/Magento/Search/Model/SynonymGroupRepository.php +++ b/app/code/Magento/Search/Model/SynonymGroupRepository.php @@ -150,7 +150,7 @@ private function create(SynonymGroupInterface $synonymGroup, $errorOnMergeConfli */ private function merge(SynonymGroupInterface $synonymGroupToMerge, array $matchingGroupIds) { - $mergedSynonyms = [[]]; + $mergedSynonyms = []; foreach ($matchingGroupIds as $groupId) { /** @var SynonymGroup $synonymGroupModel */ $synonymGroupModel = $this->synonymGroupFactory->create(); @@ -160,7 +160,7 @@ private function merge(SynonymGroupInterface $synonymGroupToMerge, array $matchi } $mergedSynonyms[] = explode(',', $synonymGroupToMerge->getSynonymGroup()); - return array_unique(array_merge(...$mergedSynonyms)); + return array_unique(array_merge([], ...$mergedSynonyms)); } /** diff --git a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php index 7863b70f6626a..d34e863d56c54 100644 --- a/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php +++ b/app/code/Magento/Tax/Model/ResourceModel/Calculation/Rate/Collection.php @@ -206,7 +206,7 @@ public function getOptionRates() { $size = self::TAX_RULES_CHUNK_SIZE; $page = 1; - $rates = [[]]; + $rates = []; do { $offset = $size * ($page - 1); $this->getSelect()->reset(); @@ -222,6 +222,6 @@ public function getOptionRates() $page++; } while ($this->getSize() > $offset); - return array_merge(...$rates); + return array_merge([], ...$rates); } } diff --git a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php index 13b8aa23073ce..fb47d415e88d5 100644 --- a/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php +++ b/app/code/Magento/Theme/Model/PageLayout/Config/Builder.php @@ -68,7 +68,7 @@ protected function getConfigFiles() foreach ($this->themeCollection->loadRegisteredThemes() as $theme) { $configFiles[] = $this->fileCollector->getFilesContent($theme, 'layouts.xml'); } - $this->configFiles = array_merge(...$configFiles); + $this->configFiles = array_merge([], ...$configFiles); } return $this->configFiles; diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index b6e539bdadcb9..b14550451ad7e 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1362,10 +1362,11 @@ public function getAllowedMethods() protected function _formShipmentRequest(DataObject $request) { $packages = $request->getPackages(); + $shipmentItems = []; foreach ($packages as $package) { $shipmentItems[] = $package['items']; } - $shipmentItems = array_merge(...$shipmentItems); + $shipmentItems = array_merge([], ...$shipmentItems); $xmlRequest = $this->_xmlElFactory->create( ['data' => '<?xml version = "1.0" ?><ShipmentConfirmRequest xml:lang="en-US"/>'] diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index f38c0f0978536..40fe97c61f051 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -758,7 +758,7 @@ private function handleComplex($name, $type, $prefix, $isArray) ); } - return empty($queryNames) ? [] : array_merge(...$queryNames); + return array_merge([], ...$queryNames); } /** diff --git a/app/code/Magento/Weee/Model/Total/Quote/Weee.php b/app/code/Magento/Weee/Model/Total/Quote/Weee.php index 449c6cd688668..e7ae84c15a51f 100644 --- a/app/code/Magento/Weee/Model/Total/Quote/Weee.php +++ b/app/code/Magento/Weee/Model/Total/Quote/Weee.php @@ -306,12 +306,12 @@ protected function getNextIncrement() */ protected function recalculateParent(AbstractItem $item) { - $associatedTaxables = [[]]; + $associatedTaxables = []; foreach ($item->getChildren() as $child) { $associatedTaxables[] = $child->getAssociatedTaxables(); } $item->setAssociatedTaxables( - array_unique(array_merge(...$associatedTaxables)) + array_unique(array_merge([], ...$associatedTaxables)) ); } 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 a9699ea4a8050..bb45f2574893c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -729,7 +729,7 @@ function ($input) { ) ); // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $option = array_merge(...$option); + $option = array_merge([], ...$option); if (!empty($option['type']) && !empty($option['name'])) { $lastOptionKey = $option['type'] . '|' . $option['name']; @@ -2252,7 +2252,7 @@ function (ProductInterface $item) { $collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); $collection - ->addAttributeToFilter('entity_id', ['in' => \array_unique(\array_merge(...$categoryIds))]) + ->addAttributeToFilter('entity_id', ['in' => \array_unique(\array_merge([], ...$categoryIds))]) ->load() ->delete(); diff --git a/lib/internal/Magento/Framework/App/Utility/Files.php b/lib/internal/Magento/Framework/App/Utility/Files.php index 4298577f3147b..96bd6d061a13f 100644 --- a/lib/internal/Magento/Framework/App/Utility/Files.php +++ b/lib/internal/Magento/Framework/App/Utility/Files.php @@ -371,11 +371,11 @@ public function getMainConfigFiles($asDataSet = true) } $globPaths = [BP . '/app/etc/config.xml', BP . '/app/etc/*/config.xml']; $configXmlPaths = array_merge($globPaths, $configXmlPaths); - $files = [[]]; + $files = []; foreach ($configXmlPaths as $xmlPath) { $files[] = glob($xmlPath, GLOB_NOSORT); } - self::$_cache[$cacheKey] = array_merge(...$files); + self::$_cache[$cacheKey] = array_merge([], ...$files); } if ($asDataSet) { return self::composeDataSets(self::$_cache[$cacheKey]); diff --git a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php index 6b7fcd131ba8b..f702b71378bb9 100644 --- a/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php +++ b/lib/internal/Magento/Framework/Model/EntitySnapshot/AttributeProvider.php @@ -74,7 +74,7 @@ public function getAttributes($entityType) $attributes[] = $provider->getAttributes($entityType); } - $this->registry[$entityType] = \array_merge(...$attributes); + $this->registry[$entityType] = \array_merge([], ...$attributes); } return $this->registry[$entityType]; diff --git a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php index 7e484407d7a54..f30d38b6e112e 100644 --- a/lib/internal/Magento/Framework/Module/ModuleList/Loader.php +++ b/lib/internal/Magento/Framework/Module/ModuleList/Loader.php @@ -210,6 +210,6 @@ private function expandSequence($list, $name, $accumulated = []) $allResults[] = $this->expandSequence($list, $relatedName, $accumulated); } $allResults[] = $result; - return array_unique(array_merge(...$allResults)); + return array_unique(array_merge([], ...$allResults)); } } diff --git a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php index b662a2a34c813..b57f4665aff21 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php +++ b/lib/internal/Magento/Framework/ObjectManager/Factory/AbstractFactory.php @@ -239,7 +239,7 @@ protected function resolveArgumentsInRuntime($requestedType, array $parameters, $resolvedArguments[] = $this->getResolvedArgument((string)$requestedType, $parameter, $arguments); } - return empty($resolvedArguments) ? [] : array_merge(...$resolvedArguments); + return array_merge([], ...$resolvedArguments); } /** diff --git a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php index 60cfcbb67c217..3fa9e9d50d24a 100644 --- a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php +++ b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php @@ -605,7 +605,7 @@ private function getBlockLines( int $prefixLength, int $indentLength ) { - $lines = [[]]; + $lines = []; foreach ($messages as $key => $message) { $message = OutputFormatter::escape($message); $wordwrap = wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true); @@ -614,7 +614,7 @@ private function getBlockLines( $lines[][] = ''; } } - $lines = array_merge(...$lines); + $lines = array_merge([], ...$lines); return $lines; } diff --git a/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php b/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php index acb55e29afddd..7355ac30ac59d 100644 --- a/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php +++ b/setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php @@ -175,7 +175,7 @@ protected function _fetchMissingExtensionAttributesClasses($reflectionClass, $fi */ public function collectEntities(array $files) { - $output = [[]]; + $output = []; foreach ($files as $file) { $classes = $this->getDeclaredClasses($file); foreach ($classes as $className) { @@ -184,7 +184,7 @@ public function collectEntities(array $files) $output[] = $this->_fetchMissingExtensionAttributesClasses($reflectionClass, $file); } } - return array_unique(array_merge(...$output)); + return array_unique(array_merge([], ...$output)); } /** From f6c4cbd23fca8a7cd7b73c4c609f5179d644d527 Mon Sep 17 00:00:00 2001 From: Ihor Sviziev <svizev.igor@gmail.com> Date: Wed, 23 Sep 2020 09:12:11 +0300 Subject: [PATCH 0558/1013] Use one format in all places for array_merge Fix failing static tests --- app/code/Magento/Catalog/Model/Product.php | 4 +-- .../Model/Product/Price/TierPriceStorage.php | 3 -- .../Observer/AfterImportDataObserver.php | 2 -- app/code/Magento/Deploy/Package/Package.php | 2 +- .../Eav/Model/ResourceModel/Helper.php | 1 + app/code/Magento/Ups/Model/Carrier.php | 32 +++++++------------ .../Webapi/Model/Rest/Swagger/Generator.php | 2 -- .../Setup/Console/Style/MagentoStyle.php | 1 + 8 files changed, 17 insertions(+), 30 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index 5e6f3ca5fe970..f5363cc591c96 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -1030,7 +1030,7 @@ public function priceReindexCallback() */ public function eavReindexCallback() { - if ($this->isObjectNew() || $this->isDataChanged($this)) { + if ($this->isObjectNew() || $this->isDataChanged()) { $this->_productEavIndexerProcessor->reindexRow($this->getEntityId()); } } @@ -1176,7 +1176,7 @@ public function getTierPrice($qty = null) /** * Get formatted by currency product price * - * @return array|double + * @return array|double * @since 102.0.6 */ public function getFormattedPrice() diff --git a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php index 7e410f0e5feb3..7d458401c950e 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php +++ b/app/code/Magento/Catalog/Model/Product/Price/TierPriceStorage.php @@ -12,9 +12,6 @@ use Magento\Catalog\Model\Product\Price\Validation\TierPriceValidator; use Magento\Catalog\Model\ProductIdLocatorInterface; -/** - * Tier price storage. - */ class TierPriceStorage implements TierPriceStorageInterface { /** diff --git a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php index e8e6f55f80051..b467771408ec0 100644 --- a/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php +++ b/app/code/Magento/CatalogUrlRewrite/Observer/AfterImportDataObserver.php @@ -37,8 +37,6 @@ use RuntimeException; /** - * Class AfterImportDataObserver - * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ diff --git a/app/code/Magento/Deploy/Package/Package.php b/app/code/Magento/Deploy/Package/Package.php index 3c4af7039b4a8..5780b46365680 100644 --- a/app/code/Magento/Deploy/Package/Package.php +++ b/app/code/Magento/Deploy/Package/Package.php @@ -535,7 +535,7 @@ private function collectParentPaths( $area, $theme, $locale, - array & $result = [], + array &$result = [], ThemeInterface $themeModel = null ) { if (($package->getArea() != $area) || ($package->getTheme() != $theme) || ($package->getLocale() != $locale)) { diff --git a/app/code/Magento/Eav/Model/ResourceModel/Helper.php b/app/code/Magento/Eav/Model/ResourceModel/Helper.php index 569da0ac2bb4f..c81db40c608a8 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Helper.php @@ -19,6 +19,7 @@ class Helper extends \Magento\Framework\DB\Helper * @param \Magento\Framework\App\ResourceConnection $resource * @param string $modulePrefix * @codeCoverageIgnore + * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod */ public function __construct(\Magento\Framework\App\ResourceConnection $resource, $modulePrefix = 'Magento_Eav') { diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index b14550451ad7e..4d0e3efee35aa 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1135,7 +1135,7 @@ protected function _getXmlTracking($trackings) </TrackRequest> XMLAuth; - $trackingResponses[] = $this->asyncHttpClient->request( + $trackingResponses[$tracking] = $this->asyncHttpClient->request( new Request( $url, Request::METHOD_POST, @@ -1144,13 +1144,9 @@ protected function _getXmlTracking($trackings) ) ); } - foreach ($trackingResponses as $response) { + foreach ($trackingResponses as $tracking => $response) { $httpResponse = $response->get(); - if ($httpResponse->getStatusCode() >= 400) { - $xmlResponse = ''; - } else { - $xmlResponse = $httpResponse->getBody(); - } + $xmlResponse = $httpResponse->getStatusCode() >= 400 ? '' : $httpResponse->getBody(); $this->_parseXmlTrackingResponse($tracking, $xmlResponse); } @@ -1529,24 +1525,18 @@ protected function _formShipmentRequest(DataObject $request) } if ($deliveryConfirmation && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_PACKAGE) { - $serviceOptionsNode = $packagePart[$packageId]->addChild('PackageServiceOptions'); - $serviceOptionsNode->addChild( - 'DeliveryConfirmation' - )->addChild( - 'DCISType', - $deliveryConfirmation - ); + $serviceOptionsNode = $packagePart[$packageId]->addChild('PackageServiceOptions'); + $serviceOptionsNode + ->addChild('DeliveryConfirmation') + ->addChild('DCISType', $deliveryConfirmation); } } if (isset($deliveryConfirmation) && $deliveryConfirmationLevel === self::DELIVERY_CONFIRMATION_SHIPMENT) { $serviceOptionsNode = $shipmentPart->addChild('ShipmentServiceOptions'); - $serviceOptionsNode->addChild( - 'DeliveryConfirmation' - )->addChild( - 'DCISType', - $deliveryConfirmation - ); + $serviceOptionsNode + ->addChild('DeliveryConfirmation') + ->addChild('DCISType', $deliveryConfirmation); } $shipmentPart->addChild('PaymentInformation') @@ -1628,6 +1618,7 @@ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) try { $response = $this->_xmlElFactory->create(['data' => $xmlResponse]); } catch (Throwable $e) { + $response = $this->_xmlElFactory->create(['data' => '']); $debugData['result'] = ['error' => $e->getMessage(), 'code' => $e->getCode()]; } @@ -1801,6 +1792,7 @@ protected function _doShipmentRequest(DataObject $request) $this->setXMLAccessRequest(); $xmlRequest = $this->_xmlAccessRequest . $rawXmlRequest; $xmlResponse = $this->_getCachedQuotes($xmlRequest); + $debugData = []; if ($xmlResponse === null) { $debugData['request'] = $this->filterDebugData($this->_xmlAccessRequest) . $rawXmlRequest; diff --git a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php index 40fe97c61f051..5ead1beb722dd 100644 --- a/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php +++ b/app/code/Magento/Webapi/Model/Rest/Swagger/Generator.php @@ -33,10 +33,8 @@ class Generator extends AbstractSchemaGenerator */ const ERROR_SCHEMA = '#/definitions/error-response'; - /** Unauthorized description */ const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized'; - /** Array signifier */ const ARRAY_SIGNIFIER = '[0]'; /** diff --git a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php index 3fa9e9d50d24a..43f85d092b0a7 100644 --- a/setup/src/Magento/Setup/Console/Style/MagentoStyle.php +++ b/setup/src/Magento/Setup/Console/Style/MagentoStyle.php @@ -565,6 +565,7 @@ private function createBlock( ) { $indentLength = 0; $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); + $lineIndentation = ''; if (null !== $type) { $type = sprintf('[%s] ', $type); $indentLength = strlen($type); From a17053a0450f0469d044e2b63cd520bc49593ec8 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Wed, 23 Sep 2020 09:38:11 +0300 Subject: [PATCH 0559/1013] Use keyCode const to check if forward slash was pressed --- app/code/Magento/Ui/view/base/web/js/lib/key-codes.js | 1 + app/design/adminhtml/Magento/backend/web/js/theme.js | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js b/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js index 1f5a4210793ba..1f25e0d2c089f 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js @@ -21,6 +21,7 @@ define([], function () { 17: 'ctrlKey', 18: 'altKey', 16: 'shiftKey', + 191: 'forwardSlashKey', 66: 'bKey', 73: 'iKey', 85: 'uKey' diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index 996f6c05935f2..f556a388f6cf0 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -312,8 +312,9 @@ define('globalNavigation', [ define('globalSearch', [ 'jquery', + 'Magento_Ui/js/lib/key-codes', 'jquery/ui' -], function ($) { +], function ($, keyCodes) { 'use strict'; $.widget('mage.globalSearch', { @@ -353,7 +354,7 @@ define('globalSearch', [ 'textarea' ]; - if (e.which !== 191 || // forward slash - '/' + if (keyCodes[e.which] !== 'forwardSlashKey' || inputs.indexOf(e.target.tagName.toLowerCase()) !== -1 || e.target.isContentEditable ) { From d03302151a75807da0c16e2e00ced665760f9845 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Wed, 23 Sep 2020 09:39:43 +0300 Subject: [PATCH 0560/1013] Rename 'e' into 'event' --- app/design/adminhtml/Magento/backend/web/js/theme.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index f556a388f6cf0..e2b8d8cfc884d 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -347,21 +347,21 @@ define('globalSearch', [ self.field.addClass(self.options.fieldActiveClass); }); - $(document).keydown(function (e) { + $(document).keydown(function (event) { var inputs = [ 'input', 'select', 'textarea' ]; - if (keyCodes[e.which] !== 'forwardSlashKey' || - inputs.indexOf(e.target.tagName.toLowerCase()) !== -1 || - e.target.isContentEditable + if (keyCodes[event.which] !== 'forwardSlashKey' || + inputs.indexOf(event.target.tagName.toLowerCase()) !== -1 || + event.target.isContentEditable ) { return; } - e.preventDefault(); + event.preventDefault(); self.input.focus(); }); From 2cc1dab7aa16f5a4a3d5b0bef8e32c84b08cb377 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Wed, 23 Sep 2020 09:47:16 +0300 Subject: [PATCH 0561/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- ...eForSimpleProductWithCustomOptionsTest.xml | 11 +- ...ForSimpleProductsWithCustomOptionsTest.xml | 161 ++++++++++++++++++ 2 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index 8efc5b1c5769c..31aed3f71608f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductWithCustomOptionsTest"> + <test name="ApplyCatalogRuleForSimpleProductWithCustomOptionsTest" deprecated="Use ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> - <title value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> + <title value="Deprecated. Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> <description value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> <severity value="CRITICAL"/> <testCaseId value="MC-14769"/> <group value="CatalogRule"/> <group value="mtf_migrated"/> + <skip> + <issueId value="DEPRECATED">Use ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead</issueId> + </skip> </annotations> <before> <!-- Login as Admin --> @@ -67,9 +70,7 @@ <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> <!-- Save and apply the new catalog price rule --> - <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> - <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="waitForSave"/> + <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml new file mode 100644 index 0000000000000..e5453f6ecfd36 --- /dev/null +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -0,0 +1,161 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest"> + <annotations> + <features value="CatalogRule"/> + <stories value="Apply catalog price rule"/> + <title value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> + <description value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-14769"/> + <group value="catalogRule"/> + <group value="mtf_migrated"/> + </annotations> + <before> + <!-- Login as Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct3"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">56.78</field> + </createData> + + <!-- Update all products to have custom options --> + <updateData createDataKey="createProduct1" entity="productWithCustomOptions" stepKey="updateProductWithOptions1"/> + <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProductWithOptions2"/> + <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProductWithOptions3"/> + <magentoCron stepKey="runCronIndex" groups="index"/> + </before> + <after> + <!-- Delete products and category --> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct3" stepKey="deleteProduct3"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete the catalog price rule --> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{_defaultCatalogRule.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + + <!-- Logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- 1. Begin creating a new catalog price rule --> + <actionGroup ref="NewCatalogPriceRuleByUIWithConditionIsCategoryActionGroup" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> + <argument name ="categoryId" value="$createCategory.id$"/> + </actionGroup> + + <!-- Select not logged in customer group --> + <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> + + <!-- Save and apply the new catalog price rule --> + <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> + <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> + <waitForPageLoad stepKey="waitForSave"/> + <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"/> + + <!-- Check product 1 price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$51.10" stepKey="storefrontProduct1Price"/> + + <!-- Check product 1 regular price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$56.78" stepKey="storefrontProduct1RegularPrice"/> + + <!-- Check product 2 price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$51.10" stepKey="storefrontProduct2Price"/> + + <!-- Check product 2 regular price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$56.78" stepKey="storefrontProduct2RegularPrice"/> + + <!-- Check product 3 price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$51.10" stepKey="storefrontProduct3Price"/> + + <!-- Check product 3 regular price on store front category page --> + <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$56.78" stepKey="storefrontProduct3RegularPrice"/> + + <!-- Navigate to product 1 on store front --> + <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> + + <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> + <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> + <argument name="customOption" value="{{ProductOptionValueDropdown1.title}} +$0.01"/> + <argument name="productPrice" value="$56.79"/> + <argument name="productFinalPrice" value="$51.11"/> + </actionGroup> + + <!-- Add product 1 to cart --> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct1ToCart"> + <argument name="productQty" value="1"/> + </actionGroup> + + <!-- Navigate to product 2 on store front --> + <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage2"/> + + <!-- Assert regular and special price after selecting ProductOptionValueDropdown3 --> + <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices2"> + <argument name="customOption" value="{{ProductOptionValueDropdown3.title}} +$5.11"/> + <argument name="productPrice" value="$62.46"/> + <argument name="productFinalPrice" value="$56.21"/> + </actionGroup> + + <!-- Add product 2 to cart --> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct2ToCart"> + <argument name="productQty" value="1"/> + </actionGroup> + + <!-- Navigate to product 3 on store front --> + <amOnPage url="{{StorefrontProductPage.url($createProduct3.name$)}}" stepKey="goToProductPage3"/> + + <!-- Add product 3 to cart with no custom option --> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct3ToCart"> + <argument name="productQty" value="1"/> + </actionGroup> + + <!-- Assert sub total on mini shopping cart --> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$158.42"/> + </actionGroup> + + <!-- Navigate to checkout shipping page --> + <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> + <waitForPageLoad stepKey="waitFoCheckoutShippingPageLoad"/> + + <!-- Fill Shipping information --> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillOrderShippingInfo"> + <argument name="customerVar" value="Simple_US_Customer"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + </actionGroup> + + <!-- Verify order summary on payment page --> + <actionGroup ref="VerifyCheckoutPaymentOrderSummaryActionGroup" stepKey="verifyCheckoutPaymentOrderSummaryActionGroup"> + <argument name="orderSummarySubTotal" value="$158.42"/> + <argument name="orderSummaryShippingTotal" value="$15.00"/> + <argument name="orderSummaryTotal" value="$173.42"/> + </actionGroup> + </test> +</tests> From 988f31b90c50d3b5c45db210f82483a0ac86fe7a Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 23 Sep 2020 10:36:43 +0300 Subject: [PATCH 0562/1013] add return type --- lib/internal/Magento/Framework/Test/Unit/File/NameTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php index 819996f159143..43dea2356e630 100644 --- a/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/File/NameTest.php @@ -30,7 +30,10 @@ class NameTest extends \PHPUnit\Framework\TestCase */ private $name; - protected function setUp() + /** + * @inheritDoc + */ + protected function setUp(): void { $this->name = new Name(); $this->existingFilePath = __DIR__ . '/../_files/source.txt'; From 9c7bbc7fbb2922cb0962d8810486801e1851943b Mon Sep 17 00:00:00 2001 From: Viktor Petryk <victor.petryk@transoftgroup.com> Date: Wed, 23 Sep 2020 11:04:18 +0300 Subject: [PATCH 0563/1013] MC-37802: Broken Integration tests in Magento 2.4.1-beta1 --- .../out_of_stock_product_with_category.php | 2 +- ...f_stock_product_with_category_rollback.php | 2 +- .../Magento/Catalog/_files/products_new.php | 33 ++++++++++++++----- .../Catalog/_files/products_new_rollback.php | 21 +++++++----- ...compared_out_of_stock_product_rollback.php | 2 +- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category.php index dfaee4e8efdba..c38b77d886bfc 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category.php @@ -50,5 +50,5 @@ ->setCanSaveCustomOptions(true) ->setHasOptions(true); /** @var ProductRepositoryInterface $productRepositoryFactory */ -$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category_rollback.php index ee25a9c29ded1..e4669597479b6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/out_of_stock_product_with_category_rollback.php @@ -20,7 +20,7 @@ $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); /** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); try { $productRepository->deleteById('out-of-stock-product'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php index 2cd0dd2c77560..e08ded4da7b5d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new.php @@ -4,19 +4,36 @@ * See COPYING.txt for license details. */ -/** @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) +use Magento\Catalog\Api\ProductRepositoryInterface; +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\Model\ProductFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +/** @var Product $product */ +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('New Product') ->setSku('simple') ->setPrice(10) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) ->setWebsiteIds([1]) ->setStockData(['qty' => 100, 'is_in_stock' => 1, 'manage_stock' => 1]) ->setNewsFromDate(date('Y-m-d H:i:s', strtotime('-2 day'))) ->setNewsToDate(date('Y-m-d H:i:s', strtotime('+2 day'))) ->setDescription('description') - ->setShortDescription('short desc') - ->save(); + ->setShortDescription('short desc'); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new_rollback.php index 08451b6fccaa3..4b39f975373f3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_new_rollback.php @@ -4,21 +4,26 @@ * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @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); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); try { - $firstProduct = $productRepository->get('simple', false, null, true); - $productRepository->delete($firstProduct); -} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + $productRepository->deleteById('simple'); +} catch (NoSuchEntityException $exception) { //Product already removed } 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 index 677bfb32cd8e9..f3d31bee387f6 100644 --- 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 @@ -7,5 +7,5 @@ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/out_of_stock_product_with_category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/out_of_stock_product_with_category_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); From 84b22c12db6287c8789953397e8f570ec4f55136 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 23 Sep 2020 11:18:57 +0300 Subject: [PATCH 0564/1013] fix static --- .../Magento/Catalog/Model/ImageUploader.php | 81 ++++++++----------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ImageUploader.php b/app/code/Magento/Catalog/Model/ImageUploader.php index 6ce0387825b55..6aff6488164f9 100644 --- a/app/code/Magento/Catalog/Model/ImageUploader.php +++ b/app/code/Magento/Catalog/Model/ImageUploader.php @@ -5,9 +5,17 @@ */ namespace Magento\Catalog\Model; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Uploader; use Magento\Framework\App\ObjectManager; use Magento\Framework\File\Name; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\MediaStorage\Model\File\UploaderFactory; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; /** * Catalog image uploader @@ -15,91 +23,76 @@ class ImageUploader { /** - * Core file storage database - * - * @var \Magento\MediaStorage\Helper\File\Storage\Database + * @var Database */ protected $coreFileStorageDatabase; /** - * Media directory object (writable). - * - * @var \Magento\Framework\Filesystem\Directory\WriteInterface + * @var WriteInterface */ protected $mediaDirectory; /** - * Uploader factory - * - * @var \Magento\MediaStorage\Model\File\UploaderFactory + * @var UploaderFactory */ private $uploaderFactory; /** - * Store manager - * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ protected $logger; /** - * Base tmp path - * * @var string */ protected $baseTmpPath; /** - * Base path - * * @var string */ protected $basePath; /** - * Allowed extensions - * * @var string */ protected $allowedExtensions; /** - * List of allowed image mime types - * * @var string[] */ private $allowedMimeTypes; /** - * @var \Magento\Framework\File\Name + * @var Name */ private $fileNameLookup; /** * ImageUploader constructor. * - * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase - * @param \Magento\Framework\Filesystem $filesystem - * @param \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Psr\Log\LoggerInterface $logger + * @param Database $coreFileStorageDatabase + * @param Filesystem $filesystem + * @param UploaderFactory $uploaderFactory + * @param StoreManagerInterface $storeManager + * @param LoggerInterface $logger * @param string $baseTmpPath * @param string $basePath * @param string[] $allowedExtensions * @param string[] $allowedMimeTypes * @param Name|null $fileNameLookup + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDatabase, - \Magento\Framework\Filesystem $filesystem, - \Magento\MediaStorage\Model\File\UploaderFactory $uploaderFactory, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Psr\Log\LoggerInterface $logger, + Database $coreFileStorageDatabase, + Filesystem $filesystem, + UploaderFactory $uploaderFactory, + StoreManagerInterface $storeManager, + LoggerInterface $logger, $baseTmpPath, $basePath, $allowedExtensions, @@ -107,7 +100,7 @@ public function __construct( Name $fileNameLookup = null ) { $this->coreFileStorageDatabase = $coreFileStorageDatabase; - $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); + $this->mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->uploaderFactory = $uploaderFactory; $this->storeManager = $storeManager; $this->logger = $logger; @@ -122,7 +115,6 @@ public function __construct( * Set base tmp path * * @param string $baseTmpPath - * * @return void */ public function setBaseTmpPath($baseTmpPath) @@ -134,7 +126,6 @@ public function setBaseTmpPath($baseTmpPath) * Set base path * * @param string $basePath - * * @return void */ public function setBasePath($basePath) @@ -146,7 +137,6 @@ public function setBasePath($basePath) * Set allowed extensions * * @param string[] $allowedExtensions - * * @return void */ public function setAllowedExtensions($allowedExtensions) @@ -189,7 +179,6 @@ public function getAllowedExtensions() * * @param string $path * @param string $imageName - * * @return string */ public function getFilePath($path, $imageName) @@ -204,7 +193,7 @@ public function getFilePath($path, $imageName) * @param bool $returnRelativePath * @return string * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function moveFileFromTmp($imageName, $returnRelativePath = false) { @@ -232,10 +221,7 @@ public function moveFileFromTmp($imageName, $returnRelativePath = false) ); } catch (\Exception $e) { $this->logger->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( - __('Something went wrong while saving the file(s).'), - $e - ); + throw new LocalizedException(__('Something went wrong while saving the file(s).'), $e); } return $returnRelativePath ? $baseImagePath : $imageName; @@ -245,10 +231,9 @@ public function moveFileFromTmp($imageName, $returnRelativePath = false) * Checking file for save and save it to tmp dir * * @param string $fileId - * * @return string[] * - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function saveFileToTmpDir($fileId) { @@ -259,15 +244,13 @@ public function saveFileToTmpDir($fileId) $uploader->setAllowedExtensions($this->getAllowedExtensions()); $uploader->setAllowRenameFiles(true); if (!$uploader->checkMimeType($this->allowedMimeTypes)) { - throw new \Magento\Framework\Exception\LocalizedException(__('File validation failed.')); + throw new LocalizedException(__('File validation failed.')); } $result = $uploader->save($this->mediaDirectory->getAbsolutePath($baseTmpPath)); unset($result['path']); if (!$result) { - throw new \Magento\Framework\Exception\LocalizedException( - __('File can not be saved to the destination folder.') - ); + throw new LocalizedException(__('File can not be saved to the destination folder.')); } /** @@ -287,7 +270,7 @@ public function saveFileToTmpDir($fileId) $this->coreFileStorageDatabase->saveFile($relativePath); } catch (\Exception $e) { $this->logger->critical($e); - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Something went wrong while saving the file(s).'), $e ); From c7822ff08cd1e9ae844f5133f590e3eb677c8feb Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Mon, 21 Sep 2020 18:18:00 +0300 Subject: [PATCH 0565/1013] refactoring --- .../Catalog/Model/CategoryRepository.php | 4 +- .../Model/SetSaveRewriteHistory.php | 69 +++++++++ .../Rest/CategoryInputParamsResolver.php | 56 +++++++ .../Controller/Rest/InputParamsResolver.php | 142 ------------------ .../Rest/ProductInputParamsResolver.php | 56 +++++++ .../CatalogUrlRewrite/etc/webapi_rest/di.xml | 3 +- 6 files changed, 185 insertions(+), 145 deletions(-) create mode 100644 app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php delete mode 100644 app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 0d0ceec6221f7..fe4637a6fb9c8 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -251,8 +251,8 @@ private function getExistingData(CategoryInterface $category, int $storeId) $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); $existingData['store_id'] = $storeId; - if (is_array($category->getData())) { - $existingData = array_replace($existingData, $category->getData()); + if ($category->getData('save_rewrites_history') !== null) { + $existingData['save_rewrites_history'] = $category->getData('save_rewrites_history'); } return $existingData; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php b/app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php new file mode 100644 index 0000000000000..bb6119a9e8334 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Model; + +use Magento\Framework\Webapi\Rest\Request as RestRequest; + +class SetSaveRewriteHistory +{ + private const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; + + /** + * @var RestRequest + */ + private $request; + + /** + * @param RestRequest $request + */ + public function __construct(RestRequest $request) + { + $this->request = $request; + } + + /** + * Add 'save_rewrites_history' param to the data + * + * @param array $result + * @param string $entityCode + * @param string $type + * @return mixed + */ + public function execute($result, $entityCode, $type) + { + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isCustomAttributesExists($requestBodyParams, $entityCode)) { + foreach ($requestBodyParams[$entityCode]['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { + foreach ($result as $resultItem) { + if ($resultItem instanceof $type) { + $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); + break 2; + } + } + break; + } + } + } + + return $result; + } + + /** + * Check is any custom options exists in data + * + * @param array $requestBodyParams + * @param string $entityCode + * @return bool + */ + private function isCustomAttributesExists(array $requestBodyParams, string $entityCode): bool + { + return !empty($requestBodyParams[$entityCode]['custom_attributes']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php new file mode 100644 index 0000000000000..000562f4ef3eb --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\CatalogUrlRewrite\Model\SetSaveRewriteHistory; + +/** + * Plugin for CategoryInputParamsResolver + * + * Used to modify category data with save_rewrites_history flag + */ +class CategoryInputParamsResolver +{ + /** + * @var SetSaveRewriteHistory + */ + private $rewriteHistory; + + /** + * @param SetSaveRewriteHistory $rewriteHistory + */ + public function __construct(SetSaveRewriteHistory $rewriteHistory) + { + $this->rewriteHistory = $rewriteHistory; + } + + /** + * Add 'save_rewrites_history' param to the category data + * + * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper + * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject + * @param array $result + * @return array + */ + public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array + { + $route = $subject->getRoute(); + + if ($route->getServiceClass() === CategoryRepositoryInterface::class && $route->getServiceMethod() === 'save') { + $result = $this->rewriteHistory->execute( + $result, + 'category', + Category::class + ); + } + + return $result; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php deleted file mode 100644 index cff868f1ecceb..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php +++ /dev/null @@ -1,142 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest; - -use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\Webapi\Rest\Request as RestRequest; -use Magento\Webapi\Controller\Rest\Router\Route; - -/** - * Plugin for InputParamsResolver - * - * Used to modify product data with save_rewrites_history flag - */ -class InputParamsResolver -{ - private const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; - - /** - * @var RestRequest - */ - private $request; - - /** - * @param RestRequest $request - */ - public function __construct(RestRequest $request) - { - $this->request = $request; - } - - /** - * Add 'save_rewrites_history' param to the product and category data - * - * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper - * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject - * @param array $result - * @return array - */ - public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array - { - $this->processProductCall($subject, $result); - $this->processCategoryCall($subject, $result); - - return $result; - } - - /** - * Check that product save method called - * - * @param Route $route - * @return bool - */ - private function isProductSaveCalled(Route $route): bool - { - $serviceMethodName = $route->getServiceMethod(); - $serviceClassName = $route->getServiceClass(); - - return $serviceClassName === ProductRepositoryInterface::class && $serviceMethodName === 'save'; - } - - /** - * Check that category save method called - * - * @param Route $route - * @return bool - */ - private function isCategorySaveCalled(Route $route): bool - { - $serviceMethodName = $route->getServiceMethod(); - $serviceClassName = $route->getServiceClass(); - - return $serviceClassName === CategoryRepositoryInterface::class && $serviceMethodName === 'save'; - } - - /** - * Check is any custom options exists in product data - * - * @param array $requestBodyParams - * @param string $entityCode - * @return bool - */ - private function isCustomAttributesExists(array $requestBodyParams, string $entityCode): bool - { - return !empty($requestBodyParams[$entityCode]['custom_attributes']); - } - - /** - * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject - * @param array $result - * @return array - */ - private function processProductCall(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): void - { - $requestBodyParams = $this->request->getBodyParams(); - - if ($this->isProductSaveCalled($subject->getRoute()) - && $this->isCustomAttributesExists($requestBodyParams, 'product')) { - foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { - if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { - foreach ($result as $resultItem) { - if ($resultItem instanceof \Magento\Catalog\Model\Product) { - $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); - break 2; - } - } - break; - } - } - } - } - - /** - * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject - * @param array $result - */ - private function processCategoryCall(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): void - { - $requestBodyParams = $this->request->getBodyParams(); - - if ($this->isCategorySaveCalled($subject->getRoute()) - && $this->isCustomAttributesExists($requestBodyParams, 'category')) { - foreach ($requestBodyParams['category']['custom_attributes'] as $attribute) { - if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { - foreach ($result as $resultItem) { - if ($resultItem instanceof \Magento\Catalog\Model\Category) { - $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); - break 2; - } - } - break; - } - } - } - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php new file mode 100644 index 0000000000000..8334a52ea5bc0 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\SetSaveRewriteHistory; + +/** + * Plugin for InputParamsResolver + * + * Used to modify product data with save_rewrites_history flag + */ +class ProductInputParamsResolver +{ + /** + * @var SetSaveRewriteHistory + */ + private $rewriteHistory; + + /** + * @param SetSaveRewriteHistory $rewriteHistory + */ + public function __construct(SetSaveRewriteHistory $rewriteHistory) + { + $this->rewriteHistory = $rewriteHistory; + } + + /** + * Add 'save_rewrites_history' param to the product data + * + * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper + * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject + * @param array $result + * @return array + */ + public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array + { + $route = $subject->getRoute(); + + if ($route->getServiceClass() === ProductRepositoryInterface::class && $route->getServiceMethod() === 'save') { + $result = $this->rewriteHistory->execute( + $result, + 'product', + Product::class + ); + } + + return $result; + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml index 9c5186a5ec0ac..37bbee597e809 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml @@ -7,6 +7,7 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Webapi\Controller\Rest\InputParamsResolver"> - <plugin name="product_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver" sortOrder="1" disabled="false" /> + <plugin name="product_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\ProductInputParamsResolver" disabled="false" /> + <plugin name="category_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\CategoryInputParamsResolver" disabled="false" /> </type> </config> From a8e07e93ca5598b19bf7ce3b3c47a0e576362a50 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Wed, 23 Sep 2020 12:20:31 +0300 Subject: [PATCH 0566/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- ...plyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml index e5453f6ecfd36..cbb75dc4c9aff 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -70,12 +70,6 @@ <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> <waitForPageLoad stepKey="waitForSave"/> - <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"/> From f0bdf493035d7d341bf8a4fe6c4c1081597a6fff Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Wed, 23 Sep 2020 12:23:33 +0300 Subject: [PATCH 0567/1013] MC-23908: Tax estimation fails on CI --- ...ontCartShippingMethodSelectActionGroup.xml | 23 ------------------- .../Section/CheckoutCartSummarySection.xml | 3 +-- ...tCheckoutUsingFreeShippingAndTaxesTest.xml | 10 ++++---- 3 files changed, 5 insertions(+), 31 deletions(-) delete mode 100644 app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml deleted file mode 100644 index f8f24331d04ff..0000000000000 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCartShippingMethodSelectActionGroup.xml +++ /dev/null @@ -1,23 +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="StorefrontCartPageSelectShippingMethodActionGroup"> - <annotations> - <description>Select a shipping method in the Estimate Shipping and Tax block on the Storefront Shopping Cart page.</description> - </annotations> - <arguments> - <argument name="carrierCode" defaultValue="flatrate" type="string"/> - <argument name="methodCode" defaultValue="flatrate" type="string"/> - </arguments> - - <conditionalClick selector="{{CheckoutCartSummarySection.shippingMethodElementId(carrierCode, methodCode)}}" dependentSelector="{{CheckoutCartSummarySection.shippingMethodChecked(carrierCode, methodCode)}}" visible="false" stepKey="selectShippingMethod"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> - </actionGroup> -</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml index 960b070b08f3d..d555079f48475 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartSummarySection.xml @@ -34,8 +34,7 @@ <element name="shippingMethodLabel" type="text" selector="#co-shipping-method-form dl dt span"/> <element name="methodName" type="text" selector="#co-shipping-method-form label"/> <element name="shippingPrice" type="text" selector="#co-shipping-method-form span .price"/> - <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true"/> - <element name="shippingMethodChecked" type="radio" parameterized="true" selector="#co-shipping-method-form #s_method_{{carrierCode}}_{{methodCode}}:checked"/> + <element name="shippingMethodElementId" type="radio" selector="#s_method_{{carrierCode}}_{{methodCode}}" parameterized="true" timeout="30"/> <element name="estimateShippingAndTaxForm" type="block" selector="#shipping-zip-form"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml index e731b31d33abc..49af0a285b5f4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -10,13 +10,14 @@ <test name="StorefrontVerifyGuestCheckoutUsingFreeShippingAndTaxesTest"> <annotations> <features value="Checkout"/> - <stories value="Checkout"/> + <stories value="Checkout via Guest Checkout"/> <title value="Verify guest checkout using free shipping and tax variations"/> <description value="Verify guest checkout using free shipping and tax variations"/> <severity value="CRITICAL"/> <testCaseId value="MC-28285"/> <group value="mtf_migrated"/> <group value="checkout"/> + <group value="tax"/> </annotations> <before> <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> @@ -107,7 +108,7 @@ <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> <argument name="product" value="$simpleProduct$"/> </actionGroup> - <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="addProductToTheCart"> + <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="addSimpleProductToTheCart"> <argument name="productQty" value="1"/> </actionGroup> <actionGroup ref="StorefrontAddConfigurableProductToTheCartActionGroup" stepKey="addConfigurableProductToCart"> @@ -128,10 +129,7 @@ <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"> <argument name="address" value="US_Address_NY_Default_Shipping"/> </actionGroup> - <actionGroup ref="StorefrontCartPageSelectShippingMethodActionGroup" stepKey="selectFreeShippingShippingMethod"> - <argument name="carrierCode" value="freeshipping"/> - <argument name="methodCode" value="freeshipping"/> - </actionGroup> + <click selector="{{CheckoutCartSummarySection.shippingMethodElementId('freeshipping', 'freeshipping')}}" stepKey="selectShippingMethod"/> <see selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.72" stepKey="seeTaxAmount"/> <reloadPage stepKey="reloadThePage"/> <waitForPageLoad stepKey="waitForPageToReload"/> From df69e820fac74bf257b7c6ce133ec402747a27ee Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Wed, 23 Sep 2020 12:58:00 +0300 Subject: [PATCH 0568/1013] fixed 'unit' test for related changes --- .../Rest/InputParamsResolverTest.php | 164 ------------------ .../Rest/ProductInputParamsResolverTest.php | 107 ++++++++++++ .../Catalog/Api/CategoryRepositoryTest.php | 3 +- 3 files changed, 109 insertions(+), 165 deletions(-) delete mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php deleted file mode 100644 index 7cc88020b694b..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Webapi\Controller\Rest; - -use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Category; -use Magento\Catalog\Model\Product; -use Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\InputParamsResolver as InputParamsResolverPlugin; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Framework\Webapi\Rest\Request as RestRequest; -use Magento\Webapi\Controller\Rest\InputParamsResolver; -use Magento\Webapi\Controller\Rest\Router\Route; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Unit test for InputParamsResolver plugin - */ -class InputParamsResolverTest extends TestCase -{ - /** - * @var string - */ - private $saveRewritesHistory; - - /** - * @var array - */ - private $requestBodyParams; - - /** - * @var array - */ - private $result; - - /** - * @var ObjectManager - */ - private $objectManager; - - /** - * @var InputParamsResolver|MockObject - */ - private $subject; - - /** - * @var RestRequest|MockObject - */ - private $request; - - /** - * @var Product|MockObject - */ - private $product; - - /** - * @var Route|MockObject - */ - private $route; - - /** - * @var InputParamsResolverPlugin - */ - private $plugin; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->saveRewritesHistory = 'save_rewrites_history'; - } - - public function testAfterResolveWithProduct() - { - $this->requestBodyParams = [ - 'product' => [ - 'sku' => 'test', - 'custom_attributes' => [ - ['attribute_code' => $this->saveRewritesHistory, 'value' => 1] - ] - ] - ]; - - $this->route = $this->createPartialMock(Route::class, ['getServiceMethod', 'getServiceClass']); - $this->request = $this->createPartialMock(RestRequest::class, ['getBodyParams']); - $this->request->expects($this->any())->method('getBodyParams')->willReturn($this->requestBodyParams); - $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); - $this->subject->expects($this->any())->method('getRoute')->willReturn($this->route); - $this->product = $this->createPartialMock(Product::class, ['setData']); - - $this->result = [false, $this->product, 'test']; - - $this->objectManager = new ObjectManager($this); - $this->plugin = $this->objectManager->getObject( - InputParamsResolverPlugin::class, - [ - 'request' => $this->request - ] - ); - - $this->route->expects($this->once()) - ->method('getServiceClass') - ->willReturn(ProductRepositoryInterface::class); - $this->route->expects($this->once()) - ->method('getServiceMethod') - ->willReturn('save'); - $this->product->expects($this->once()) - ->method('setData') - ->with($this->saveRewritesHistory, true); - - $this->plugin->afterResolve($this->subject, $this->result); - } - - - public function testAfterResolveWithCategory() - { - $this->requestBodyParams = [ - 'category' => [ - 'name' => 'new name', - 'custom_attributes' => [ - ['attribute_code' => $this->saveRewritesHistory, 'value' => 1], - ['attribute_code' => 'url_key', 'value' => 'new name'] - ] - ] - ]; - - $this->route = $this->createPartialMock(Route::class, ['getServiceMethod', 'getServiceClass']); - $this->request = $this->createPartialMock(RestRequest::class, ['getBodyParams']); - $this->request->expects($this->any())->method('getBodyParams')->willReturn($this->requestBodyParams); - $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); - $this->subject->expects($this->any())->method('getRoute')->willReturn($this->route); - $category = $this->createPartialMock(Category::class, ['setData']); - - $this->result = [false, $category, 'test']; - - $this->objectManager = new ObjectManager($this); - $this->plugin = $this->objectManager->getObject( - InputParamsResolverPlugin::class, - [ - 'request' => $this->request - ] - ); - - $this->route->expects($this->once()) - ->method('getServiceClass') - ->willReturn(CategoryRepositoryInterface::class); - $this->route->expects($this->once()) - ->method('getServiceMethod') - ->willReturn('save'); - $category->expects($this->once()) - ->method('setData') - ->with($this->saveRewritesHistory, true); - - $this->plugin->afterResolve($this->subject, $this->result); - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php new file mode 100644 index 0000000000000..38517e26472f8 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Webapi\Controller\Rest; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\CatalogUrlRewrite\Model\SetSaveRewriteHistory; +use Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\ProductInputParamsResolver; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Webapi\Controller\Rest\InputParamsResolver; +use Magento\Webapi\Controller\Rest\Router\Route; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Unit test for ProductInputParamsResolver plugin + */ +class ProductInputParamsResolverTest extends TestCase +{ + /** + * @var array + */ + private $result; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var InputParamsResolver|MockObject + */ + private $subject; + + /** + * @var RestRequest|MockObject + */ + private $request; + + /** + * @var Product|MockObject + */ + private $product; + + /** + * @var Route|MockObject + */ + private $route; + + /** + * @var SetSaveRewriteHistory|MockObject + */ + private $rewriteHistoryMock; + + /** + * @var ProductInputParamsResolver + */ + private $plugin; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->route = $this->createMock(Route::class); + $this->request = $this->createMock(RestRequest::class); + $this->subject = $this->createMock(InputParamsResolver::class); + $this->product = $this->createMock(Product::class); + $this->rewriteHistoryMock = $this->createMock(SetSaveRewriteHistory::class); + } + + public function testAfterResolveWithProduct() + { + $this->subject->expects($this->any()) + ->method('getRoute') + ->willReturn($this->route); + + $this->result = [false, $this->product, 'test']; + + $this->objectManager = new ObjectManager($this); + $this->plugin = $this->objectManager->getObject( + ProductInputParamsResolver::class, + [ + 'rewriteHistory' => $this->rewriteHistoryMock + ] + ); + + $this->route->expects($this->once()) + ->method('getServiceClass') + ->willReturn(ProductRepositoryInterface::class); + $this->route->expects($this->once()) + ->method('getServiceMethod') + ->willReturn('save'); + $this->rewriteHistoryMock->expects($this->once()) + ->method('execute') + ->with($this->result, 'product', Product::class) + ->willReturn($this->result); + + $this->plugin->afterResolve($this->subject, $this->result); + } +} 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 288cc6658598c..681e092d50cbc 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -252,6 +252,8 @@ public function testUpdateWithDefaultSortByAttribute() */ public function testUpdateUrlKey() { + $this->_markTestAsRestOnly('Functionality available in REST mode only.'); + $categoryId = 333; $categoryData = [ 'name' => 'Update Category Test Old Name', @@ -302,7 +304,6 @@ public function testUpdateUrlKey() $this->assertEquals(CategoryUrlRewriteGenerator::ENTITY_TYPE, $urlRewrite->getEntityType()); $this->assertEquals('update-category-test-new-name.html', $urlRewrite->getRequestPath()); - // check for the forward from the old name to the new name $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); $data = [ From 5ce73ab1f2b04da9dacd274f6d71ea6bbcc65c1f Mon Sep 17 00:00:00 2001 From: Bas van Poppel <vanpoppel@redkiwi.nl> Date: Wed, 23 Sep 2020 14:17:03 +0200 Subject: [PATCH 0569/1013] Set correct discount package value for tablerate --- .../Magento/OfflineShipping/Model/Carrier/Tablerate.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php index bbc199c91263a..112accbae8070 100644 --- a/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php +++ b/app/code/Magento/OfflineShipping/Model/Carrier/Tablerate.php @@ -140,10 +140,9 @@ public function collectRates(RateRequest $request) $freePackageValue += $item->getBaseRowTotal(); } } - $oldValue = $request->getPackageValue(); - $newPackageValue = $oldValue - $freePackageValue; - $request->setPackageValue($newPackageValue); - $request->setPackageValueWithDiscount($newPackageValue); + + $request->setPackageValue($request->getPackageValue() - $freePackageValue); + $request->setPackageValueWithDiscount($request->getPackageValueWithDiscount() - $freePackageValue); } if (!$request->getConditionName()) { From 1bb4814eea6677714c49f95559af107b6bd41aa9 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Wed, 23 Sep 2020 17:07:50 +0300 Subject: [PATCH 0570/1013] return to equal comparison --- lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php index bbdcf70c9fb16..9eb409ab4a593 100644 --- a/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php +++ b/lib/internal/Magento/Framework/GraphQl/Query/EnumLookup.php @@ -60,7 +60,7 @@ public function getEnumValueFromField(string $enumName, string $fieldValue) : st $mappedValues = $this->enumDataMapper->getMappedEnums($enumName); foreach ($enumObject->getValues() as $enumItem) { - if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] === $fieldValue) { + if (isset($mappedValues[$enumItem->getName()]) && $mappedValues[$enumItem->getName()] == $fieldValue) { return $enumItem->getValue(); } } From 09eb0fc98f063983a2cf958ee00f16ef8ad9422c Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Wed, 23 Sep 2020 09:49:05 -0500 Subject: [PATCH 0571/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Theme/Model/Indexer/Design/Config.php | 20 +--------------- .../Model/Indexer/Design/IndexerHandler.php | 23 +++++++++++++++---- app/code/Magento/Theme/etc/indexer.xml | 2 +- .../Framework/Indexer/SaveHandler/Grid.php | 14 ++++------- 4 files changed, 24 insertions(+), 35 deletions(-) diff --git a/app/code/Magento/Theme/Model/Indexer/Design/Config.php b/app/code/Magento/Theme/Model/Indexer/Design/Config.php index 86a72ebc64391..43b58257b343c 100644 --- a/app/code/Magento/Theme/Model/Indexer/Design/Config.php +++ b/app/code/Magento/Theme/Model/Indexer/Design/Config.php @@ -5,7 +5,6 @@ */ namespace Magento\Theme\Model\Indexer\Design; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\ActionInterface; use Magento\Framework\Indexer\FieldsetPool; use Magento\Framework\Indexer\HandlerPool; @@ -94,30 +93,13 @@ protected function execute(array $ids = []) /** @var \Magento\Theme\Model\ResourceModel\Design\Config\Scope\Collection $collection */ $collection = $this->collectionFactory->create(); $this->prepareFields(); - - $tmp = $this->isFlatTableExists(); - - if (!$this->isFlatTableExists()) { - // instead of clean index check if table exists and create it if not + if (!count($ids)) { $this->getSaveHandler()->cleanIndex([]); } $this->getSaveHandler()->deleteIndex([], new \ArrayObject($ids)); $this->getSaveHandler()->saveIndex([], $collection); } - private function isFlatTableExists() - { - /** @var \Magento\Framework\App\ResourceConnection $resource */ - $resource = ObjectManager::getInstance()->get(\Magento\Framework\App\ResourceConnection::class); - - /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */ - $connection = ObjectManager::getInstance()->get(\Magento\Framework\App\ResourceConnection::class)->getConnection(); - - $tableName = $resource->getTableName('design_config_grid_flat'); - - return $connection->isTableExists($tableName); - } - /** * Execute full indexation * diff --git a/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php index f1acf01b4fc81..92296c5dc9902 100644 --- a/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php +++ b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php @@ -44,16 +44,29 @@ public function __construct( } /** - * Clean index table by truncation + * @return bool + */ + private function isFlatTableExists() + { + $adapter = $this->resource->getConnection('write'); + $tableName = $this->flatScopeResolver->resolve($this->getIndexName(), []); + + return $adapter->isTableExists($tableName); + } + + /** + * Clean index table by deleting all records * * @inheritdoc */ public function cleanIndex($dimensions) { - $adapter = $this->resource->getConnection('write'); - $tableName = $this->flatScopeResolver->resolve($this->getIndexName(), $dimensions); - if ($adapter->isTableExists($tableName)) { - $adapter->truncateTable($tableName); + if ($this->isFlatTableExists()) { + $adapter = $this->resource->getConnection('write'); + $tableName = $this->flatScopeResolver->resolve($this->getIndexName(), $dimensions); + $adapter->delete($tableName); + } else { + $this->indexStructure->create($this->getIndexName(), $this->fields, $dimensions); } } } diff --git a/app/code/Magento/Theme/etc/indexer.xml b/app/code/Magento/Theme/etc/indexer.xml index 7ed25878e383c..8cc8971024e48 100644 --- a/app/code/Magento/Theme/etc/indexer.xml +++ b/app/code/Magento/Theme/etc/indexer.xml @@ -17,7 +17,7 @@ <field name="store_group_id" xsi:type="filterable" dataType="int"/> <field name="store_id" xsi:type="filterable" dataType="int"/> </fieldset> - <saveHandler class="Magento\Framework\Indexer\SaveHandler\Grid"/> + <saveHandler class="Magento\Theme\Model\Indexer\Design\IndexerHandler"/> <structure class="Magento\Framework\Indexer\GridStructure"/> </indexer> </config> diff --git a/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php b/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php index c5307b8823bce..71d937d016e9c 100644 --- a/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php +++ b/lib/internal/Magento/Framework/Indexer/SaveHandler/Grid.php @@ -69,16 +69,10 @@ protected function prepareFilterableFields(array $documents) */ public function deleteIndex($dimensions, \Traversable $ids) { - if (!empty(iterator_to_array($ids))) { - foreach ($this->batch->getItems($ids, $this->batchSize) as $batchIds) { - $this->connection->delete( - $this->getTableName('filterable', $dimensions), - ['entity_id IN(?)' => $batchIds] - ); - } - } else { - $this->connection->truncateTable( - $this->getTableName('filterable', $dimensions) + foreach ($this->batch->getItems($ids, $this->batchSize) as $batchIds) { + $this->connection->delete( + $this->getTableName('filterable', $dimensions), + ['entity_id IN(?)' => $batchIds] ); } } From 0873cf12a1f4da8975efa5afe989165dfd4f531b Mon Sep 17 00:00:00 2001 From: Nikita Sarychev <nsarychev@lachestry.com> Date: Wed, 23 Sep 2020 17:55:58 +0300 Subject: [PATCH 0572/1013] fixed select field list in getStatusByState method --- app/code/Magento/Sales/Model/ResourceModel/Order/Status.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php index 58284759b2fee..3ff2ed66a846b 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Status.php @@ -257,7 +257,7 @@ protected function getStatusByState($state) { return (string)$this->getConnection()->fetchOne( $select = $this->getConnection()->select() - ->from(['sss' => $this->stateTable, []]) + ->from(['sss' => $this->stateTable], []) ->where('state = ?', $state) ->limit(1) ->columns(['status']) From 7b93b53a703415f706bf16cfeb4a3abf36756698 Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Wed, 23 Sep 2020 21:37:13 +0300 Subject: [PATCH 0573/1013] depricaded action group --- .../Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml | 2 +- .../Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml | 4 ++-- .../Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml | 3 ++- .../Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml | 2 +- .../Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml index 73748b9a6bad6..ca6a1498c4215 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/NavigateCustomerActionGroup.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="NavigateToAllCustomerPage"> + <actionGroup name="NavigateToAllCustomerPage" deprecated="Use AdminOpenCustomersGridActionGroup instead."> <annotations> <description>Goes to the Admin Customers grid page.</description> </annotations> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml index a7383af2d7eea..5833bf07aeae2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminChangeSingleCustomerGroupViaGridTest.xml @@ -29,12 +29,12 @@ <!--Delete created data--> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createCustomerGroup" stepKey="deleteCustomerGroup"/> - <actionGroup ref="NavigateToAllCustomerPage" stepKey="navigateToCustomersPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomersPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearCustomersGridFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <actionGroup ref="NavigateToAllCustomerPage" stepKey="navigateToCustomersPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomersPage"/> <actionGroup ref="AdminFilterCustomerGridByEmail" stepKey="filterCustomer"> <argument name="email" value="$$createCustomer.email$$"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index d12a89f01cb96..cb003ed837294 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -29,7 +29,8 @@ </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> - <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="navigateToCustomers"/> + <amOnPage url="{{AdminCustomerPage.url}}" stepKey="navigateToCustomers"/> + <waitForPageLoad stepKey="waitForLoad1"/> <click selector="{{AdminCustomerGridMainActionsSection.addNewCustomer}}" stepKey="clickCreateCustomer"/> <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/AdminGridSearchSelectAllTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml index 64e9f6d10bdb3..7f1b1dfee7ce0 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSearchSelectAllTest.xml @@ -29,7 +29,7 @@ <deleteData createDataKey="secondCustomer" stepKey="deleteSecondCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomerPage"/> <!-- search Admin Data Grid By Keyword --> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> <fillField selector="{{AdminDataGridHeaderSection.search}}" userInput="$$secondCustomer.email$$" stepKey="fillKeywordSearchFieldWithSecondCustomerEmail"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml index bfc49fd476dd0..aa7cdbb9207b9 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminGridSelectAllOnPageTest.xml @@ -30,7 +30,7 @@ <deleteData createDataKey="thirdCustomer" stepKey="deleteThirdCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminCustomerPage.url}}" stepKey="openCustomerPage"/> + <actionGroup ref="AdminOpenCustomersGridActionGroup" stepKey="openCustomerPage"/> <!-- Select all from dropdown --> <actionGroup ref="AdminGridSelectAllActionGroup" stepKey="selectAllCustomers"/> <!-- Deselect third customer --> From e44b4868c0a93c863b511f347a868e731e2b3102 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Wed, 23 Sep 2020 14:19:33 -0500 Subject: [PATCH 0574/1013] [AWS S3] MC-37452: Introduce new Adapter (#6166) MC-37532: Introduce new module with adapter --- app/code/Magento/AwsS3/Driver/AwsS3.php | 553 +++++++ .../Magento/AwsS3/Driver/AwsS3Factory.php | 47 + app/code/Magento/AwsS3/LICENSE.txt | 48 + app/code/Magento/AwsS3/LICENSE_AFL.txt | 48 + app/code/Magento/AwsS3/Model/Config.php | 75 + app/code/Magento/AwsS3/README.md | 3 + app/code/Magento/AwsS3/composer.json | 23 + app/code/Magento/AwsS3/etc/adminhtml/di.xml | 19 + .../Magento/AwsS3/etc/adminhtml/system.xml | 35 + app/code/Magento/AwsS3/etc/di.xml | 16 + app/code/Magento/AwsS3/etc/module.xml | 14 + app/code/Magento/AwsS3/registration.php | 9 + .../Reader/Source/Deployed/DocumentRoot.php | 35 +- .../Driver/DriverFactoryInterface.php | 23 + .../RemoteStorage/Driver/DriverPool.php | 72 + app/code/Magento/RemoteStorage/LICENSE.txt | 48 + .../Magento/RemoteStorage/LICENSE_AFL.txt | 48 + .../Magento/RemoteStorage/Model/Config.php | 42 + .../Model/Config/Source/FileStorage.php | 37 + .../RemoteStorage/Model/Filesystem.php | 55 + .../Magento/RemoteStorage/Plugin/Sitemap.php | 64 + app/code/Magento/RemoteStorage/README.md | 1 + .../Test/Unit/Model/ConfigTest.php | 43 + app/code/Magento/RemoteStorage/composer.json | 25 + .../RemoteStorage/etc/adminhtml/di.xml | 19 + .../RemoteStorage/etc/adminhtml/system.xml | 20 + app/code/Magento/RemoteStorage/etc/di.xml | 51 + app/code/Magento/RemoteStorage/etc/module.xml | 15 + .../Magento/RemoteStorage/registration.php | 11 + app/code/Magento/Sitemap/Block/Robots.php | 4 + app/code/Magento/Sitemap/Model/Sitemap.php | 9 +- app/etc/di.xml | 1 + composer.json | 8 +- composer.lock | 1300 ++++++++++------- .../Magento/Framework/Api/Uploader.php | 20 + .../Test/Unit/Config}/DocumentRootTest.php | 4 +- .../Magento/Framework/Config/DocumentRoot.php | 50 + .../Magento/Framework/File/Uploader.php | 77 +- .../Filesystem/Directory/ReadFactory.php | 10 +- .../Filesystem/Directory/TargetDirectory.php | 60 + .../Filesystem/Directory/WriteFactory.php | 10 +- .../Framework/Filesystem/Driver/File.php | 6 +- .../Framework/Filesystem/DriverPool.php | 2 +- .../Filesystem/DriverPoolInterface.php | 22 + .../Framework/Filesystem/File/ReadFactory.php | 8 +- .../Filesystem/File/WriteFactory.php | 5 +- .../Framework/Setup/FilePermissions.php | 9 +- .../Setup/Test/Unit/FilePermissionsTest.php | 38 +- 48 files changed, 2546 insertions(+), 596 deletions(-) create mode 100644 app/code/Magento/AwsS3/Driver/AwsS3.php create mode 100644 app/code/Magento/AwsS3/Driver/AwsS3Factory.php create mode 100644 app/code/Magento/AwsS3/LICENSE.txt create mode 100644 app/code/Magento/AwsS3/LICENSE_AFL.txt create mode 100644 app/code/Magento/AwsS3/Model/Config.php create mode 100644 app/code/Magento/AwsS3/README.md create mode 100644 app/code/Magento/AwsS3/composer.json create mode 100644 app/code/Magento/AwsS3/etc/adminhtml/di.xml create mode 100644 app/code/Magento/AwsS3/etc/adminhtml/system.xml create mode 100644 app/code/Magento/AwsS3/etc/di.xml create mode 100644 app/code/Magento/AwsS3/etc/module.xml create mode 100644 app/code/Magento/AwsS3/registration.php create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverPool.php create mode 100644 app/code/Magento/RemoteStorage/LICENSE.txt create mode 100644 app/code/Magento/RemoteStorage/LICENSE_AFL.txt create mode 100644 app/code/Magento/RemoteStorage/Model/Config.php create mode 100644 app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php create mode 100644 app/code/Magento/RemoteStorage/Model/Filesystem.php create mode 100644 app/code/Magento/RemoteStorage/Plugin/Sitemap.php create mode 100644 app/code/Magento/RemoteStorage/README.md create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php create mode 100644 app/code/Magento/RemoteStorage/composer.json create mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/di.xml create mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/system.xml create mode 100644 app/code/Magento/RemoteStorage/etc/di.xml create mode 100644 app/code/Magento/RemoteStorage/etc/module.xml create mode 100644 app/code/Magento/RemoteStorage/registration.php rename {app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed => lib/internal/Magento/Framework/App/Test/Unit/Config}/DocumentRootTest.php (92%) create mode 100644 lib/internal/Magento/Framework/Config/DocumentRoot.php create mode 100644 lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php create mode 100644 lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php new file mode 100644 index 0000000000000..602e81ec480ff --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -0,0 +1,553 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Driver; + +use Aws\S3\S3Client; +use League\Flysystem\AwsS3v3\AwsS3Adapter; +use League\Flysystem\Config; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; + +/** + * Driver for AWS S3 IO operations. + */ +class AwsS3 implements DriverInterface +{ + public const S3 = 'aws-s3'; + + private const TYPE_DIR = 'dir'; + private const TYPE_FILE = 'file'; + + /** + * @var AwsS3Adapter + */ + private $adapter; + + /** + * @var array + */ + private $streams = []; + + /** + * @param string $region + * @param string $bucket + * @param string|null $key + * @param string|null $secret + */ + public function __construct(string $region, string $bucket, string $key = null, string $secret = null) + { + $config = [ + 'region' => $region, + 'version' => 'latest' + ]; + + if ($key && $secret) { + $config['credentials'] = [ + 'key' => $key, + 'secret' => $secret, + ]; + } + + $client = new S3Client($config); + $this->adapter = new AwsS3Adapter($client, $bucket); + } + + /** + * Destroy opened streams. + * + * @throws FileSystemException + */ + public function __destruct() + { + foreach ($this->streams as $stream) { + $this->fileClose($stream); + } + } + + /** + * @inheritDoc + */ + public function fileGetContents($path, $flag = null, $context = null): string + { + $path = $this->getRelativePath('', $path); + + if (isset($this->streams[$path])) { + //phpcs:disable + return file_get_contents(stream_get_meta_data($this->streams[$path])['uri']); + //phpcs:enable + } + + return $this->adapter->read($path)['contents']; + } + + /** + * @inheritDoc + */ + public function isExists($path): bool + { + if ($path === '/') { + return true; + } + + $path = $this->getRelativePath('', $path); + + if (!$path || $path === '/') { + return true; + } + + return $this->adapter->has($path); + } + + /** + * @inheritDoc + */ + public function isWritable($path): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function createDirectory($path, $permissions = 0777): bool + { + if ($path === '/') { + return true; + } + + $path = $this->getRelativePath('', $path); + + return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config([])); + } + + /** + * @inheritDoc + */ + public function copy($source, $destination, DriverInterface $targetDriver = null): bool + { + $source = $this->getRelativePath('', $source); + $destination = $this->getRelativePath('', $destination); + + return $this->adapter->copy($source, $destination); + } + + /** + * @inheritDoc + */ + public function deleteFile($path): bool + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->delete($path); + } + + /** + * @inheritDoc + */ + public function deleteDirectory($path): bool + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->deleteDir($path); + } + + /** + * @inheritDoc + */ + public function filePutContents($path, $content, $mode = null, $context = null): int + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->write($path, $content, new Config(['ACL' => 'public-read']))['size']; + } + + /** + * @inheritDoc + */ + public function readDirectoryRecursively($path = null): array + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->listContents($path, true); + } + + /** + * @inheritDoc + */ + public function readDirectory($path): array + { + $path = $this->getRelativePath('', $path); + + return $this->adapter->listContents($path, false); + } + + /** + * @inheritDoc + */ + public function getRealPathSafety($path) + { + return '/'; + } + + /** + * @inheritDoc + */ + public function getAbsolutePath($basePath, $path, $scheme = null) + { + $path = $this->getRelativePath($basePath, $path); + + if ($path === '/') { + $path = ''; + } + + if ($basePath !== '/') { + $path = $basePath . $path; + } + + $path = $path ?: '.'; + + return $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), $path); + } + + /** + * @inheritDoc + */ + public function isReadable($path): bool + { + return $this->isExists($path); + } + + /** + * @inheritDoc + */ + public function isFile($path): bool + { + if ($path === '/') { + return false; + } + + $path = $this->getRelativePath('', $path); + $path = rtrim($path, '/'); + + return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_FILE; + } + + /** + * @inheritDoc + */ + public function isDirectory($path): bool + { + if ($path === '/') { + return true; + } + + $path = $this->getRelativePath('', $path); + + if (!$path || $path === '/') { + return true; + } + + $path = rtrim($path, '/') . '/'; + + return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_DIR; + } + + /** + * @inheritDoc + */ + public function getRelativePath($basePath, $path = null): string + { + $relativePath = str_replace( + $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), '.'), + '', + $path + ); + + if ($basePath && $basePath !== '/') { + $relativePath = str_replace($basePath, '', $relativePath); + } + + $relativePath = ltrim($relativePath, '/'); + + if (!$relativePath) { + $relativePath = '/'; + } + + return $relativePath; + } + + /** + * @inheritDoc + */ + public function getParentDirectory($path): string + { + return '/'; + } + + /** + * @inheritDoc + */ + public function getRealPath($path) + { + return $this->getAbsolutePath('', $path); + } + + /** + * @inheritDoc + */ + public function rename($oldPath, $newPath, DriverInterface $targetDriver = null): bool + { + $oldPath = $this->getRelativePath('', $oldPath); + $newPath = $this->getRelativePath('', $newPath); + + return $this->adapter->rename($oldPath, $newPath); + } + + /** + * @inheritDoc + */ + public function stat($path): array + { + $path = $this->getRelativePath('', $path); + $metaInfo = $this->adapter->getMetadata($path); + + if (!$metaInfo) { + throw new FileSystemException(__('Cannot gather stats! %1', (array)$path)); + } + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'atime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + 'size' => $metaInfo['size'], + 'type' => $metaInfo['type'], + 'mtime' => $metaInfo['timestamp'], + 'disposition' => null, + ]; + } + + /** + * @inheritDoc + */ + public function search($pattern, $path): array + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function symlink($source, $destination, DriverInterface $targetDriver = null): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function changePermissions($path, $permissions): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function changePermissionsRecursively($path, $dirPermissions, $filePermissions): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function touch($path, $modificationTime = null) + { + return true; + } + + /** + * @inheritDoc + */ + public function fileReadLine($resource, $length, $ending = null): string + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileRead($resource, $length): string + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = fread($resource, $length); + if ($result === false) { + throw new FileSystemException(__('File cannot be read %1', [$this->getWarningMessage()])); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function fileGetCsv($resource, $length = 0, $delimiter = ',', $enclosure = '"', $escape = '\\') + { + //phpcs:disable + $metadata = stream_get_meta_data($resource); + //phpcs:enable + $file = $this->adapter->read($metadata['uri'])['contents']; + + return str_getcsv($file, $delimiter, $enclosure, $escape); + } + + /** + * @inheritDoc + */ + public function fileTell($resource): int + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileSeek($resource, $offset, $whence = SEEK_SET): int + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function endOfFile($resource): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function filePutCsv($resource, array $data, $delimiter = ',', $enclosure = '"') + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return fputcsv($resource, $data, $delimiter, $enclosure); + } + + /** + * @inheritDoc + */ + public function fileFlush($resource): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileLock($resource, $lockMode = LOCK_EX): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileUnlock($resource): bool + { + throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + } + + /** + * @inheritDoc + */ + public function fileWrite($resource, $data) + { + //phpcs:disable + $resourcePath = stream_get_meta_data($resource)['uri']; + + foreach ($this->streams as $stream) { + if (stream_get_meta_data($stream)['uri'] === $resourcePath) { + return fwrite($stream, $data); + } + } + //phpcs:enable + + return false; + } + + /** + * @inheritDoc + */ + public function fileClose($resource): bool + { + //phpcs:disable + $resourcePath = stream_get_meta_data($resource)['uri']; + + foreach ($this->streams as $path => $stream) { + if (stream_get_meta_data($stream)['uri'] === $resourcePath) { + $this->adapter->writeStream($path, $resource, new Config(['ACL' => 'public-read'])); + + // Remove path from streams after + unset($this->streams[$path]); + + return fclose($stream); + } + } + //phpcs:enable + + return false; + } + + /** + * @inheritDoc + */ + public function fileOpen($path, $mode) + { + $path = $this->getRelativePath('', $path); + + if (!isset($this->streams[$path])) { + $this->streams[$path] = tmpfile(); + if ($this->adapter->has($path)) { + $file = tmpfile(); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + fwrite($file, $this->adapter->read($path)['contents']); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + fseek($file, 0); + } else { + $file = tmpfile(); + } + $this->streams[$path] = $file; + } + + return $this->streams[$path]; + } + + /** + * Returns last warning message string + * + * @return string|null + */ + private function getWarningMessage(): ?string + { + $warning = error_get_last(); + if ($warning && $warning['type'] === E_WARNING) { + return 'Warning!' . $warning['message']; + } + + return null; + } +} diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php new file mode 100644 index 0000000000000..e71c3a84d3ce5 --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Driver; + +use Magento\AwsS3\Model\Config; + +use Magento\Framework\Filesystem\DriverInterface; +use Magento\RemoteStorage\Driver\DriverFactoryInterface; + +/** + * Creates a pre-configured instance of AWS S3 driver. + */ +class AwsS3Factory implements DriverFactoryInterface +{ + /** + * @var Config + */ + private $config; + + /** + * @param Config $config + */ + public function __construct(Config $config) + { + $this->config = $config; + } + + /** + * Creates an instance of AWS S3 driver. + * + * @return DriverInterface + */ + public function create(): DriverInterface + { + return new AwsS3( + $this->config->getRegion(), + $this->config->getBucket(), + $this->config->getAccessKey(), + $this->config->getSecretKey() + ); + } +} diff --git a/app/code/Magento/AwsS3/LICENSE.txt b/app/code/Magento/AwsS3/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AwsS3/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/AwsS3/LICENSE_AFL.txt b/app/code/Magento/AwsS3/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AwsS3/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/AwsS3/Model/Config.php b/app/code/Magento/AwsS3/Model/Config.php new file mode 100644 index 0000000000000..00cd5b36740e6 --- /dev/null +++ b/app/code/Magento/AwsS3/Model/Config.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Configuration for AWS S3. + */ +class Config +{ + public const PATH_DRIVER = 'system/file_system/driver'; + public const PATH_REGION = 'system/file_system/region'; + public const PATH_BUCKET = 'system/file_system/bucket'; + public const PATH_ACCESS_KEY = 'system/file_system/access_key'; + public const PATH_SECRET_KEY = 'system/file_system/secret_key'; + + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @param ScopeConfigInterface $config + */ + public function __construct(ScopeConfigInterface $config) + { + $this->config = $config; + } + + /** + * Retrieves region. + * + * @return string + */ + public function getRegion(): string + { + return (string)$this->config->getValue(self::PATH_REGION); + } + + /** + * Retrieves bucket. + * + * @return string + */ + public function getBucket(): string + { + return (string)$this->config->getValue(self::PATH_BUCKET); + } + + /** + * Retrieves access key. + * + * @return string + */ + public function getAccessKey(): string + { + return (string)$this->config->getValue(self::PATH_ACCESS_KEY); + } + + /** + * Retrieves secret key. + * + * @return string + */ + public function getSecretKey(): string + { + return (string)$this->config->getValue(self::PATH_SECRET_KEY); + } +} diff --git a/app/code/Magento/AwsS3/README.md b/app/code/Magento/AwsS3/README.md new file mode 100644 index 0000000000000..fc07df1717136 --- /dev/null +++ b/app/code/Magento/AwsS3/README.md @@ -0,0 +1,3 @@ +# Magento_AwsS3 module + +The Magento_AwsS3 module integrates your Magento with the [AWS S3](https://aws.amazon.com/s3) storage. diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json new file mode 100644 index 0000000000000..02733f01d2285 --- /dev/null +++ b/app/code/Magento/AwsS3/composer.json @@ -0,0 +1,23 @@ +{ + "name": "magento/module-aws-s-3", + "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "^100.0.2", + "magento/module-remote-storage": "*", + "league/flysystem": "^1.0", + "league/flysystem-aws-s3-v3": "^1.0" + }, + "type": "magento2-module", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AwsS3\\": "" + } + } +} diff --git a/app/code/Magento/AwsS3/etc/adminhtml/di.xml b/app/code/Magento/AwsS3/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..4d3dcd601047f --- /dev/null +++ b/app/code/Magento/AwsS3/etc/adminhtml/di.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\RemoteStorage\Model\Config\Source\FileStorage"> + <arguments> + <argument name="options" xsi:type="array"> + <item name="aws-s3" xsi:type="array"> + <item name="value" xsi:type="string">aws-s3</item> + <item name="label" xsi:type="string" translate="true">AWS S3</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/AwsS3/etc/adminhtml/system.xml b/app/code/Magento/AwsS3/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..0f97b96107ed3 --- /dev/null +++ b/app/code/Magento/AwsS3/etc/adminhtml/system.xml @@ -0,0 +1,35 @@ +<?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="file_system"> + <field id="access_key" translate="label comment" type="password" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Access Key</label> + <validate>required-entry</validate> + <depends><field id="driver">aws-s3</field></depends> + </field> + <field id="secret_key" translate="label comment" type="password" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Secret Key</label> + <validate>required-entry</validate> + <depends><field id="driver">aws-s3</field></depends> + </field> + <field id="bucket" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Bucket</label> + <validate>required-entry</validate> + <depends><field id="driver">aws-s3</field></depends> + </field> + <field id="region" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Region</label> + <validate>required-entry</validate> + <depends><field id="driver">aws-s3</field></depends> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/AwsS3/etc/di.xml b/app/code/Magento/AwsS3/etc/di.xml new file mode 100644 index 0000000000000..2b66da74299ea --- /dev/null +++ b/app/code/Magento/AwsS3/etc/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\RemoteStorage\Driver\DriverPool"> + <arguments> + <argument name="remotePool" xsi:type="array"> + <item name="aws-s3" xsi:type="object">Magento\AwsS3\Driver\AwsS3Factory</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/AwsS3/etc/module.xml b/app/code/Magento/AwsS3/etc/module.xml new file mode 100644 index 0000000000000..ab99195d45ab5 --- /dev/null +++ b/app/code/Magento/AwsS3/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_AwsS3"> + <sequence> + <module name="Magento_RemoteStorage"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/AwsS3/registration.php b/app/code/Magento/AwsS3/registration.php new file mode 100644 index 0000000000000..496fbad1d3371 --- /dev/null +++ b/app/code/Magento/AwsS3/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_AwsS3', __DIR__); 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 bf59c729790a7..2e50bbb8ef3c9 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 @@ -3,57 +3,58 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Config\Model\Config\Reader\Source\Deployed; -use Magento\Framework\Config\ConfigOptionsListConstants; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\DocumentRoot as BaseDocumentRoot; /** - * Class DocumentRoot - * @package Magento\Config\Model\Config\Reader\Source\Deployed + * Document root detector. + * * @api * @since 101.0.0 + * + * @deprecated Use new implementation + * @see \Magento\Framework\Config\DocumentRoot */ class DocumentRoot { /** - * @var DeploymentConfig + * @var BaseDocumentRoot */ - private $config; + private $documentRoot; /** - * DocumentRoot constructor. * @param DeploymentConfig $config + * @param BaseDocumentRoot $documentRoot + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function __construct(DeploymentConfig $config) + public function __construct(DeploymentConfig $config, BaseDocumentRoot $documentRoot = null) { - $this->config = $config; + $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(BaseDocumentRoot::class); } /** - * A shortcut to load the document root path from the DirectoryList based on the - * deployment configuration. + * A shortcut to load the document root path from the DirectoryList. * * @return string * @since 101.0.0 */ public function getPath() { - return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; + return $this->documentRoot->getPath(); } /** - * Returns whether the deployment configuration specifies that the document root is - * in the pub/ folder. This affects ares such as sitemaps and robots.txt (and will - * likely be extended to control other areas). + * Checks if root folder is /pub. * * @return bool * @since 101.0.0 */ public function isPub() { - return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); + return $this->documentRoot->isPub(); } } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php new file mode 100644 index 0000000000000..a95284fb27391 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Filesystem\DriverInterface; + +/** + * Factory for drivers with additional configuration. + */ +interface DriverFactoryInterface +{ + /** + * Creates pre-configured driver. + * + * @return DriverInterface + */ + public function create(): DriverInterface; +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php new file mode 100644 index 0000000000000..11a49147f4f19 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\DriverPool as BaseDriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; + +/** + * The remote driver pool. + */ +class DriverPool implements DriverPoolInterface +{ + public const PATH_DRIVER = 'system/file_system/driver'; + public const REMOTE = 'remote'; + + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var DriverPool + */ + private $driverPool; + + /** + * @var DriverInterface[] + */ + private $pool = []; + + /** + * @var DriverFactoryInterface[] + */ + private $remotePool; + + /** + * @param BaseDriverPool $driverPool + * @param ScopeConfigInterface $config + * @param array $remotePool + */ + public function __construct(BaseDriverPool $driverPool, ScopeConfigInterface $config, array $remotePool = []) + { + $this->driverPool = $driverPool; + $this->config = $config; + $this->remotePool = $remotePool; + } + + /** + * @inheritDoc + */ + public function getDriver($code = self::REMOTE): DriverInterface + { + $driver = $this->config->getValue('system/file_system/driver'); + + if (isset($this->pool[$code])) { + return $this->pool[$code]; + } + + if ($driver && $driver !== BaseDriverPool::FILE) { + return $this->pool[$code] = $this->remotePool[$driver]->create(); + } + + return $this->pool[$code] = $this->driverPool->getDriver($code); + } +} diff --git a/app/code/Magento/RemoteStorage/LICENSE.txt b/app/code/Magento/RemoteStorage/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/RemoteStorage/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/RemoteStorage/LICENSE_AFL.txt b/app/code/Magento/RemoteStorage/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/RemoteStorage/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/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php new file mode 100644 index 0000000000000..b49c647ab6894 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Filesystem\DriverPool; + +/** + * Configuration for remote storage. + */ +class Config +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check if remote FS is enabled. + * + * @return bool + */ + public function isEnabled(): bool + { + $driver = $this->scopeConfig->getValue('system/file_system/driver'); + + return $driver && $driver !== DriverPool::FILE; + } +} diff --git a/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php b/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php new file mode 100644 index 0000000000000..4972cdda18d9b --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Model\Config\Source; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Provides a list of supported file storages. + */ +class FileStorage implements OptionSourceInterface +{ + /** + * @var array + */ + private $options; + + /** + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = $options; + } + + /** + * @inheritDoc + */ + public function toOptionArray(): array + { + return $this->options; + } +} diff --git a/app/code/Magento/RemoteStorage/Model/Filesystem.php b/app/code/Magento/RemoteStorage/Model/Filesystem.php new file mode 100644 index 0000000000000..040ee005cd57d --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Filesystem.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Model; + +use Magento\Framework\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\Filesystem\Directory\WriteFactory; + +/** + * Filesystem implementation for remote storage. + */ +class Filesystem extends \Magento\Framework\Filesystem +{ + /** + * @var bool + */ + private $isEnabled; + + /** + * @param DirectoryList $directoryList + * @param ReadFactory $readFactory + * @param WriteFactory $writeFactory + * @param Config $config + */ + public function __construct( + DirectoryList $directoryList, + ReadFactory $readFactory, + WriteFactory $writeFactory, + Config $config + ) { + $this->isEnabled = $config->isEnabled(); + + parent::__construct($directoryList, $readFactory, $writeFactory); + } + + /** + * Gets URL path by code. + * + * @param string $code + * @return string + */ + protected function getDirPath($code): string + { + if ($this->isEnabled) { + return $this->directoryList->getUrlPath($code) ?: '/'; + } + + return parent::getDirPath($code); + } +} diff --git a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php new file mode 100644 index 0000000000000..e84f216ba996c --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; +use Magento\Sitemap\Model\Sitemap as BaseSitemap; + +/** + * Plugin to replace file URL with remote URL. + */ +class Sitemap +{ + /** + * @var Config + */ + private $config; + + /** + * @var DriverPool + */ + private $driverPool; + + /** + * @param DriverPool $driverPool + * @param Config $config + */ + public function __construct(DriverPool $driverPool, Config $config) + { + $this->driverPool = $driverPool; + $this->config = $config; + } + + /** + * Modifies image URl to point to correct remote storage. + * + * @param BaseSitemap $subject + * @param string $result + * @param string $sitemapPath + * @param string $sitemapFileName + * @return string + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetSitemapUrl( + BaseSitemap $subject, + string $result, + string $sitemapPath, + string $sitemapFileName + ): string { + if ($this->config->isEnabled()) { + $path = trim($sitemapPath . $sitemapFileName, '/'); + + return $this->driverPool->getDriver()->getAbsolutePath('', $path); + } + + return $result; + } +} diff --git a/app/code/Magento/RemoteStorage/README.md b/app/code/Magento/RemoteStorage/README.md new file mode 100644 index 0000000000000..f33b25795a995 --- /dev/null +++ b/app/code/Magento/RemoteStorage/README.md @@ -0,0 +1 @@ +# Magento_RemoteStorage module diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..1e121c1d1dda1 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; +use PHPUnit\Framework\TestCase; + +/** + * @see Config + */ +class ConfigTest extends TestCase +{ + /** + * @var Config + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $configMock->method('getValue') + ->willReturnMap([ + [DriverPool::PATH_DRIVER, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, DriverPool::REMOTE], + ]); + + $this->model = new Config($configMock); + } + + public function testIsEnabled(): void + { + self::assertTrue($this->model->isEnabled()); + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json new file mode 100644 index 0000000000000..105b2b2b21a46 --- /dev/null +++ b/app/code/Magento/RemoteStorage/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-remote-storage", + "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "^100.0.2" + }, + "suggest": { + "magento/module-backend": "*", + "magento/module-sitemap": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\RemoteStorage\\": "" + } + } +} diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..1437f0636ac03 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\RemoteStorage\Model\Config\Source\FileStorage"> + <arguments> + <argument name="options" xsi:type="array"> + <item name="file" xsi:type="array"> + <item name="value" xsi:type="const">Magento\Framework\Filesystem\DriverPool::FILE</item> + <item name="label" xsi:type="string" translate="true">File System</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..aa5865c099dc5 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="file_system" translate="label" type="text" sortOrder="850" showInDefault="1"> + <label>Storage Configuration</label> + <field id="driver" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>General Storage</label> + <source_model>Magento\RemoteStorage\Model\Config\Source\FileStorage</source_model> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml new file mode 100644 index 0000000000000..9bc960691d034 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -0,0 +1,51 @@ +<?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="remoteWriteFactory" type="Magento\Framework\Filesystem\Directory\WriteFactory"> + <arguments> + <argument name="driverPool" xsi:type="object">Magento\RemoteStorage\Driver\DriverPool</argument> + </arguments> + </virtualType> + <virtualType name="remoteReadFactory" type="Magento\Framework\Filesystem\Directory\ReadFactory"> + <arguments> + <argument name="driverPool" xsi:type="object">Magento\RemoteStorage\Driver\DriverPool</argument> + </arguments> + </virtualType> + <virtualType name="remoteFilesystem" type="Magento\RemoteStorage\Model\Filesystem"> + <arguments> + <argument name="writeFactory" xsi:type="object">remoteWriteFactory</argument> + <argument name="readFactory" xsi:type="object">remoteReadFactory</argument> + </arguments> + </virtualType> + <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Save"> + <arguments> + <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Block\Adminhtml\Grid\Renderer\Link"> + <arguments> + <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete"> + <arguments> + <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Model\Sitemap"> + <arguments> + <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + </arguments> + <plugin name="remote_sitemap" type="Magento\RemoteStorage\Plugin\Sitemap" /> + </type> + <type name="Magento\Framework\Filesystem\Directory\TargetDirectory"> + <arguments> + <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/RemoteStorage/etc/module.xml b/app/code/Magento/RemoteStorage/etc/module.xml new file mode 100644 index 0000000000000..cc9f2e7328292 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/module.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_RemoteStorage" > + <sequence> + <module name="Magento_Backend"/> + <module name="Magento_Sitemap"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/RemoteStorage/registration.php b/app/code/Magento/RemoteStorage/registration.php new file mode 100644 index 0000000000000..3a6d6b67a8dcf --- /dev/null +++ b/app/code/Magento/RemoteStorage/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_RemoteStorage', + __DIR__ +); diff --git a/app/code/Magento/Sitemap/Block/Robots.php b/app/code/Magento/Sitemap/Block/Robots.php index a074e95ce2f80..2fe7f8807d6a0 100644 --- a/app/code/Magento/Sitemap/Block/Robots.php +++ b/app/code/Magento/Sitemap/Block/Robots.php @@ -11,6 +11,7 @@ use Magento\Robots\Model\Config\Value; use Magento\Sitemap\Helper\Data as SitemapHelper; use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; +use Magento\Sitemap\Model\Sitemap; use Magento\Sitemap\Model\SitemapConfigReader; use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; @@ -115,6 +116,9 @@ protected function getSitemapLinks(array $storeIds) $collection->addStoreFilter($storeIds); $sitemapLinks = []; + /** + * @var Sitemap $sitemap + */ foreach ($collection as $sitemap) { $sitemapUrl = $sitemap->getSitemapUrl($sitemap->getSitemapPath(), $sitemap->getSitemapFilename()); $sitemapLinks[$sitemapUrl] = 'Sitemap: ' . $sitemapUrl; diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 9a8d2c57a280c..ddb04f28d58d1 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -475,12 +475,9 @@ public function generateXml() if ($this->_sitemapIncrement == 1) { // In case when only one increment file was created use it as default sitemap - $path = rtrim( - $this->getSitemapPath(), - '/' - ) . '/' . $this->_getCurrentSitemapFilename( - $this->_sitemapIncrement - ); + $path = rtrim($this->getSitemapPath(), '/') + . '/' + . $this->_getCurrentSitemapFilename($this->_sitemapIncrement); $destination = rtrim($this->getSitemapPath(), '/') . '/' . $this->getSitemapFilename(); $this->_directory->renameFile($path, $destination); diff --git a/app/etc/di.xml b/app/etc/di.xml index 585c88f68ff6f..008671f2705e3 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -212,6 +212,7 @@ <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" /> + <preference for="Magento\Framework\Filesystem\DriverPoolInterface" type="Magento\Framework\Filesystem\DriverPool" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> diff --git a/composer.json b/composer.json index 57fbfaaa35c2b..985bf0d9e16ea 100644 --- a/composer.json +++ b/composer.json @@ -80,7 +80,9 @@ "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", "webonyx/graphql-php": "^0.13.8", - "wikimedia/less.php": "~1.8.0" + "wikimedia/less.php": "~1.8.0", + "league/flysystem": "^1.0", + "league/flysystem-aws-s3-v3": "^1.0" }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", @@ -323,7 +325,9 @@ "twbs/bootstrap": "3.1.0", "tinymce/tinymce": "3.4.7", "magento/module-tinymce-3": "*", - "magento/module-csp": "*" + "magento/module-csp": "*", + "magento/module-aws-s-3": "*", + "magento/module-remote-storage": "*" }, "conflict": { "gene/bluefoot": "*" diff --git a/composer.lock b/composer.lock index 8a5d82536cee4..6d5c895670800 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,93 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", + "content-hash": "3eb0d410285c05a9f2649b65d8b9a1d5", "packages": [ + { + "name": "aws/aws-sdk-php", + "version": "3.154.7", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "fa2bf35b5d80e9597e5a2cd7e337eeeb44d09d9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/fa2bf35b5d80e9597e5a2cd7e337eeeb44d09d9c", + "reference": "fa2bf35b5d80e9597e5a2cd7e337eeeb44d09d9c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "^2.5", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "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", + "sebastian/comparator": "^1.2.3" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2020-09-21T18:12:58+00:00" + }, { "name": "colinmollenhour/cache-backend-file", "version": "v1.4.5", @@ -154,16 +239,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.7", + "version": "1.2.8", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd" + "reference": "8a7ecad675253e4654ea05505233285377405215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", + "reference": "8a7ecad675253e4654ea05505233285377405215", "shasum": "" }, "require": { @@ -211,25 +296,29 @@ "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-04-08T08:27:21+00:00" + "time": "2020-08-23T12:54:47+00:00" }, { "name": "composer/composer", - "version": "1.10.9", + "version": "1.10.13", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "83c3250093d5491600a822e176b107a945baf95a" + "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/83c3250093d5491600a822e176b107a945baf95a", - "reference": "83c3250093d5491600a822e176b107a945baf95a", + "url": "https://api.github.com/repos/composer/composer/zipball/47c841ba3b2d3fc0b4b13282cf029ea18b66d78b", + "reference": "47c841ba3b2d3fc0b4b13282cf029ea18b66d78b", "shasum": "" }, "require": { @@ -310,20 +399,20 @@ "type": "tidelift" } ], - "time": "2020-07-16T10:57:00+00:00" + "time": "2020-09-09T09:46:34+00:00" }, { "name": "composer/semver", - "version": "1.5.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" + "reference": "114f819054a2ea7db03287f5efb757e2af6e4079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", + "url": "https://api.github.com/repos/composer/semver/zipball/114f819054a2ea7db03287f5efb757e2af6e4079", + "reference": "114f819054a2ea7db03287f5efb757e2af6e4079", "shasum": "" }, "require": { @@ -371,7 +460,21 @@ "validation", "versioning" ], - "time": "2020-01-13T12:06:48+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-09-09T09:34:06+00:00" }, { "name": "composer/spdx-licenses", @@ -449,16 +552,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.2", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", "shasum": "" }, "require": { @@ -503,7 +606,7 @@ "type": "tidelift" } ], - "time": "2020-06-04T11:16:35+00:00" + "time": "2020-08-19T10:27:58+00:00" }, { "name": "container-interop/container-interop", @@ -1356,6 +1459,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" }, { @@ -1535,41 +1644,41 @@ }, { "name": "laminas/laminas-eventmanager", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-eventmanager.git", - "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748" + "reference": "1940ccf30e058b2fd66f5a9d696f1b5e0027b082" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", - "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", + "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/1940ccf30e058b2fd66f5a9d696f1b5e0027b082", + "reference": "1940ccf30e058b2fd66f5a9d696f1b5e0027b082", "shasum": "" }, "require": { "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ^8.0" }, "replace": { - "zendframework/zend-eventmanager": "self.version" + "zendframework/zend-eventmanager": "^3.2.1" }, "require-dev": { - "athletic/athletic": "^0.1", - "container-interop/container-interop": "^1.1.0", + "container-interop/container-interop": "^1.1", "laminas/laminas-coding-standard": "~1.0.0", "laminas/laminas-stdlib": "^2.7.3 || ^3.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "^8.5.8" }, "suggest": { - "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", + "container-interop/container-interop": "^1.1, to use the lazy listeners feature", "laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev", - "dev-develop": "3.3-dev" + "dev-master": "3.3.x-dev", + "dev-develop": "3.4.x-dev" } }, "autoload": { @@ -1589,20 +1698,26 @@ "events", "laminas" ], - "time": "2019-12-31T16:44:52+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-25T11:10:44+00:00" }, { "name": "laminas/laminas-feed", - "version": "2.12.2", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-feed.git", - "reference": "8a193ac96ebcb3e16b6ee754ac2a889eefacb654" + "reference": "3c91415633cb1be6f9d78683d69b7dcbfe6b4012" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/8a193ac96ebcb3e16b6ee754ac2a889eefacb654", - "reference": "8a193ac96ebcb3e16b6ee754ac2a889eefacb654", + "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/3c91415633cb1be6f9d78683d69b7dcbfe6b4012", + "reference": "3c91415633cb1be6f9d78683d69b7dcbfe6b4012", "shasum": "" }, "require": { @@ -1656,7 +1771,13 @@ "feed", "laminas" ], - "time": "2020-03-29T12:36:29+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-18T13:45:04+00:00" }, { "name": "laminas/laminas-filter", @@ -1817,16 +1938,16 @@ }, { "name": "laminas/laminas-http", - "version": "2.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-http.git", - "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575" + "reference": "33b7942f51ce905ce9bfc8bf28badc501d3904b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-http/zipball/48bd06ffa3a6875e2b77d6852405eb7b1589d575", - "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575", + "url": "https://api.github.com/repos/laminas/laminas-http/zipball/33b7942f51ce905ce9bfc8bf28badc501d3904b5", + "reference": "33b7942f51ce905ce9bfc8bf28badc501d3904b5", "shasum": "" }, "require": { @@ -1849,12 +1970,6 @@ "paragonie/certainty": "For automated management of cacert.pem" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.12.x-dev", - "dev-develop": "2.13.x-dev" - } - }, "autoload": { "psr-4": { "Laminas\\Http\\": "src/" @@ -1877,7 +1992,7 @@ "type": "community_bridge" } ], - "time": "2020-06-23T15:14:37+00:00" + "time": "2020-08-18T17:11:58+00:00" }, { "name": "laminas/laminas-hydrator", @@ -2263,16 +2378,16 @@ }, { "name": "laminas/laminas-mail", - "version": "2.11.0", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-mail.git", - "reference": "4c5545637eea3dc745668ddff1028692ed004c4b" + "reference": "c154a733b122539ac2c894561996c770db289f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/4c5545637eea3dc745668ddff1028692ed004c4b", - "reference": "4c5545637eea3dc745668ddff1028692ed004c4b", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/c154a733b122539ac2c894561996c770db289f70", + "reference": "c154a733b122539ac2c894561996c770db289f70", "shasum": "" }, "require": { @@ -2282,7 +2397,7 @@ "laminas/laminas-stdlib": "^2.7 || ^3.0", "laminas/laminas-validator": "^2.10.2", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0", + "php": "^7.1", "true/punycode": "^2.1" }, "replace": { @@ -2292,8 +2407,8 @@ "laminas/laminas-coding-standard": "~1.0.0", "laminas/laminas-config": "^2.6", "laminas/laminas-crypt": "^2.6 || ^3.0", - "laminas/laminas-servicemanager": "^2.7.10 || ^3.3.1", - "phpunit/phpunit": "^5.7.25 || ^6.4.4 || ^7.1.4" + "laminas/laminas-servicemanager": "^3.2.1", + "phpunit/phpunit": "^7.5.20" }, "suggest": { "laminas/laminas-crypt": "Crammd5 support in SMTP Auth", @@ -2301,10 +2416,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.11.x-dev", - "dev-develop": "2.12.x-dev" - }, "laminas": { "component": "Laminas\\Mail", "config-provider": "Laminas\\Mail\\ConfigProvider" @@ -2331,7 +2442,7 @@ "type": "community_bridge" } ], - "time": "2020-06-30T20:17:23+00:00" + "time": "2020-08-12T14:51:33+00:00" }, { "name": "laminas/laminas-math", @@ -2443,16 +2554,16 @@ }, { "name": "laminas/laminas-modulemanager", - "version": "2.8.4", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-modulemanager.git", - "reference": "92b1cde1aab5aef687b863face6dd5d9c6751c78" + "reference": "789bbd4ab391da9221f265f6bb2d594f8f11855b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-modulemanager/zipball/92b1cde1aab5aef687b863face6dd5d9c6751c78", - "reference": "92b1cde1aab5aef687b863face6dd5d9c6751c78", + "url": "https://api.github.com/repos/laminas/laminas-modulemanager/zipball/789bbd4ab391da9221f265f6bb2d594f8f11855b", + "reference": "789bbd4ab391da9221f265f6bb2d594f8f11855b", "shasum": "" }, "require": { @@ -2460,10 +2571,11 @@ "laminas/laminas-eventmanager": "^3.2 || ^2.6.3", "laminas/laminas-stdlib": "^3.1 || ^2.7", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0", + "webimpress/safe-writer": "^1.0.2 || ^2.1" }, "replace": { - "zendframework/zend-modulemanager": "self.version" + "zendframework/zend-modulemanager": "^2.8.4" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", @@ -2483,8 +2595,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" } }, "autoload": { @@ -2502,7 +2614,13 @@ "laminas", "modulemanager" ], - "time": "2019-12-31T17:26:56+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-25T09:29:22+00:00" }, { "name": "laminas/laminas-mvc", @@ -2952,35 +3070,35 @@ }, { "name": "laminas/laminas-stdlib", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-stdlib.git", - "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6" + "reference": "b9d84eaa39fde733356ea948cdef36c631f202b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/2b18347625a2f06a1a485acfbc870f699dbe51c6", - "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/b9d84eaa39fde733356ea948cdef36c631f202b6", + "reference": "b9d84eaa39fde733356ea948cdef36c631f202b6", "shasum": "" }, "require": { "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ^8.0" }, "replace": { - "zendframework/zend-stdlib": "self.version" + "zendframework/zend-stdlib": "^3.2.1" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", - "phpbench/phpbench": "^0.13", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "^9.3.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev", - "dev-develop": "3.3.x-dev" + "dev-master": "3.3.x-dev", + "dev-develop": "3.4.x-dev" } }, "autoload": { @@ -2998,7 +3116,13 @@ "laminas", "stdlib" ], - "time": "2019-12-31T17:51:15+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-08-25T09:08:16+00:00" }, { "name": "laminas/laminas-text", @@ -3275,31 +3399,27 @@ }, { "name": "laminas/laminas-zendframework-bridge", - "version": "1.0.4", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-zendframework-bridge.git", - "reference": "fcd87520e4943d968557803919523772475e8ea3" + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/fcd87520e4943d968557803919523772475e8ea3", - "reference": "fcd87520e4943d968557803919523772475e8ea3", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev", - "dev-develop": "1.1.x-dev" - }, "laminas": { "module": "Laminas\\ZendFrameworkBridge" } @@ -3323,7 +3443,202 @@ "laminas", "zf" ], - "time": "2020-05-20T16:45:56+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-09-14T14:23:00+00:00" + }, + { + "name": "league/flysystem", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/mime-type-detection": "^1.3", + "php": "^7.2.5 || ^8.0" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/prophecy": "^1.11.1", + "phpunit/phpunit": "^8.5.8" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], + "time": "2020-08-23T07:39:11+00:00" + }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "1.0.28", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/af7384a12f7cd7d08183390d930c9d0ec629c990", + "reference": "af7384a12f7cd7d08183390d930c9d0ec629c990", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.20.0", + "league/flysystem": "^1.0.40", + "php": ">=5.5.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3v3\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for the AWS S3 SDK v3.x", + "time": "2020-08-22T08:43:01+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ea2fbfc988bade315acd5967e6d02274086d0f28", + "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.36", + "phpunit/phpunit": "^8.5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2020-09-21T18:10:53+00:00" }, { "name": "magento/composer", @@ -3489,16 +3804,16 @@ }, { "name": "monolog/monolog", - "version": "1.25.4", + "version": "1.25.5", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "3022efff205e2448b560c833c6fbbf91c3139168" + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/3022efff205e2448b560c833c6fbbf91c3139168", - "reference": "3022efff205e2448b560c833c6fbbf91c3139168", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1817faadd1846cd08be9a49e905dc68823bc38c0", + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0", "shasum": "" }, "require": { @@ -3562,7 +3877,74 @@ "logging", "psr-3" ], - "time": "2020-05-22T07:31:27+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-07-23T08:35:51+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^1.4", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2020-07-31T21:01:56+00:00" }, { "name": "paragonie/random_compat", @@ -3889,16 +4271,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.28", + "version": "2.0.29", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260" + "reference": "497856a8d997f640b4a516062f84228a772a48a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", - "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/497856a8d997f640b4a516062f84228a772a48a8", + "reference": "497856a8d997f640b4a516062f84228a772a48a8", "shasum": "" }, "require": { @@ -3907,7 +4289,6 @@ "require-dev": { "phing/phing": "~2.7", "phpunit/phpunit": "^4.8.35|^5.7|^6.0", - "sami/sami": "~2.0", "squizlabs/php_codesniffer": "~2.0" }, "suggest": { @@ -3991,7 +4372,7 @@ "type": "tidelift" } ], - "time": "2020-07-08T09:08:33+00:00" + "time": "2020-09-08T04:24:43+00:00" }, { "name": "psr/container", @@ -4309,16 +4690,16 @@ }, { "name": "seld/jsonlint", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1" + "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/590cfec960b77fd55e39b7d9246659e95dd6d337", + "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337", "shasum": "" }, "require": { @@ -4354,7 +4735,17 @@ "parser", "validator" ], - "time": "2020-04-30T19:05:18+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2020-08-25T06:56:57+00:00" }, { "name": "seld/phar-utils", @@ -4402,16 +4793,16 @@ }, { "name": "symfony/console", - "version": "v4.4.10", + "version": "v4.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "326b064d804043005526f5a0494cfb49edb59bb0" + "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/326b064d804043005526f5a0494cfb49edb59bb0", - "reference": "326b064d804043005526f5a0494cfb49edb59bb0", + "url": "https://api.github.com/repos/symfony/console/zipball/b39fd99b9297b67fb7633b7d8083957a97e1e727", + "reference": "b39fd99b9297b67fb7633b7d8083957a97e1e727", "shasum": "" }, "require": { @@ -4489,11 +4880,11 @@ "type": "tidelift" } ], - "time": "2020-05-30T20:06:45+00:00" + "time": "2020-09-02T07:07:21+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -4560,16 +4951,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v4.4.10", + "version": "v4.4.13", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866" + "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5370aaa7807c7a439b21386661ffccf3dff2866", - "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/3e8ea5ccddd00556b86d69d42f99f1061a704030", + "reference": "3e8ea5ccddd00556b86d69d42f99f1061a704030", "shasum": "" }, "require": { @@ -4640,7 +5031,7 @@ "type": "tidelift" } ], - "time": "2020-05-20T08:37:50+00:00" + "time": "2020-08-13T14:18:44+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4720,16 +5111,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6e4320f06d5f2cce0d96530162491f4465179157" + "reference": "f7b9ed6142a34252d219801d9767dedbd711da1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", - "reference": "6e4320f06d5f2cce0d96530162491f4465179157", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/f7b9ed6142a34252d219801d9767dedbd711da1a", + "reference": "f7b9ed6142a34252d219801d9767dedbd711da1a", "shasum": "" }, "require": { @@ -4780,20 +5171,20 @@ "type": "tidelift" } ], - "time": "2020-05-30T20:35:19+00:00" + "time": "2020-08-21T17:19:47+00:00" }, { "name": "symfony/finder", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" + "reference": "2b765f0cf6612b3636e738c0689b29aa63088d5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", - "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", + "url": "https://api.github.com/repos/symfony/finder/zipball/2b765f0cf6612b3636e738c0689b29aa63088d5d", + "reference": "2b765f0cf6612b3636e738c0689b29aa63088d5d", "shasum": "" }, "require": { @@ -4843,11 +5234,11 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-08-17T10:01:29+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4923,16 +5314,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe" + "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251", + "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251", "shasum": "" }, "require": { @@ -5004,11 +5395,11 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-08-04T06:02:08+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5089,7 +5480,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -5166,7 +5557,7 @@ }, { "name": "symfony/polyfill-php70", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", @@ -5243,7 +5634,7 @@ }, { "name": "symfony/polyfill-php72", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", @@ -5316,7 +5707,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -5392,7 +5783,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.18.0", + "version": "v1.18.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -5472,20 +5863,20 @@ }, { "name": "symfony/process", - "version": "v4.4.10", + "version": "v4.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5" + "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c714958428a85c86ab97e3a0c96db4c4f381b7f5", - "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5", + "url": "https://api.github.com/repos/symfony/process/zipball/65e70bab62f3da7089a8d4591fb23fbacacb3479", + "reference": "65e70bab62f3da7089a8d4591fb23fbacacb3479", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "type": "library", "extra": { @@ -5531,20 +5922,20 @@ "type": "tidelift" } ], - "time": "2020-05-30T20:06:45+00:00" + "time": "2020-07-23T08:31:43+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.1.3", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442" + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442", - "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", "shasum": "" }, "require": { @@ -5557,7 +5948,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -5607,7 +5998,7 @@ "type": "tidelift" } ], - "time": "2020-07-06T13:23:11+00:00" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "tedivm/jshrink", @@ -5754,6 +6145,61 @@ ], "time": "2018-01-15T15:26:51+00:00" }, + { + "name": "webimpress/safe-writer", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "5cfafdec5873c389036f14bf832a5efc9390dcdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/5cfafdec5873c389036f14bf832a5efc9390dcdd", + "reference": "5cfafdec5873c389036f14bf832a5efc9390dcdd", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.8 || ^9.3.7", + "vimeo/psalm": "^3.14.2", + "webimpress/coding-standard": "^1.1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], + "time": "2020-08-25T07:21:11+00:00" + }, { "name": "webonyx/graphql-php", "version": "v0.13.9", @@ -5877,16 +6323,16 @@ "packages-dev": [ { "name": "allure-framework/allure-codeception", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "9e0e25f8960fa5ac17c65c932ea8153ce6700713" + "reference": "a69800eeef83007ced9502a3349ff72f5fb6b4e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/9e0e25f8960fa5ac17c65c932ea8153ce6700713", - "reference": "9e0e25f8960fa5ac17c65c932ea8153ce6700713", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/a69800eeef83007ced9502a3349ff72f5fb6b4e2", + "reference": "a69800eeef83007ced9502a3349ff72f5fb6b4e2", "shasum": "" }, "require": { @@ -5924,7 +6370,7 @@ "steps", "testing" ], - "time": "2020-03-13T11:07:13+00:00" + "time": "2020-09-09T10:51:33+00:00" }, { "name": "allure-framework/allure-php-api", @@ -5984,111 +6430,26 @@ "version": "1.2.4", "source": { "type": "git", - "url": "https://github.com/allure-framework/allure-phpunit.git", - "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-phpunit/zipball/9399629c6eed79da4be18fd22adf83ef36c2d2e0", - "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0", - "shasum": "" - }, - "require": { - "allure-framework/allure-php-api": "~1.1.0", - "mikey179/vfsstream": "1.*", - "php": ">=7.1.0", - "phpunit/phpunit": ">=7.0.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Yandex": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Ivan Krutov", - "email": "vania-pooh@yandex-team.ru", - "role": "Developer" - } - ], - "description": "A PHPUnit adapter for Allure report.", - "homepage": "http://allure.qatools.ru/", - "keywords": [ - "allure", - "attachments", - "cases", - "phpunit", - "report", - "steps", - "testing" - ], - "time": "2018-10-25T12:03:54+00:00" - }, - { - "name": "aws/aws-sdk-php", - "version": "3.147.1", - "source": { - "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc" + "url": "https://github.com/allure-framework/allure-phpunit.git", + "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8a561a4a1645ccdd06413a4f2defe55d35e0eecc", - "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc", + "url": "https://api.github.com/repos/allure-framework/allure-phpunit/zipball/9399629c6eed79da4be18fd22adf83ef36c2d2e0", + "reference": "9399629c6eed79da4be18fd22adf83ef36c2d2e0", "shasum": "" }, "require": { - "ext-json": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4.1", - "mtdowling/jmespath.php": "^2.5", - "php": ">=5.5" - }, - "require-dev": { - "andrewsville/php-token-reflection": "^1.4", - "aws/aws-php-sns-message-validator": "~1.0", - "behat/behat": "~3.0", - "doctrine/cache": "~1.4", - "ext-dom": "*", - "ext-openssl": "*", - "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", - "sebastian/comparator": "^1.2.3" - }, - "suggest": { - "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", - "doctrine/cache": "To use the DoctrineCacheAdapter", - "ext-curl": "To send requests using cURL", - "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", - "ext-sockets": "To use client-side monitoring" + "allure-framework/allure-php-api": "~1.1.0", + "mikey179/vfsstream": "1.*", + "php": ">=7.1.0", + "phpunit/phpunit": ">=7.0.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, "autoload": { - "psr-4": { - "Aws\\": "src/" - }, - "files": [ - "src/functions.php" - ] + "psr-0": { + "Yandex": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6096,23 +6457,23 @@ ], "authors": [ { - "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" + "name": "Ivan Krutov", + "email": "vania-pooh@yandex-team.ru", + "role": "Developer" } ], - "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", - "homepage": "http://aws.amazon.com/sdkforphp", + "description": "A PHPUnit adapter for Allure report.", + "homepage": "http://allure.qatools.ru/", "keywords": [ - "amazon", - "aws", - "cloud", - "dynamodb", - "ec2", - "glacier", - "s3", - "sdk" + "allure", + "attachments", + "cases", + "phpunit", + "report", + "steps", + "testing" ], - "time": "2020-07-20T18:18:31+00:00" + "time": "2018-10-25T12:03:54+00:00" }, { "name": "beberlei/assert", @@ -6330,16 +6691,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.6", + "version": "4.1.7", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9" + "reference": "220ad18d3c192137d9dc2d0dd8d69a0d82083a26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", - "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/220ad18d3c192137d9dc2d0dd8d69a0d82083a26", + "reference": "220ad18d3c192137d9dc2d0dd8d69a0d82083a26", "shasum": "" }, "require": { @@ -6417,24 +6778,25 @@ "type": "open_collective" } ], - "time": "2020-06-07T16:31:51+00:00" + "time": "2020-08-28T06:37:06+00:00" }, { "name": "codeception/lib-asserts", - "version": "1.12.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "acd0dc8b394595a74b58dcc889f72569ff7d8e71" + "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/acd0dc8b394595a74b58dcc889f72569ff7d8e71", - "reference": "acd0dc8b394595a74b58dcc889f72569ff7d8e71", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/263ef0b7eff80643e82f4cf55351eca553a09a10", + "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10", "shasum": "" }, "require": { "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", + "ext-dom": "*", "php": ">=5.6.0 <8.0" }, "type": "library", @@ -6455,32 +6817,36 @@ }, { "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } ], "description": "Assertion methods used by Codeception core and Asserts module", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "codeception" ], - "time": "2020-04-17T18:20:46+00:00" + "time": "2020-08-28T07:49:36+00:00" }, { "name": "codeception/module-asserts", - "version": "1.2.1", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/Codeception/module-asserts.git", - "reference": "79f13d05b63f2fceba4d0e78044bab668c9b2a6b" + "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/79f13d05b63f2fceba4d0e78044bab668c9b2a6b", - "reference": "79f13d05b63f2fceba4d0e78044bab668c9b2a6b", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/32e5be519faaeb60ed3692383dcd1b3390ec2667", + "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667", "shasum": "" }, "require": { "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^1.12.0", + "codeception/lib-asserts": "^1.13.1", "php": ">=5.6.0 <8.0" }, "conflict": { @@ -6505,16 +6871,20 @@ }, { "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } ], "description": "Codeception module containing various assertions", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "assertions", "asserts", "codeception" ], - "time": "2020-04-20T07:26:11+00:00" + "time": "2020-08-28T08:06:29+00:00" }, { "name": "codeception/module-sequence", @@ -6561,16 +6931,16 @@ }, { "name": "codeception/module-webdriver", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "09c167817393090ce3dbce96027d94656b1963ce" + "reference": "237c6cb42d3e914f011d0419e966cbe0cb5d82c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/09c167817393090ce3dbce96027d94656b1963ce", - "reference": "09c167817393090ce3dbce96027d94656b1963ce", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/237c6cb42d3e914f011d0419e966cbe0cb5d82c6", + "reference": "237c6cb42d3e914f011d0419e966cbe0cb5d82c6", "shasum": "" }, "require": { @@ -6612,20 +6982,20 @@ "browser-testing", "codeception" ], - "time": "2020-05-31T08:47:24+00:00" + "time": "2020-08-06T07:39:31+00:00" }, { "name": "codeception/phpunit-wrapper", - "version": "9.0.2", + "version": "9.0.4", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "eb27243d8edde68593bf8d9ef5e9074734777931" + "reference": "bb0925f1fe7a30105208352e619a11d6096e7047" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/eb27243d8edde68593bf8d9ef5e9074734777931", - "reference": "eb27243d8edde68593bf8d9ef5e9074734777931", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/bb0925f1fe7a30105208352e619a11d6096e7047", + "reference": "bb0925f1fe7a30105208352e619a11d6096e7047", "shasum": "" }, "require": { @@ -6656,7 +7026,7 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2020-04-17T18:16:31+00:00" + "time": "2020-08-26T18:15:09+00:00" }, { "name": "codeception/stub", @@ -6842,16 +7212,16 @@ }, { "name": "doctrine/annotations", - "version": "1.10.3", + "version": "1.10.4", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" + "reference": "bfe91e31984e2ba76df1c1339681770401ec262f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", - "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/bfe91e31984e2ba76df1c1339681770401ec262f", + "reference": "bfe91e31984e2ba76df1c1339681770401ec262f", "shasum": "" }, "require": { @@ -6861,7 +7231,8 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^7.5" + "phpstan/phpstan": "^0.12.20", + "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", "extra": { @@ -6907,7 +7278,7 @@ "docblock", "parser" ], - "time": "2020-05-25T17:24:27+00:00" + "time": "2020-08-10T19:35:50+00:00" }, { "name": "doctrine/cache", @@ -8038,90 +8409,6 @@ ], "time": "2020-02-22T20:59:37+00:00" }, - { - "name": "league/flysystem", - "version": "1.0.69", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "7106f78428a344bc4f643c233a94e48795f10967" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7106f78428a344bc4f643c233a94e48795f10967", - "reference": "7106f78428a344bc4f643c233a94e48795f10967", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": ">=5.5.9" - }, - "conflict": { - "league/flysystem-sftp": "<1.0.6" - }, - "require-dev": { - "phpspec/phpspec": "^3.4", - "phpunit/phpunit": "^5.7.26" - }, - "suggest": { - "ext-fileinfo": "Required for MimeType", - "ext-ftp": "Allows you to use FTP server storage", - "ext-openssl": "Allows you to use FTPS server storage", - "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", - "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", - "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", - "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", - "league/flysystem-webdav": "Allows you to use WebDAV storage", - "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", - "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Flysystem\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frenky.net" - } - ], - "description": "Filesystem abstraction: Many filesystems, one API.", - "keywords": [ - "Cloud Files", - "WebDAV", - "abstraction", - "aws", - "cloud", - "copy.com", - "dropbox", - "file systems", - "files", - "filesystem", - "filesystems", - "ftp", - "rackspace", - "remote", - "s3", - "sftp", - "storage" - ], - "time": "2020-05-18T15:13:39+00:00" - }, { "name": "lusitanian/oauth", "version": "v0.8.11", @@ -8365,63 +8652,6 @@ "homepage": "http://vfs.bovigo.org/", "time": "2019-10-30T15:31:00+00:00" }, - { - "name": "mtdowling/jmespath.php", - "version": "2.5.0", - "source": { - "type": "git", - "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "52168cb9472de06979613d365c7f1ab8798be895" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/52168cb9472de06979613d365c7f1ab8798be895", - "reference": "52168cb9472de06979613d365c7f1ab8798be895", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "symfony/polyfill-mbstring": "^1.4" - }, - "require-dev": { - "composer/xdebug-handler": "^1.2", - "phpunit/phpunit": "^4.8.36|^7.5.15" - }, - "bin": [ - "bin/jp.php" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-4": { - "JmesPath\\": "src/" - }, - "files": [ - "src/JmesPath.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Declaratively specify how to extract elements from a JSON document", - "keywords": [ - "json", - "jsonpath" - ], - "time": "2019-12-30T18:03:34+00:00" - }, { "name": "mustache/mustache", "version": "v2.13.0", @@ -9006,16 +9236,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.0", + "version": "5.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df" + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", "shasum": "" }, "require": { @@ -9054,20 +9284,20 @@ } ], "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-07-20T20:05:34+00:00" + "time": "2020-09-03T19:13:55+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", "shasum": "" }, "require": { @@ -9099,20 +9329,20 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" + "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpmd/phpmd", - "version": "2.8.2", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "714629ed782537f638fe23c4346637659b779a77" + "reference": "2a346575a45a6f00e631f4d7f3f71b6a05e0d46d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/714629ed782537f638fe23c4346637659b779a77", - "reference": "714629ed782537f638fe23c4346637659b779a77", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/2a346575a45a6f00e631f4d7f3f71b6a05e0d46d", + "reference": "2a346575a45a6f00e631f4d7f3f71b6a05e0d46d", "shasum": "" }, "require": { @@ -9123,6 +9353,8 @@ }, "require-dev": { "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", "gregwar/rst": "^1.0", "mikey179/vfsstream": "^1.6.4", "phpunit/phpunit": "^4.8.36 || ^5.7.27", @@ -9169,7 +9401,13 @@ "phpmd", "pmd" ], - "time": "2020-02-16T20:15:50+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], + "time": "2020-09-02T09:12:27+00:00" }, { "name": "phpoption/phpoption", @@ -9339,6 +9577,20 @@ "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" }, { @@ -9469,16 +9721,16 @@ }, { "name": "phpunit/php-invoker", - "version": "3.0.2", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66" + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f6eedfed1085dd1f4c599629459a0277d25f9a66", - "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7a85b66acc48cacffdf87dadd3694e7123674298", + "reference": "7a85b66acc48cacffdf87dadd3694e7123674298", "shasum": "" }, "require": { @@ -9494,7 +9746,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -9524,7 +9776,7 @@ "type": "github" } ], - "time": "2020-06-26T11:53:53+00:00" + "time": "2020-08-06T07:04:15+00:00" }, { "name": "phpunit/php-text-template", @@ -9628,20 +9880,26 @@ "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.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374" + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5672711b6b07b14d5ab694e700c62eeb82fcf374", - "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", "shasum": "" }, "require": { @@ -9684,7 +9942,7 @@ } ], "abandoned": true, - "time": "2020-06-27T06:36:25+00:00" + "time": "2020-08-04T08:28:15+00:00" }, { "name": "phpunit/phpunit", @@ -9772,6 +10030,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" }, { @@ -10775,16 +11043,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.5", + "version": "3.5.6", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6" + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", + "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", "shasum": "" }, "require": { @@ -10822,20 +11090,20 @@ "phpcs", "standards" ], - "time": "2020-04-17T01:09:41+00:00" + "time": "2020-08-10T04:50:15+00:00" }, { "name": "symfony/config", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "b8623ef3d99fe62a34baf7a111b576216965f880" + "reference": "22f961ddffdc81389670b2ca74a1cc0213761ec0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/b8623ef3d99fe62a34baf7a111b576216965f880", - "reference": "b8623ef3d99fe62a34baf7a111b576216965f880", + "url": "https://api.github.com/repos/symfony/config/zipball/22f961ddffdc81389670b2ca74a1cc0213761ec0", + "reference": "22f961ddffdc81389670b2ca74a1cc0213761ec0", "shasum": "" }, "require": { @@ -10902,20 +11170,20 @@ "type": "tidelift" } ], - "time": "2020-05-23T13:08:13+00:00" + "time": "2020-08-17T07:48:54+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "6508423eded583fc07e88a0172803e1a62f0310c" + "reference": "48d6890e12ce9cd8e68aaa4fb72010139312fd73" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6508423eded583fc07e88a0172803e1a62f0310c", - "reference": "6508423eded583fc07e88a0172803e1a62f0310c", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/48d6890e12ce9cd8e68aaa4fb72010139312fd73", + "reference": "48d6890e12ce9cd8e68aaa4fb72010139312fd73", "shasum": "" }, "require": { @@ -10991,20 +11259,20 @@ "type": "tidelift" } ], - "time": "2020-06-12T08:11:32+00:00" + "time": "2020-09-01T18:07:16+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.1.3", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", "shasum": "" }, "require": { @@ -11013,7 +11281,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -11055,20 +11323,20 @@ "type": "tidelift" } ], - "time": "2020-06-06T08:49:21+00:00" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f93055171b847915225bd5b0a5792888419d8d75" + "reference": "41a4647f12870e9d41d9a7d72ff0614a27208558" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f93055171b847915225bd5b0a5792888419d8d75", - "reference": "f93055171b847915225bd5b0a5792888419d8d75", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/41a4647f12870e9d41d9a7d72ff0614a27208558", + "reference": "41a4647f12870e9d41d9a7d72ff0614a27208558", "shasum": "" }, "require": { @@ -11130,20 +11398,20 @@ "type": "tidelift" } ], - "time": "2020-06-15T06:52:54+00:00" + "time": "2020-08-17T07:48:54+00:00" }, { "name": "symfony/mime", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "c0c418f05e727606e85b482a8591519c4712cf45" + "reference": "89a2c9b4cb7b5aa516cf55f5194c384f444c81dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/c0c418f05e727606e85b482a8591519c4712cf45", - "reference": "c0c418f05e727606e85b482a8591519c4712cf45", + "url": "https://api.github.com/repos/symfony/mime/zipball/89a2c9b4cb7b5aa516cf55f5194c384f444c81dc", + "reference": "89a2c9b4cb7b5aa516cf55f5194c384f444c81dc", "shasum": "" }, "require": { @@ -11207,20 +11475,20 @@ "type": "tidelift" } ], - "time": "2020-06-09T15:07:35+00:00" + "time": "2020-08-17T10:01:29+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" + "reference": "9ff59517938f88d90b6e65311fef08faa640f681" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", - "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/9ff59517938f88d90b6e65311fef08faa640f681", + "reference": "9ff59517938f88d90b6e65311fef08faa640f681", "shasum": "" }, "require": { @@ -11277,11 +11545,11 @@ "type": "tidelift" } ], - "time": "2020-05-23T13:08:13+00:00" + "time": "2020-07-12T12:58:00+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -11345,16 +11613,16 @@ }, { "name": "symfony/yaml", - "version": "v5.1.2", + "version": "v5.1.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23" + "reference": "a44bd3a91bfbf8db12367fa6ffac9c3eb1a8804a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ea342353a3ef4f453809acc4ebc55382231d4d23", - "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a44bd3a91bfbf8db12367fa6ffac9c3eb1a8804a", + "reference": "a44bd3a91bfbf8db12367fa6ffac9c3eb1a8804a", "shasum": "" }, "require": { @@ -11418,20 +11686,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-08-26T08:30:57+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.1.3", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb" + "reference": "53e6692d8ad1a7d72078093ced170c218a2e8b79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/9f277171e296a3c8629c04ac93ec95ff0f208ccb", - "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/53e6692d8ad1a7d72078093ced170c218a2e8b79", + "reference": "53e6692d8ad1a7d72078093ced170c218a2e8b79", "shasum": "" }, "require": { @@ -11551,7 +11819,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-07-10T09:34:29+00:00" + "time": "2020-09-03T14:09:13+00:00" }, { "name": "theseer/fdomdocument", diff --git a/lib/internal/Magento/Framework/Api/Uploader.php b/lib/internal/Magento/Framework/Api/Uploader.php index 5cea3a34569a9..3a4019b9caf84 100644 --- a/lib/internal/Magento/Framework/Api/Uploader.php +++ b/lib/internal/Magento/Framework/Api/Uploader.php @@ -13,6 +13,8 @@ class Uploader extends \Magento\Framework\File\Uploader { /** * Avoid running the default constructor specific to FILE upload + * + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ public function __construct() { @@ -30,9 +32,27 @@ public function processFileAttributes($fileAttributes) $this->_file = $fileAttributes; if (!file_exists($this->_file['tmp_name'])) { $code = empty($this->_file['tmp_name']) ? self::TMP_NAME_EMPTY : 0; + + // phpcs:ignore Magento2.Exceptions.DirectThrow.FoundDirectThrow throw new \Exception('File was not processed correctly.', $code); } else { $this->_fileExists = true; } } + + /** + * Move files from TMP folder into destination folder + * + * @param string $tmpPath + * @param string $destPath + * @return bool|void + */ + protected function _moveFile($tmpPath, $destPath) + { + if (is_uploaded_file($tmpPath)) { + return move_uploaded_file($tmpPath, $destPath); + } elseif (is_file($tmpPath)) { + return rename($tmpPath, $destPath); + } + } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php similarity index 92% rename from app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php rename to lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php index 6f1758f3d2b92..90c32b54f17c5 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php @@ -5,13 +5,13 @@ */ declare(strict_types=1); -namespace Magento\Config\Test\Unit\Model\Config\Reader\Source\Deployed; +namespace Magento\Framework\App\Test\Unit\Config; -use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; use Magento\Framework\App\Config; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Config\DocumentRoot; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php new file mode 100644 index 0000000000000..45ccc34f0ce5b --- /dev/null +++ b/lib/internal/Magento/Framework/Config/DocumentRoot.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Config; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\DeploymentConfig; + +/** + * Document root detector. + * + * @api + */ +class DocumentRoot +{ + /** + * @var DeploymentConfig + */ + private $config; + + /** + * @param DeploymentConfig $config + */ + public function __construct(DeploymentConfig $config) + { + $this->config = $config; + } + + /** + * A shortcut to load the document root path from the DirectoryList. + * + * @return string + */ + public function getPath(): string + { + return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; + } + + /** + * Checks if root folder is /pub. + * + * @return bool + */ + public function isPub(): bool + { + return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); + } +} diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index 7ec5843ddcf18..d8c2ecf8cf99d 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -7,7 +7,9 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; use Magento\Framework\Validation\ValidationException; @@ -19,6 +21,7 @@ * validation by protected file extension list to extended class * * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api * @since 100.0.2 @@ -180,6 +183,16 @@ class Uploader */ private $fileDriver; + /** + * @var TargetDirectory + */ + private $targetDirectory; + + /** + * @var DocumentRoot + */ + private $documentRoot; + /** * Init upload * @@ -187,13 +200,17 @@ class Uploader * @param \Magento\Framework\File\Mime|null $fileMime * @param DirectoryList|null $directoryList * @param DriverPool|null $driverPool + * @param TargetDirectory|null $targetDirectory + * @param DocumentRoot|null $documentRoot * @throws \DomainException */ public function __construct( $fileId, Mime $fileMime = null, DirectoryList $directoryList = null, - DriverPool $driverPool = null + DriverPool $driverPool = null, + TargetDirectory $targetDirectory = null, + DocumentRoot $documentRoot = null ) { $this->directoryList= $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); @@ -205,7 +222,9 @@ public function __construct( $this->_fileExists = true; } $this->fileMime = $fileMime ?: ObjectManager::getInstance()->get(Mime::class); - $this->driverPool = $driverPool; + $this->driverPool = $driverPool ?: ObjectManager::getInstance()->get(DriverPool::class); + $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); + $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); } /** @@ -319,11 +338,57 @@ protected function chmod($file) */ protected function _moveFile($tmpPath, $destPath) { - if (is_uploaded_file($tmpPath)) { - return move_uploaded_file($tmpPath, $destPath); - } elseif (is_file($tmpPath)) { - return rename($tmpPath, $destPath); + $rootPath = $this->getDocumentRoot()->getPath(); + $destPath = str_replace($this->getDirectoryList()->getPath($rootPath), '', $destPath); + $directory = $this->getTargetDirectory()->getDirectoryWrite($rootPath); + + return $this->getFileDriver()->rename( + $tmpPath, + $directory->getAbsolutePath($destPath), + $directory->getDriver() + ); + } + + /** + * Retrieves target directory. + * + * @return TargetDirectory + */ + private function getTargetDirectory(): TargetDirectory + { + if (!isset($this->targetDirectory)) { + $this->targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); } + + return $this->targetDirectory; + } + + /** + * Retrieves document root. + * + * @return DocumentRoot + */ + private function getDocumentRoot(): DocumentRoot + { + if (!isset($this->documentRoot)) { + $this->documentRoot = ObjectManager::getInstance()->get(DocumentRoot::class); + } + + return $this->documentRoot; + } + + /** + * Retrieves directory list. + * + * @return DirectoryList + */ + private function getDirectoryList(): DirectoryList + { + if (!isset($this->directoryList)) { + $this->directoryList = ObjectManager::getInstance()->get(DirectoryList::class); + } + + return $this->directoryList; } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php index 25a290455dc46..a3364d3be1c8c 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php @@ -6,22 +6,26 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; +/** + * The factory of the filesystem directory instances for read operations. + */ class ReadFactory { /** * Pool of filesystem drivers * - * @var DriverPool + * @var DriverPoolInterface */ private $driverPool; /** * Constructor * - * @param DriverPool $driverPool + * @param DriverPoolInterface $driverPool */ - public function __construct(DriverPool $driverPool) + public function __construct(DriverPoolInterface $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php b/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php new file mode 100644 index 0000000000000..836eb680c24f7 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Directory; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; + +/** + * A target directory for remote filesystems. + */ +class TargetDirectory +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $driverCode; + + /** + * @param Filesystem $filesystem + * @param string $driverCode + */ + public function __construct(Filesystem $filesystem, $driverCode = Filesystem\DriverPool::FILE) + { + $this->filesystem = $filesystem; + $this->driverCode = $driverCode; + } + + /** + * Create an instance of directory with write permissions. + * + * @param string $directoryCode + * @return WriteInterface + * @throws FileSystemException + */ + public function getDirectoryWrite(string $directoryCode): WriteInterface + { + return $this->filesystem->getDirectoryWrite($directoryCode, $this->driverCode); + } + + /** + * Create an instance of directory with read permissions. + * + * @param string $directoryCode + * @return ReadInterface + */ + public function getDirectoryRead(string $directoryCode): ReadInterface + { + return $this->filesystem->getDirectoryRead($directoryCode, $this->driverCode); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php index ff14b12f62047..6f6bfe558176d 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php @@ -6,22 +6,26 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; +/** + * The factory of the filesystem directory instances for write operations. + */ class WriteFactory { /** * Pool of filesystem drivers * - * @var DriverPool + * @var DriverPoolInterface */ private $driverPool; /** * Constructor * - * @param DriverPool $driverPool + * @param DriverPoolInterface $driverPool */ - public function __construct(DriverPool $driverPool) + public function __construct(DriverPoolInterface $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 1affad5521372..1fdde276e4e51 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -257,7 +257,7 @@ public function readDirectory($path) $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - + $iterator = new \FilesystemIterator($path, $flags); $result = []; /** @var \FilesystemIterator $file */ @@ -305,7 +305,7 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) } else { $content = $this->fileGetContents($oldPath); if (false !== $targetDriver->filePutContents($newPath, $content)) { - $result = $this->deleteFile($newPath); + $result = $this->deleteFile($oldPath); } } if (!$result) { @@ -952,7 +952,7 @@ public function readDirectoryRecursively($path = null) $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - + try { $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($path, $flags), diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPool.php b/lib/internal/Magento/Framework/Filesystem/DriverPool.php index 435e51c26b012..dfd1e2abce01a 100644 --- a/lib/internal/Magento/Framework/Filesystem/DriverPool.php +++ b/lib/internal/Magento/Framework/Filesystem/DriverPool.php @@ -9,7 +9,7 @@ /** * A pool of stream wrappers */ -class DriverPool +class DriverPool implements DriverPoolInterface { /**#@+ * Available driver types diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php new file mode 100644 index 0000000000000..b88db7518ba17 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.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\Filesystem; + +/** + * A pool of stream wrappers + */ +interface DriverPoolInterface +{ + /** + * Gets a driver instance by code + * + * @param string $code + * @return DriverInterface + */ + public function getDriver($code); +} diff --git a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php index e46b00bc5c74f..b442d6d1c05c3 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Filesystem\File; use Magento\Framework\Filesystem\DriverInterface; -use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; /** * Opens a file for reading @@ -18,16 +18,16 @@ class ReadFactory /** * Pool of filesystem drivers * - * @var DriverPool + * @var DriverPoolInterface */ private $driverPool; /** * Constructor * - * @param DriverPool $driverPool + * @param DriverPoolInterface $driverPool */ - public function __construct(DriverPool $driverPool) + public function __construct(DriverPoolInterface $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php index 7a9596586f56a..8bc62cb6573c4 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php @@ -7,6 +7,7 @@ use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; /** * Opens a file for reading and/or writing @@ -25,9 +26,9 @@ class WriteFactory extends ReadFactory /** * Constructor * - * @param DriverPool $driverPool + * @param DriverPoolInterface $driverPool */ - public function __construct(DriverPool $driverPool) + public function __construct(DriverPoolInterface $driverPool) { parent::__construct($driverPool); $this->driverPool = $driverPool; diff --git a/lib/internal/Magento/Framework/Setup/FilePermissions.php b/lib/internal/Magento/Framework/Setup/FilePermissions.php index af0db6498144e..8003f2241f22a 100644 --- a/lib/internal/Magento/Framework/Setup/FilePermissions.php +++ b/lib/internal/Magento/Framework/Setup/FilePermissions.php @@ -93,12 +93,16 @@ public function getInstallationWritableDirectories() $data = [ DirectoryList::CONFIG, DirectoryList::VAR_DIR, - DirectoryList::MEDIA, - DirectoryList::STATIC_VIEW, + DirectoryList::MEDIA ]; if ($this->state->getMode() !== State::MODE_PRODUCTION) { $data[] = DirectoryList::GENERATED; + /** + * Static files may be pre-generated on separate machine. + */ + $data[] = DirectoryList::STATIC_VIEW; } + foreach ($data as $code) { $this->installationWritableDirectories[$code] = $this->directoryList->getPath($code); } @@ -260,6 +264,7 @@ public function getMissingWritablePathsForInstallation($associative = false) if ($associative) { $missingPaths[$missingPath] = $this->nonWritablePathsInDirectories[$missingPath]; } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $missingPaths = array_merge( $missingPaths, $this->nonWritablePathsInDirectories[$missingPath] diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php index e3428c411130c..6e2b83887561d 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php @@ -76,8 +76,8 @@ public function testGetInstallationWritableDirectories($mageMode) BP . '/app/etc', BP . '/var', BP . '/pub/media', + BP . '/generated', BP . '/pub/static', - BP . '/generated' ]; $this->assertEquals($expected, $this->filePermissions->getInstallationWritableDirectories()); @@ -94,7 +94,6 @@ public function testGetInstallationWritableDirectoriesInProduction() BP . '/app/etc', BP . '/var', BP . '/pub/media', - BP . '/pub/static' ]; $this->assertEquals($expected, $this->filePermissions->getInstallationWritableDirectories()); @@ -188,8 +187,8 @@ public function testGetMissingWritableDirectoriesAndPathsForInstallation($mageMo $expected = [ BP . '/var', BP . '/pub/media', + BP . '/generated', BP . '/pub/static', - BP . '/generated' ]; $this->assertEquals( @@ -213,8 +212,7 @@ public function testGetMissingWritableDirectoriesAndPathsForInstallationInProduc $expected = [ BP . '/var', - BP . '/pub/media', - BP . '/pub/static' + BP . '/pub/media' ]; $this->assertEquals( @@ -283,10 +281,15 @@ public function setUpDirectoryListInstallation() { $this->setUpDirectoryListInstallationInProduction(); $this->directoryListMock - ->expects($this->at(4)) + ->expects($this->at(3)) ->method('getPath') ->with(DirectoryList::GENERATED) ->willReturn(BP . '/generated'); + $this->directoryListMock + ->expects($this->at(4)) + ->method('getPath') + ->with(DirectoryList::STATIC_VIEW) + ->willReturn(BP . '/pub/static'); } public function setUpDirectoryListInstallationInProduction() @@ -306,11 +309,6 @@ public function setUpDirectoryListInstallationInProduction() ->method('getPath') ->with(DirectoryList::MEDIA) ->willReturn(BP . '/pub/media'); - $this->directoryListMock - ->expects($this->at(3)) - ->method('getPath') - ->with(DirectoryList::STATIC_VIEW) - ->willReturn(BP . '/pub/static'); } public function setUpDirectoryWriteInstallation() @@ -348,24 +346,6 @@ public function setUpDirectoryWriteInstallation() ->expects($this->at(6)) ->method('isDirectory') ->willReturn(false); - - // STATIC_VIEW - $this->directoryWriteMock - ->expects($this->at(7)) - ->method('isExist') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(8)) - ->method('isDirectory') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(9)) - ->method('isReadable') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(10)) - ->method('isWritable') - ->willReturn(false); } /** From 88a795442f1510126d79c71512bc224c560a5d1c Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Wed, 16 Sep 2020 21:43:43 -0500 Subject: [PATCH 0575/1013] MC-37539: Order status is changed incorrectly during shipment via REST API --- app/code/Magento/Sales/Model/ShipOrder.php | 16 ++++---- .../Sales/Test/Unit/Model/ShipOrderTest.php | 8 ++-- .../Sales/Service/V1/ShipOrderTest.php | 39 +++++++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/app/code/Magento/Sales/Model/ShipOrder.php b/app/code/Magento/Sales/Model/ShipOrder.php index 26fe5a8e4b457..f955f6574a7b2 100644 --- a/app/code/Magento/Sales/Model/ShipOrder.php +++ b/app/code/Magento/Sales/Model/ShipOrder.php @@ -177,11 +177,13 @@ public function execute( $connection->beginTransaction(); try { $this->orderRegistrar->register($order, $shipment); - $order->setState( - $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS]) - ); - $order->setStatus($this->config->getStateDefaultStatus($order->getState())); - $shippingData = $this->shipmentRepository->save($shipment); + $shipment = $this->shipmentRepository->save($shipment); + if ($order->getState() === Order::STATE_NEW) { + $order->setState( + $this->orderStateResolver->getStateForOrder($order, [OrderStateResolverInterface::IN_PROGRESS]) + ); + $order->setStatus($this->config->getStateDefaultStatus($order->getState())); + } $this->orderRepository->save($order); $connection->commit(); } catch (\Exception $e) { @@ -191,9 +193,7 @@ public function execute( __('Could not save a shipment, see error log for details') ); } - if ($shipment && empty($shipment->getEntityId())) { - $shipment->setEntityId($shippingData->getEntityId()); - } + if ($notify) { if (!$appendComment) { $comment = null; diff --git a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php index 5909ebd76feb1..77cd6a058df6f 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php @@ -270,12 +270,12 @@ public function testExecute($orderId, $items, $notify, $appendComment) ->method('setState') ->with(Order::STATE_PROCESSING) ->willReturnSelf(); - $this->orderMock->expects($this->once()) + $this->orderMock->expects($this->exactly(2)) ->method('getState') - ->willReturn(Order::STATE_PROCESSING); + ->willReturn(Order::STATE_NEW); $this->configMock->expects($this->once()) ->method('getStateDefaultStatus') - ->with(Order::STATE_PROCESSING) + ->with(Order::STATE_NEW) ->willReturn('Processing'); $this->orderMock->expects($this->once()) ->method('setStatus') @@ -294,7 +294,7 @@ public function testExecute($orderId, $items, $notify, $appendComment) ->method('notify') ->with($this->orderMock, $this->shipmentMock, $this->shipmentCommentCreationMock); } - $this->shipmentMock->expects($this->exactly(2)) + $this->shipmentMock->expects($this->exactly(1)) ->method('getEntityId') ->willReturn(2); $this->assertEquals( diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php index 2d8c308389452..64fc612120332 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipOrderTest.php @@ -62,6 +62,7 @@ public function testConfigurableShipOrder() $shipmentId = (int)$this->_webApiCall($this->getServiceInfo($existingOrder), $requestData); $this->assertNotEmpty($shipmentId); + $shipment = null; try { $shipment = $this->shipmentRepository->get($shipmentId); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { @@ -89,6 +90,42 @@ public function testConfigurableShipOrder() ); } + /** + * Tests that order doesn't change a status from custom to the default after shipment creation. + * + * @magentoApiDataFixture Magento/Sales/_files/order_status.php + */ + public function testShipOrderStatusPreserve() + { + $incrementId = '100000001'; + $orderStatus = 'example'; + + /** @var Order $existingOrder */ + $order = $this->getOrder($incrementId); + $this->assertEquals($orderStatus, $order->getStatus()); + + $requestData = [ + 'orderId' => $order->getId() + ]; + /** @var OrderItemInterface $item */ + foreach ($order->getAllItems() as $item) { + $requestData['items'][] = [ + 'order_item_id' => $item->getItemId(), + 'qty' => $item->getQtyOrdered(), + ]; + } + + $shipmentId = $this->_webApiCall($this->getServiceInfo($order), $requestData); + $this->assertNotEmpty($shipmentId); + $actualOrder = $this->getOrder($order->getIncrementId()); + + $this->assertEquals( + $order->getStatus(), + $actualOrder->getStatus(), + 'Failed asserting that Order status wasn\'t changed' + ); + } + /** * @magentoApiDataFixture Magento/Sales/_files/order_new.php */ @@ -214,6 +251,7 @@ public function testPartialShipOrderWithBundleShippedSeparately() $shipmentId = $this->_webApiCall($this->getServiceInfo($existingOrder), $requestData); $this->assertNotEmpty($shipmentId); + $shipment = null; try { $shipment = $this->shipmentRepository->get($shipmentId); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { @@ -268,6 +306,7 @@ public function testPartialShipOrderWithTwoBundleShippedSeparatelyContainsSameSi $shipmentId = $this->_webApiCall($this->getServiceInfo($order), $requestData); $this->assertNotEmpty($shipmentId); + $shipment = null; try { $shipment = $this->shipmentRepository->get($shipmentId); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { From 4d4ed878c50ce8152966069af898445d60384f20 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 23 Sep 2020 19:43:48 -0500 Subject: [PATCH 0576/1013] MC-36785: Unable to set YouTube API key by CLI --- app/code/Magento/Analytics/etc/config.xml | 1 + .../Config/Source/ConfigStructureSource.php | 44 ++++++------ .../Source/ConfigStructureSourceTest.php | 72 +++++++++++++++++++ app/code/Magento/Config/etc/di.xml | 15 ++++ .../Magento/ReleaseNotification/etc/di.xml | 4 +- app/etc/di.xml | 5 ++ .../Model/Config/Export/CommentTest.php | 65 +++++++++++++++++ 7 files changed, 182 insertions(+), 24 deletions(-) create mode 100644 app/code/Magento/Config/Test/Unit/App/Config/Source/ConfigStructureSourceTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Config/Model/Config/Export/CommentTest.php diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml index b6194ba12993f..1c85fb3cbbd60 100644 --- a/app/code/Magento/Analytics/etc/config.xml +++ b/app/code/Magento/Analytics/etc/config.xml @@ -20,6 +20,7 @@ <general> <collection_time>02,00,00</collection_time> </general> + <token/> </analytics> </default> </config> diff --git a/app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php b/app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php index b2831aee0144c..f176e0d518230 100644 --- a/app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php +++ b/app/code/Magento/Config/App/Config/Source/ConfigStructureSource.php @@ -7,8 +7,7 @@ namespace Magento\Config\App\Config\Source; -use Magento\Config\Model\Config\Structure\Reader; -use Magento\Framework\App\Area; +use Magento\Config\Model\Config\Structure; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\DataObject; @@ -18,16 +17,16 @@ class ConfigStructureSource implements ConfigSourceInterface { /** - * @var Reader + * @var Structure */ - private $reader; + private $structure; /** - * @param Reader $reader + * @param Structure $structure */ - public function __construct(Reader $reader) + public function __construct(Structure $structure) { - $this->reader = $reader; + $this->structure = $structure; } /** @@ -35,32 +34,33 @@ public function __construct(Reader $reader) */ public function get($path = '') { - $configStructure = $this->reader->read(Area::AREA_ADMINHTML); - $sections = $configStructure['config']['system']['sections'] ?? []; - $defaultConfig = $this->merge([], $sections); + $fieldPaths = array_keys($this->structure->getFieldPaths()); + $defaultConfig = []; + foreach ($fieldPaths as $fieldPath) { + $defaultConfig = $this->addPathToConfig($defaultConfig, $fieldPath); + } $data = new DataObject(['default' => $defaultConfig]); return $data->getData($path); } /** - * Merge existed config with config structure + * Add config path to config structure * * @param array $config - * @param array $sections + * @param string $path * @return array */ - private function merge(array $config, array $sections): array + private function addPathToConfig(array $config, string $path): array { - foreach ($sections as $section) { - if (isset($section['children'])) { - $config[$section['id']] = $this->merge( - $config[$section['id']] ?? [], - $section['children'] - ); - } elseif ($section['_elementType'] === 'field') { - $config += [$section['id'] => null]; - } + if (strpos($path, '/') !== false) { + list ($key, $subPath) = explode('/', $path, 2); + $config[$key] = $this->addPathToConfig( + $config[$key] ?? [], + $subPath + ); + } else { + $config[$path] = null; } return $config; diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Source/ConfigStructureSourceTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Source/ConfigStructureSourceTest.php new file mode 100644 index 0000000000000..8fc7b04a13c64 --- /dev/null +++ b/app/code/Magento/Config/Test/Unit/App/Config/Source/ConfigStructureSourceTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Test\Unit\App\Config\Source; + +use Magento\Config\App\Config\Source\ConfigStructureSource; +use Magento\Config\Model\Config\Structure; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class ConfigStructureSourceTest extends TestCase +{ + /** + * @var Structure|MockObject + */ + private $structure; + + /** + * @var ConfigStructureSource + */ + private $source; + + protected function setUp(): void + { + $this->structure = $this->createMock(Structure::class); + $this->source = new ConfigStructureSource($this->structure); + } + + /** + * @dataProvider getDataProvider + * @param array $fieldPaths + * @param array $expectedConfig + */ + public function testGet(array $fieldPaths, array $expectedConfig) + { + $this->structure->expects($this->once()) + ->method('getFieldPaths') + ->willReturn($fieldPaths); + $this->assertEquals($expectedConfig, $this->source->get('default')); + } + + /** + * @return array + */ + public function getDataProvider(): array + { + return [ + [ + [ + 'general/single_store_mode/enabled' => [], + 'general/locale/timezone' => [], + 'general/locale/code' => [], + ], + [ + 'general' => [ + 'single_store_mode' => [ + 'enabled' => null, + ], + 'locale' => [ + 'timezone' => null, + 'code' => null, + ], + ], + ], + ], + ]; + } +} diff --git a/app/code/Magento/Config/etc/di.xml b/app/code/Magento/Config/etc/di.xml index 1fa1b2d1cfd5b..4277ca0a6de26 100644 --- a/app/code/Magento/Config/etc/di.xml +++ b/app/code/Magento/Config/etc/di.xml @@ -350,4 +350,19 @@ <argument name="excludeList" xsi:type="object">Magento\Config\Model\Config\Export\ExcludeList</argument> </arguments> </type> + <virtualType name="adminhtmlConfigStructureData" type="\Magento\Config\Model\Config\Structure\Data"> + <arguments> + <argument name="configScope" xsi:type="object">adminhtmlConfigScope</argument> + </arguments> + </virtualType> + <virtualType name="adminhtmlConfigStructure" type="Magento\Config\Model\Config\Structure"> + <arguments> + <argument name="structureData" xsi:type="object">adminhtmlConfigStructureData</argument> + </arguments> + </virtualType> + <type name="Magento\Config\App\Config\Source\ConfigStructureSource"> + <arguments> + <argument name="structure" xsi:type="object">adminhtmlConfigStructure</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ReleaseNotification/etc/di.xml b/app/code/Magento/ReleaseNotification/etc/di.xml index a4c434ff7f623..118f9346bb643 100644 --- a/app/code/Magento/ReleaseNotification/etc/di.xml +++ b/app/code/Magento/ReleaseNotification/etc/di.xml @@ -10,8 +10,8 @@ <type name="Magento\Config\Model\Config\TypePool"> <arguments> <argument name="sensitive" xsi:type="array"> - <item name="releaseNotification/content_url" xsi:type="string">1</item> - <item name="releaseNotification/use_https" xsi:type="string">1</item> + <item name="system/release_notification/content_url" xsi:type="string">1</item> + <item name="system/release_notification/use_https" xsi:type="string">1</item> </argument> </arguments> </type> diff --git a/app/etc/di.xml b/app/etc/di.xml index 585c88f68ff6f..7d28ad76acca9 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -320,6 +320,11 @@ <argument name="defaultScope" xsi:type="string">global</argument> </arguments> </virtualType> + <virtualType name="adminhtmlConfigScope" type="Magento\Framework\Config\Scope"> + <arguments> + <argument name="defaultScope" xsi:type="string">adminhtml</argument> + </arguments> + </virtualType> <type name="Magento\Framework\App\State"> <arguments> <argument name="mode" xsi:type="init_parameter">Magento\Framework\App\State::PARAM_MODE</argument> diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Export/CommentTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Export/CommentTest.php new file mode 100644 index 0000000000000..55821a6d64941 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Export/CommentTest.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Config\Model\Config\Export; + +use Magento\Config\Model\Config\TypePool; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class CommentTest extends TestCase +{ + /** + * @var Comment + */ + private $comment; + + protected function setUp(): void + { + $this->comment = Bootstrap::getObjectManager()->create(Comment::class); + } + + public function testGet() + { + $sensitivePaths = $this->getSensitivePaths(); + $comments = $this->comment->get(); + + $missedPaths = []; + foreach ($sensitivePaths as $sensitivePath) { + if (stripos($comments, $sensitivePath) === false) { + $missedPaths[] = $sensitivePath; + } + } + + $this->assertEmpty( + $missedPaths, + 'Sensitive paths are missed: ' . implode(', ', $missedPaths) + ); + } + + /** + * Retrieve sensitive paths from class that is used to check is path sensitive. + * + * There is no public method to get this data. + * It's why they are read using private method. + * + * @return array + */ + private function getSensitivePaths(): array + { + $typePool = Bootstrap::getObjectManager()->get(TypePool::class); + $sensitivePathsReader = \Closure::bind( + function () { + return $this->getPathsByType(TypePool::TYPE_SENSITIVE); + }, + $typePool, + $typePool + ); + + return $sensitivePathsReader(); + } +} From 080044a53a313a07d3b2df69a5538e6e890ca04e Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 23 Sep 2020 21:45:52 -0500 Subject: [PATCH 0577/1013] MC-36785: Unable to set YouTube API key by CLI --- app/code/Magento/Analytics/etc/config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml index 1c85fb3cbbd60..fb17aad1eed77 100644 --- a/app/code/Magento/Analytics/etc/config.xml +++ b/app/code/Magento/Analytics/etc/config.xml @@ -19,8 +19,8 @@ <integration_name>Magento Analytics user</integration_name> <general> <collection_time>02,00,00</collection_time> + <token/> </general> - <token/> </analytics> </default> </config> From a22010091673e485be42a2bd8097488d241da254 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 24 Sep 2020 09:31:06 +0300 Subject: [PATCH 0578/1013] MC-35016: Out of stock products doesn't filter properly using "price" filter --- ...seSelectProcessor.php => BaseStockStatusSelectProcessor.php} | 2 +- app/code/Magento/ConfigurableProduct/etc/di.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/{StockStatusBaseSelectProcessor.php => BaseStockStatusSelectProcessor.php} (96%) diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php similarity index 96% rename from app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php rename to app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php index b5cbaa57858c9..cbeaf2cea90e0 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/StockStatusBaseSelectProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php @@ -18,7 +18,7 @@ * * Adds stock status limitations to a given Select object. */ -class StockStatusBaseSelectProcessor implements BaseSelectProcessorInterface +class BaseStockStatusSelectProcessor implements BaseSelectProcessorInterface { /** * @var ResourceConnection diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index 9f01af66f9713..c7f67a69d669f 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -198,7 +198,7 @@ <arguments> <argument name="tableStrategy" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy</argument> <argument name="connectionName" xsi:type="string">indexer</argument> - <argument name="baseSelectProcessor" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\StockStatusBaseSelectProcessor</argument> + <argument name="baseSelectProcessor" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\BaseStockStatusSelectProcessor</argument> </arguments> </type> <type name="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product"> From f01a8f5680e129246ee568f9810d22cd30abb1f2 Mon Sep 17 00:00:00 2001 From: klg <k.langenberg@imi.de> Date: Thu, 24 Sep 2020 08:54:27 +0200 Subject: [PATCH 0579/1013] Fix docblock annotation for PublisherInterface message --- .../Magento/Framework/MessageQueue/PublisherInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php index 9f33b5b39d2ad..282d06dc143f4 100644 --- a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php @@ -18,7 +18,7 @@ interface PublisherInterface * Publishes a message to a specific queue or exchange. * * @param string $topicName - * @param array|object $data + * @param $data * @return null|mixed * @throws \InvalidArgumentException If message is not formed properly * @since 103.0.0 From 4cc567dfbbc031e3b7bd8d1f918b2aec2fb85268 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <51681547+engcom-Golf@users.noreply.github.com> Date: Thu, 24 Sep 2020 10:43:47 +0300 Subject: [PATCH 0580/1013] fix static test failure --- lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 05a08819b8ef9..7e36cdb334eb2 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -617,8 +617,7 @@ private function getColorspace(): int } /** - * Convert colorspace to SRGB if current colorspace - * is COLORSPACE_CMYK or COLORSPACE_UNDEFINED. + * Convert colorspace to SRGB if current colorspace is COLORSPACE_CMYK or COLORSPACE_UNDEFINED. * * @return void */ From 386cbe7ae344ca486167a36e339157f5f17955f4 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Thu, 24 Sep 2020 11:03:04 +0300 Subject: [PATCH 0581/1013] MC-36405: Reorder is not working with custom options date with JavaScript Calendar enabled --- .../Catalog/Model/ProductOptionProcessor.php | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php index a5e1d05409e43..dc5a4778c992e 100644 --- a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php +++ b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php @@ -5,13 +5,16 @@ */ namespace Magento\Catalog\Model; -use Magento\Catalog\Api\Data\ProductOptionExtensionFactory; use Magento\Catalog\Api\Data\ProductOptionInterface; use Magento\Catalog\Model\CustomOptions\CustomOption; use Magento\Catalog\Model\CustomOptions\CustomOptionFactory; use Magento\Framework\DataObject; use Magento\Framework\DataObject\Factory as DataObjectFactory; +use Magento\Framework\Serialize\Serializer\Json; +/** + * Processor ofr product options + */ class ProductOptionProcessor implements ProductOptionProcessorInterface { /** @@ -29,16 +32,27 @@ class ProductOptionProcessor implements ProductOptionProcessorInterface */ private $urlBuilder; + /** + * Serializer interface instance. + * + * @var Json + */ + private $serializer; + /** * @param DataObjectFactory $objectFactory * @param CustomOptionFactory $customOptionFactory + * @param Json|null $serializer */ public function __construct( DataObjectFactory $objectFactory, - CustomOptionFactory $customOptionFactory + CustomOptionFactory $customOptionFactory, + Json $serializer = null ) { $this->objectFactory = $objectFactory; $this->customOptionFactory = $customOptionFactory; + $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\Json::class); } /** @@ -88,7 +102,8 @@ public function convertToProductOption(DataObject $request) if (!empty($options) && is_array($options)) { $data = []; foreach ($options as $optionId => $optionValue) { - if (is_array($optionValue)) { + + if (is_array($optionValue) && !$this->isDateWithDateInternal($optionValue)) { $optionValue = $this->processFileOptionValue($optionValue); $optionValue = implode(',', $optionValue); } @@ -126,6 +141,8 @@ private function processFileOptionValue(array $optionValue) } /** + * Get url builder + * * @return \Magento\Catalog\Model\Product\Option\UrlBuilder * * @deprecated 101.0.0 @@ -138,4 +155,15 @@ private function getUrlBuilder() } return $this->urlBuilder; } + + /** + * Returns date option value only with 'date_internal data + * + * @param array $optionValue + * @return bool + */ + private function isDateWithDateInternal(array $optionValue): bool + { + return array_key_exists('date_internal', $optionValue) && array_key_exists('date', $optionValue); + } } From bca6bf6db9f3eeca2f10c753c34a23b568307bdf Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Thu, 24 Sep 2020 11:36:14 +0300 Subject: [PATCH 0582/1013] fix import image from external url --- .../Model/Import/Product.php | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index e249e5b3722e7..3e72824674773 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1750,11 +1750,7 @@ protected function _saveProducts() $position = 0; foreach ($rowImages as $column => $columnImages) { foreach ($columnImages as $columnImageKey => $columnImage) { - $filePath = filter_var($columnImage, FILTER_VALIDATE_URL) - ? $columnImage - : $importDir . DS . $columnImage; - - $uploadedFile = $this->getAlreadyExistedImage($rowExistingImages, $filePath); + $uploadedFile = $this->getAlreadyExistedImage($rowExistingImages, $columnImage, $importDir); if (!$uploadedFile && !isset($uploadedImages[$columnImage])) { $uploadedFile = $this->uploadMediaFiles($columnImage); $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); @@ -1948,30 +1944,30 @@ protected function _saveProducts() /** * Returns image hash by path * - * @param string $filePath + * @param string $path * @return string */ - private function getFileHash(string $filePath): string + private function getFileHash(string $path): string { - try { - $fileExists = $this->fileDriver->isExists($filePath); - } catch (\Exception $exception) { - $fileExists = false; - } - - return $fileExists ? hash_file(self::HASH_ALGORITHM, $filePath) : ''; + return hash_file(self::HASH_ALGORITHM, $path); } /** * Returns existed image * * @param array $imageRow - * @param string $filePath + * @param string $columnImage + * @param string $importDir * @return string */ - private function getAlreadyExistedImage(array $imageRow, string $filePath): string + private function getAlreadyExistedImage(array $imageRow, string $columnImage, string $importDir): string { - $hash = $this->getFileHash($filePath); + if (filter_var($columnImage, FILTER_VALIDATE_URL)) { + $hash = $this->getFileHash($columnImage); + } else { + $path = $importDir . DS . $columnImage; + $hash = $this->isFileExists($path) ? $this->getFileHash($path) : ''; + } return array_reduce( $imageRow, @@ -2013,6 +2009,23 @@ private function addImageHashes(array &$images): void } } + /** + * Is file exists + * + * @param string $path + * @return bool + */ + private function isFileExists(string $path): bool + { + try { + $fileExists = $this->fileDriver->isExists($path); + } catch (\Exception $exception) { + $fileExists = false; + } + + return $fileExists; + } + /** * Clears entries from Image Set and Row Data marked as no_selection * From 1a69c208925495363359f9eca649201e43d4997d Mon Sep 17 00:00:00 2001 From: "taras.gamanov" <engcom-vendorworker-hotel@adobe.com> Date: Thu, 24 Sep 2020 11:43:46 +0300 Subject: [PATCH 0583/1013] Code refactoring --- app/code/Magento/CatalogSearch/Model/Search/Category.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/code/Magento/CatalogSearch/Model/Search/Category.php b/app/code/Magento/CatalogSearch/Model/Search/Category.php index cc13ca2d4625f..200bc81526e66 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/Category.php +++ b/app/code/Magento/CatalogSearch/Model/Search/Category.php @@ -45,9 +45,15 @@ class Category extends DataObject */ private $string; + /** + * @var SearchCriteriaBuilder|void + */ + private $searchCriteriaBuilder; + /** * @param Data $adminhtmlData * @param CategoryListInterface $categoryRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory * @param FilterBuilder $filterBuilder * @param StringUtils $string @@ -55,12 +61,14 @@ class Category extends DataObject public function __construct( Data $adminhtmlData, CategoryListInterface $categoryRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, SearchCriteriaBuilderFactory $searchCriteriaBuilderFactory, FilterBuilder $filterBuilder, StringUtils $string ) { $this->adminhtmlData = $adminhtmlData; $this->categoryRepository = $categoryRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->searchCriteriaBuilderFactory = $searchCriteriaBuilderFactory; $this->filterBuilder = $filterBuilder; $this->string = $string; From 01a715933ab73d9fc3af90c2244277d5eb1d30dc Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 24 Sep 2020 12:19:47 +0300 Subject: [PATCH 0584/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Product/View/Options/AbstractOptions.php | 8 +++- .../Magento/Catalog/Model/Product/Option.php | 11 ++++- .../Model/Product/Option/Type/DefaultType.php | 11 ++++- .../Model/Product/Option/Type/Select.php | 22 +++++++++- .../Catalog/Model/Product/Option/Value.php | 7 +++- .../CalculateCustomOptionCatalogRule.php | 41 +++---------------- ...ForSimpleProductsWithCustomOptionsTest.xml | 17 +++++++- 7 files changed, 71 insertions(+), 46 deletions(-) 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 310158ed99948..2093ac68e321f 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -124,6 +124,7 @@ public function getOption() * Retrieve formatted price * * @return string + * @since 102.0.6 */ public function getFormattedPrice() { @@ -143,7 +144,7 @@ public function getFormattedPrice() * * @return string * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedPrice() */ public function getFormatedPrice() @@ -175,11 +176,14 @@ protected function _formatPrice($value, $flag = true) $customOptionPrice = $this->getProduct()->getPriceInfo()->getPrice('custom_option_price'); if (!$value['is_percent']) { - $value['pricing_value'] = $this->calculateCustomOptionCatalogRule->execute( + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( $this->getProduct(), (float)$value['pricing_value'], (bool)$value['is_percent'] ); + if ($catalogPriceValue!==null) { + $value['pricing_value'] = $catalogPriceValue; + } } $context = [CustomOptionPriceInterface::CONFIGURATION_OPTION_FLAG => true]; diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 15c40af09d7b9..4b8fd5d1a602a 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -18,6 +18,7 @@ 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\BasePrice; use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; @@ -473,12 +474,18 @@ public function afterSave() */ public function getPrice($flag = false) { - if ($flag) { - return $this->calculateCustomOptionCatalogRule->execute( + if ($flag && $this->getPriceType() == self::$typePercent) { + $price = $this->calculateCustomOptionCatalogRule->execute( $this->getProduct(), (float)$this->getData(self::KEY_PRICE), $this->getPriceType() === Value::TYPE_PERCENT ); + + if ($price == null) { + $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); } 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 a6ab77a090b4e..86b3a72f73ec0 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -352,11 +352,20 @@ public function getOptionPrice($optionValue, $basePrice) { $option = $this->getOption(); - return $this->calculateCustomOptionCatalogRule->execute( + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( $option->getProduct(), (float)$option->getPrice(), $option->getPriceType() === Value::TYPE_PERCENT ); + if ($catalogPriceValue!==null) { + return $catalogPriceValue; + } else { + return $this->_getChargeableOptionPrice( + $option->getPrice(), + $option->getPriceType() == 'percent', + $basePrice + ); + } } /** 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 17f0fb3b25f99..25aeb340605ab 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -260,11 +260,20 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $result += $this->calculateCustomOptionCatalogRule->execute( + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( $option->getProduct(), (float)$_result->getPrice(), $_result->getPriceType() === Value::TYPE_PERCENT ); + if ($catalogPriceValue!==null) { + $result += $catalogPriceValue; + } else { + $result += $this->_getChargeableOptionPrice( + $_result->getPrice(), + $_result->getPriceType() == 'percent', + $basePrice + ); + } } else { if ($this->getListener()) { $this->getListener()->setHasError(true)->setMessage($this->_getWrongConfigurationMessage()); @@ -275,11 +284,20 @@ public function getOptionPrice($optionValue, $basePrice) } elseif ($this->_isSingleSelection()) { $_result = $option->getValueById($optionValue); if ($_result) { - $result = $this->calculateCustomOptionCatalogRule->execute( + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( $option->getProduct(), (float)$_result->getPrice(), $_result->getPriceType() === Value::TYPE_PERCENT ); + if ($catalogPriceValue !== null) { + $result = $catalogPriceValue; + } else { + $result = $this->_getChargeableOptionPrice( + $_result->getPrice(), + $_result->getPriceType() == 'percent', + $basePrice + ); + } } else { if ($this->getListener()) { $this->getListener()->setHasError(true)->setMessage($this->_getWrongConfigurationMessage()); diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 6eeaa44cda706..638eaca328ff5 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -264,11 +264,16 @@ public function saveValues() public function getPrice($flag = false) { if ($flag) { - return $this->calculateCustomOptionCatalogRule->execute( + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( $this->getProduct(), (float)$this->getData(self::KEY_PRICE), $this->getPriceType() === self::TYPE_PERCENT ); + if ($catalogPriceValue!==null) { + return $catalogPriceValue; + } else { + return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE); + } } return $this->_getData(self::KEY_PRICE); } diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php index cf7c12b4560e0..2f7156bc70dbf 100644 --- a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php +++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php @@ -9,8 +9,6 @@ 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; /** @@ -29,6 +27,7 @@ class CalculateCustomOptionCatalogRule private $priceModifier; /** + * CalculateCustomOptionCatalogRule constructor. * @param PriceCurrencyInterface $priceCurrency * @param PriceModifierInterface $priceModifier */ @@ -46,13 +45,13 @@ public function __construct( * @param Product $product * @param float $optionPriceValue * @param bool $isPercent - * @return float + * @return float|null */ public function execute( Product $product, float $optionPriceValue, bool $isPercent - ): float { + ) { $regularPrice = (float)$product->getPriceInfo() ->getPrice(RegularPrice::PRICE_CODE) ->getValue(); @@ -68,39 +67,9 @@ public function execute( $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 $this->priceCurrency->convertAndRound($finalOptionPrice); } - - return $basePrice ?? $product->getPrice(); + return null; } /** diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml index cbb75dc4c9aff..2944b76434330 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -20,7 +20,6 @@ </annotations> <before> <!-- Login as Admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="_defaultProduct" stepKey="createProduct1"> <requiredEntity createDataKey="createCategory"/> @@ -39,7 +38,7 @@ <updateData createDataKey="createProduct1" entity="productWithCustomOptions" stepKey="updateProductWithOptions1"/> <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProductWithOptions2"/> <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProductWithOptions3"/> - <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> <!-- Delete products and category --> @@ -55,6 +54,13 @@ <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAdterTest"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTest"> + <argument name="tags" value=""/> + </actionGroup> + <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -71,6 +77,13 @@ <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> <waitForPageLoad stepKey="waitForSave"/> + <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"/> From 391a17799ad0e69ddb7c2282141e8be0381c6ab1 Mon Sep 17 00:00:00 2001 From: Viktor Sevch <viktor.sevch@transoftgroup.com> Date: Thu, 24 Sep 2020 13:30:18 +0300 Subject: [PATCH 0585/1013] MC-23536: CatalogProductListWidgetOrderTest is flaky and fails randomly --- .../Block/Product/ProductsList.php | 4 + ...CatalogProductListCheckWidgetOrderTest.xml | 90 +++++++++++++++++++ .../CatalogProductListWidgetOrderTest.xml | 46 +++++----- 3 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml diff --git a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php index fa81dab4ef7a1..4c5cdca7ff126 100644 --- a/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php +++ b/app/code/Magento/CatalogWidget/Block/Product/ProductsList.php @@ -337,6 +337,10 @@ public function createCollection() $collection->setVisibility($this->catalogProductVisibility->getVisibleInCatalogIds()); + /** + * Change sorting attribute to entity_id because created_at can be the same for products fastly created + * one by one and sorting by created_at is indeterministic in this case. + */ $collection = $this->_addProductAttributesAndPrices($collection) ->addStoreFilter() ->addAttributeToSort('entity_id', 'desc') diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml new file mode 100644 index 0000000000000..c71e8098c5c7f --- /dev/null +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="CatalogProductListCheckWidgetOrderTest"> + <annotations> + <features value="CatalogWidget"/> + <stories value="Product list widget"/> + <title value="Checking order of products in the 'catalog Products List' widget"/> + <description value="Check that products are ordered with recently added products first"/> + <severity value="MAJOR"/> + <testCaseId value="MC-13794"/> + <useCaseId value="MC-5905"/> + <group value="catalogWidget"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="simplecategory"/> + <createData entity="SimpleProduct" stepKey="createFirstProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">10</field> + </createData> + <createData entity="SimpleProduct" stepKey="createSecondProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">20</field> + </createData> + <createData entity="SimpleProduct" stepKey="createThirdProduct"> + <requiredEntity createDataKey="simplecategory"/> + <field key="price">30</field> + </createData> + <createData entity="_defaultCmsPage" stepKey="createPreReqPage"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> + </before> + <after> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Open created cms page--> + <actionGroup ref="AdminOpenCmsPageActionGroup" stepKey="openEditPage"> + <argument name="page_id" value="$createPreReqPage.id$"/> + </actionGroup> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <!--Add widget to cms page--> + <waitForElementVisible selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="waitInsertWidgetIconVisible"/> + <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> + <waitForPageLoad stepKey="waitForPageLoad1" /> + <waitForElementVisible selector="{{WidgetSection.WidgetType}}" stepKey="waitForWidgetTypeSelectorVisible"/> + <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear1" /> + <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParamBtnVisible"/> + <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> + <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> + <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Category" stepKey="selectCategoryCondition" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear2" /> + <waitForElementVisible selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParamVisible"/> + <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> + <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> + <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> + <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="waitForCategoryVisible" /> + <click selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="selectCategory" /> + <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> + <waitForPageLoad stepKey="waitForPageLoad2" /> + <!--Save cms page and go to Storefront--> + <actionGroup ref="SaveCmsPageActionGroup" stepKey="saveCmsPage"/> + <actionGroup ref="NavigateToStorefrontForCreatedPageActionGroup" stepKey="navigateToTheStoreFront1"> + <argument name="page" value="$createPreReqPage.identifier$"/> + </actionGroup> + <!--Check order of products: recently added first--> + <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="waitForThirdProductVisible"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="seeElementByName1"/> + <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="waitForSecondProductVisible"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="seeElementByName2"/> + <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="waitForFirstProductVisible"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="seeElementByName3"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml index f73939948c8ec..5bd9981a50236 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOrderTest.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="CatalogProductListWidgetOrderTest"> + <test name="CatalogProductListWidgetOrderTest" deprecated="Use CatalogProductListCheckWidgetOrderTest instead"> <annotations> <features value="CatalogWidget"/> <stories value="MC-5905: Wrong sorting on Products component"/> - <title value="Checking order of products in the 'catalog Products List' widget"/> + <title value="Deprecated. Checking order of products in the 'catalog Products List' widget"/> <description value="Check that products are ordered with recently added products first"/> <severity value="MAJOR"/> <testCaseId value="MC-13794"/> <group value="CatalogWidget"/> <group value="WYSIWYGDisabled"/> + <skip> + <issueId value="DEPRECATED">Use CatalogProductListCheckWidgetOrderTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> @@ -37,58 +40,49 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> </before> - <after> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> - <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> - <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> - <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> - <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> - <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> <!--Open created cms page--> <comment userInput="Open created cms page" stepKey="commentOpenCreatedCmsPage"/> <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage1"> - <argument name="CMSPage" value="$createPreReqPage$"/> + <argument name="CMSPage" value="$$createPreReqPage$$"/> </actionGroup> <!--Add widget to cms page--> <comment userInput="Add widget to cms page" stepKey="commentAddWidgetToCmsPage"/> - <waitForElementVisible selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="waitInsertWidgetIconVisible"/> <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> <waitForPageLoad stepKey="waitForPageLoad1" /> - <waitForElementVisible selector="{{WidgetSection.WidgetType}}" stepKey="waitForWidgetTypeSelectorVisible"/> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear1" /> - <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParamBtnVisible"/> <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> <selectOption selector="{{WidgetSection.ConditionsDropdown}}" userInput="Category" stepKey="selectCategoryCondition" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear2" /> - <waitForElementVisible selector="{{WidgetSection.RuleParam}}" stepKey="waitForRuleParamVisible"/> <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> - <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="waitForCategoryVisible" /> - <click selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="selectCategory" /> + <click selector="{{WidgetSection.PreCreateCategory('$$simplecategory.name$$')}}" stepKey="selectCategory" /> <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> <waitForPageLoad stepKey="waitForPageLoad2" /> <!--Save cms page and go to Storefront--> <comment userInput="Save cms page and go to Storefront" stepKey="commentSaveCmsPageAndGoToStorefront"/> - <waitForElementVisible selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="waitForExpandButtonMenuVisible"/> <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> <see userInput="You saved the page." stepKey="seeSuccessMessage"/> - <amOnPage url="$createPreReqPage.identifier$" stepKey="amOnPageTestPage"/> + <amOnPage url="$$createPreReqPage.identifier$$" stepKey="amOnPageTestPage"/> <waitForPageLoad stepKey="waitForPageLoad3" /> <!--Check order of products: recently added first--> <comment userInput="Check order of products: recently added first" stepKey="commentCheckOrderOfProductsRecentlyAddedFirst"/> - <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="waitForThirdProductVisible"/> - <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="seeElementByName1"/> - <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="waitForSecondProductVisible"/> - <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="seeElementByName2"/> - <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="waitForFirstProductVisible"/> - <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="seeElementByName3"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$$createThirdProduct.name$$')}}" stepKey="seeElementByName1"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$$createSecondProduct.name$$')}}" stepKey="seeElementByName2"/> + <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$$createFirstProduct.name$$')}}" stepKey="seeElementByName3"/> + <after> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createPreReqPage" stepKey="deletePreReqPage" /> + <deleteData createDataKey="simplecategory" stepKey="deleteSimpleCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> </test> </tests> From 3e2bf19fcfb7d43baf01155731302ff0f785e8a4 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Thu, 24 Sep 2020 14:46:33 +0300 Subject: [PATCH 0586/1013] MC-36405: Reorder is not working with custom options date with JavaScript Calendar enabled --- .../Catalog/Model/Product/Option/Type/Date.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 6ac48c565e842..8cfecd7dd4c47 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\Product\Option\Type; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use function PHPUnit\Framework\matches; /** * Catalog product option date type @@ -72,6 +73,11 @@ public function validateUserValue($values) $dateValid = true; if ($this->_dateExists()) { if ($this->useCalendar()) { + if (is_string($value) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4},+(\w|\W)*$/', $value)) { + $value = [ + 'date' => preg_replace('/,([^,]+),?$/', '', $value), + ]; + } $dateValid = isset($value['date']) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4}$/', $value['date']); } else { $dateValid = isset( @@ -184,8 +190,10 @@ public function prepareForCart() $date = (new \DateTime())->setTimestamp($timestamp); $result = $date->format('Y-m-d H:i:s'); + $originDate = (isset($value['date']) && $value['date'] != '') ? $value['date'] : null; + // Save date in internal format to avoid locale date bugs - $this->_setInternalInRequest($result); + $this->_setInternalInRequest($result, $originDate); return $result; } else { @@ -352,9 +360,10 @@ public function getYearEnd() * Save internal value of option in infoBuy_request * * @param string $internalValue Datetime value in internal format + * @param string|null $originDate date value in origin format * @return void */ - protected function _setInternalInRequest($internalValue) + protected function _setInternalInRequest($internalValue, $originDate = null) { $requestOptions = $this->getRequest()->getOptions(); if (!isset($requestOptions[$this->getOption()->getId()])) { @@ -364,6 +373,9 @@ protected function _setInternalInRequest($internalValue) $requestOptions[$this->getOption()->getId()] = []; } $requestOptions[$this->getOption()->getId()]['date_internal'] = $internalValue; + if ($originDate) { + $requestOptions[$this->getOption()->getId()]['date'] = $originDate; + } $this->getRequest()->setOptions($requestOptions); } From d88eba29e8ff17ef285d6ee42aedf04e7a7c8f30 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Thu, 24 Sep 2020 14:47:27 +0300 Subject: [PATCH 0587/1013] MC-36405: Reorder is not working with custom options date with JavaScript Calendar enabled --- app/code/Magento/Catalog/Model/Product/Option/Type/Date.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 8cfecd7dd4c47..1396167154b09 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -7,7 +7,6 @@ namespace Magento\Catalog\Model\Product\Option\Type; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; -use function PHPUnit\Framework\matches; /** * Catalog product option date type From f45c3751a459125419a3882d65719cb49604aba1 Mon Sep 17 00:00:00 2001 From: "taras.gamanov" <engcom-vendorworker-hotel@adobe.com> Date: Thu, 24 Sep 2020 14:51:17 +0300 Subject: [PATCH 0588/1013] Test coverage for changes has been added. --- .../adminhtml/js/order/create/scripts.test.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js index 0071d5af7df4e..e4a2b95a4c975 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Sales/adminhtml/js/order/create/scripts.test.js @@ -14,6 +14,7 @@ define([ var formEl, jQueryAjax, order, + confirmSpy = jasmine.createSpy('confirm'), tmpl = '<form id="edit_form" action="/">' + '<section id="order-methods">' + '<div id="order-billing_method"></div>' + @@ -129,7 +130,7 @@ define([ mocks = { 'jquery': $, 'Magento_Catalog/catalog/product/composite/configure': jasmine.createSpy(), - 'Magento_Ui/js/modal/confirm': jasmine.createSpy(), + 'Magento_Ui/js/modal/confirm': confirmSpy, 'Magento_Ui/js/modal/alert': jasmine.createSpy(), 'Magento_Ui/js/lib/view/utils/async': jasmine.createSpy() }; @@ -159,6 +160,22 @@ define([ jQueryAjax = undefined; }); + describe('Testing the process customer group change', function () { + it('and confirm method is called', function () { + init(); + spyOn(window, '$$').and.returnValue(['testing']); + order.processCustomerGroupChange( + 1, + 'testMsg', + 'customerGroupMsg', + 'errorMsg', + 1, + 'change' + ); + expect(confirmSpy).toHaveBeenCalledTimes(1); + }); + }); + describe('submit()', function () { function testSubmit(currentPaymentMethod, paymentMethod, ajaxParams) { $.ajax = jasmine.createSpy('$.ajax'); From 55ba690c4f3c6c724b5863aeb7b0db126e0ccf34 Mon Sep 17 00:00:00 2001 From: mastiuhin-olexandr <mastiuhin.olexandr@transoftgroup.com> Date: Thu, 24 Sep 2020 14:52:35 +0300 Subject: [PATCH 0589/1013] MC-35812: Overriding CreatePost can automatically confirm a customer --- .../Customer/Model/AccountManagement.php | 32 ------------------- .../Customer/Model/AccountManagementApi.php | 31 ++++++++++++++++++ app/code/Magento/Customer/etc/graphql/di.xml | 2 ++ .../Magento/Customer/etc/webapi_rest/di.xml | 2 ++ .../Magento/Customer/etc/webapi_soap/di.xml | 2 ++ 5 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 app/code/Magento/Customer/Model/AccountManagementApi.php diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index d22a10145c7be..67017562e105c 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -977,7 +977,6 @@ protected function sendEmailConfirmation(CustomerInterface $customer, $redirectU $templateType = self::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD; } $this->getEmailNotification()->newAccount($customer, $templateType, $redirectUrl, $customer->getStoreId()); - $customer->setConfirmation(null); } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); @@ -1615,37 +1614,6 @@ private function getEmailNotification() } } - /** - * Destroy all active customer sessions by customer id (current session will not be destroyed). - * - * Customer sessions which should be deleted are collecting from the "customer_visitor" table considering - * configured session lifetime. - * - * @param string|int $customerId - * @return void - */ - private function destroyCustomerSessions($customerId) - { - $this->sessionManager->regenerateId(); - $sessionLifetime = $this->scopeConfig->getValue( - \Magento\Framework\Session\Config::XML_PATH_COOKIE_LIFETIME, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - $dateTime = $this->dateTimeFactory->create(); - $activeSessionsTime = $dateTime->setTimestamp($dateTime->getTimestamp() - $sessionLifetime) - ->format(DateTime::DATETIME_PHP_FORMAT); - /** @var \Magento\Customer\Model\ResourceModel\Visitor\Collection $visitorCollection */ - $visitorCollection = $this->visitorCollectionFactory->create(); - $visitorCollection->addFieldToFilter('customer_id', $customerId); - $visitorCollection->addFieldToFilter('last_visit_at', ['from' => $activeSessionsTime]); - $visitorCollection->addFieldToFilter('session_id', ['neq' => $this->sessionManager->getSessionId()]); - /** @var \Magento\Customer\Model\Visitor $visitor */ - foreach ($visitorCollection->getItems() as $visitor) { - $sessionId = $visitor->getSessionId(); - $this->saveHandler->destroy($sessionId); - } - } - /** * Set ignore_validation_flag for reset password flow to skip unnecessary address and customer validation * diff --git a/app/code/Magento/Customer/Model/AccountManagementApi.php b/app/code/Magento/Customer/Model/AccountManagementApi.php new file mode 100644 index 0000000000000..02a05705b57ef --- /dev/null +++ b/app/code/Magento/Customer/Model/AccountManagementApi.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Customer\Model; + +use Magento\Customer\Api\Data\CustomerInterface; + +/** + * Account Management service implementation for external API access. + * Handle various customer account actions. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class AccountManagementApi extends AccountManagement +{ + /** + * @inheritDoc + * + * Override createAccount method to unset confirmation attribute for security purposes. + */ + public function createAccount(CustomerInterface $customer, $password = null, $redirectUrl = '') + { + $customer = parent::createAccount($customer, $password, $redirectUrl); + $customer->setConfirmation(null); + + return $customer; + } +} diff --git a/app/code/Magento/Customer/etc/graphql/di.xml b/app/code/Magento/Customer/etc/graphql/di.xml index e5ed4078b4a04..f1377f927b7c1 100644 --- a/app/code/Magento/Customer/etc/graphql/di.xml +++ b/app/code/Magento/Customer/etc/graphql/di.xml @@ -16,4 +16,6 @@ </argument> </arguments> </type> + <preference for="Magento\Customer\Api\AccountManagementInterface" + type="Magento\Customer\Model\AccountManagementApi" /> </config> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index d07d1a61c3d62..18627b68320ed 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -31,4 +31,6 @@ </argument> </arguments> </type> + <preference for="Magento\Customer\Api\AccountManagementInterface" + type="Magento\Customer\Model\AccountManagementApi" /> </config> diff --git a/app/code/Magento/Customer/etc/webapi_soap/di.xml b/app/code/Magento/Customer/etc/webapi_soap/di.xml index c23de8ef3f7e1..cb0b1ce58a594 100644 --- a/app/code/Magento/Customer/etc/webapi_soap/di.xml +++ b/app/code/Magento/Customer/etc/webapi_soap/di.xml @@ -18,4 +18,6 @@ </argument> </arguments> </type> + <preference for="Magento\Customer\Api\AccountManagementInterface" + type="Magento\Customer\Model\AccountManagementApi" /> </config> From f79ba0e15b06b1b59afdf714535b426c6ca282a7 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Thu, 24 Sep 2020 15:04:19 +0300 Subject: [PATCH 0590/1013] refactoring MFTF --- .../AdminUIShownIfLoginAsCustomerEnabledTest.xml | 14 ++++++++++++-- .../Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml | 6 ++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml index ea06263901b9e..26ffc877bbe42 100644 --- a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml @@ -23,13 +23,18 @@ 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"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCachesAfterSet"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> @@ -39,7 +44,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAfter"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCachesDefault"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Verify Login as Customer Login action works correctly from Customer page --> diff --git a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml index eaebc7fdaf74a..aceb4196bbef4 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/StorefrontAddStoreCodeInUrlTest.xml @@ -21,9 +21,15 @@ </annotations> <before> <magentoCLI command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlEnable"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCachesAfterSet"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" stepKey="addStoreCodeToUrlDisable"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCachesDefault"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <actionGroup ref="StorefrontClickOnHeaderLogoActionGroup" stepKey="clickOnStorefrontHeaderLogo"/> From bb78ad80e8b2613ea34f2b432da8f044eaf2e1e3 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 24 Sep 2020 15:18:43 +0300 Subject: [PATCH 0591/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Model/Product/Option/Type/Select.php | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) 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 25aeb340605ab..435b1629d0c85 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -260,20 +260,7 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( - $option->getProduct(), - (float)$_result->getPrice(), - $_result->getPriceType() === Value::TYPE_PERCENT - ); - if ($catalogPriceValue!==null) { - $result += $catalogPriceValue; - } else { - $result += $this->_getChargeableOptionPrice( - $_result->getPrice(), - $_result->getPriceType() == 'percent', - $basePrice - ); - } + $result += $this->getCalculatedOptionValue($option, $_result, $basePrice); } else { if ($this->getListener()) { $this->getListener()->setHasError(true)->setMessage($this->_getWrongConfigurationMessage()); @@ -359,4 +346,31 @@ protected function _isSingleSelection() { return in_array($this->getOption()->getType(), $this->singleSelectionTypes, true); } + + /** + * Returns calculated price of option + * + * @param \Magento\Catalog\Model\Product\Option $option + * @param \Magento\Catalog\Model\Product\Option\Value $result + * @param float $basePrice + * @return float|null + */ + protected function getCalculatedOptionValue($option, $result, $basePrice) + { + $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( + $option->getProduct(), + (float)$result->getPrice(), + $result->getPriceType() === Value::TYPE_PERCENT + ); + if ($catalogPriceValue!==null) { + $optionCalculatedValue = $catalogPriceValue; + } else { + $optionCalculatedValue = $this->_getChargeableOptionPrice( + $result->getPrice(), + $result->getPriceType() == 'percent', + $basePrice + ); + } + return $optionCalculatedValue; + } } From 6393f71c271ed57c6a70b4b6b1c3450b90984b48 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Thu, 24 Sep 2020 15:27:54 +0300 Subject: [PATCH 0592/1013] MC-37761: Issues when using multiple address checkout --- app/code/Magento/Tax/Model/Plugin/OrderSave.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Tax/Model/Plugin/OrderSave.php b/app/code/Magento/Tax/Model/Plugin/OrderSave.php index 38952eec02ca1..b46c5b51a9db2 100644 --- a/app/code/Magento/Tax/Model/Plugin/OrderSave.php +++ b/app/code/Magento/Tax/Model/Plugin/OrderSave.php @@ -50,11 +50,14 @@ public function afterSave( } /** + * Save order tax + * * @param \Magento\Sales\Api\Data\OrderInterface $order * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * phpcs:disable Generic.Metrics.NestingLevel.TooHigh */ protected function saveOrderTax(\Magento\Sales\Api\Data\OrderInterface $order) { @@ -176,7 +179,9 @@ protected function saveOrderTax(\Magento\Sales\Api\Data\OrderInterface $order) } elseif (isset($quoteItemId['associated_item_id'])) { //This item is associated with a product item $item = $order->getItemByQuoteItemId($quoteItemId['associated_item_id']); - $associatedItemId = $item->getId(); + if ($item !== null && $item->getId()) { + $associatedItemId = $item->getId(); + } } $data = [ From 5750f56498b6468040a71baa185f1d6357bc788c Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Thu, 24 Sep 2020 07:58:05 -0500 Subject: [PATCH 0593/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Model/Indexer/Design/IndexerHandler.php | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php index 92296c5dc9902..00f117666f3ff 100644 --- a/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php +++ b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php @@ -9,6 +9,7 @@ namespace Magento\Theme\Model\Indexer\Design; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Framework\Indexer\SaveHandler\Batch; use Magento\Framework\Indexer\SaveHandler\Grid; @@ -22,6 +23,15 @@ class IndexerHandler extends Grid */ private $flatScopeResolver; + /** + * @param IndexStructureInterface $indexStructure + * @param ResourceConnection $resource + * @param Batch $batch + * @param IndexScopeResolver $indexScopeResolver + * @param FlatScopeResolver $flatScopeResolver + * @param array $data + * @param int $batchSize + */ public function __construct( IndexStructureInterface $indexStructure, ResourceConnection $resource, @@ -29,8 +39,8 @@ public function __construct( IndexScopeResolver $indexScopeResolver, FlatScopeResolver $flatScopeResolver, array $data, - $batchSize = 100) - { + $batchSize = 100 + ) { parent::__construct( $indexStructure, $resource, @@ -38,35 +48,28 @@ public function __construct( $indexScopeResolver, $flatScopeResolver, $data, - $batchSize); + $batchSize + ); $this->flatScopeResolver = $flatScopeResolver; } /** - * @return bool - */ - private function isFlatTableExists() - { - $adapter = $this->resource->getConnection('write'); - $tableName = $this->flatScopeResolver->resolve($this->getIndexName(), []); - - return $adapter->isTableExists($tableName); - } - - /** - * Clean index table by deleting all records + * Clean index table by deleting all records unconditionally or create the index table if not exists * - * @inheritdoc + * @param $dimensions + * @return IndexerHandler */ public function cleanIndex($dimensions) { - if ($this->isFlatTableExists()) { - $adapter = $this->resource->getConnection('write'); - $tableName = $this->flatScopeResolver->resolve($this->getIndexName(), $dimensions); - $adapter->delete($tableName); + $tableName = $this->flatScopeResolver->resolve($this->getIndexName(), $dimensions); + + if ($this->connection->isTableExists($tableName)) { + $this->connection->delete($tableName); } else { $this->indexStructure->create($this->getIndexName(), $this->fields, $dimensions); } + + return $this; } } From 3045c20fb7eccfc68a06df7d857ea3ef0d1171a0 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Thu, 24 Sep 2020 16:09:54 +0300 Subject: [PATCH 0594/1013] MC-37705: [Magento Cloud] - Import / Export does not working --- .../Model/Import/Product/Option.php | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php index e12fc726f1056..7757a2ba5eb7d 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Option.php @@ -421,7 +421,7 @@ public function __construct( $this->_initMessageTemplates(); - $this->_initProductsSku()->_initOldCustomOptions(); + $this->_initProductsSku(); } /** @@ -606,6 +606,9 @@ protected function _initOldCustomOptions() 'option_title.store_id = ?', $storeId ); + if (!empty($this->_newOptionsOldData)) { + $this->_optionCollection->addProductToFilter(array_keys($this->_newOptionsOldData)); + } $this->_byPagesIterator->iterate($this->_optionCollection, $this->_pageSize, [$addCustomOptions]); } @@ -614,6 +617,20 @@ protected function _initOldCustomOptions() return $this; } + /** + * Get existing custom options data + * + * @return array + */ + private function getOldCustomOptions(): array + { + if ($this->_oldCustomOptions === null) { + $this->_initOldCustomOptions(); + } + + return $this->_oldCustomOptions; + } + /** * Imported entity type code getter * @@ -717,9 +734,9 @@ protected function _findOldOptionsWithTheSameTitles() $errorRows = []; foreach ($this->_newOptionsOldData as $productId => $options) { foreach ($options as $outerData) { - if (isset($this->_oldCustomOptions[$productId])) { + if (isset($this->getOldCustomOptions()[$productId])) { $optionsCount = 0; - foreach ($this->_oldCustomOptions[$productId] as $innerData) { + foreach ($this->getOldCustomOptions()[$productId] as $innerData) { if (count($outerData['titles']) == count($innerData['titles'])) { $outerTitles = $outerData['titles']; $innerTitles = $innerData['titles']; @@ -753,8 +770,8 @@ protected function _findNewOldOptionsTypeMismatch() $errorRows = []; foreach ($this->_newOptionsOldData as $productId => $options) { foreach ($options as $outerData) { - if (isset($this->_oldCustomOptions[$productId])) { - foreach ($this->_oldCustomOptions[$productId] as $innerData) { + if (isset($this->getOldCustomOptions()[$productId])) { + foreach ($this->getOldCustomOptions()[$productId] as $innerData) { if (count($outerData['titles']) == count($innerData['titles'])) { $outerTitles = $outerData['titles']; $innerTitles = $innerData['titles']; @@ -784,9 +801,9 @@ protected function _findNewOldOptionsTypeMismatch() protected function _findExistingOptionId(array $newOptionData, array $newOptionTitles) { $productId = $newOptionData['product_id']; - if (isset($this->_oldCustomOptions[$productId])) { + if (isset($this->getOldCustomOptions()[$productId])) { ksort($newOptionTitles); - $existingOptions = $this->_oldCustomOptions[$productId]; + $existingOptions = $this->getOldCustomOptions()[$productId]; foreach ($existingOptions as $optionId => $optionData) { if ($optionData['type'] == $newOptionData['type'] && $optionData['titles'][Store::DEFAULT_STORE_ID] == $newOptionTitles[Store::DEFAULT_STORE_ID] From a5a4ad10d326fccaa48fd23927088a170fb89f5b Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Thu, 24 Sep 2020 08:44:50 -0500 Subject: [PATCH 0595/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Magento/Theme/Model/Indexer/Design/IndexerHandler.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php index 00f117666f3ff..1acc75a6c949c 100644 --- a/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php +++ b/app/code/Magento/Theme/Model/Indexer/Design/IndexerHandler.php @@ -9,12 +9,13 @@ namespace Magento\Theme\Model\Indexer\Design; use Magento\Framework\App\ResourceConnection; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Framework\Indexer\SaveHandler\Batch; use Magento\Framework\Indexer\SaveHandler\Grid; +use Magento\Framework\Indexer\SaveHandler\IndexerInterface; use Magento\Framework\Indexer\ScopeResolver\FlatScopeResolver; use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; +use Magento\Framework\Search\Request\Dimension; class IndexerHandler extends Grid { @@ -57,8 +58,8 @@ public function __construct( /** * Clean index table by deleting all records unconditionally or create the index table if not exists * - * @param $dimensions - * @return IndexerHandler + * @param Dimension[] $dimensions + * @return IndexerInterface */ public function cleanIndex($dimensions) { From 86db3f403bbee0b574feced1649c5b72d80b0846 Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Thu, 24 Sep 2020 12:46:59 -0500 Subject: [PATCH 0596/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Unit/Model/Indexer/Design/ConfigTest.php | 244 ++++++++++++++---- 1 file changed, 196 insertions(+), 48 deletions(-) diff --git a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php index ff9a4302f49c0..bd93f8f542a0f 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php @@ -10,82 +10,193 @@ */ namespace Magento\Theme\Test\Unit\Model\Indexer\Design; +use Magento\Framework\App\ResourceConnection; use Magento\Framework\Data\Collection; use Magento\Framework\Indexer\FieldsetInterface; use Magento\Framework\Indexer\FieldsetPool; use Magento\Framework\Indexer\HandlerInterface; use Magento\Framework\Indexer\HandlerPool; use Magento\Framework\Indexer\IndexStructureInterface; -use Magento\Framework\Indexer\SaveHandler\IndexerInterface; +use Magento\Framework\Indexer\SaveHandler\Batch; use Magento\Framework\Indexer\SaveHandlerFactory; +use Magento\Framework\Indexer\ScopeResolver\FlatScopeResolver; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; use Magento\Framework\Indexer\StructureFactory; +use Magento\Theme\Model\Data\Design\Config as DesignConfig; use Magento\Theme\Model\Indexer\Design\Config; +use Magento\Theme\Model\ResourceModel\Design\Config\Scope\CollectionFactory; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Theme\Model\Indexer\Design\IndexerHandler; +use Magento\Framework\DB\Adapter\AdapterInterface; class ConfigTest extends TestCase { - /** @var Config */ - protected $model; + /** + * @var AdapterInterface|MockObject + */ + private $adapter; + /** + * @var ResourceConnection|MockObject + */ + private $resourceConnection; + /** + * @var Batch|MockObject + */ + private $batch; + /** + * @var IndexStructureInterface|MockObject + */ + private $indexerStructure; + /** + * @var IndexScopeResolver|MockObject + */ + private $indexScopeResolver; + /** + * @var FlatScopeResolver|MockObject + */ + private $flatScopeResolver; + /** + * @var SaveHandlerFactory|MockObject + */ + private $saveHandlerFactory; + /** + * @var StructureFactory|MockObject + */ + private $structureFactory; + /** + * @var FieldsetInterface|MockObject + */ + private $indexerFieldset; + /** + * @var FieldsetPool|MockObject + */ + private $fieldsetPool; + /** + * @var HandlerInterface|MockObject + */ + private $indexerHandler; + /** + * @var HandlerPool|MockObject + */ + private $handlerPool; + /** + * @var Collection|MockObject + */ + private $collection; + /** + * @var CollectionFactory|MockObject + */ + private $collectionFactory; protected function setUp(): void { - $indexerStructure = $this->getMockBuilder(IndexStructureInterface::class) + $this->indexerStructure = $this->getMockBuilder(IndexStructureInterface::class) ->getMockForAbstractClass(); - $structureFactory = $this->getMockBuilder(StructureFactory::class) + $this->structureFactory = $this->getMockBuilder(StructureFactory::class) ->disableOriginalConstructor() ->getMock(); - $structureFactory->expects($this->any()) - ->method('create') - ->willReturn($indexerStructure); - - $indexer = $this->getMockBuilder(IndexerInterface::class) + $this->resourceConnection = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->adapter = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->batch = $this->getMockBuilder(Batch::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexScopeResolver = $this->getMockBuilder(IndexScopeResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->flatScopeResolver = $this->getMockBuilder(FlatScopeResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->saveHandlerFactory = $this->getMockBuilder(SaveHandlerFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fieldsetPool = $this->getMockBuilder(FieldsetPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexerHandler = $this->getMockBuilder(HandlerInterface::class) ->getMockForAbstractClass(); - $saveHandlerFactory = $this->getMockBuilder(SaveHandlerFactory::class) + $this->handlerPool = $this->getMockBuilder(HandlerPool::class) ->disableOriginalConstructor() ->getMock(); - $saveHandlerFactory->expects($this->any()) + $this->indexerFieldset = $this->getMockBuilder(FieldsetInterface::class) + ->getMockForAbstractClass(); + } + + /** + * Generate flat index table name from design config grid index ID + * + * @return string + */ + private function getFlatIndexTableName(): string + { + return DesignConfig::DESIGN_CONFIG_GRID_INDEXER_ID . '_flat'; + } + + /** + * Initialize and return Design Config Indexer Model + * + * @return Config + */ + private function getDesignConfigIndexerModel(): Config + { + $this->structureFactory->expects($this->any()) + ->method('create') + ->willReturn($this->indexerStructure); + $this->resourceConnection + ->expects($this->any()) + ->method('getConnection') + ->willReturn($this->adapter); + $this->flatScopeResolver->expects($this->any()) + ->method('resolve') + ->willReturn($this->getFlatIndexTableName()); + + $indexer = new IndexerHandler( + $this->indexerStructure, + $this->resourceConnection, + $this->batch, + $this->indexScopeResolver, + $this->flatScopeResolver, + [ + 'fieldsets' => [], + 'indexer_id' => DesignConfig::DESIGN_CONFIG_GRID_INDEXER_ID + ] + ); + + $this->saveHandlerFactory->expects($this->any()) ->method('create') ->willReturn($indexer); - $indexerFieldset = $this->getMockBuilder(FieldsetInterface::class) - ->getMockForAbstractClass(); - $indexerFieldset->expects($this->any()) + $this->indexerFieldset->expects($this->any()) ->method('addDynamicData') ->willReturnArgument(0); - $fieldsetPool = $this->getMockBuilder(FieldsetPool::class) - ->disableOriginalConstructor() - ->getMock(); - $fieldsetPool->expects($this->any()) + + $this->fieldsetPool->expects($this->any()) ->method('get') - ->willReturn($indexerFieldset); + ->willReturn($this->indexerFieldset); - $indexerHandler = $this->getMockBuilder(HandlerInterface::class) - ->getMockForAbstractClass(); - $handlerPool = $this->getMockBuilder(HandlerPool::class) - ->disableOriginalConstructor() - ->getMock(); - $handlerPool->expects($this->any()) + $this->handlerPool->expects($this->any()) ->method('get') - ->willReturn($indexerHandler); + ->willReturn($this->indexerHandler); - $collection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->getMock(); - $collectionFactory = - $this->getMockBuilder(\Magento\Theme\Model\ResourceModel\Design\Config\Scope\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - $collectionFactory->expects($this->any()) + $this->collectionFactory->expects($this->any()) ->method('create') - ->willReturn($collection); - - $this->model = new Config( - $structureFactory, - $saveHandlerFactory, - $fieldsetPool, - $handlerPool, - $collectionFactory, + ->willReturn($this->collection); + + return new Config( + $this->structureFactory, + $this->saveHandlerFactory, + $this->fieldsetPool, + $this->handlerPool, + $this->collectionFactory, [ 'fieldsets' => ['test_fieldset' => [ 'fields' => [ @@ -102,7 +213,7 @@ protected function setUp(): void 'handler' => null, ], ], - 'provider' => $indexerFieldset, + 'provider' => $this->indexerFieldset, ] ], 'saveHandler' => 'saveHandlerClass', @@ -111,9 +222,46 @@ protected function setUp(): void ); } - public function testExecuteFull() + public function testFullReindex() + { + $this->adapter->expects($this->any()) + ->method('isTableExists') + ->willReturn(true); + $this->indexerStructure->expects($this->never())->method('create') + ->with(DesignConfig::DESIGN_CONFIG_GRID_INDEXER_ID); + $this->adapter->expects($this->once())->method('delete') + ->with($this->getFlatIndexTableName()); + $this->batch->expects($this->any()) + ->method('getItems')->willReturn([]); + + $this->getDesignConfigIndexerModel()->executeFull(); + } + + public function testFullReindexWithFlatTableCreate() + { + $this->adapter->expects($this->any())->method('isTableExists') + ->willReturn(false); + $this->indexerStructure->expects($this->once())->method('create') + ->with(DesignConfig::DESIGN_CONFIG_GRID_INDEXER_ID); + $this->adapter->expects($this->never())->method('delete') + ->with($this->getFlatIndexTableName()); + $this->batch->expects($this->any())->method('getItems') + ->willReturn([]); + + $this->getDesignConfigIndexerModel()->executeFull(); + } + + public function testPartialReindex() { - $result = $this->model->executeFull(); - $this->assertNull($result); + $this->adapter->expects($this->any())->method('isTableExists') + ->willReturn(true); + $this->indexerStructure->expects($this->never())->method('create') + ->with(DesignConfig::DESIGN_CONFIG_GRID_INDEXER_ID); + $this->adapter->expects($this->once())->method('delete') + ->with($this->getFlatIndexTableName(), ['entity_id IN(?)' => [1, 2, 3]]); + $this->batch->expects($this->any())->method('getItems') + ->willReturn([[1, 2, 3]]); + + $this->getDesignConfigIndexerModel()->executeList([1, 2, 3]); } } From 9327c28eb7b54db7b48fa2759484cfd049559e0d Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Thu, 24 Sep 2020 13:42:03 -0500 Subject: [PATCH 0597/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php index bd93f8f542a0f..acfbd818c43d1 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php @@ -7,6 +7,8 @@ /** * Test design config indexer model + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ namespace Magento\Theme\Test\Unit\Model\Indexer\Design; From 4c1d95dbdff4b9b1de6ec9942157b68e6d57e002 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Thu, 24 Sep 2020 15:26:18 -0500 Subject: [PATCH 0598/1013] MC-35934: ArrayBackend does not assign default value on save --- .../Entity/Attribute/Backend/ArrayBackend.php | 13 ++- .../Attribute/Backend/ArrayBackendTest.php | 91 ++++++++++++++++--- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php index 03c91f53287eb..112483094465e 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/ArrayBackend.php @@ -42,12 +42,15 @@ public function beforeSave($object) public function validate($object) { $attributeCode = $this->getAttribute()->getAttributeCode(); - $data = $object->getData($attributeCode); - if (is_array($data)) { - $object->setData($attributeCode, implode(',', array_filter($data))); - } elseif (empty($data)) { - $object->setData($attributeCode, null); + if ($object->hasData($attributeCode)) { + $data = $object->getData($attributeCode); + if (is_array($data)) { + $object->setData($attributeCode, implode(',', array_filter($data))); + } elseif (empty($data)) { + $object->setData($attributeCode, null); + } } + return parent::validate($object); } } diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayBackendTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayBackendTest.php index 7d04d003a0e64..c01dd045e0857 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayBackendTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/Backend/ArrayBackendTest.php @@ -17,40 +17,109 @@ class ArrayBackendTest extends TestCase /** * @var ArrayBackend */ - protected $_model; + private $_model; /** * @var Attribute */ - protected $_attribute; + private $_attribute; protected function setUp(): void { $this->_attribute = $this->createPartialMock( Attribute::class, - ['getAttributeCode', '__wakeup'] + ['getAttributeCode', 'getDefaultValue', '__wakeup'] ); $this->_model = new ArrayBackend(); $this->_model->setAttribute($this->_attribute); } /** - * @dataProvider attributeValueDataProvider + * @dataProvider validateDataProvider + * @param array $productData + * @param bool $hasData + * @param string|int|float|null $expectedValue */ - public function testValidate($data) + public function testValidate(array $productData, bool $hasData, $expectedValue) { - $this->_attribute->expects($this->atLeastOnce())->method('getAttributeCode')->willReturn('code'); - $product = new DataObject(['code' => $data, 'empty' => null]); + $this->_attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn('attr'); + + $product = new DataObject($productData); $this->_model->validate($product); - $this->assertEquals('1,2,3', $product->getCode()); - $this->assertNull($product->getEmpty()); + $this->assertEquals($hasData, $product->hasData('attr')); + $this->assertEquals($expectedValue, $product->getAttr()); + } + + /** + * @return array + */ + public static function validateDataProvider(): array + { + return [ + [ + ['sku' => 'test1', 'attr' => [1, 2, 3]], + true, + '1,2,3', + ], + [ + ['sku' => 'test1', 'attr' => '1,2,3'], + true, + '1,2,3', + ], + [ + ['sku' => 'test1', 'attr' => null], + true, + null, + ], + [ + ['sku' => 'test1'], + false, + null, + ], + ]; + } + + /** + * @dataProvider beforeSaveDataProvider + * @param array $productData + * @param string $defaultValue + * @param string $expectedValue + */ + public function testBeforeSave( + array $productData, + string $defaultValue, + string $expectedValue + ) { + $this->_attribute->expects($this->atLeastOnce()) + ->method('getAttributeCode') + ->willReturn('attr'); + $this->_attribute->expects($this->any()) + ->method('getDefaultValue') + ->willReturn($defaultValue); + + $product = new DataObject($productData); + $this->_model->beforeSave($product); + $this->assertEquals($expectedValue, $product->getAttr()); } /** * @return array */ - public static function attributeValueDataProvider() + public function beforeSaveDataProvider(): array { - return [[[1, 2, 3]], ['1,2,3']]; + return [ + [ + ['sku' => 'test1', 'attr' => 'Value 2'], + 'Default value 1', + 'Value 2', + ], + [ + ['sku' => 'test1'], + 'Default value 1', + 'Default value 1', + ], + ]; } } From 4b999ca7900bdc6afc67d3e245eabb0c0c3cc1f2 Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Thu, 24 Sep 2020 16:08:37 -0500 Subject: [PATCH 0599/1013] MC-37630: Updating Design Configuration runs full reindex which generates DDL statement DROP Table --- .../Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php index acfbd818c43d1..753a05eda21bb 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Indexer/Design/ConfigTest.php @@ -7,8 +7,6 @@ /** * Test design config indexer model - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ namespace Magento\Theme\Test\Unit\Model\Indexer\Design; @@ -32,6 +30,9 @@ use Magento\Theme\Model\Indexer\Design\IndexerHandler; use Magento\Framework\DB\Adapter\AdapterInterface; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ConfigTest extends TestCase { /** From f06f098e318198db6e17e6dfce5a2f6f74848fdc Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Sun, 13 Sep 2020 18:15:22 -0500 Subject: [PATCH 0600/1013] MC-37603: [Graphql]Item belonging to one customer's wishlist can be updated and added to a different customer's wishlist - Added validation and testcases --- .../Wishlist/RemoveProductsFromWishlist.php | 17 ++++++++- .../Wishlist/UpdateProductsInWishlist.php | 9 +++++ .../DeleteProductsFromWishlistTest.php | 33 ++++++++++++++++- .../UpdateProductsFromWishlistTest.php | 37 ++++++++++++++++++- 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php index d143830064752..3599ad237da3a 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php @@ -7,6 +7,7 @@ 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; @@ -63,7 +64,7 @@ public function __construct( public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOutput { foreach ($wishlistItemsIds as $wishlistItemId) { - $this->removeItemFromWishlist((int) $wishlistItemId); + $this->removeItemFromWishlist((int) $wishlistItemId, $wishlist); } return $this->prepareOutput($wishlist); @@ -73,12 +74,22 @@ public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOu * Remove product item from wishlist * * @param int $wishlistItemId + * @param Wishlist $wishlist * * @return void */ - private function removeItemFromWishlist(int $wishlistItemId): void + private function removeItemFromWishlist(int $wishlistItemId, Wishlist $wishlist): void { try { + if ($wishlist->getItem($wishlistItemId) == null) { + throw new LocalizedException( + __( + 'The wishlist item with ID "%id" does not belong to the wishlist', + ['id' => $wishlistItemId] + ) + ); + } + $wishlist->getItemCollection()->clear(); /** @var WishlistItem $wishlistItem */ $wishlistItem = $this->wishlistItemFactory->create(); $this->wishlistItemResource->load($wishlistItem, $wishlistItemId); @@ -90,6 +101,8 @@ private function removeItemFromWishlist(int $wishlistItemId): void } $this->wishlistItemResource->delete($wishlistItem); + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); } catch (\Exception $e) { $this->addError( __( diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php index 4abcada138362..ff3a8c135ce27 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -90,6 +90,15 @@ public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutpu private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wishlistItemData): void { try { + if ($wishlist->getItem($wishlistItemData->getId()) == null) { + throw new LocalizedException( + __( + 'The wishlist item with ID "%id" does not belong to the wishlist', + ['id' => $wishlistItemData->getId()] + ) + ); + } + $wishlist->getItemCollection()->clear(); $options = $this->buyRequestBuilder->build($wishlistItemData); /** @var WishlistItem $wishlistItem */ $wishlistItem = $this->wishlistItemFactory->create(); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php index 13aaecbc7b733..10ce5a1c0eb1c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -55,6 +55,35 @@ public function testDeleteWishlistItemFromWishlist(): void $this->assertEmpty($wishlistResponse['items_v2']); } + /** + * Test deleting the wishlist item of another customer + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + */ + public function testUnauthorizedWishlistItemDelete() + { + $wishlist = $this->getWishlist(); + $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $wishlist2 = $this->getWishlist('customer_two@example.com'); + $wishlist2Id = $wishlist2['customer']['wishlist']['id']; + $wishlistItem2 = $wishlist['customer']['wishlist']['items'][0]; + $query = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id']); + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getHeaderMap('customer_two@example.com') + ); + self::assertEquals(1, $response['removeProductsFromWishlist']['wishlist']['items_count']); + self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items'], 'empty wish list items'); + self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items']); + self::assertEquals( + 'The wishlist item with ID "'.$wishlistItem2['id'].'" does not belong to the wishlist', + $response['removeProductsFromWishlist']['user_errors'][0]['message'] + ); + } + /** * Authentication header map * @@ -116,9 +145,9 @@ private function getQuery( * * @throws Exception */ - public function getWishlist(): array + public function getWishlist(string $username = 'customer@example.com'): array { - return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap($username)); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index 08273e7936640..0459f26860d64 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -57,6 +57,38 @@ public function testUpdateSimpleProductFromWishlist(): void $this->assertEquals($description, $wishlistResponse['items_v2'][0]['description']); } + /** + * Test updating the wishlist item of another customer + * + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/Wishlist/_files/two_wishlists_for_two_diff_customers.php + */ + public function testUnauthorizedWishlistItemUpdate() + { + $wishlist = $this->getWishlist(); + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $wishlist2 = $this->getWishlist('customer_two@example.com'); + $wishlist2Id = $wishlist2['customer']['wishlist']['id']; + $wishlistItem2 = $wishlist['customer']['wishlist']['items'][0]; + $qty = 2; + $updateWishlistQuery = $this->getQueryWithNoDescription((int) $wishlist2Id, (int) $wishlistItem['id'], $qty); + $response = $this->graphQlMutation( + $updateWishlistQuery, + [], + '', + $this->getHeaderMap('customer_two@example.com') + ); + self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); + self::assertEquals( + 'The wishlist item with ID "'.$wishlistItem2['id'].'" does not belong to the wishlist', + $response['updateProductsInWishlist']['user_errors'][0]['message'] + ); + } + /** * Authentication header map * @@ -124,13 +156,14 @@ private function getQuery( /** * Get wishlist result * + * @param string $username * @return array * * @throws Exception */ - public function getWishlist(): array + public function getWishlist(string $username = 'customer@example.com'): array { - return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap($username)); } /** From 959480545522e573aea6d0149acdfd88222a7976 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Mon, 14 Sep 2020 11:34:28 -0500 Subject: [PATCH 0601/1013] MC-37603: [Graphql]Item belonging to one customer's wishlist can be updated and added to a different customer's wishlist - Fixed review comments --- .../GraphQl/Wishlist/DeleteProductsFromWishlistTest.php | 3 +-- .../GraphQl/Wishlist/UpdateProductsFromWishlistTest.php | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php index 10ce5a1c0eb1c..8befde719c96d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -67,7 +67,6 @@ public function testUnauthorizedWishlistItemDelete() $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; $wishlist2 = $this->getWishlist('customer_two@example.com'); $wishlist2Id = $wishlist2['customer']['wishlist']['id']; - $wishlistItem2 = $wishlist['customer']['wishlist']['items'][0]; $query = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id']); $response = $this->graphQlMutation( $query, @@ -79,7 +78,7 @@ public function testUnauthorizedWishlistItemDelete() self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items'], 'empty wish list items'); self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items']); self::assertEquals( - 'The wishlist item with ID "'.$wishlistItem2['id'].'" does not belong to the wishlist', + 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', $response['removeProductsFromWishlist']['user_errors'][0]['message'] ); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index 0459f26860d64..d978b4fbc6982 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -67,11 +67,9 @@ public function testUpdateSimpleProductFromWishlist(): void public function testUnauthorizedWishlistItemUpdate() { $wishlist = $this->getWishlist(); - $wishlistId = $wishlist['customer']['wishlist']['id']; $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; $wishlist2 = $this->getWishlist('customer_two@example.com'); $wishlist2Id = $wishlist2['customer']['wishlist']['id']; - $wishlistItem2 = $wishlist['customer']['wishlist']['items'][0]; $qty = 2; $updateWishlistQuery = $this->getQueryWithNoDescription((int) $wishlist2Id, (int) $wishlistItem['id'], $qty); $response = $this->graphQlMutation( @@ -84,7 +82,7 @@ public function testUnauthorizedWishlistItemUpdate() self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); self::assertEquals( - 'The wishlist item with ID "'.$wishlistItem2['id'].'" does not belong to the wishlist', + 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', $response['updateProductsInWishlist']['user_errors'][0]['message'] ); } From d032b198de76ac2418de9002a309c34e2946eee1 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Thu, 24 Sep 2020 17:26:01 -0500 Subject: [PATCH 0602/1013] MC-37603: [Graphql]Item belonging to one customer's wishlist can be updated and added to a different customer's wishlist - Fixed issue and added coverage --- .../GraphQl/Wishlist/DeleteProductsFromWishlistTest.php | 6 +++--- .../GraphQl/Wishlist/UpdateProductsFromWishlistTest.php | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php index 8befde719c96d..dd7a54cff32a0 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -64,7 +64,7 @@ public function testDeleteWishlistItemFromWishlist(): void public function testUnauthorizedWishlistItemDelete() { $wishlist = $this->getWishlist(); - $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; $wishlist2 = $this->getWishlist('customer_two@example.com'); $wishlist2Id = $wishlist2['customer']['wishlist']['id']; $query = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id']); @@ -75,8 +75,8 @@ public function testUnauthorizedWishlistItemDelete() $this->getHeaderMap('customer_two@example.com') ); self::assertEquals(1, $response['removeProductsFromWishlist']['wishlist']['items_count']); - self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items'], 'empty wish list items'); - self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items']); + self::assertNotEmpty($response['removeProductsFromWishlist']['wishlist']['items_v2'], 'empty wish list items'); + self::assertCount(1, $response['removeProductsFromWishlist']['wishlist']['items_v2']); self::assertEquals( 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', $response['removeProductsFromWishlist']['user_errors'][0]['message'] diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index d978b4fbc6982..bc00be18eed19 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -67,11 +67,12 @@ public function testUpdateSimpleProductFromWishlist(): void public function testUnauthorizedWishlistItemUpdate() { $wishlist = $this->getWishlist(); - $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; $wishlist2 = $this->getWishlist('customer_two@example.com'); $wishlist2Id = $wishlist2['customer']['wishlist']['id']; $qty = 2; - $updateWishlistQuery = $this->getQueryWithNoDescription((int) $wishlist2Id, (int) $wishlistItem['id'], $qty); + $description = 'New Description'; + $updateWishlistQuery = $this->getQuery((int) $wishlist2Id, (int) $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation( $updateWishlistQuery, [], @@ -79,8 +80,8 @@ public function testUnauthorizedWishlistItemUpdate() $this->getHeaderMap('customer_two@example.com') ); self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); - self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); - self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items_v2']); self::assertEquals( 'The wishlist item with ID "'.$wishlistItem['id'].'" does not belong to the wishlist', $response['updateProductsInWishlist']['user_errors'][0]['message'] From 30d124352154ab131ce0dcf2886bbf4e7dd81b99 Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Thu, 24 Sep 2020 17:57:44 -0500 Subject: [PATCH 0603/1013] MC-37880: Shipping address information disappears from order screen while placing order --- .../Magento/Sales/view/adminhtml/web/order/create/scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a329524c58d41..10f9ee38720ff 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 @@ -1202,7 +1202,7 @@ define([ for (var i = 0; i < this.loadingAreas.length; i++) { var id = this.loadingAreas[i]; if ($(this.getAreaId(id))) { - if ('message' != id || response[id]) { + if (id in response) { $(this.getAreaId(id)).update(response[id]); } if ($(this.getAreaId(id)).callback) { From 3da6f23f4fb20553580431bdcdbb35afb4551d7f Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 25 Sep 2020 10:24:25 +0300 Subject: [PATCH 0604/1013] refactor --- .../Magento/CatalogImportExport/Model/Import/Product.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index 3e72824674773..0a030e2d65a05 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -1972,11 +1972,7 @@ private function getAlreadyExistedImage(array $imageRow, string $columnImage, st return array_reduce( $imageRow, function ($exists, $file) use ($hash) { - if ($exists) { - return $exists; - } - - if (isset($file['hash']) && $file['hash'] === $hash) { + if (!$exists && isset($file['hash']) && $file['hash'] === $hash) { return $file['value']; } From f9c0734284d7c4cb9ad49e08a2e521f0b15b2d0e Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Fri, 25 Sep 2020 11:10:49 +0300 Subject: [PATCH 0605/1013] MC-23911: Shipping estimation fails to update DOM on CI --- .../Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml index bbad2579a47d2..4cc0ac3bc3a06 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontCheckCartActionGroup.xml @@ -27,9 +27,7 @@ <scrollTo selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="scrollToSummary"/> <see userInput="{{subtotal}}" selector="{{CheckoutCartSummarySection.subtotal}}" stepKey="assertSubtotal"/> <see userInput="({{shippingMethod}})" selector="{{CheckoutCartSummarySection.shippingMethod}}" stepKey="assertShippingMethod"/> - <reloadPage stepKey="reloadPage" after="assertShippingMethod" /> - <waitForPageLoad stepKey="WaitForPageLoaded" after="reloadPage" /> - <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping" after="WaitForPageLoaded"/> - <see userInput="{{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal" after="assertShipping"/> + <waitForText userInput="{{shipping}}" selector="{{CheckoutCartSummarySection.shipping}}" time="45" stepKey="assertShipping"/> + <see userInput="{{total}}" selector="{{CheckoutCartSummarySection.total}}" stepKey="assertTotal"/> </actionGroup> </actionGroups> From 8a19ae682fc70aea44d0f7e0cf21a83e98410fa7 Mon Sep 17 00:00:00 2001 From: Viktor Sevch <viktor.sevch@transoftgroup.com> Date: Fri, 25 Sep 2020 12:27:48 +0300 Subject: [PATCH 0606/1013] MC-23536: CatalogProductListWidgetOrderTest is flaky and fails randomly --- .../Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml index c71e8098c5c7f..db09cc96cb791 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml @@ -19,6 +19,7 @@ <useCaseId value="MC-5905"/> <group value="catalogWidget"/> <group value="catalog"/> + <group value="WYSIWYGDisabled"/> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="simplecategory"/> From 6fde4babdb7be4b72a8c1401232582d24efce545 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Fri, 25 Sep 2020 15:50:41 +0300 Subject: [PATCH 0607/1013] revert CategoryRepository, added plugin --- .../Catalog/Model/CategoryRepository.php | 28 ++------- .../Plugin/Model/UpdateCategoryDataList.php | 63 +++++++++++++++++++ .../CatalogUrlRewrite/etc/webapi_rest/di.xml | 3 + 3 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 app/code/Magento/CatalogUrlRewrite/Plugin/Model/UpdateCategoryDataList.php diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index fe4637a6fb9c8..0ce52b966c32c 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -7,10 +7,10 @@ namespace Magento\Catalog\Model; -use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; +use Magento\Catalog\Api\Data\CategoryInterface; /** * Repository for categories. @@ -77,7 +77,10 @@ public function __construct( public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) { $storeId = (int)$this->storeManager->getStore()->getId(); - $existingData = $this->getExistingData($category, $storeId); + $existingData = $this->getExtensibleDataObjectConverter() + ->toNestedArray($category, [], \Magento\Catalog\Api\Data\CategoryInterface::class); + $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); + $existingData['store_id'] = $storeId; if ($category->getId()) { $metadata = $this->getMetadataPool()->getMetadata( @@ -236,25 +239,4 @@ private function getMetadataPool() } return $this->metadataPool; } - - /** - * Get existing data category - * - * @param CategoryInterface $category - * @param int $storeId - * @return array - */ - private function getExistingData(CategoryInterface $category, int $storeId) - { - $existingData = $this->getExtensibleDataObjectConverter() - ->toNestedArray($category, [], \Magento\Catalog\Api\Data\CategoryInterface::class); - $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); - $existingData['store_id'] = $storeId; - - if ($category->getData('save_rewrites_history') !== null) { - $existingData['save_rewrites_history'] = $category->getData('save_rewrites_history'); - } - - return $existingData; - } } diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Model/UpdateCategoryDataList.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/UpdateCategoryDataList.php new file mode 100644 index 0000000000000..eb9c28c62bd8b --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/UpdateCategoryDataList.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Plugin\Model; + +use Magento\Catalog\Model\Category; +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; + +class UpdateCategoryDataList +{ + private const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; + + /** + * @var RestRequest + */ + private $request; + + /** + * @param RestRequest $request + */ + public function __construct(RestRequest $request) + { + $this->request = $request; + } + + /** + * Add 'save_rewrites_history' param to the category for list + * + * @param CategoryUrlRewriteGenerator $subject + * @param Category $category + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeGenerate(CategoryUrlRewriteGenerator $subject, Category $category) + { + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isCustomAttributesExists($requestBodyParams, CategoryUrlRewriteGenerator::ENTITY_TYPE)) { + foreach ($requestBodyParams[CategoryUrlRewriteGenerator::ENTITY_TYPE]['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { + $category->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); + } + } + } + } + + /** + * Check is any custom options exists in data + * + * @param array $requestBodyParams + * @param string $entityCode + * @return bool + */ + private function isCustomAttributesExists(array $requestBodyParams, string $entityCode): bool + { + return !empty($requestBodyParams[$entityCode]['custom_attributes']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml index 37bbee597e809..be3901cb57a2f 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml @@ -10,4 +10,7 @@ <plugin name="product_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\ProductInputParamsResolver" disabled="false" /> <plugin name="category_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Webapi\Controller\Rest\CategoryInputParamsResolver" disabled="false" /> </type> + <type name="Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator"> + <plugin name="category_set_save_rewrites_history_rest_plugin" type="Magento\CatalogUrlRewrite\Plugin\Model\UpdateCategoryDataList" disabled="false" /> + </type> </config> From 73ae44ad8d25a1002f1ec86af04af29b18987961 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 25 Sep 2020 16:45:08 +0300 Subject: [PATCH 0608/1013] MC-37761: Issues when using multiple address checkout --- .../Test/Unit/Model/Plugin/OrderSaveTest.php | 262 +++++++++++++++++- 1 file changed, 257 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php index 2925ebef958b6..edc9e40ad83fd 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php @@ -181,7 +181,8 @@ public function testAfterSave( $appliedTaxes, $itemAppliedTaxes, $expectedTaxes, - $expectedItemTaxes + $expectedItemTaxes, + $itemId ) { $orderMock = $this->setupOrderMock(); @@ -200,19 +201,19 @@ public function testAfterSave( ->disableOriginalConstructor() ->setMethods(['getId']) ->getMock(); - $orderItemMock->expects($this->atLeastOnce()) - ->method('getId') - ->willReturn(self::ORDER_ITEM_ID); + $orderItemMock->method('getId') + ->willReturn($itemId); $orderMock->expects($this->once()) ->method('getAppliedTaxIsSaved') ->willReturn(false); $orderMock->expects($this->once()) ->method('getExtensionAttributes') ->willReturn($extensionAttributeMock); + $itemByQuoteId = $itemId ? $orderItemMock : $itemId; $orderMock->expects($this->atLeastOnce()) ->method('getItemByQuoteItemId') ->with(self::ITEMID) - ->willReturn($orderItemMock); + ->willReturn($itemByQuoteId); $orderMock->expects($this->atLeastOnce()) ->method('getEntityId') ->willReturn(self::ORDERID); @@ -485,6 +486,257 @@ public function afterSaveDataProvider() 'taxable_item_type' => 'shipping', ], ], + 'item_id' => self::ORDER_ITEM_ID, + ], + 'associated_item_with_empty_order_quote_item' => [ + 'applied_taxes' => [ + [ + 'amount' => 0.66, + 'base_amount' => 0.66, + 'percent' => 11, + 'id' => 'ILUS', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ] + ], + ], + [ + 'amount' => 0.2, + 'base_amount' => 0.2, + 'percent' => 3.33, + 'id' => 'CityTax', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ] + ], + ], + ], + 'item_applied_taxes' => [ + //item tax, three tax rates + [ + //first two taxes are combined + 'item_id' => null, + 'type' => 'product', + 'associated_item_id' => self::ITEMID, + 'applied_taxes' => [ + [ + 'amount' => 0.11, + 'base_amount' => 0.11, + 'percent' => 11, + 'id' => 'ILUS', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ] + ], + ], + //city tax + [ + 'amount' => 0.03, + 'base_amount' => 0.03, + 'percent' => 3.33, + 'id' => 'CityTax', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ] + ], + ], + ], + ], + //shipping tax + [ + //first two taxes are combined + 'item_id' => null, + 'type' => 'shipping', + 'associated_item_id' => null, + 'applied_taxes' => [ + [ + 'amount' => 0.55, + 'base_amount' => 0.55, + 'percent' => 11, + 'id' => 'ILUS', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 6, + 'code' => 'IL', + 'title' => 'IL', + ], + [ + 'percent' => 5, + 'code' => 'US', + 'title' => 'US', + ], + ] + ], + ], + //city tax + [ + 'amount' => 0.17, + 'base_amount' => 0.17, + 'percent' => 3.33, + 'id' => 'CityTax', + 'extension_attributes' => [ + 'rates' => [ + [ + 'percent' => 3, + 'code' => 'CityTax', + 'title' => 'CityTax', + ], + ] + ], + ], + ], + ], + ], + 'expected_order_taxes' => [ + //state tax + '35' => [ + 'order_id' => self::ORDERID, + 'code' => 'IL', + 'title' => 'IL', + 'hidden' => 0, + 'percent' => 6, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.66, + 'base_amount' => 0.66, + 'process' => 0, + 'base_real_amount' => 0.36, + ], + //federal tax + '36' => [ + 'order_id' => self::ORDERID, + 'code' => 'US', + 'title' => 'US', + 'hidden' => 0, + 'percent' => 5, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.66, //combined amount + 'base_amount' => 0.66, + 'process' => 0, + 'base_real_amount' => 0.3, //portion for specific rate + ], + //city tax + '37' => [ + 'order_id' => self::ORDERID, + 'code' => 'CityTax', + 'title' => 'CityTax', + 'hidden' => 0, + 'percent' => 3, + 'priority' => 0, + 'position' => 0, + 'amount' => 0.2, //combined amount + 'base_amount' => 0.2, + 'process' => 0, + 'base_real_amount' => 0.18018018018018, //this number is meaningless since this is single rate + ], + ], + 'expected_item_taxes' => [ + [ + //state tax for item + 'item_id' => null, + 'tax_id' => '35', + 'tax_percent' => 6, + 'associated_item_id' => null, + 'amount' => 0.11, + 'base_amount' => 0.11, + 'real_amount' => 0.06, + 'real_base_amount' => 0.06, + 'taxable_item_type' => 'product', + ], + [ + //state tax for shipping + 'item_id' => null, + 'tax_id' => '35', + 'tax_percent' => 6, + 'associated_item_id' => null, + 'amount' => 0.55, + 'base_amount' => 0.55, + 'real_amount' => 0.3, + 'real_base_amount' => 0.3, + 'taxable_item_type' => 'shipping', + ], + [ + //federal tax for item + 'item_id' => null, + 'tax_id' => '36', + 'tax_percent' => 5, + 'associated_item_id' => null, + 'amount' => 0.11, + 'base_amount' => 0.11, + 'real_amount' => 0.05, + 'real_base_amount' => 0.05, + 'taxable_item_type' => 'product', + ], + [ + //federal tax for shipping + 'item_id' => null, + 'tax_id' => '36', + 'tax_percent' => 5, + 'associated_item_id' => null, + 'amount' => 0.55, + 'base_amount' => 0.55, + 'real_amount' => 0.25, + 'real_base_amount' => 0.25, + 'taxable_item_type' => 'shipping', + ], + [ + //city tax for item + 'item_id' => null, + 'tax_id' => '37', + 'tax_percent' => 3.33, + 'associated_item_id' => null, + 'amount' => 0.03, + 'base_amount' => 0.03, + 'real_amount' => 0.03, + 'real_base_amount' => 0.03, + 'taxable_item_type' => 'product', + ], + [ + //city tax for shipping + 'item_id' => null, + 'tax_id' => '37', + 'tax_percent' => 3.33, + 'associated_item_id' => null, + 'amount' => 0.17, + 'base_amount' => 0.17, + 'real_amount' => 0.17, + 'real_base_amount' => 0.17, + 'taxable_item_type' => 'shipping', + ], + ], + 'item_id' => null, ], ]; } From 138cd4954a0539612e5f85de6afd5163981aaced Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 25 Sep 2020 16:57:13 +0300 Subject: [PATCH 0609/1013] MC-37761: Issues when using multiple address checkout --- .../Test/Unit/Model/Plugin/OrderSaveTest.php | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php index edc9e40ad83fd..f1772ba97edc9 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php @@ -175,15 +175,23 @@ public function verifyItemTaxes($expectedItemTaxes) } /** + * Test for order afterSave + * * @dataProvider afterSaveDataProvider + * @param array $appliedTaxes + * @param array $itemAppliedTaxes + * @param array $expectedTaxes + * @param array $expectedItemTaxes + * @param int|null $itemId + * @return void */ public function testAfterSave( - $appliedTaxes, - $itemAppliedTaxes, - $expectedTaxes, - $expectedItemTaxes, - $itemId - ) { + array $appliedTaxes, + array $itemAppliedTaxes, + array $expectedTaxes, + array $expectedItemTaxes, + ?int $itemId + ): void { $orderMock = $this->setupOrderMock(); $extensionAttributeMock = $this->setupExtensionAttributeMock(); @@ -229,10 +237,12 @@ public function testAfterSave( } /** + * After save data provider + * * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function afterSaveDataProvider() + public function afterSaveDataProvider(): array { return [ //one item with shipping From 77674414a47e1a49b0c0d560a07ed48b22370261 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 25 Sep 2020 17:08:26 +0300 Subject: [PATCH 0610/1013] MC-37761: Issues when using multiple address checkout --- .../Test/Unit/Model/Plugin/OrderSaveTest.php | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php index f1772ba97edc9..d98bd4a0722ee 100644 --- a/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php +++ b/app/code/Magento/Tax/Test/Unit/Model/Plugin/OrderSaveTest.php @@ -195,14 +195,11 @@ public function testAfterSave( $orderMock = $this->setupOrderMock(); $extensionAttributeMock = $this->setupExtensionAttributeMock(); - $extensionAttributeMock->expects($this->any()) - ->method('getConvertingFromQuote') + $extensionAttributeMock->method('getConvertingFromQuote') ->willReturn(true); - $extensionAttributeMock->expects($this->any()) - ->method('getAppliedTaxes') + $extensionAttributeMock->method('getAppliedTaxes') ->willReturn($appliedTaxes); - $extensionAttributeMock->expects($this->any()) - ->method('getItemAppliedTaxes') + $extensionAttributeMock->method('getItemAppliedTaxes') ->willReturn($itemAppliedTaxes); $orderItemMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Item::class) @@ -211,23 +208,18 @@ public function testAfterSave( ->getMock(); $orderItemMock->method('getId') ->willReturn($itemId); - $orderMock->expects($this->once()) - ->method('getAppliedTaxIsSaved') + $orderMock->method('getAppliedTaxIsSaved') ->willReturn(false); - $orderMock->expects($this->once()) - ->method('getExtensionAttributes') + $orderMock->method('getExtensionAttributes') ->willReturn($extensionAttributeMock); $itemByQuoteId = $itemId ? $orderItemMock : $itemId; - $orderMock->expects($this->atLeastOnce()) - ->method('getItemByQuoteItemId') + $orderMock->method('getItemByQuoteItemId') ->with(self::ITEMID) ->willReturn($itemByQuoteId); - $orderMock->expects($this->atLeastOnce()) - ->method('getEntityId') + $orderMock->method('getEntityId') ->willReturn(self::ORDERID); - $orderMock->expects($this->once()) - ->method('setAppliedTaxIsSaved') + $orderMock->method('setAppliedTaxIsSaved') ->with(true); $this->verifyOrderTaxes($expectedTaxes); From d69bd9d9a916f9c3a13793fd1a01bbe77257c7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Corr=C3=AAa=20Gomes?= <rafaelstz@users.noreply.github.com> Date: Fri, 25 Sep 2020 11:41:16 -0400 Subject: [PATCH 0611/1013] New link to Magento BI Co-authored-by: Ihor Sviziev <ihor-sviziev@users.noreply.github.com> --- app/code/Magento/Analytics/etc/config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Analytics/etc/config.xml b/app/code/Magento/Analytics/etc/config.xml index 5229e2a1abc09..fed3dd0155c87 100644 --- a/app/code/Magento/Analytics/etc/config.xml +++ b/app/code/Magento/Analytics/etc/config.xml @@ -15,7 +15,7 @@ <otp>https://advancedreporting.rjmetrics.com/otp</otp> <report>https://advancedreporting.rjmetrics.com/report</report> <notify_data_changed>https://advancedreporting.rjmetrics.com/report</notify_data_changed> - <documentation>https://docs.magento.com/m2/ce/user_guide/reports/advanced-reporting.html</documentation> + <documentation>https://docs.magento.com/user-guide/reports/advanced-reporting.html</documentation> </url> <integration_name>Magento Analytics user</integration_name> <general> From 047629aeb29354c35c7ebe4f12b7eed160e241e0 Mon Sep 17 00:00:00 2001 From: ogorkun <ogorkun@adobe.com> Date: Fri, 25 Sep 2020 12:05:32 -0500 Subject: [PATCH 0612/1013] MC-37799: Improve dynamic CSP whitelisting for cached blocks --- app/code/Magento/Csp/Model/BlockCache.php | 2 +- .../Csp/Model/Collector/CompositeMerger.php | 57 +++++++++++++++++++ .../Csp/Model/Collector/DynamicCollector.php | 25 +++++++- app/code/Magento/Csp/etc/di.xml | 17 ++++-- 4 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/Csp/Model/Collector/CompositeMerger.php diff --git a/app/code/Magento/Csp/Model/BlockCache.php b/app/code/Magento/Csp/Model/BlockCache.php index f0469c3251379..fac0beec51c07 100644 --- a/app/code/Magento/Csp/Model/BlockCache.php +++ b/app/code/Magento/Csp/Model/BlockCache.php @@ -111,7 +111,7 @@ public function save($data, $identifier, $tags = [], $lifeTime = null) ]; } } - $data = $this->serializer->serialize(['policies' => $policiesData, 'html' => $data]); + $data = $this->serializer->serialize(['policies' => $policiesData, 'html' => (string)$data]); } return $this->cache->save($data, $identifier, $tags, $lifeTime); diff --git a/app/code/Magento/Csp/Model/Collector/CompositeMerger.php b/app/code/Magento/Csp/Model/Collector/CompositeMerger.php new file mode 100644 index 0000000000000..16430f1ff8aa9 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CompositeMerger.php @@ -0,0 +1,57 @@ +<?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; + +/** + * Merges policies using different mergers. + */ +class CompositeMerger implements MergerInterface +{ + /** + * @var MergerInterface[] + */ + private $mergers; + + /** + * @param MergerInterface[] $mergers + */ + public function __construct(array $mergers) + { + $this->mergers = $mergers; + } + + /** + * @inheritDoc + */ + public function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + { + foreach ($this->mergers as $merger) { + if ($merger->canMerge($policy1, $policy2)) { + return $merger->merge($policy1, $policy2); + } + } + + throw new \RuntimeException('Cannot merge 2 policies of ' .get_class($policy1)); + } + + /** + * @inheritDoc + */ + public function canMerge(PolicyInterface $policy1, PolicyInterface $policy2): bool + { + foreach ($this->mergers as $merger) { + if ($merger->canMerge($policy1, $policy2)) { + return true; + } + } + + return false; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/DynamicCollector.php b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php index 6478e9622f910..743f77c93f3d8 100644 --- a/app/code/Magento/Csp/Model/Collector/DynamicCollector.php +++ b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php @@ -20,6 +20,19 @@ class DynamicCollector implements PolicyCollectorInterface */ private $added = []; + /** + * @var MergerInterface + */ + private $merger; + + /** + * @param MergerInterface $merger + */ + public function __construct(MergerInterface $merger) + { + $this->merger = $merger; + } + /** * Add a policy for current page. * @@ -28,7 +41,15 @@ class DynamicCollector implements PolicyCollectorInterface */ public function add(PolicyInterface $policy): void { - $this->added[] = $policy; + if (array_key_exists($policy->getId(), $this->added)) { + if ($this->merger->canMerge($this->added[$policy->getId()], $policy)) { + $this->added[$policy->getId()] = $this->merger->merge($this->added[$policy->getId()], $policy); + } else { + throw new \RuntimeException('Cannot merge a policy of ' .get_class($policy)); + } + } else { + $this->added[$policy->getId()] = $policy; + } } /** @@ -36,6 +57,6 @@ public function add(PolicyInterface $policy): void */ public function collect(array $defaultPolicies = []): array { - return array_merge($defaultPolicies, $this->added); + return array_merge($defaultPolicies, array_values($this->added)); } } diff --git a/app/code/Magento/Csp/etc/di.xml b/app/code/Magento/Csp/etc/di.xml index 7b1129a0e1a41..238392fe1c8d1 100644 --- a/app/code/Magento/Csp/etc/di.xml +++ b/app/code/Magento/Csp/etc/di.xml @@ -15,6 +15,17 @@ </arguments> </type> <preference for="Magento\Csp\Api\PolicyCollectorInterface" type="Magento\Csp\Model\CompositePolicyCollector" /> + <preference for="Magento\Csp\Model\Collector\MergerInterface" type="Magento\Csp\Model\Collector\CompositeMerger" /> + <type name="Magento\Csp\Model\Collector\CompositeMerger"> + <arguments> + <argument name="mergers" xsi:type="array"> + <item name="fetch" xsi:type="object">Magento\Csp\Model\Collector\FetchPolicyMerger</item> + <item name="flag" xsi:type="object">Magento\Csp\Model\Collector\FlagPolicyMerger</item> + <item name="plugins" xsi:type="object">Magento\Csp\Model\Collector\PluginTypesPolicyMerger</item> + <item name="sandbox" xsi:type="object">Magento\Csp\Model\Collector\SandboxPolicyMerger</item> + </argument> + </arguments> + </type> <type name="Magento\Csp\Model\CompositePolicyCollector"> <arguments> <argument name="collectors" xsi:type="array"> @@ -24,10 +35,7 @@ <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> - <item name="flag" xsi:type="object">Magento\Csp\Model\Collector\FlagPolicyMerger</item> - <item name="plugins" xsi:type="object">Magento\Csp\Model\Collector\PluginTypesPolicyMerger</item> - <item name="sandbox" xsi:type="object">Magento\Csp\Model\Collector\SandboxPolicyMerger</item> + <item name="composite" xsi:type="object">Magento\Csp\Model\Collector\MergerInterface</item> </argument> </arguments> </type> @@ -93,6 +101,7 @@ <type name="Magento\Csp\Model\BlockCache"> <arguments> <argument name="cache" xsi:type="object">configured_block_cache</argument> + <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Serialize</argument> </arguments> </type> <type name="Magento\Framework\View\Element\Context"> From 159232dd22be74bc408af5ff2b99f01dd5955acf Mon Sep 17 00:00:00 2001 From: Viktor Tymchynskyi <vtymchynskyi@magento.com> Date: Fri, 25 Sep 2020 13:09:56 -0500 Subject: [PATCH 0613/1013] MC-37880: Shipping address information disappears from order screen while placing order --- .../Magento/Sales/view/adminhtml/web/order/create/scripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 10f9ee38720ff..ec94025a0cb82 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 @@ -1202,7 +1202,7 @@ define([ for (var i = 0; i < this.loadingAreas.length; i++) { var id = this.loadingAreas[i]; if ($(this.getAreaId(id))) { - if (id in response) { + if ((id in response) && id !== 'message' || response[id]) { $(this.getAreaId(id)).update(response[id]); } if ($(this.getAreaId(id)).callback) { From eac6ba25636b525d909f5af43648fb101fab3d13 Mon Sep 17 00:00:00 2001 From: Stanislav Ilnytskyi <stailx1@gmail.com> Date: Sat, 26 Sep 2020 09:42:19 +0200 Subject: [PATCH 0614/1013] remove spaces caused patching problem in Product/View It was impossible to apply patch to this file because whitespaces --- app/code/Magento/Catalog/Helper/Product/View.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Helper/Product/View.php b/app/code/Magento/Catalog/Helper/Product/View.php index cf5b15cadc997..95698d382f09e 100644 --- a/app/code/Magento/Catalog/Helper/Product/View.php +++ b/app/code/Magento/Catalog/Helper/Product/View.php @@ -193,7 +193,7 @@ public function initProductLayout(ResultPage $resultPage, $product, $params = nu $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku], $handle); } } - + $resultPage->addPageLayoutHandles(['type' => $product->getTypeId()], null, false); $resultPage->addPageLayoutHandles(['id' => $product->getId(), 'sku' => $urlSafeSku]); From a70b2075b3aba9b1d8afc18efa3095e624df6bec Mon Sep 17 00:00:00 2001 From: Marjan <petkovski.marjan@gmail.com> Date: Sat, 26 Sep 2020 12:19:34 +0200 Subject: [PATCH 0615/1013] magento/magento2#29927: Search should be disabled from products query when general configuration chooses to disabled it Static tests --- .../Model/Resolver/Products.php | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index 7032262629ffa..ba158fab0120c 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -8,14 +8,11 @@ namespace Magento\CatalogGraphQl\Model\Resolver; use Magento\CatalogGraphQl\Model\Resolver\Products\Query\ProductQueryInterface; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Filter; -use Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search; 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\Argument\SearchCriteria\Builder; -use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\SearchFilter; use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Model\Layer\Resolver; use Magento\CatalogGraphQl\DataProvider\Product\SearchCriteriaBuilder; @@ -57,20 +54,7 @@ public function resolve( array $value = null, array $args = null ) { - if (isset($args['searchAllowed']) && $args['searchAllowed'] === false) { - throw new GraphQlInputException(__('Product search has been disabled.')); - } - 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.')); - } - if (!isset($args['search']) && !isset($args['filter'])) { - throw new GraphQlInputException( - __("'search' or 'filter' input argument is required.") - ); - } + $this->validateInput($args); $searchResult = $this->searchQuery->getResult($args, $info, $context); @@ -97,4 +81,29 @@ public function resolve( return $data; } + + /** + * Validate input arguments + * + * @param array $args + * @throws GraphQlAuthorizationException + * @throws GraphQlInputException + */ + private function validateInput(array $args) + { + if (isset($args['searchAllowed']) && $args['searchAllowed'] === false) { + throw new GraphQlAuthorizationException(__('Product search has been disabled.')); + } + 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.')); + } + if (!isset($args['search']) && !isset($args['filter'])) { + throw new GraphQlInputException( + __("'search' or 'filter' input argument is required.") + ); + } + } } From 7af0e4dd91b68a6db9d7fa78435fe49c111d82ec Mon Sep 17 00:00:00 2001 From: Kate Kyzyma <kate@atwix.com> Date: Mon, 28 Sep 2020 11:56:38 +0300 Subject: [PATCH 0616/1013] - creating new action groups to open first record in the grid --- .../AdminOpenFirstRowInStoresGridActionGroup.xml | 16 ++++++++++++++++ ...penStoreInFirstRowInStoresGridActionGroup.xml | 16 ++++++++++++++++ ...roupAcceptAlertAndVerifyStoreViewFormTest.xml | 4 ++-- ...pdateStoreGroupAndVerifyStoreViewFormTest.xml | 4 ++-- .../Test/Mftf/Test/AdminUpdateStoreViewTest.xml | 4 ++-- 5 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenFirstRowInStoresGridActionGroup.xml create mode 100644 app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenStoreInFirstRowInStoresGridActionGroup.xml diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenFirstRowInStoresGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenFirstRowInStoresGridActionGroup.xml new file mode 100644 index 0000000000000..a3be7b0d8a8c4 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenFirstRowInStoresGridActionGroup.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="AdminOpenFirstRowInStoresGridActionGroup"> + + <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> + <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenStoreInFirstRowInStoresGridActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenStoreInFirstRowInStoresGridActionGroup.xml new file mode 100644 index 0000000000000..6af4a4f159a7e --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminOpenStoreInFirstRowInStoresGridActionGroup.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="AdminOpenStoreInFirstRowInStoresGridActionGroup"> + + <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> + <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml index 09a33d5eb86a6..40a912617ee0b 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAcceptAlertAndVerifyStoreViewFormTest.xml @@ -51,8 +51,8 @@ <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> <argument name="storeGroupName" value="{{staticStoreGroup.name}}"/> </actionGroup> - <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> - <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <actionGroup ref="AdminOpenFirstRowInStoresGridActionGroup" stepKey="clickFirstRow"/> + <!--Update created Store group as per requirement and accept alert message--> <actionGroup ref="EditCustomStoreGroupAcceptWarningMessageActionGroup" stepKey="updateCustomStoreGroup"> <argument name="website" value="{{customWebsite.name}}"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml index 1c5d58c13538e..02125aab26496 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreGroupAndVerifyStoreViewFormTest.xml @@ -41,8 +41,8 @@ <actionGroup ref="AssertStoreGroupInGridActionGroup" stepKey="openCreatedStoreGroupInGrid"> <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> </actionGroup> - <click selector="{{AdminStoresGridSection.firstRow}}" stepKey="clickFirstRow"/> - <waitForPageLoad stepKey="AdminSystemStoreGroupPageToOpen"/> + <actionGroup ref="AdminOpenFirstRowInStoresGridActionGroup" stepKey="clickFirstRow"/> + <!--Update created Store group as per requirement--> <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createNewCustomStoreGroup"> <argument name="website" value="{{_defaultWebsite.name}}"/> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml index c7c846c51af4d..b4aac676f2bc9 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminUpdateStoreViewTest.xml @@ -39,8 +39,8 @@ <actionGroup ref="AssertStoreViewInGridActionGroup" stepKey="searchCreatedStoreViewInGrid"> <argument name="storeViewName" value="{{storeViewData.name}}"/> </actionGroup> - <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickStoreViewFirstRowInGrid"/> - <waitForPageLoad stepKey="waitForAdminSystemStoreViewPageLoad"/> + <actionGroup ref="AdminOpenStoreInFirstRowInStoresGridActionGroup" stepKey="clickStoreViewFirstRowInGrid"/> + <!--Update created store view as per requirements--> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="updateStoreView"> <argument name="StoreGroup" value="_defaultStoreGroup"/> From 1140fcc6836469349379b05478b275081329c03e Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Mon, 28 Sep 2020 12:19:12 +0300 Subject: [PATCH 0617/1013] MC-36405: Reorder is not working with custom options date with JavaScript Calendar enabled --- .../Model/Product/Option/Type/Date.php | 2 ++ .../Catalog/Model/ProductOptionProcessor.php | 13 +---------- .../Magento/Sales/Model/AdminOrder/Create.php | 22 ++++++++++++++++++- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 1396167154b09..8001d692c011b 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -72,6 +72,8 @@ public function validateUserValue($values) $dateValid = true; if ($this->_dateExists()) { if ($this->useCalendar()) { + /* Fixed validation if the date was not saved correctly after re-saved the order + for example: "09\/24\/2020,2020-09-24 00:00:00" */ if (is_string($value) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4},+(\w|\W)*$/', $value)) { $value = [ 'date' => preg_replace('/,([^,]+),?$/', '', $value), diff --git a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php index dc5a4778c992e..443d740fa7970 100644 --- a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php +++ b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php @@ -32,27 +32,16 @@ class ProductOptionProcessor implements ProductOptionProcessorInterface */ private $urlBuilder; - /** - * Serializer interface instance. - * - * @var Json - */ - private $serializer; - /** * @param DataObjectFactory $objectFactory * @param CustomOptionFactory $customOptionFactory - * @param Json|null $serializer */ public function __construct( DataObjectFactory $objectFactory, - CustomOptionFactory $customOptionFactory, - Json $serializer = null + CustomOptionFactory $customOptionFactory ) { $this->objectFactory = $objectFactory; $this->customOptionFactory = $customOptionFactory; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); } /** diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 8ef12e5889520..5f21a29b10e42 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -642,6 +642,7 @@ protected function _initShippingAddressFromOrder(\Magento\Sales\Model\Order $ord * @param \Magento\Sales\Model\Order\Item $orderItem * @param int $qty * @return \Magento\Quote\Model\Quote\Item|string|$this + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $qty = null) { @@ -667,9 +668,15 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q if ($productOptions !== null && !empty($productOptions['options'])) { $formattedOptions = []; foreach ($productOptions['options'] as $option) { + if (in_array($option['option_type'], ['date', 'date_time']) && $this->useFrontendCalendar()) { + $product->setSkipCheckRequiredOption(false); + break; + } $formattedOptions[$option['option_id']] = $option['option_value']; } - $buyRequest->setData('options', $formattedOptions); + if (!empty($formattedOptions)) { + $buyRequest->setData('options', $formattedOptions); + } } $item = $this->getQuote()->addProduct($product, $buyRequest); if (is_string($item)) { @@ -2113,4 +2120,17 @@ private function isAddressesAreEqual(Order $order) return $shippingData == $billingData; } + + /** + * Use Calendar on frontend or not + * + * @return bool + */ + private function useFrontendCalendar(): bool + { + return (bool)$this->_scopeConfig->getValue( + 'catalog/custom_options/use_calendar', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + } } From 470eaff6c2064d6db1cd99e0f72a88bdcff228f1 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Mon, 28 Sep 2020 14:15:05 +0300 Subject: [PATCH 0618/1013] MC-36219: Incorrect order created date in shipment grid table. --- app/code/Magento/Sales/etc/db_schema.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index de062029fb53b..ab524a0f552f6 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -769,7 +769,7 @@ <column xsi:type="varchar" name="order_increment_id" nullable="false" length="32" comment="Order Increment ID"/> <column xsi:type="int" name="order_id" unsigned="true" nullable="false" identity="false" comment="Order ID"/> - <column xsi:type="timestamp" name="order_created_at" on_update="true" nullable="false" + <column xsi:type="timestamp" name="order_created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Order Increment ID"/> <column xsi:type="varchar" name="customer_name" nullable="false" length="128" comment="Customer Name"/> <column xsi:type="decimal" name="total_qty" scale="4" precision="12" unsigned="false" nullable="true" From 9b2f7ecfb2da4d452281c5ed668ae3cd3e449f0f Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Mon, 28 Sep 2020 15:09:15 +0300 Subject: [PATCH 0619/1013] add MFTF test --- .../AdminSelectFieldToColumnActionGroup.xml | 22 +++++++++++ .../Mftf/Section/AdminOrdersGridSection.xml | 1 + ...minVerifyFieldToFilterOnOrdersGridTest.xml | 38 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectFieldToColumnActionGroup.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectFieldToColumnActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectFieldToColumnActionGroup.xml new file mode 100644 index 0000000000000..361787948a133 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminSelectFieldToColumnActionGroup.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="AdminSelectFieldToColumnActionGroup"> + <annotations> + <description>Select or clear the checkbox to display the column on the Orders grid page.</description> + </annotations> + <arguments> + <argument name="column" type="string" defaultValue="Purchase Point"/> + </arguments> + <click selector="{{AdminOrdersGridSection.columnsDropdown}}" stepKey="openColumnsDropdown" /> + <click selector="{{AdminOrdersGridSection.viewColumnCheckbox(column)}}" stepKey="disableColumn"/> + <click selector="{{AdminOrdersGridSection.columnsDropdown}}" stepKey="closeColumnsDropdown" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml index a18ca0c415567..02878e79f3d70 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/AdminOrdersGridSection.xml @@ -18,6 +18,7 @@ <element name="idFilter" type="input" selector=".admin__data-grid-filters input[name='increment_id']"/> <element name="selectStatus" type="select" selector="select[name='status']" timeout="60"/> <element name="billToNameFilter" type="input" selector=".admin__data-grid-filters input[name='billing_name']"/> + <element name="purchasePoint" type="select" selector=".admin__data-grid-filters select[name='store_id']"/> <element name="enabledFilters" type="block" selector=".admin__data-grid-header .admin__data-grid-filters-current._show"/> <element name="clearFilters" type="button" selector=".admin__data-grid-header [data-action='grid-filter-reset']" timeout="30"/> <element name="applyFilters" type="button" selector="button[data-action='grid-filter-apply']" timeout="30"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.xml new file mode 100644 index 0000000000000..b0c6b3a2fc6ca --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminVerifyFieldToFilterOnOrdersGridTest.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="AdminVerifyFieldToFilterOnOrdersGridTest"> + <annotations> + <features value="Sales"/> + <stories value="Github issue: #28385 Resolve issue with filter visibility with column visibility in grid"/> + <title value="Verify field to filter"/> + <description value="Verify not appear fields to filter on Orders grid if it disables in columns dropdown."/> + <severity value="MAJOR"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin" /> + </before> + + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout" /> + </after> + + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="goToOrders"/> + <actionGroup ref="AdminSelectFieldToColumnActionGroup" stepKey="unSelectPurchasePoint" /> + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openColumnsDropdown" /> + <dontSeeElement selector="{{AdminOrdersGridSection.purchasePoint}}" stepKey="dontSeeElement"/> + + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="closeColumnsDropdown" /> + <actionGroup ref="AdminSelectFieldToColumnActionGroup" stepKey="selectPurchasePoint" /> + <click selector="{{AdminOrdersGridSection.filters}}" stepKey="openColumnsDropdown2" /> + <seeElement selector="{{AdminOrdersGridSection.purchasePoint}}" stepKey="seeElement"/> + </test> +</tests> \ No newline at end of file From 85c48b7d763b2d2c8ac437f126899d8d10c6d63d Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Mon, 28 Sep 2020 15:14:10 +0300 Subject: [PATCH 0620/1013] MC-37496: Create automated test for "Reorder Order from Admin for Offline Payment Methods" --- ...inAssertOrderShippingMethodActionGroup.xml | 22 ++++++ .../Sales/Test/Mftf/Data/AddressData.xml | 2 + ...orderOrderWithOfflinePaymentMethodTest.xml | 74 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml new file mode 100644 index 0000000000000..a084e858d5f46 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.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="AdminAssertOrderShippingMethodActionGroup"> + <annotations> + <description>Assert that shipping method and shipping price is present for the order.</description> + </annotations> + <arguments> + <argument name="shippingMethod" type="string" defaultValue="Flat Rate - Fixed"/> + <argument name="shippingPrice" type="string"/> + </arguments> + <see selector="{{AdminOrderShippingInformationSection.shippingMethod}}" userInput="{{shippingMethod}}" stepKey="seeShippingMethod"/> + <see selector="{{AdminOrderShippingInformationSection.shippingPrice}}" userInput="{{shippingPrice}}" stepKey="seeShippingMethodPrice"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml index 920618a70dfb8..f96028405c4e5 100644 --- a/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Sales/Test/Mftf/Data/AddressData.xml @@ -24,6 +24,7 @@ <data key="postcode">78758</data> <data key="email" unique="prefix">joe.buyer@email.com</data> <data key="telephone">512-345-6789</data> + <data key="country">United States</data> </entity> <entity name="BillingAddressTX" type="billing_address"> <data key="firstname">Joe</data> @@ -41,5 +42,6 @@ <data key="postcode">78758</data> <data key="email" unique="prefix">joe.buyer@email.com</data> <data key="telephone">512-345-6789</data> + <data key="country">United States</data> </entity> </entities> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml new file mode 100644 index 0000000000000..6767ee25b21ed --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReorderOrderWithOfflinePaymentMethodTest"> + <annotations> + <features value="Sales"/> + <stories value="Reorder"/> + <title value="Reorder Order from Admin for Offline Payment Methods"/> + <description value="Create reorder for order with two products and Check Money payment method"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37495"/> + <group value="sales"/> + </annotations> + <before> + <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> + <createData entity="FlatRateShippingMethodDefault" stepKey="setDefaultFlatRateShippingMethod"/> + <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="createSecondSimpleProduct"/> + <createData entity="CustomerCart" stepKey="createCartForCustomer"> + <requiredEntity createDataKey="createCustomer"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addFirstProductToCustomerCart"> + <requiredEntity createDataKey="createCartForCustomer"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <createData entity="CustomerCartItem" stepKey="addSecondProductToCustomerCart"> + <requiredEntity createDataKey="createCartForCustomer"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="createCartForCustomer"/> + </createData> + <updateData createDataKey="createCartForCustomer" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformation"> + <requiredEntity createDataKey="createCartForCustomer"/> + </updateData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> + </after> + <actionGroup ref="AdminOpenOrderByEntityIdActionGroup" stepKey="openOrderById"> + <argument name="entityId" value="$createCartForCustomer.return$"/> + </actionGroup> + <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorderButton"/> + <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitReorder"/> + <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> + <actionGroup ref="AssertOrderAddressInformationActionGroup" stepKey="verifyOrderInformation"> + <argument name="customer" value="$createCustomer$"/> + <argument name="shippingAddress" value="ShippingAddressTX"/> + <argument name="billingAddress" value="BillingAddressTX"/> + </actionGroup> + <see selector="{{AdminOrderDetailsInformationSection.paymentInformation}}" userInput="Check / Money order" stepKey="seePaymentMethod"/> + <actionGroup ref="AdminAssertOrderShippingMethodActionGroup" stepKey="assertShippingOrderInformation"> + <argument name="shippingPrice" value="$10.00"/> + </actionGroup> + <actionGroup ref="SeeProductInItemsOrderedActionGroup" stepKey="seeFirstProductInItemsOrdered"> + <argument name="product" value="$createFirstSimpleProduct$"/> + </actionGroup> + <actionGroup ref="SeeProductInItemsOrderedActionGroup" stepKey="seeSecondProductInItemsOrdered"> + <argument name="product" value="$createSecondSimpleProduct$"/> + </actionGroup> + </test> +</tests> From 6c11aeba2dfb8f96917ee4a834545dbb582dbd03 Mon Sep 17 00:00:00 2001 From: Bas van Poppel <vanpoppel@redkiwi.nl> Date: Mon, 28 Sep 2020 14:49:22 +0200 Subject: [PATCH 0621/1013] no message --- .../ResourceModel/Advanced/Collection.php | 56 +++++++--- .../CatalogSearch/etc/search_request.xml | 5 + .../ProductCollectionPrepareStrategy.php | 18 +++- .../Console/Command/IndexerReindexCommand.php | 5 +- .../Model/CatalogSearch/AdvancedTest.php | 101 ++++++++++++++++++ .../Command/IndexerReindexCommandTest.php | 25 +++-- .../Indexer/_files/wrong_config_data.php | 16 +++ .../_files/wrong_config_data_rollback.php | 17 +++ 8 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php create mode 100644 dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_rollback.php diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 47160bff1d571..6005455a6ef83 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -6,27 +6,30 @@ namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Advanced; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyChecker; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyCheckerInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; -use Magento\Framework\Search\EngineResolverInterface; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\DB\Select; use Magento\Framework\Api\Search\SearchCriteriaBuilder; use Magento\Framework\Api\Search\SearchResultFactory; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Search\EngineResolverInterface; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Api\Search\SearchResultInterface; /** * Advanced search collection @@ -106,6 +109,11 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $defaultFilterStrategyApplyChecker; + /** + * @var Advanced + */ + private $advancedSearchResource; + /** * Collection constructor * @@ -141,6 +149,7 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory * @param EngineResolverInterface|null $engineResolver * @param DefaultFilterStrategyApplyCheckerInterface|null $defaultFilterStrategyApplyChecker + * @param Advanced|null $advancedSearchResource * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -176,7 +185,8 @@ public function __construct( SearchResultApplierFactory $searchResultApplierFactory = null, TotalRecordsResolverFactory $totalRecordsResolverFactory = null, EngineResolverInterface $engineResolver = null, - DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null + DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null, + Advanced $advancedSearchResource = null ) { $this->searchRequestName = $searchRequestName; if ($searchResultFactory === null) { @@ -193,6 +203,8 @@ public function __construct( ->get(EngineResolverInterface::class); $this->defaultFilterStrategyApplyChecker = $defaultFilterStrategyApplyChecker ?: ObjectManager::getInstance() ->get(DefaultFilterStrategyApplyChecker::class); + $this->advancedSearchResource = $advancedSearchResource ?: ObjectManager::getInstance() + ->get(Advanced::class); parent::__construct( $entityFactory, $logger, @@ -258,6 +270,7 @@ public function setOrder($attribute, $dir = Select::SQL_DESC) */ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { + $this->setAttributeFilterData(Category::ENTITY, 'category_ids', $category->getId()); /** * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. @@ -265,7 +278,6 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::addCategoryFilter($category); } else { - $this->addFieldToFilter('category_ids', $category->getId()); $this->_productLimitationPrice(); } @@ -278,14 +290,13 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) */ public function setVisibility($visibility) { + $this->setAttributeFilterData(Product::ENTITY, 'visibility', $visibility); /** * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. */ if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::setVisibility($visibility); - } else { - $this->addFieldToFilter('visibility', $visibility); } return $this; @@ -306,6 +317,25 @@ private function setSearchOrder($field, $direction) $this->searchOrders[$field] = $direction; } + /** + * Prepare attribute data to filter. + * + * @param string $entityType + * @param string $attributeCode + * @param mixed $condition + * @return $this + */ + private function setAttributeFilterData(string $entityType, string $attributeCode, $condition): self + { + /** @var AbstractAttribute $attribute */ + $attribute = $this->_eavConfig->getAttribute($entityType, $attributeCode); + $table = $attribute->getBackend()->getTable(); + $condition = $this->advancedSearchResource->prepareCondition($attribute, $condition); + $this->addFieldsToFilter([$table => [$attributeCode => $condition]]); + + return $this; + } + /** * @inheritdoc */ @@ -377,7 +407,7 @@ public function _loadEntities($printQuery = false, $logQuery = false) $query = $this->getSelect(); $rows = $this->_fetchAll($query); } catch (\Exception $e) { - $this->printLogQuery(false, true, $query); + $this->printLogQuery(false, true, $query ?? null); throw $e; } diff --git a/app/code/Magento/CatalogSearch/etc/search_request.xml b/app/code/Magento/CatalogSearch/etc/search_request.xml index 2111c469986ec..ecda6112ba03e 100644 --- a/app/code/Magento/CatalogSearch/etc/search_request.xml +++ b/app/code/Magento/CatalogSearch/etc/search_request.xml @@ -66,6 +66,7 @@ <queryReference clause="should" ref="sku_query"/> <queryReference clause="should" ref="price_query"/> <queryReference clause="should" ref="category_query"/> + <queryReference clause="must" ref="visibility_query"/> </query> <query name="sku_query" xsi:type="filteredQuery"> <filterReference clause="must" ref="sku_query_filter"/> @@ -76,11 +77,15 @@ <query name="category_query" xsi:type="filteredQuery"> <filterReference clause="must" ref="category_filter"/> </query> + <query name="visibility_query" xsi:type="filteredQuery"> + <filterReference clause="must" ref="visibility_filter"/> + </query> </queries> <filters> <filter xsi:type="wildcardFilter" name="sku_query_filter" field="sku" value="$sku$"/> <filter xsi:type="rangeFilter" name="price_query_filter" field="price" from="$price.from$" to="$price.to$"/> <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_ids$"/> + <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/> </filters> <from>0</from> <size>10000</size> diff --git a/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php index b3f8a56110f8d..d7054e2bb4b11 100644 --- a/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php +++ b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -5,9 +5,11 @@ */ namespace Magento\Elasticsearch\Model\Advanced; -use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyInterface; +use Magento\Framework\App\ObjectManager; /** * Strategy interface for preparing product collection. @@ -19,13 +21,22 @@ class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrate */ private $catalogConfig; + /** + * @var Visibility + */ + private $catalogProductVisibility; + /** * @param Config $catalogConfig + * @param Visibility|null $catalogProductVisibility */ public function __construct( - Config $catalogConfig + Config $catalogConfig, + Visibility $catalogProductVisibility = null ) { $this->catalogConfig = $catalogConfig; + $this->catalogProductVisibility = $catalogProductVisibility + ?? ObjectManager::getInstance()->get(Visibility::class); } /** @@ -36,6 +47,7 @@ public function prepare(Collection $collection) $collection ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) ->addMinimalPrice() - ->addTaxPercents(); + ->addTaxPercents() + ->setVisibility($this->catalogProductVisibility->getVisibleInSearchIds()); } } diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index 775f585519947..e7517ba0c8818 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -74,7 +74,7 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $returnValue = Cli::RETURN_FAILURE; + $returnValue = Cli::RETURN_SUCCESS; foreach ($this->getIndexers($input) as $indexer) { try { $this->validateIndexerStatus($indexer); @@ -97,14 +97,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln( __('has been rebuilt successfully in %time', ['time' => gmdate('H:i:s', $resultTime)]) ); - $returnValue = Cli::RETURN_SUCCESS; } catch (LocalizedException $e) { $output->writeln(__('exception: %message', ['message' => $e->getMessage()])); + $returnValue = Cli::RETURN_FAILURE; } catch (\Exception $e) { $output->writeln('process unknown error:'); $output->writeln($e->getMessage()); $output->writeln($e->getTraceAsString(), OutputInterface::VERBOSITY_DEBUG); + $returnValue = Cli::RETURN_FAILURE; } } diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php new file mode 100644 index 0000000000000..815f480cf7b53 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\CatalogSearch; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogSearch\Model\Advanced; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Check catalog Advanced Search process with Elasticsearch enabled. + */ +class AdvancedTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Registry + */ + private $registry; + + /** + * @var Visibility + */ + private $productVisibility; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->productVisibility = $this->objectManager->get(Visibility::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + } + + /** + * Check that Advanced Search does NOT return products that do NOT have search visibility. + * + * @magentoDbIsolation disabled + * @magentoConfigFixture default/catalog/search/engine elasticsearch7 + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_two_child_products.php + * @return void + */ + public function testAddFilters(): void + { + $this->assertResultsAfterRequest(1); + + /** @var ProductInterface $configurableProductOption */ + $configurableProductOption = $this->productRepository->get('Simple option 1'); + $configurableProductOption->setVisibility(Visibility::VISIBILITY_IN_SEARCH); + $this->productRepository->save($configurableProductOption); + + $this->registry->unregister('advanced_search_conditions'); + $this->assertResultsAfterRequest(2); + } + + /** + * Do Elasticsearch query and assert results. + * + * @param int $count + * @return void + */ + private function assertResultsAfterRequest(int $count): void + { + /** @var Advanced $advancedSearch */ + $advancedSearch = $this->objectManager->create(Advanced::class); + $advancedSearch->addFilters(['name' => 'Configurable']); + + /** @var ProductInterface[] $itemsResult */ + $itemsResult = $advancedSearch->getProductCollection() + ->addAttributeToSelect(ProductInterface::VISIBILITY) + ->getItems(); + + $this->assertCount($count, $itemsResult); + foreach ($itemsResult as $product) { + $this->assertStringContainsString('Configurable', $product->getName()); + $this->assertContains((int)$product->getVisibility(), $this->productVisibility->getVisibleInSearchIds()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php b/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php index f4730b0b32c18..67de7913b4603 100644 --- a/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php @@ -7,9 +7,11 @@ namespace Magento\Indexer\Console\Command; +use Magento\Framework\Console\Cli; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\MockObject\MockObject as Mock; +use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -19,7 +21,7 @@ * @magentoDbIsolation disabled * @magentoAppIsolation enabled */ -class IndexerReindexCommandTest extends \PHPUnit\Framework\TestCase +class IndexerReindexCommandTest extends TestCase { /** * @var ObjectManagerInterface @@ -56,14 +58,23 @@ protected function setUp(): void /** * @magentoDataFixture Magento/Store/_files/second_store_group_with_second_website.php + * @return void */ - public function testReindexAll() + public function testReindexAll(): void { $status = $this->command->run($this->inputMock, $this->outputMock); - $this->assertEquals( - \Magento\Framework\Console\Cli::RETURN_SUCCESS, - $status, - 'Index wasn\'t success' - ); + $this->assertEquals(Cli::RETURN_SUCCESS, $status, 'Index wasn\'t success'); + } + + /** + * Check that 'indexer:reindex' command return right code. + * + * @magentoDataFixture Magento/Indexer/_files/wrong_config_data.php + * @return void + */ + public function testReindexAllWhenSomethingIsWrong(): void + { + $status = $this->command->run($this->inputMock, $this->outputMock); + $this->assertEquals(Cli::RETURN_FAILURE, $status, 'Index didn\'t return failure code'); } } diff --git a/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php b/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php new file mode 100644 index 0000000000000..c6ec6235b61e6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php @@ -0,0 +1,16 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Config\Model\Config\Factory; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Factory $configFactory */ +$configFactory = Bootstrap::getObjectManager()->get(Factory::class); +$config = $configFactory->create(); +$config->setScope('stores'); +$config->setDataByPath('catalog/search/elasticsearch7_server_port', 2309); +$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_rollback.php b/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_rollback.php new file mode 100644 index 0000000000000..7ca0b986fc206 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_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\Framework\App\ResourceConnection; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var ResourceConnection $resource */ +$resource = Bootstrap::getObjectManager()->get(ResourceConnection::class); +$connection = $resource->getConnection(); +$tableName = $resource->getTableName('core_config_data'); + +$connection->query("DELETE FROM $tableName WHERE path = 'catalog/search/elasticsearch7_server_port'" + ." AND scope = 'stores';"); From 2602b9fdbc848a699ebdbde1d34157d159c8a851 Mon Sep 17 00:00:00 2001 From: Bas van Poppel <vanpoppel@redkiwi.nl> Date: Mon, 28 Sep 2020 14:50:01 +0200 Subject: [PATCH 0622/1013] Revert "no message" This reverts commit 6c11aeba2dfb8f96917ee4a834545dbb582dbd03. --- .../ResourceModel/Advanced/Collection.php | 56 +++------- .../CatalogSearch/etc/search_request.xml | 5 - .../ProductCollectionPrepareStrategy.php | 18 +--- .../Console/Command/IndexerReindexCommand.php | 5 +- .../Model/CatalogSearch/AdvancedTest.php | 101 ------------------ .../Command/IndexerReindexCommandTest.php | 25 ++--- .../Indexer/_files/wrong_config_data.php | 16 --- .../_files/wrong_config_data_rollback.php | 17 --- 8 files changed, 25 insertions(+), 218 deletions(-) delete mode 100644 dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php delete mode 100644 dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php delete mode 100644 dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_rollback.php diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 6005455a6ef83..47160bff1d571 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -6,30 +6,27 @@ namespace Magento\CatalogSearch\Model\ResourceModel\Advanced; -use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; -use Magento\CatalogSearch\Model\ResourceModel\Advanced; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyChecker; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\DefaultFilterStrategyApplyCheckerInterface; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverInterface; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierInterface; -use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; +use Magento\Framework\Search\EngineResolverInterface; use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverInterface; -use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\TotalRecordsResolverFactory; use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\DB\Select; use Magento\Framework\Api\Search\SearchCriteriaBuilder; use Magento\Framework\Api\Search\SearchResultFactory; -use Magento\Framework\Api\Search\SearchResultInterface; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\DB\Select; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\Search\EngineResolverInterface; use Magento\Framework\Search\Request\EmptyRequestDataException; use Magento\Framework\Search\Request\NonExistingRequestNameException; +use Magento\Catalog\Model\ResourceModel\Product\Collection\ProductLimitationFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchCriteriaResolverFactory; +use Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection\SearchResultApplierFactory; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Api\Search\SearchResultInterface; /** * Advanced search collection @@ -109,11 +106,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection */ private $defaultFilterStrategyApplyChecker; - /** - * @var Advanced - */ - private $advancedSearchResource; - /** * Collection constructor * @@ -149,7 +141,6 @@ class Collection extends \Magento\Catalog\Model\ResourceModel\Product\Collection * @param TotalRecordsResolverFactory|null $totalRecordsResolverFactory * @param EngineResolverInterface|null $engineResolver * @param DefaultFilterStrategyApplyCheckerInterface|null $defaultFilterStrategyApplyChecker - * @param Advanced|null $advancedSearchResource * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -185,8 +176,7 @@ public function __construct( SearchResultApplierFactory $searchResultApplierFactory = null, TotalRecordsResolverFactory $totalRecordsResolverFactory = null, EngineResolverInterface $engineResolver = null, - DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null, - Advanced $advancedSearchResource = null + DefaultFilterStrategyApplyCheckerInterface $defaultFilterStrategyApplyChecker = null ) { $this->searchRequestName = $searchRequestName; if ($searchResultFactory === null) { @@ -203,8 +193,6 @@ public function __construct( ->get(EngineResolverInterface::class); $this->defaultFilterStrategyApplyChecker = $defaultFilterStrategyApplyChecker ?: ObjectManager::getInstance() ->get(DefaultFilterStrategyApplyChecker::class); - $this->advancedSearchResource = $advancedSearchResource ?: ObjectManager::getInstance() - ->get(Advanced::class); parent::__construct( $entityFactory, $logger, @@ -270,7 +258,6 @@ public function setOrder($attribute, $dir = Select::SQL_DESC) */ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { - $this->setAttributeFilterData(Category::ENTITY, 'category_ids', $category->getId()); /** * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. @@ -278,6 +265,7 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::addCategoryFilter($category); } else { + $this->addFieldToFilter('category_ids', $category->getId()); $this->_productLimitationPrice(); } @@ -290,13 +278,14 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) */ public function setVisibility($visibility) { - $this->setAttributeFilterData(Product::ENTITY, 'visibility', $visibility); /** * This changes need in backward compatible reasons for support dynamic improved algorithm * for price aggregation process. */ if ($this->defaultFilterStrategyApplyChecker->isApplicable()) { parent::setVisibility($visibility); + } else { + $this->addFieldToFilter('visibility', $visibility); } return $this; @@ -317,25 +306,6 @@ private function setSearchOrder($field, $direction) $this->searchOrders[$field] = $direction; } - /** - * Prepare attribute data to filter. - * - * @param string $entityType - * @param string $attributeCode - * @param mixed $condition - * @return $this - */ - private function setAttributeFilterData(string $entityType, string $attributeCode, $condition): self - { - /** @var AbstractAttribute $attribute */ - $attribute = $this->_eavConfig->getAttribute($entityType, $attributeCode); - $table = $attribute->getBackend()->getTable(); - $condition = $this->advancedSearchResource->prepareCondition($attribute, $condition); - $this->addFieldsToFilter([$table => [$attributeCode => $condition]]); - - return $this; - } - /** * @inheritdoc */ @@ -407,7 +377,7 @@ public function _loadEntities($printQuery = false, $logQuery = false) $query = $this->getSelect(); $rows = $this->_fetchAll($query); } catch (\Exception $e) { - $this->printLogQuery(false, true, $query ?? null); + $this->printLogQuery(false, true, $query); throw $e; } diff --git a/app/code/Magento/CatalogSearch/etc/search_request.xml b/app/code/Magento/CatalogSearch/etc/search_request.xml index ecda6112ba03e..2111c469986ec 100644 --- a/app/code/Magento/CatalogSearch/etc/search_request.xml +++ b/app/code/Magento/CatalogSearch/etc/search_request.xml @@ -66,7 +66,6 @@ <queryReference clause="should" ref="sku_query"/> <queryReference clause="should" ref="price_query"/> <queryReference clause="should" ref="category_query"/> - <queryReference clause="must" ref="visibility_query"/> </query> <query name="sku_query" xsi:type="filteredQuery"> <filterReference clause="must" ref="sku_query_filter"/> @@ -77,15 +76,11 @@ <query name="category_query" xsi:type="filteredQuery"> <filterReference clause="must" ref="category_filter"/> </query> - <query name="visibility_query" xsi:type="filteredQuery"> - <filterReference clause="must" ref="visibility_filter"/> - </query> </queries> <filters> <filter xsi:type="wildcardFilter" name="sku_query_filter" field="sku" value="$sku$"/> <filter xsi:type="rangeFilter" name="price_query_filter" field="price" from="$price.from$" to="$price.to$"/> <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_ids$"/> - <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/> </filters> <from>0</from> <size>10000</size> diff --git a/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php index d7054e2bb4b11..b3f8a56110f8d 100644 --- a/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php +++ b/app/code/Magento/Elasticsearch/Model/Advanced/ProductCollectionPrepareStrategy.php @@ -5,11 +5,9 @@ */ namespace Magento\Elasticsearch\Model\Advanced; -use Magento\Catalog\Model\Config; -use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\Config; use Magento\CatalogSearch\Model\Advanced\ProductCollectionPrepareStrategyInterface; -use Magento\Framework\App\ObjectManager; /** * Strategy interface for preparing product collection. @@ -21,22 +19,13 @@ class ProductCollectionPrepareStrategy implements ProductCollectionPrepareStrate */ private $catalogConfig; - /** - * @var Visibility - */ - private $catalogProductVisibility; - /** * @param Config $catalogConfig - * @param Visibility|null $catalogProductVisibility */ public function __construct( - Config $catalogConfig, - Visibility $catalogProductVisibility = null + Config $catalogConfig ) { $this->catalogConfig = $catalogConfig; - $this->catalogProductVisibility = $catalogProductVisibility - ?? ObjectManager::getInstance()->get(Visibility::class); } /** @@ -47,7 +36,6 @@ public function prepare(Collection $collection) $collection ->addAttributeToSelect($this->catalogConfig->getProductAttributes()) ->addMinimalPrice() - ->addTaxPercents() - ->setVisibility($this->catalogProductVisibility->getVisibleInSearchIds()); + ->addTaxPercents(); } } diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index e7517ba0c8818..775f585519947 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -74,7 +74,7 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $returnValue = Cli::RETURN_SUCCESS; + $returnValue = Cli::RETURN_FAILURE; foreach ($this->getIndexers($input) as $indexer) { try { $this->validateIndexerStatus($indexer); @@ -97,15 +97,14 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln( __('has been rebuilt successfully in %time', ['time' => gmdate('H:i:s', $resultTime)]) ); + $returnValue = Cli::RETURN_SUCCESS; } catch (LocalizedException $e) { $output->writeln(__('exception: %message', ['message' => $e->getMessage()])); - $returnValue = Cli::RETURN_FAILURE; } catch (\Exception $e) { $output->writeln('process unknown error:'); $output->writeln($e->getMessage()); $output->writeln($e->getTraceAsString(), OutputInterface::VERBOSITY_DEBUG); - $returnValue = Cli::RETURN_FAILURE; } } diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php deleted file mode 100644 index 815f480cf7b53..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/CatalogSearch/AdvancedTest.php +++ /dev/null @@ -1,101 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Elasticsearch\Model\CatalogSearch; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\CatalogSearch\Model\Advanced; -use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\Registry; -use Magento\TestFramework\Helper\Bootstrap; -use PHPUnit\Framework\TestCase; - -/** - * Check catalog Advanced Search process with Elasticsearch enabled. - */ -class AdvancedTest extends TestCase -{ - /** - * @var ObjectManagerInterface - */ - private $objectManager; - - /** - * @var Registry - */ - private $registry; - - /** - * @var Visibility - */ - private $productVisibility; - - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - - /** - * @inheritDoc - */ - protected function setUp(): void - { - parent::setUp(); - - $this->objectManager = Bootstrap::getObjectManager(); - $this->registry = $this->objectManager->get(Registry::class); - $this->productVisibility = $this->objectManager->get(Visibility::class); - $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - } - - /** - * Check that Advanced Search does NOT return products that do NOT have search visibility. - * - * @magentoDbIsolation disabled - * @magentoConfigFixture default/catalog/search/engine elasticsearch7 - * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_two_child_products.php - * @return void - */ - public function testAddFilters(): void - { - $this->assertResultsAfterRequest(1); - - /** @var ProductInterface $configurableProductOption */ - $configurableProductOption = $this->productRepository->get('Simple option 1'); - $configurableProductOption->setVisibility(Visibility::VISIBILITY_IN_SEARCH); - $this->productRepository->save($configurableProductOption); - - $this->registry->unregister('advanced_search_conditions'); - $this->assertResultsAfterRequest(2); - } - - /** - * Do Elasticsearch query and assert results. - * - * @param int $count - * @return void - */ - private function assertResultsAfterRequest(int $count): void - { - /** @var Advanced $advancedSearch */ - $advancedSearch = $this->objectManager->create(Advanced::class); - $advancedSearch->addFilters(['name' => 'Configurable']); - - /** @var ProductInterface[] $itemsResult */ - $itemsResult = $advancedSearch->getProductCollection() - ->addAttributeToSelect(ProductInterface::VISIBILITY) - ->getItems(); - - $this->assertCount($count, $itemsResult); - foreach ($itemsResult as $product) { - $this->assertStringContainsString('Configurable', $product->getName()); - $this->assertContains((int)$product->getVisibility(), $this->productVisibility->getVisibleInSearchIds()); - } - } -} diff --git a/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php b/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php index 67de7913b4603..f4730b0b32c18 100644 --- a/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Indexer/Console/Command/IndexerReindexCommandTest.php @@ -7,11 +7,9 @@ namespace Magento\Indexer\Console\Command; -use Magento\Framework\Console\Cli; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\MockObject\MockObject as Mock; -use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -21,7 +19,7 @@ * @magentoDbIsolation disabled * @magentoAppIsolation enabled */ -class IndexerReindexCommandTest extends TestCase +class IndexerReindexCommandTest extends \PHPUnit\Framework\TestCase { /** * @var ObjectManagerInterface @@ -58,23 +56,14 @@ protected function setUp(): void /** * @magentoDataFixture Magento/Store/_files/second_store_group_with_second_website.php - * @return void */ - public function testReindexAll(): void + public function testReindexAll() { $status = $this->command->run($this->inputMock, $this->outputMock); - $this->assertEquals(Cli::RETURN_SUCCESS, $status, 'Index wasn\'t success'); - } - - /** - * Check that 'indexer:reindex' command return right code. - * - * @magentoDataFixture Magento/Indexer/_files/wrong_config_data.php - * @return void - */ - public function testReindexAllWhenSomethingIsWrong(): void - { - $status = $this->command->run($this->inputMock, $this->outputMock); - $this->assertEquals(Cli::RETURN_FAILURE, $status, 'Index didn\'t return failure code'); + $this->assertEquals( + \Magento\Framework\Console\Cli::RETURN_SUCCESS, + $status, + 'Index wasn\'t success' + ); } } diff --git a/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php b/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php deleted file mode 100644 index c6ec6235b61e6..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data.php +++ /dev/null @@ -1,16 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -use Magento\Config\Model\Config\Factory; -use Magento\TestFramework\Helper\Bootstrap; - -/** @var Factory $configFactory */ -$configFactory = Bootstrap::getObjectManager()->get(Factory::class); -$config = $configFactory->create(); -$config->setScope('stores'); -$config->setDataByPath('catalog/search/elasticsearch7_server_port', 2309); -$config->save(); diff --git a/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_rollback.php b/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_rollback.php deleted file mode 100644 index 7ca0b986fc206..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Indexer/_files/wrong_config_data_rollback.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -use Magento\Framework\App\ResourceConnection; -use Magento\TestFramework\Helper\Bootstrap; - -/** @var ResourceConnection $resource */ -$resource = Bootstrap::getObjectManager()->get(ResourceConnection::class); -$connection = $resource->getConnection(); -$tableName = $resource->getTableName('core_config_data'); - -$connection->query("DELETE FROM $tableName WHERE path = 'catalog/search/elasticsearch7_server_port'" - ." AND scope = 'stores';"); From 6a58f7ef0cf75b830f7a7c462d8fa7d010de317e Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Mon, 28 Sep 2020 16:44:20 +0300 Subject: [PATCH 0623/1013] magento/magento2#29478: refactoring, test coverage --- .../Console/Command/IndexerReindexCommand.php | 79 ++----- app/code/Magento/Indexer/Model/Processor.php | 79 ++----- .../Model/Processor/MakeSharedIndexValid.php | 95 +++++++++ .../Command/IndexerReindexCommandTest.php | 26 ++- .../Indexer/Test/Unit/Model/ProcessorTest.php | 199 +++++++++++++++++- 5 files changed, 333 insertions(+), 145 deletions(-) create mode 100644 app/code/Magento/Indexer/Model/Processor/MakeSharedIndexValid.php diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index 775f585519947..67cde2e1d7d58 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -6,6 +6,7 @@ namespace Magento\Indexer\Console\Command; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ObjectManagerFactory; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\LocalizedException; @@ -14,11 +15,13 @@ use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Indexer\StateInterface; +use Magento\Indexer\Model\Processor\MakeSharedIndexValid; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Command to run indexers + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class IndexerReindexCommand extends AbstractIndexerManageCommand { @@ -42,18 +45,26 @@ class IndexerReindexCommand extends AbstractIndexerManageCommand */ private $dependencyInfoProvider; + /** + * @var MakeSharedIndexValid|null + */ + private $makeSharedValid; + /** * @param ObjectManagerFactory $objectManagerFactory * @param IndexerRegistry|null $indexerRegistry * @param DependencyInfoProvider|null $dependencyInfoProvider + * @param MakeSharedIndexValid|null $makeSharedValid */ public function __construct( ObjectManagerFactory $objectManagerFactory, IndexerRegistry $indexerRegistry = null, - DependencyInfoProvider $dependencyInfoProvider = null + DependencyInfoProvider $dependencyInfoProvider = null, + MakeSharedIndexValid $makeSharedValid = null ) { $this->indexerRegistry = $indexerRegistry; $this->dependencyInfoProvider = $dependencyInfoProvider; + $this->makeSharedValid = $makeSharedValid ?: ObjectManager::getInstance()->get(MakeSharedIndexValid::class); parent::__construct($objectManagerFactory); } @@ -88,8 +99,8 @@ protected function execute(InputInterface $input, OutputInterface $output) // Skip indexers having shared index that was already complete if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { $indexer->reindexAll(); - if ($sharedIndex) { - $this->validateSharedIndex($sharedIndex); + if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { + $this->sharedIndexesComplete[] = $sharedIndex; } } $resultTime = microtime(true) - $startTime; @@ -214,54 +225,6 @@ private function validateIndexerStatus(IndexerInterface $indexer) } } - /** - * Get indexer ids that have common shared index - * - * @param string $sharedIndex - * @return array - */ - private function getIndexerIdsBySharedIndex($sharedIndex) - { - $indexers = $this->getConfig()->getIndexers(); - $result = []; - foreach ($indexers as $indexerConfig) { - if ($indexerConfig['shared_index'] == $sharedIndex) { - $result[] = $indexerConfig['indexer_id']; - } - } - return $result; - } - - /** - * Validate indexers by shared index ID - * - * @param string $sharedIndex - * @return $this - */ - private function validateSharedIndex($sharedIndex) - { - if (empty($sharedIndex)) { - throw new \InvalidArgumentException( - 'The sharedIndex is an invalid shared index identifier. Verify the identifier and try again.' - ); - } - $indexerIds = $this->getIndexerIdsBySharedIndex($sharedIndex); - if (empty($indexerIds)) { - return $this; - } - foreach ($indexerIds as $indexerId) { - $indexer = $this->getIndexerRegistry()->get($indexerId); - /** @var \Magento\Indexer\Model\Indexer\State $state */ - $state = $indexer->getState(); - $state->setStatus(StateInterface::STATUS_WORKING); - $state->save(); - $state->setStatus(StateInterface::STATUS_VALID); - $state->save(); - } - $this->sharedIndexesComplete[] = $sharedIndex; - return $this; - } - /** * Get config * @@ -276,20 +239,6 @@ private function getConfig() return $this->config; } - /** - * Get indexer registry - * - * @return IndexerRegistry - * @deprecated 100.2.0 - */ - private function getIndexerRegistry() - { - if (!$this->indexerRegistry) { - $this->indexerRegistry = $this->getObjectManager()->get(IndexerRegistry::class); - } - return $this->indexerRegistry; - } - /** * Get dependency info provider * diff --git a/app/code/Magento/Indexer/Model/Processor.php b/app/code/Magento/Indexer/Model/Processor.php index 01f530488fbe7..78b8fa070b155 100644 --- a/app/code/Magento/Indexer/Model/Processor.php +++ b/app/code/Magento/Indexer/Model/Processor.php @@ -5,10 +5,12 @@ */ namespace Magento\Indexer\Model; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\ConfigInterface; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerInterfaceFactory; -use Magento\Framework\Indexer\StateInterface; +use Magento\Framework\Mview\ProcessorInterface; +use Magento\Indexer\Model\Processor\MakeSharedIndexValid; /** * Indexer processor @@ -36,26 +38,34 @@ class Processor protected $indexersFactory; /** - * @var \Magento\Framework\Mview\ProcessorInterface + * @var ProcessorInterface */ protected $mviewProcessor; + /** + * @var MakeSharedIndexValid + */ + protected $makeSharedValid; + /** * @param ConfigInterface $config * @param IndexerInterfaceFactory $indexerFactory * @param Indexer\CollectionFactory $indexersFactory - * @param \Magento\Framework\Mview\ProcessorInterface $mviewProcessor + * @param ProcessorInterface $mviewProcessor + * @param MakeSharedIndexValid|null $makeSharedValid */ public function __construct( ConfigInterface $config, IndexerInterfaceFactory $indexerFactory, Indexer\CollectionFactory $indexersFactory, - \Magento\Framework\Mview\ProcessorInterface $mviewProcessor + ProcessorInterface $mviewProcessor, + MakeSharedIndexValid $makeSharedValid = null ) { $this->config = $config; $this->indexerFactory = $indexerFactory; $this->indexersFactory = $indexersFactory; $this->mviewProcessor = $mviewProcessor; + $this->makeSharedValid = $makeSharedValid ?: ObjectManager::getInstance()->get(MakeSharedIndexValid::class); } /** @@ -70,7 +80,6 @@ public function reindexAllInvalid() $indexer = $this->indexerFactory->create(); $indexer->load($indexerId); $indexerConfig = $this->config->getIndexer($indexerId); - $sharedIndex = $indexerConfig['shared_index']; if ($indexer->isInvalid()) { // Skip indexers having shared index that was already complete @@ -78,70 +87,14 @@ public function reindexAllInvalid() if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { $indexer->reindexAll(); - if ($sharedIndex) { - $this->validateSharedIndex($sharedIndex); + if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { + $this->sharedIndexesComplete[] = $sharedIndex; } } } } } - /** - * Get indexer ids that have common shared index - * - * @param string $sharedIndex - * @return array - */ - private function getIndexerIdsBySharedIndex(string $sharedIndex): array - { - $indexers = $this->config->getIndexers(); - - $result = []; - foreach ($indexers as $indexerConfig) { - if ($indexerConfig['shared_index'] == $sharedIndex) { - $result[] = $indexerConfig['indexer_id']; - } - } - - return $result; - } - - /** - * Validate indexers by shared index ID - * - * @param string $sharedIndex - * @return $this - */ - private function validateSharedIndex(string $sharedIndex): self - { - if (empty($sharedIndex)) { - throw new \InvalidArgumentException( - 'The sharedIndex is an invalid shared index identifier. Verify the identifier and try again.' - ); - } - - $indexerIds = $this->getIndexerIdsBySharedIndex($sharedIndex); - if (empty($indexerIds)) { - return $this; - } - - foreach ($indexerIds as $indexerId) { - /** @var \Magento\Indexer\Model\Indexer $indexer */ - $indexer = $this->indexerFactory->create(); - $indexer->load($indexerId); - /** @var \Magento\Indexer\Model\Indexer\State $state */ - $state = $indexer->getState(); - $state->setStatus(StateInterface::STATUS_WORKING); - $state->save(); - $state->setStatus(StateInterface::STATUS_VALID); - $state->save(); - } - - $this->sharedIndexesComplete[] = $sharedIndex; - - return $this; - } - /** * Regenerate indexes for all indexers * diff --git a/app/code/Magento/Indexer/Model/Processor/MakeSharedIndexValid.php b/app/code/Magento/Indexer/Model/Processor/MakeSharedIndexValid.php new file mode 100644 index 0000000000000..338891589bf33 --- /dev/null +++ b/app/code/Magento/Indexer/Model/Processor/MakeSharedIndexValid.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Indexer\Model\Processor; + +use Magento\Framework\Indexer\ConfigInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Indexer\StateInterface; +use Magento\Indexer\Model\Indexer\State; + +/** + * Class processor makes indexers valid by shared index ID + */ +class MakeSharedIndexValid +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * ValidateSharedIndex constructor. + * + * @param ConfigInterface $config + * @param IndexerRegistry $indexerRegistry + */ + public function __construct(ConfigInterface $config, IndexerRegistry $indexerRegistry) + { + $this->config = $config; + $this->indexerRegistry = $indexerRegistry; + } + + /** + * Validate indexers by shared index ID + * + * @param string $sharedIndex + * @return bool + * @throws \Exception + */ + public function execute(string $sharedIndex): bool + { + if (empty($sharedIndex)) { + throw new \InvalidArgumentException( + "The '{$sharedIndex}' is an invalid shared index identifier. Verify the identifier and try again.", + ); + } + + $indexerIds = $this->getIndexerIdsBySharedIndex($sharedIndex); + if (empty($indexerIds)) { + return false; + } + + foreach ($indexerIds as $indexerId) { + $indexer = $this->indexerRegistry->get($indexerId); + /** @var State $state */ + $state = $indexer->getState(); + $state->setStatus(StateInterface::STATUS_WORKING); + $state->save(); + $state->setStatus(StateInterface::STATUS_VALID); + $state->save(); + } + + return true; + } + + /** + * Get indexer ids that have common shared index + * + * @param string $sharedIndex + * @return array + */ + private function getIndexerIdsBySharedIndex(string $sharedIndex): array + { + $indexers = $this->config->getIndexers(); + + $result = []; + foreach ($indexers as $indexerConfig) { + if ($indexerConfig['shared_index'] == $sharedIndex) { + $result[] = $indexerConfig['indexer_id']; + } + } + + return $result; + } +} diff --git a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php index 6d96841bc3dab..8bdceb92b247b 100644 --- a/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Console/Command/IndexerReindexCommandTest.php @@ -18,6 +18,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Indexer\Console\Command\IndexerReindexCommand; use Magento\Indexer\Model\Config; +use Magento\Indexer\Model\Processor\MakeSharedIndexValid; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\Console\Tester\CommandTester; @@ -49,6 +50,11 @@ class IndexerReindexCommandTest extends AbstractIndexerCommandCommonSetup */ private $dependencyInfoProviderMock; + /** + * @var MakeSharedIndexValid|MockObject + */ + private $makeSharedValidMock; + /** * @var ObjectManagerHelper */ @@ -64,12 +70,12 @@ protected function setUp(): void $this->indexerRegistryMock = $this->getMockBuilder(IndexerRegistry::class) ->disableOriginalConstructor() ->getMock(); - $this->dependencyInfoProviderMock = $this->objectManagerHelper->getObject( - DependencyInfoProvider::class, - [ - 'config' => $this->configMock, - ] - ); + $this->makeSharedValidMock = $this->getMockBuilder(MakeSharedIndexValid::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dependencyInfoProviderMock = $this->objectManagerHelper->getObject(DependencyInfoProvider::class, [ + 'config' => $this->configMock, + ]); parent::setUp(); } @@ -174,11 +180,17 @@ public function testExecuteWithIndex( $emptyIndexer->method('getState') ->willReturn($this->getStateMock(['setStatus', 'save'])); + $this->makeSharedValidMock = $this->objectManagerHelper->getObject(MakeSharedIndexValid::class, [ + 'config' => $this->configMock, + 'indexerRegistry' => $this->indexerRegistryMock + ]); $this->configureAdminArea(); $this->command = new IndexerReindexCommand( $this->objectManagerFactory, - $this->indexerRegistryMock + $this->indexerRegistryMock, + $this->dependencyInfoProviderMock, + $this->makeSharedValidMock ); $commandTester = new CommandTester($this->command); diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index 9cc0277997289..9f9b4c2157bb7 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -9,6 +9,7 @@ use Magento\Framework\Indexer\ConfigInterface; use Magento\Framework\Indexer\IndexerInterfaceFactory; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Indexer\StateInterface; use Magento\Framework\Mview\ProcessorInterface; use Magento\Indexer\Model\Indexer; @@ -16,6 +17,7 @@ use Magento\Indexer\Model\Indexer\CollectionFactory; use Magento\Indexer\Model\Indexer\State; use Magento\Indexer\Model\Processor; +use Magento\Indexer\Model\Processor\MakeSharedIndexValid; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -79,20 +81,15 @@ protected function setUp(): void ); } - public function testReindexAllInvalid() + /** + * @return void + */ + public function testReindexAllInvalid(): void { $indexers = ['indexer1' => [], 'indexer2' => []]; $this->configMock->expects($this->once())->method('getIndexers')->willReturn($indexers); - $this->configMock->expects($this->exactly(2)) - ->method('getIndexer') - ->willReturn( - [ - 'shared_index' => null - ] - ); - $state1Mock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); $state1Mock->expects( $this->once() @@ -129,7 +126,68 @@ public function testReindexAllInvalid() $this->model->reindexAllInvalid(); } - public function testReindexAll() + /** + * @dataProvider sharedIndexDataProvider + * @param array $indexers + * @param array $indexerStates + * @param array $expectedReindexAllCalls + * @param array $executedSharedIndexers + */ + public function testReindexAllInvalidWithSharedIndex( + array $indexers, + array $indexerStates, + array $expectedReindexAllCalls, + array $executedSharedIndexers + ): void { + $this->configMock->expects($this->any())->method('getIndexers')->willReturn($indexers); + $this->configMock + ->method('getIndexer') + ->willReturnMap( + array_map( + function ($elem) { + return [$elem['indexer_id'], $elem]; + }, + $indexers + ) + ); + $indexerMocks = []; + foreach ($indexers as $indexerData) { + $stateMock = $this->createPartialMock(State::class, ['getStatus', '__wakeup']); + $stateMock->expects($this->any()) + ->method('getStatus') + ->willReturn($indexerStates[$indexerData['indexer_id']]); + $indexerMock = $this->createPartialMock(Indexer::class, ['load', 'getState', 'reindexAll']); + $indexerMock->expects($this->any())->method('getState')->willReturn($stateMock); + $indexerMock->expects($expectedReindexAllCalls[$indexerData['indexer_id']])->method('reindexAll'); + + $this->indexerFactoryMock->expects($this->at(count($indexerMocks))) + ->method('create') + ->willReturn($indexerMock); + + $indexerMocks[] = $indexerMock; + } + $indexerRegistryMock = $this->getIndexRegistryMock($executedSharedIndexers); + + $makeSharedValidMock = new MakeSharedIndexValid( + $this->configMock, + $indexerRegistryMock + ); + $model = new Processor( + $this->configMock, + $this->indexerFactoryMock, + $this->indexersFactoryMock, + $this->viewProcessorMock, + $makeSharedValidMock + ); + $model->reindexAllInvalid(); + } + + /** + * Reindex all test + * + * return void + */ + public function testReindexAll(): void { $indexerMock = $this->createMock(Indexer::class); $indexerMock->expects($this->exactly(2))->method('reindexAll'); @@ -142,15 +200,136 @@ public function testReindexAll() $this->model->reindexAll(); } + /** + * Update mview test + * + * @return void + */ public function testUpdateMview() { $this->viewProcessorMock->expects($this->once())->method('update')->with('indexer')->willReturnSelf(); $this->model->updateMview(); } + /** + * Clear change log test + * + * @return void + */ public function testClearChangelog() { $this->viewProcessorMock->expects($this->once())->method('clearChangelog')->with('indexer')->willReturnSelf(); $this->model->clearChangelog(); } + + /** + * @return array + */ + public function sharedIndexDataProvider() + { + return [ + 'Without dependencies' => [ + 'indexers' => [ + 'indexer_1' => [ + 'indexer_id' => 'indexer_1', + 'title' => 'Title_indexer_1', + 'shared_index' => null, + 'dependencies' => [], + ], + 'indexer_2' => [ + 'indexer_id' => 'indexer_2', + 'title' => 'Title_indexer_2', + 'shared_index' => 'with_indexer_3', + 'dependencies' => [], + ], + 'indexer_3' => [ + 'indexer_id' => 'indexer_3', + 'title' => 'Title_indexer_3', + 'shared_index' => 'with_indexer_3', + 'dependencies' => [], + ], + ], + 'indexer_states' => [ + 'indexer_1' => StateInterface::STATUS_INVALID, + 'indexer_2' => StateInterface::STATUS_VALID, + 'indexer_3' => StateInterface::STATUS_VALID, + ], + 'expected_reindex_all_calls' => [ + 'indexer_1' => $this->once(), + 'indexer_2' => $this->never(), + 'indexer_3' => $this->never(), + ], + 'executed_shared_indexers' => [], + ], + 'With dependencies and some indexers is invalid' => [ + 'indexers' => [ + 'indexer_1' => [ + 'indexer_id' => 'indexer_1', + 'title' => 'Title_indexer_1', + 'shared_index' => null, + 'dependencies' => ['indexer_2', 'indexer_3'], + ], + 'indexer_2' => [ + 'indexer_id' => 'indexer_2', + 'title' => 'Title_indexer_2', + 'shared_index' => 'with_indexer_3', + 'dependencies' => [], + ], + 'indexer_3' => [ + 'indexer_id' => 'indexer_3', + 'title' => 'Title_indexer_3', + 'shared_index' => 'with_indexer_3', + 'dependencies' => [], + ], + 'indexer_4' => [ + 'indexer_id' => 'indexer_4', + 'title' => 'Title_indexer_4', + 'shared_index' => null, + 'dependencies' => ['indexer_1'], + ], + ], + 'indexer_states' => [ + 'indexer_1' => StateInterface::STATUS_INVALID, + 'indexer_2' => StateInterface::STATUS_VALID, + 'indexer_3' => StateInterface::STATUS_INVALID, + 'indexer_4' => StateInterface::STATUS_VALID, + ], + 'expected_reindex_all_calls' => [ + 'indexer_1' => $this->once(), + 'indexer_2' => $this->never(), + 'indexer_3' => $this->once(), + 'indexer_4' => $this->never(), + ], + 'executed_shared_indexers' => [['indexer_2'], ['indexer_3']], + ], + ]; + } + + /** + * @param array $executedSharedIndexers + * @return IndexerRegistry|MockObject + */ + private function getIndexRegistryMock(array $executedSharedIndexers) + { + /** @var MockObject|IndexerRegistry $indexerRegistryMock */ + $indexerRegistryMock = $this->getMockBuilder(IndexerRegistry::class) + ->disableOriginalConstructor() + ->getMock(); + $emptyIndexer = $this->createPartialMock(Indexer::class, ['load', 'getState', 'reindexAll']); + /** @var MockObject|StateInterface $state */ + $state = $this->getMockBuilder(StateInterface::class) + ->setMethods(['setStatus', 'save']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $state->method('getStatus') + ->willReturn(StateInterface::STATUS_INVALID); + $emptyIndexer->method('getState')->willReturn($state); + $indexerRegistryMock + ->expects($this->exactly(count($executedSharedIndexers))) + ->method('get') + ->withConsecutive(...$executedSharedIndexers) + ->willReturn($emptyIndexer); + + return $indexerRegistryMock; + } } From 6814317db1decca4d63454a819166f0f1f631d5f Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 28 Sep 2020 10:14:27 -0500 Subject: [PATCH 0624/1013] MC-38011: [Integration] UpdateHandlerTest::testDeleteWithMultiWebsites failed on 2.4.2-develop - Fix integration test --- .../Model/Product/Gallery/UpdateHandlerTest.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 2659f14c07c7a..7ee2c62453df5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -416,7 +416,7 @@ public function testDeleteWithMultiWebsites(): void $product->setWebsiteIds([$defaultWebsiteId, $secondWebsiteId]); $this->productRepository->save($product); // Assert that product image has roles in global scope only - $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $imageRolesPerStore = $this->getProductStoreImageRoles($product, $imageRoles); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); @@ -428,7 +428,7 @@ public function testDeleteWithMultiWebsites(): void $product->addData(array_fill_keys($imageRoles, $image)); $this->productRepository->save($product); // Assert that roles are assigned to product image for second store - $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $imageRolesPerStore = $this->getProductStoreImageRoles($product, $imageRoles); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['small_image']); $this->assertEquals($image, $imageRolesPerStore[$globalScopeId]['thumbnail']); @@ -454,7 +454,7 @@ public function testDeleteWithMultiWebsites(): void $this->assertEmpty($product->getMediaGalleryEntries()); $this->assertFileDoesNotExist($path); // Load image roles - $imageRolesPerStore = $this->getProductStoreImageRoles($product); + $imageRolesPerStore = $this->getProductStoreImageRoles($product, $imageRoles); // Assert that image roles are reset on global scope and removed on second store // as the product is no longer assigned to second website $this->assertEquals('no_selection', $imageRolesPerStore[$globalScopeId]['image']); @@ -466,14 +466,17 @@ public function testDeleteWithMultiWebsites(): void /** * @param Product $product + * @param array $roles * @return array */ - private function getProductStoreImageRoles(Product $product): array + private function getProductStoreImageRoles(Product $product, array $roles = []): array { $imageRolesPerStore = []; $stores = array_keys($this->storeManager->getStores(true)); foreach ($this->galleryResource->getProductImages($product, $stores) as $role) { - $imageRolesPerStore[$role['store_id']][$role['attribute_code']] = $role['filepath']; + if (empty($roles) || in_array($role['attribute_code'], $roles)) { + $imageRolesPerStore[$role['store_id']][$role['attribute_code']] = $role['filepath']; + } } return $imageRolesPerStore; } From 69687f2bb277152f7fd8bba204c2adcb0de8171d Mon Sep 17 00:00:00 2001 From: mastiuhin-olexandr <mastiuhin.olexandr@transoftgroup.com> Date: Mon, 28 Sep 2020 19:26:50 +0300 Subject: [PATCH 0625/1013] MC-35812: Overriding CreatePost can automatically confirm a customer --- app/code/Magento/Customer/etc/graphql/di.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/code/Magento/Customer/etc/graphql/di.xml b/app/code/Magento/Customer/etc/graphql/di.xml index f1377f927b7c1..e5ed4078b4a04 100644 --- a/app/code/Magento/Customer/etc/graphql/di.xml +++ b/app/code/Magento/Customer/etc/graphql/di.xml @@ -16,6 +16,4 @@ </argument> </arguments> </type> - <preference for="Magento\Customer\Api\AccountManagementInterface" - type="Magento\Customer\Model\AccountManagementApi" /> </config> From dfcb7b1b583b889c328b68df4ade146643a8e589 Mon Sep 17 00:00:00 2001 From: Dan Wallis <mrdanwallis@gmail.com> Date: Mon, 28 Sep 2020 17:58:49 +0100 Subject: [PATCH 0626/1013] Add test payment method without <model> node --- .../Magento/TestModuleFakePaymentMethod/etc/config.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml b/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml index 42c21d544d01e..adf5af6f037ab 100644 --- a/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml +++ b/dev/tests/integration/_files/Magento/TestModuleFakePaymentMethod/etc/config.xml @@ -32,6 +32,11 @@ <supported>1</supported> </instant_purchase> </fake_vault> + <fake_no_model> + <!-- This method on purpose does not have a 'model' node. --> + <title>Fake Payment Method without <model> + 0 + - \ No newline at end of file + From e548c2e10cc90b00691609817112e0012c8d5dda Mon Sep 17 00:00:00 2001 From: Oleh Usik Date: Mon, 28 Sep 2020 22:47:39 +0300 Subject: [PATCH 0627/1013] create AdminOpenCartPriceRulesPageActionGroup --- .../Test/CheckTierPricingOfProductsTest.xml | 3 +-- ...SubtotalOrdersWithProcessingStatusTest.xml | 4 +--- ...editMemoTotalAfterShippingDiscountTest.xml | 3 +-- ...AdminOpenCartPriceRulesPageActionGroup.xml | 19 +++++++++++++++++++ .../Mftf/Test/AdminCreateBuyXGetYFreeTest.xml | 3 +-- ...eConditionAndFreeShippingIsAppliedTest.xml | 3 +-- ...AndVerifyRuleConditionIsNotAppliedTest.xml | 3 +-- ...inCreateCartPriceRuleEmptyFromDateTest.xml | 3 +-- ...inCreateCartPriceRuleForCouponCodeTest.xml | 3 +-- ...ateCartPriceRuleForGeneratedCouponTest.xml | 3 +-- ...talAndVerifyRuleConditionIsAppliedTest.xml | 3 +-- ...oryAndVerifyRuleConditionIsAppliedTest.xml | 3 +-- ...ghtAndVerifyRuleConditionIsAppliedTest.xml | 3 +-- .../AdminCreateFixedAmountDiscountTest.xml | 3 +-- ...CreateFixedAmountWholeCartDiscountTest.xml | 3 +-- .../Mftf/Test/AdminCreateInvalidRuleTest.xml | 3 +-- .../AdminCreatePercentOfProductPriceTest.xml | 3 +-- ...artPriceRuleForConfigurableProductTest.xml | 3 +-- .../StorefrontAutoGeneratedCouponCodeTest.xml | 3 +-- .../StorefrontCartPriceRuleCountryTest.xml | 3 +-- ...frontCartPriceRuleForBundleProductTest.xml | 3 +-- .../StorefrontCartPriceRulePostcodeTest.xml | 3 +-- .../StorefrontCartPriceRuleQuantityTest.xml | 3 +-- .../Test/StorefrontCartPriceRuleStateTest.xml | 3 +-- .../StorefrontCartPriceRuleSubtotalTest.xml | 3 +-- 25 files changed, 43 insertions(+), 49 deletions(-) create mode 100644 app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenCartPriceRulesPageActionGroup.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index 55d697e35deba..5211e0b2812ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -133,8 +133,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml index 1828251e68635..f38061dbf6a6c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ZeroSubtotalOrdersWithProcessingStatusTest.xml @@ -41,9 +41,7 @@ - - - + diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index 20dcb262b5831..b2bdf8ce5d90b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -39,8 +39,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenCartPriceRulesPageActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenCartPriceRulesPageActionGroup.xml new file mode 100644 index 0000000000000..b12bdf56e0ed8 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminOpenCartPriceRulesPageActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + Open cart price rules page. + + + + + + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml index ed2dd16b7df9d..5f2b40dc63e2a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeTest.xml @@ -34,8 +34,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml index 34152ea06745c..88853b2c40d9a 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionAndFreeShippingIsAppliedTest.xml @@ -31,8 +31,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml index 9ac73ceae586e..25d9d431d1c51 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleAndVerifyRuleConditionIsNotAppliedTest.xml @@ -33,8 +33,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml index f956d036d7080..e206633808057 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml @@ -47,8 +47,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml index 557a585858868..16af210066997 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml @@ -34,8 +34,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index e18a9eaadcd23..6577ff9440456 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -34,8 +34,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml index 34714e9637d46..da8c8e4bc1f9d 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForMatchingSubtotalAndVerifyRuleConditionIsAppliedTest.xml @@ -31,8 +31,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml index a3e6331e31cf6..f6e736c73db74 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingCategoryAndVerifyRuleConditionIsAppliedTest.xml @@ -38,8 +38,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml index e9f7f3ec6c70a..5f110f7074f6f 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleWithMatchingTotalWeightAndVerifyRuleConditionIsAppliedTest.xml @@ -31,8 +31,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml index 0d98abfba3f62..2c3574906848c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountDiscountTest.xml @@ -34,8 +34,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml index bc4139435ab55..1b24480b5808b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateFixedAmountWholeCartDiscountTest.xml @@ -34,8 +34,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml index 56c4506196d24..83648cec149d0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateInvalidRuleTest.xml @@ -26,8 +26,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml index 23e472518ba84..724860b12603c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreatePercentOfProductPriceTest.xml @@ -36,8 +36,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml index ad1ff69a60901..d60a81dcdcef9 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -96,8 +96,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index c2aeca657db3b..7a12bca389672 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -40,8 +40,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml index eef5dadfbe5d8..ea96fa41e5cad 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml @@ -37,8 +37,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml index c5f4e8a07f622..56486d2331bd6 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml @@ -107,8 +107,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml index 69097e3269fcb..62c494b988bbd 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml @@ -37,8 +37,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml index 18057965c28e1..70ed09df7a2cc 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml @@ -38,8 +38,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml index c13b74b6990d0..da9ca9055d31b 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml @@ -37,8 +37,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml index 97b75ae772f08..ce0d814e50308 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml @@ -38,8 +38,7 @@ - - + From a2e08dc5da8dbbfbb8eb244f2a955ef72e557fb5 Mon Sep 17 00:00:00 2001 From: Soumya Unnikrishnan Date: Mon, 28 Sep 2020 15:13:21 -0500 Subject: [PATCH 0628/1013] MQE-2306: Release 3.1.1 Delivery Composer update --- composer.lock | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/composer.lock b/composer.lock index 8a5d82536cee4..59920b387a331 100644 --- a/composer.lock +++ b/composer.lock @@ -8120,6 +8120,12 @@ "sftp", "storage" ], + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], "time": "2020-05-18T15:13:39+00:00" }, { @@ -8230,16 +8236,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "8a106ea029f222f4354854636861273c7577bee9" + "reference": "c6760313811f2c04545a261c706d2a73dd727b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", - "reference": "8a106ea029f222f4354854636861273c7577bee9", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/c6760313811f2c04545a261c706d2a73dd727b9a", + "reference": "c6760313811f2c04545a261c706d2a73dd727b9a", "shasum": "" }, "require": { @@ -8317,7 +8323,7 @@ "magento", "testing" ], - "time": "2020-08-19T19:57:27+00:00" + "time": "2020-09-28T18:26:59+00:00" }, { "name": "mikey179/vfsstream", From 9713cde24c81683269bd0495330b013feca5e683 Mon Sep 17 00:00:00 2001 From: Oleh Usik Date: Mon, 28 Sep 2020 23:19:18 +0300 Subject: [PATCH 0629/1013] add new ReloadPageActionGroup --- .../Test/AdminCheckAnalyticsTrackingTest.xml | 3 +-- .../Mftf/Test/AdminExpireAdminSessionTest.xml | 2 +- .../Test/AdminExpireCustomerSessionTest.xml | 2 +- .../CaptchaWithDisabledGuestCheckoutTest.xml | 3 +-- ...egoryIndexerInUpdateOnScheduleModeTest.xml | 4 ++-- ...rontCatalogNavigationMenuUIDesktopTest.xml | 12 ++++-------- ...UKCustomerRemainOptionAfterRefreshTest.xml | 3 +-- ...sNotAffectedStartedCheckoutProcessTest.xml | 3 +-- ...tCheckoutUsingFreeShippingAndTaxesTest.xml | 3 +-- ...aForGuestCustomerWithPhysicalQuoteTest.xml | 6 ++---- ...riceInShoppingCartAfterProductSaveTest.xml | 3 +-- ...efrontVisibilityOfDuplicateProductTest.xml | 6 ++---- .../Mftf/Test/AdminCreateCustomerTest.xml | 3 +-- ...dAreaSessionMustNotAffectAdminAreaTest.xml | 6 ++---- ...ingCartBehaviorAfterSessionExpiredTest.xml | 3 +-- ...efrontGuestCheckoutDisabledProductTest.xml | 6 ++---- ...ateCartPriceRuleForGeneratedCouponTest.xml | 3 +-- .../StorefrontAutoGeneratedCouponCodeTest.xml | 3 +-- .../Test/AdminDisablingSwatchTooltipsTest.xml | 3 +-- ...refrontInlineTranslationOnCheckoutTest.xml | 6 ++---- .../ActionGroup/ReloadPageActionGroup.xml | 19 +++++++++++++++++++ 21 files changed, 48 insertions(+), 54 deletions(-) create mode 100644 app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml index 4f0e9bb000a27..99c60eba67854 100644 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml @@ -22,14 +22,13 @@ - - + diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml index 2469151337bfe..b87b92e86528c 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml @@ -29,7 +29,7 @@ - + diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml index 0e3bf07d32441..b2b71c4ad3eca 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml @@ -40,7 +40,7 @@ - + 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 9e99fa96ee766..39a774369c331 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -46,8 +46,7 @@ - - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index eebd3472cbd95..4eac36c28ab98 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -99,7 +99,7 @@ - + @@ -128,7 +128,7 @@ - + diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml index 2a59be6306a30..07c67f6f290f1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -40,8 +40,7 @@ - - + @@ -87,8 +86,7 @@ - - + @@ -167,8 +165,7 @@ - - + @@ -203,8 +200,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index e7e8f9f0ef699..de4e64e3c5938 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -42,8 +42,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index a1065daedd4f8..3128387e4155b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -90,8 +90,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index 5a0610f5c5b0a..aa05a4828e555 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -172,8 +172,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index e42d5e1bae956..6ac85e77766e1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -49,8 +49,7 @@ - - + @@ -71,8 +70,7 @@ - - + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index a7a0917532dcb..299d33244f1fb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -62,8 +62,7 @@ - - + diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 976be77122547..a066d5077f713 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -62,8 +62,7 @@ - - + @@ -142,8 +141,7 @@ - - + diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index cb003ed837294..710e4bba29e05 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -40,8 +40,7 @@ - - + diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index 1c280acd63a7b..9eb0558bdfd2e 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -76,10 +76,8 @@ - - - - + + diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml index 18e19c4276548..533a06986b70a 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml @@ -55,8 +55,7 @@ - - + diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 80af412439338..92a1c1facd6a6 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -124,8 +124,7 @@ - - + @@ -151,8 +150,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index e18a9eaadcd23..953d142a49ab1 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -60,8 +60,7 @@ - - + diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index c2aeca657db3b..631c516153fa2 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -64,8 +64,7 @@ - - + diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index b48f181c8d199..8ad9578e9184a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -153,8 +153,7 @@ - - + diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index e30ab98982b78..cfee0785ac1d1 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -120,8 +120,7 @@ - - + @@ -490,8 +489,7 @@ - - + diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml new file mode 100644 index 0000000000000..3976a2ac0f872 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml @@ -0,0 +1,19 @@ + + + + + + + Reload page and wait for page load. + + + + + + From b853f92423902ef332c090abd2b4aa77700fdf58 Mon Sep 17 00:00:00 2001 From: Prabhu Ram Date: Mon, 28 Sep 2020 17:45:32 -0500 Subject: [PATCH 0630/1013] MC-36898: [Graphql]Unable to add bundle-product items with user defined quantity to wishlist - Added support for custom option quantity - Added end to end test coverage --- .../BuyRequest/BundleDataProvider.php | 40 ++++- .../AddBundleProductToWishlistTest.php | 152 ++++++++++++++++++ 2 files changed, 189 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php index 1cfa316c3cd01..3a4532d53624a 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php @@ -7,6 +7,7 @@ namespace Magento\Wishlist\Model\Wishlist\BuyRequest; +use Magento\Framework\Exception\LocalizedException; use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; /** @@ -32,15 +33,48 @@ public function execute(WishlistItem $wishlistItem, ?int $productId): array continue; } - [, $optionId, $optionValueId, $optionQuantity] = $optionData; + [$optionType, $optionId, $optionValueId, $optionQuantity] = $optionData; - $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; - $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + if ($optionType == self::PROVIDER_OPTION_TYPE) { + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + } + //for bundle options with custom quantity + foreach ($wishlistItem->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::PROVIDER_OPTION_TYPE) { + $optionQuantity = $option->getValue(); + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } } return $bundleOptionsData; } + /** + * 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); + } + } + /** * Checks whether this provider is applicable for the current option * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php index b97cd379e4384..04518fad47052 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -98,6 +98,46 @@ public function testAddBundleProductWithOptions(): void $this->assertEquals(Select::NAME, $bundleOptions[0]['type']); } + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @throws Exception + */ + public function testAddingBundleItemWithCustomOptionQuantity() + { + $response = $this->graphQlQuery($this->getProductQuery("bundle-product")); + $bundleItem = $response['products']['items'][0]; + $sku = $bundleItem['sku']; + $bundleOptions = $bundleItem['items']; + $customerId = 1; + $uId0 = $bundleOptions[0]['options'][0]['uid']; + $uId1 = $bundleOptions[1]['options'][0]['uid']; + $query= $this->getQueryWithCustomOptionQuantity($sku, 5, $uId0, $uId1); + $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_v2'][0]['quantity']); + $this->assertEquals($item->getDescription(), $response['items_v2'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items_v2'][0]['added_at']); + $this->assertNotEmpty($response['items_v2'][0]['bundle_options']); + $bundleOptions = $response['items_v2'][0]['bundle_options']; + $this->assertEquals('Option 1', $bundleOptions[0]['label']); + $bundleOptionOneValues = $bundleOptions[0]['values']; + $this->assertEquals(7, $bundleOptionOneValues[0]['quantity']); + $this->assertEquals('Option 2', $bundleOptions[1]['label']); + $bundleOptionTwoValues = $bundleOptions[1]['values']; + $this->assertEquals(1, $bundleOptionTwoValues[0]['quantity']); + } + /** * Authentication header map * @@ -179,6 +219,118 @@ private function getQuery( MUTATION; } + /** + * Query with custom option quantity + * + * @param string $sku + * @param int $qty + * @param string $uId0 + * @param string $uId1 + * @param int $wishlistId + * @return string + */ + private function getQueryWithCustomOptionQuantity( + string $sku, + int $qty, + string $uId0, + string $uId1, + int $wishlistId = 0 + ): string { + return << Date: Tue, 29 Sep 2020 11:06:26 +0300 Subject: [PATCH 0631/1013] MC-36405: Reorder is not working with custom options date with JavaScript Calendar enabled --- .../Sales/Controller/Order/ReorderTest.php | 27 +++++++ .../order_with_js_date_option_product.php | 81 +++++++++++++++++++ ...r_with_js_date_option_product_rollback.php | 12 +++ 3 files changed, 120 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php index 3b32e7238cc76..b0b1cd7262906 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php @@ -136,6 +136,33 @@ public function testReorderByAnotherCustomer(): void } } + /** + * Reorder with JS calendar options + * + * @magentoDataFixture Magento/Sales/_files/order_with_js_date_option_product.php + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * + * @return void + */ + public function testReorderWithJSCalendar(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $items = $order->getItems(); + $orderItem = array_pop($items); + $orderRequestOptions = $orderItem->getProductOptionByCode('info_buyRequest')['options']; + $order->save(); + $this->customerSession->setCustomerId($order->getCustomerId()); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('checkout/cart')); + $this->quote = $this->checkoutSession->getQuote(); + $quoteItemsCollection = $this->quote->getItemsCollection(); + $this->assertCount(1, $quoteItemsCollection); + $items = $this->quote->getItems(); + $quoteItem = array_pop($items); + $quoteRequestOptions = $quoteItem->getBuyRequest()->getOptions(); + $this->assertEquals($orderRequestOptions, $quoteRequestOptions); + } + /** * Dispatch reorder request. * diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product.php new file mode 100644 index 0000000000000..bdf576cfb5182 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product.php @@ -0,0 +1,81 @@ +requireDataFixture('Magento/Customer/_files/customer.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'); + +/** @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' => [ + 'date' => '09/30/2022', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + '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->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->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product_rollback.php new file mode 100644 index 0000000000000..0966f21645e3b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_js_date_option_product_rollback.php @@ -0,0 +1,12 @@ +requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); From 9f81b49ed1cc3ccf4c9abd437657bcfdacc5a8ab Mon Sep 17 00:00:00 2001 From: engcom-Echo Date: Tue, 29 Sep 2020 12:34:56 +0300 Subject: [PATCH 0632/1013] return to source, plugin added --- .../Model/SetSaveRewriteHistory.php | 69 ---------- ....php => CategorySetSaveRewriteHistory.php} | 2 +- .../Rest/CategoryInputParamsResolver.php | 56 --------- .../Controller/Rest/InputParamsResolver.php | 88 +++++++++++++ .../Rest/ProductInputParamsResolver.php | 56 --------- .../Rest/InputParamsResolverTest.php | 118 ++++++++++++++++++ .../Rest/ProductInputParamsResolverTest.php | 107 ---------------- .../CatalogUrlRewrite/etc/webapi_rest/di.xml | 5 +- 8 files changed, 209 insertions(+), 292 deletions(-) delete mode 100644 app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php rename app/code/Magento/CatalogUrlRewrite/Plugin/Model/{UpdateCategoryDataList.php => CategorySetSaveRewriteHistory.php} (98%) delete mode 100644 app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php delete mode 100644 app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php create mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php delete mode 100644 app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php diff --git a/app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php b/app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php deleted file mode 100644 index bb6119a9e8334..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Model/SetSaveRewriteHistory.php +++ /dev/null @@ -1,69 +0,0 @@ -request = $request; - } - - /** - * Add 'save_rewrites_history' param to the data - * - * @param array $result - * @param string $entityCode - * @param string $type - * @return mixed - */ - public function execute($result, $entityCode, $type) - { - $requestBodyParams = $this->request->getBodyParams(); - - if ($this->isCustomAttributesExists($requestBodyParams, $entityCode)) { - foreach ($requestBodyParams[$entityCode]['custom_attributes'] as $attribute) { - if ($attribute['attribute_code'] === self::SAVE_REWRITES_HISTORY) { - foreach ($result as $resultItem) { - if ($resultItem instanceof $type) { - $resultItem->setData(self::SAVE_REWRITES_HISTORY, (bool)$attribute['value']); - break 2; - } - } - break; - } - } - } - - return $result; - } - - /** - * Check is any custom options exists in data - * - * @param array $requestBodyParams - * @param string $entityCode - * @return bool - */ - private function isCustomAttributesExists(array $requestBodyParams, string $entityCode): bool - { - return !empty($requestBodyParams[$entityCode]['custom_attributes']); - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Model/UpdateCategoryDataList.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php similarity index 98% rename from app/code/Magento/CatalogUrlRewrite/Plugin/Model/UpdateCategoryDataList.php rename to app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php index eb9c28c62bd8b..3f83e379f303e 100644 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Model/UpdateCategoryDataList.php +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php @@ -11,7 +11,7 @@ use Magento\Framework\Webapi\Rest\Request as RestRequest; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -class UpdateCategoryDataList +class CategorySetSaveRewriteHistory { private const SAVE_REWRITES_HISTORY = 'save_rewrites_history'; diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php deleted file mode 100644 index 000562f4ef3eb..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/CategoryInputParamsResolver.php +++ /dev/null @@ -1,56 +0,0 @@ -rewriteHistory = $rewriteHistory; - } - - /** - * Add 'save_rewrites_history' param to the category data - * - * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper - * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject - * @param array $result - * @return array - */ - public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array - { - $route = $subject->getRoute(); - - if ($route->getServiceClass() === CategoryRepositoryInterface::class && $route->getServiceMethod() === 'save') { - $result = $this->rewriteHistory->execute( - $result, - 'category', - Category::class - ); - } - - return $result; - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php new file mode 100644 index 0000000000000..4e8e3840693a5 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/InputParamsResolver.php @@ -0,0 +1,88 @@ +request = $request; + } + + /** + * Add 'save_rewrites_history' param to the product data + * + * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper + * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject + * @param array $result + * @return array + */ + public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array + { + $route = $subject->getRoute(); + $serviceMethodName = $route->getServiceMethod(); + $serviceClassName = $route->getServiceClass(); + $requestBodyParams = $this->request->getBodyParams(); + + if ($this->isProductSaveCalled($serviceClassName, $serviceMethodName) + && $this->isCustomAttributesExists($requestBodyParams)) { + foreach ($requestBodyParams['product']['custom_attributes'] as $attribute) { + if ($attribute['attribute_code'] === 'save_rewrites_history') { + foreach ($result as $resultItem) { + if ($resultItem instanceof \Magento\Catalog\Model\Product) { + $resultItem->setData('save_rewrites_history', (bool)$attribute['value']); + break 2; + } + } + break; + } + } + } + return $result; + } + + /** + * Check that product save method called + * + * @param string $serviceClassName + * @param string $serviceMethodName + * @return bool + */ + private function isProductSaveCalled(string $serviceClassName, string $serviceMethodName): bool + { + return $serviceClassName === ProductRepositoryInterface::class && $serviceMethodName === 'save'; + } + + /** + * Check is any custom options exists in product data + * + * @param array $requestBodyParams + * @return bool + */ + private function isCustomAttributesExists(array $requestBodyParams): bool + { + return !empty($requestBodyParams['product']['custom_attributes']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php deleted file mode 100644 index 8334a52ea5bc0..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Webapi/Controller/Rest/ProductInputParamsResolver.php +++ /dev/null @@ -1,56 +0,0 @@ -rewriteHistory = $rewriteHistory; - } - - /** - * Add 'save_rewrites_history' param to the product data - * - * @see \Magento\CatalogUrlRewrite\Plugin\Catalog\Controller\Adminhtml\Product\Initialization\Helper - * @param \Magento\Webapi\Controller\Rest\InputParamsResolver $subject - * @param array $result - * @return array - */ - public function afterResolve(\Magento\Webapi\Controller\Rest\InputParamsResolver $subject, array $result): array - { - $route = $subject->getRoute(); - - if ($route->getServiceClass() === ProductRepositoryInterface::class && $route->getServiceMethod() === 'save') { - $result = $this->rewriteHistory->execute( - $result, - 'product', - Product::class - ); - } - - return $result; - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php new file mode 100644 index 0000000000000..5edc463ac49f3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/InputParamsResolverTest.php @@ -0,0 +1,118 @@ +saveRewritesHistory = 'save_rewrites_history'; + $this->requestBodyParams = [ + 'product' => [ + 'sku' => 'test', + 'custom_attributes' => [ + ['attribute_code' => $this->saveRewritesHistory, 'value' => 1] + ] + ] + ]; + + $this->route = $this->createPartialMock(Route::class, ['getServiceMethod', 'getServiceClass']); + $this->request = $this->createPartialMock(RestRequest::class, ['getBodyParams']); + $this->request->expects($this->any())->method('getBodyParams')->willReturn($this->requestBodyParams); + $this->subject = $this->createPartialMock(InputParamsResolver::class, ['getRoute']); + $this->subject->expects($this->any())->method('getRoute')->willReturn($this->route); + $this->product = $this->createPartialMock(Product::class, ['setData']); + + $this->result = [false, $this->product, 'test']; + + $this->objectManager = new ObjectManager($this); + $this->plugin = $this->objectManager->getObject( + InputParamsResolverPlugin::class, + [ + 'request' => $this->request + ] + ); + } + + public function testAfterResolve() + { + $this->route->expects($this->once()) + ->method('getServiceClass') + ->willReturn(ProductRepositoryInterface::class); + $this->route->expects($this->once()) + ->method('getServiceMethod') + ->willReturn('save'); + $this->product->expects($this->once()) + ->method('setData') + ->with($this->saveRewritesHistory, true); + + $this->plugin->afterResolve($this->subject, $this->result); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php deleted file mode 100644 index 38517e26472f8..0000000000000 --- a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Webapi/Controller/Rest/ProductInputParamsResolverTest.php +++ /dev/null @@ -1,107 +0,0 @@ -route = $this->createMock(Route::class); - $this->request = $this->createMock(RestRequest::class); - $this->subject = $this->createMock(InputParamsResolver::class); - $this->product = $this->createMock(Product::class); - $this->rewriteHistoryMock = $this->createMock(SetSaveRewriteHistory::class); - } - - public function testAfterResolveWithProduct() - { - $this->subject->expects($this->any()) - ->method('getRoute') - ->willReturn($this->route); - - $this->result = [false, $this->product, 'test']; - - $this->objectManager = new ObjectManager($this); - $this->plugin = $this->objectManager->getObject( - ProductInputParamsResolver::class, - [ - 'rewriteHistory' => $this->rewriteHistoryMock - ] - ); - - $this->route->expects($this->once()) - ->method('getServiceClass') - ->willReturn(ProductRepositoryInterface::class); - $this->route->expects($this->once()) - ->method('getServiceMethod') - ->willReturn('save'); - $this->rewriteHistoryMock->expects($this->once()) - ->method('execute') - ->with($this->result, 'product', Product::class) - ->willReturn($this->result); - - $this->plugin->afterResolve($this->subject, $this->result); - } -} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml index be3901cb57a2f..9348b03d17270 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/webapi_rest/di.xml @@ -7,10 +7,9 @@ --> - - + - + From 4b9f004015721e0bb161fdc9f4b6d98120573185 Mon Sep 17 00:00:00 2001 From: engcom-Echo Date: Tue, 29 Sep 2020 12:39:37 +0300 Subject: [PATCH 0633/1013] update severity, added testCaseId --- .../Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml index a79651a39cc37..776b5b9b70f33 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryUrlRewriteDifferentStoreTest.xml @@ -13,7 +13,8 @@ <description value="Verify url category for different store view, after change ukr_key category for one of them store view."/> <features value="CatalogUrlRewrite"/> - <severity value="MAJOR"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-38053"/> </annotations> <before> <magentoCLI command="config:set catalog/seo/product_use_categories 1" stepKey="setEnableUseCategoriesPath"/> From d9b3eaa2e19a395f1190e426cb57d638e4497a1d Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Tue, 29 Sep 2020 12:42:09 +0300 Subject: [PATCH 0634/1013] MC-36967: Create automated test for "Check that restricted countries on default websites are not restricted on other sites when editing addresses in an existing order" --- .../Adminhtml/Order/Address/FormTest.php | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) 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 index 0a8db20d86966..612bdd11be472 100644 --- 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 @@ -10,7 +10,10 @@ 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\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\App\Config; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -46,6 +49,7 @@ protected function setUp(): void $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Form::class); $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); $this->registry = $this->objectManager->get(Registry::class); + $this->objectManager->removeSharedInstance(Config::class); } /** @@ -65,11 +69,86 @@ protected function tearDown(): 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); + $address = $this->getOrderAddress('100000001'); + $this->prepareFormBlock($address); $formValues = $this->block->getFormValues(); $this->assertEquals($address->getData(), $formValues); } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoConfigFixture default_store general/country/default US + * @magentoConfigFixture default_store general/country/allow US + * @magentoConfigFixture fixture_second_store_store general/country/default UY + * @magentoConfigFixture fixture_second_store_store general/country/allow UY + * @return void + */ + public function testCountryIdInAllowedList(): void + { + $address = $this->getOrderAddress('100000001'); + $this->prepareFormBlock($address); + $this->assertEquals('US', $address->getCountryId()); + $this->assertCountryField('US'); + } + + /** + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoConfigFixture default_store general/country/default CA + * @magentoConfigFixture default_store general/country/allow CA + * @magentoConfigFixture fixture_second_store_store general/country/default UY + * @magentoConfigFixture fixture_second_store_store general/country/allow UY + * @return void + */ + public function testCountryIdInNotAllowedList(): void + { + $address = $this->getOrderAddress('100000001'); + $this->prepareFormBlock($address); + $this->assertCountryField('CA'); + } + + /** + * Prepares address edit from block. + * + * @param OrderAddressInterface $address + * @return void + */ + private function prepareFormBlock(OrderAddressInterface $address): void + { + $this->registry->unregister('order_address'); + $this->registry->register('order_address', $address); + } + + /** + * Return order billing address. + * + * @param string $orderIncrementId + * @return OrderAddressInterface + */ + private function getOrderAddress(string $orderIncrementId): OrderAddressInterface + { + /** @var OrderInterface $order */ + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrementId); + + return $order->getBillingAddress(); + } + + /** + * Asserts country field data. + * + * @param string $countryCode + * @return void + */ + private function assertCountryField(string $countryCode): void + { + $countryIdField = $this->block->getForm()->getElement('country_id'); + $this->assertEquals($countryCode, $countryIdField->getValue()); + $options = $countryIdField->getValues(); + $this->assertCount(1, $options); + $firstOption = reset($options); + $this->assertEquals($countryCode, $firstOption['value']); + } } From 19e9f8518b7b967f7192917c361e44ac233e44d9 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 29 Sep 2020 14:09:04 +0300 Subject: [PATCH 0635/1013] MC-36405: Reorder is not working with custom options date with JavaScript Calendar enabled --- .../testsuite/Magento/Sales/Controller/Order/ReorderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php index b0b1cd7262906..6a508e9a95eec 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Order/ReorderTest.php @@ -157,7 +157,7 @@ public function testReorderWithJSCalendar(): void $this->quote = $this->checkoutSession->getQuote(); $quoteItemsCollection = $this->quote->getItemsCollection(); $this->assertCount(1, $quoteItemsCollection); - $items = $this->quote->getItems(); + $items = $quoteItemsCollection->getItems(); $quoteItem = array_pop($items); $quoteRequestOptions = $quoteItem->getBuyRequest()->getOptions(); $this->assertEquals($orderRequestOptions, $quoteRequestOptions); From bf2b4bf9a346714f3d92a86d528e2078aff90385 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Tue, 29 Sep 2020 17:25:39 +0530 Subject: [PATCH 0636/1013] 30179: resetPassword mutation returns generic error - fixed the error text issue --- .../Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php index fa2ae669cc89d..a098325c820d6 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php @@ -118,7 +118,7 @@ public function resolve( $args['newPassword'] ); } catch (LocalizedException $e) { - throw new GraphQlInputException(__('Cannot set the customer\'s password'), $e); + throw new GraphQlInputException(__($e->getMessage()), $e); } } } From 1ad44825f3ae89a97f9ca4957b519bf41e7b8f5b Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Tue, 29 Sep 2020 15:44:20 +0300 Subject: [PATCH 0637/1013] fix static --- .../Plugin/Model/CategorySetSaveRewriteHistory.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php index 3f83e379f303e..b7e4ef12f76d2 100644 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Model/CategorySetSaveRewriteHistory.php @@ -33,11 +33,17 @@ public function __construct(RestRequest $request) * * @param CategoryUrlRewriteGenerator $subject * @param Category $category - * @return void + * @param bool $overrideStoreUrls + * @param int|null $rootCategoryId + * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeGenerate(CategoryUrlRewriteGenerator $subject, Category $category) - { + public function beforeGenerate( + CategoryUrlRewriteGenerator $subject, + Category $category, + bool $overrideStoreUrls = false, + ?int $rootCategoryId = null + ) { $requestBodyParams = $this->request->getBodyParams(); if ($this->isCustomAttributesExists($requestBodyParams, CategoryUrlRewriteGenerator::ENTITY_TYPE)) { @@ -47,6 +53,8 @@ public function beforeGenerate(CategoryUrlRewriteGenerator $subject, Category $c } } } + + return [$category, $overrideStoreUrls, $rootCategoryId]; } /** From cd25b828c392d2f994f9fd3dd3cc21a9105e0d1e Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Tue, 29 Sep 2020 15:48:23 +0300 Subject: [PATCH 0638/1013] MC-36967: Create automated test for "Check that restricted countries on default websites are not restricted on other sites when editing addresses in an existing order" --- .../Sales/Block/Adminhtml/Order/Address/FormTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 612bdd11be472..493bf7ec37ec3 100644 --- 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 @@ -70,7 +70,7 @@ protected function tearDown(): void public function testGetFormValues(): void { $address = $this->getOrderAddress('100000001'); - $this->prepareFormBlock($address); + $this->registerOrderAddress($address); $formValues = $this->block->getFormValues(); $this->assertEquals($address->getData(), $formValues); } @@ -88,7 +88,7 @@ public function testGetFormValues(): void public function testCountryIdInAllowedList(): void { $address = $this->getOrderAddress('100000001'); - $this->prepareFormBlock($address); + $this->registerOrderAddress($address); $this->assertEquals('US', $address->getCountryId()); $this->assertCountryField('US'); } @@ -106,7 +106,7 @@ public function testCountryIdInAllowedList(): void public function testCountryIdInNotAllowedList(): void { $address = $this->getOrderAddress('100000001'); - $this->prepareFormBlock($address); + $this->registerOrderAddress($address); $this->assertCountryField('CA'); } @@ -116,7 +116,7 @@ public function testCountryIdInNotAllowedList(): void * @param OrderAddressInterface $address * @return void */ - private function prepareFormBlock(OrderAddressInterface $address): void + private function registerOrderAddress(OrderAddressInterface $address): void { $this->registry->unregister('order_address'); $this->registry->register('order_address', $address); From 09d13b8c2b13df0bd376c8b2ebbb3b81effdc252 Mon Sep 17 00:00:00 2001 From: Bas van Poppel <vanpoppel@redkiwi.nl> Date: Tue, 29 Sep 2020 15:57:59 +0200 Subject: [PATCH 0639/1013] Test showing that a discount applied by a salesrule affects the table rate shipping method --- ...sAppliedOnPackageValueForTableRateTest.xml | 94 +++++++++++++++++++ .../Data/TableRatesShippingMethodData.xml | 6 ++ 2 files changed, 100 insertions(+) create mode 100644 app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml new file mode 100644 index 0000000000000..ef87696c37718 --- /dev/null +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest"> + <annotations> + <features value="Shipping"/> + <stories value="Offline Shipping Methods"/> + <title value="SalesRule Discount Is Applied On PackageValue For TableRate"/> + <description value="SalesRule Discount Is Applied On PackageValue For TableRate"/> + <severity value="AVERAGE"/> + <group value="shipping"/> + </annotations> + <before> + <!-- Add simple product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">13.00</field> + </createData> + + <!-- Create cart price rule --> + <createData entity="ActiveSalesRuleForNotLoggedIn" stepKey="createCartPriceRule"/> + <createData entity="SimpleSalesRuleCoupon" stepKey="createCouponForCartPriceRule"> + <requiredEntity createDataKey="createCartPriceRule"/> + </createData> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + + <!-- Enable Table Rate method and save config --> + <actionGroup ref="AdminChangeTableRatesShippingMethodStatusActionGroup" stepKey="enableTableRatesShippingMethod"/> + + <!-- Uncheck Use Default checkbox for Default Condition --> + <uncheckOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" stepKey="disableUseDefaultCondition"/> + + <!-- Make sure you have Condition Price vs. Destination --> + <selectOption selector="{{AdminShippingMethodTableRatesSection.condition}}" userInput="{{TableRateShippingMethodConfig.package_value_with_discount}}" stepKey="setCondition"/> + + <!-- Import file and save config --> + <attachFile selector="{{AdminShippingMethodTableRatesSection.importFile}}" userInput="usa_tablerates.csv" stepKey="attachFileForImport"/> + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + </before> + <after> + <!-- Go to Stores > Configuration > Sales > Shipping Methods --> + <actionGroup ref="AdminOpenShippingMethodsConfigPageActionGroup" stepKey="openShippingMethodConfigPage"/> + + <!-- Switch to Website scope --> + <actionGroup ref="AdminSwitchWebsiteActionGroup" stepKey="AdminSwitchStoreView"> + <argument name="website" value="_defaultWebsite"/> + </actionGroup> + + <!-- Check Use Default checkbox for Default Condition and Active --> + <checkOption selector="{{AdminShippingMethodTableRatesSection.carriersTableRateConditionName}}" stepKey="enableUseDefaultCondition"/> + <checkOption selector="{{AdminShippingMethodTableRatesSection.enabledUseSystemValue}}" stepKey="enableUseDefaultActive"/> + + <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfigs"/> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Remove simple product--> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete sales rule --> + <deleteData createDataKey="createCartPriceRule" stepKey="deleteCartPriceRule"/> + + </after> + <!-- Add simple product to cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Assert that table rate value is correct for US --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCheckout"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.estimateShippingAndTaxForm}}" stepKey="waitForEstimateShippingAndTaxForm"/> + <waitForElement time="30" selector="{{CheckoutCartSummarySection.shippingMethodForm}}" stepKey="waitForShippingMethodForm"/> + <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingandTax" /> + <selectOption selector="{{CheckoutCartSummarySection.country}}" userInput="United States" stepKey="selectUSCountry"/> + <waitForPageLoad stepKey="waitForSelectCountry"/> + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$5.99" stepKey="seeShippingForUS"/> + + <!-- Apply Coupon --> + <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyDiscount"> + <argument name="coupon" value="$$createCouponForCartPriceRule$$"/> + </actionGroup> + + <see selector="{{CheckoutCartSummarySection.shippingPrice}}" userInput="$7.99" stepKey="seeShippingForUSWithDiscount"/> + </test> +</tests> diff --git a/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml index 47ef68cc9d765..ceae9c546bd3b 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Data/TableRatesShippingMethodData.xml @@ -16,4 +16,10 @@ <data key="title">Best Way</data> <data key="methodName">Table Rate</data> </entity> + <!-- Set Table Rate Shipping method Condition --> + <entity name="TableRateShippingMethodConfig" type="shipping_method"> + <data key="package_weight">Weight vs. Destination</data> + <data key="package_value_with_discount">Price vs. Destination</data> + <data key="package_qty"># of Items vs. Destination</data> + </entity> </entities> From 7955c8b16a990cd1acf603dd5b1819c6b16e99dc Mon Sep 17 00:00:00 2001 From: klg <k.langenberg@imi.de> Date: Tue, 29 Sep 2020 16:26:15 +0200 Subject: [PATCH 0640/1013] Change docblock annotation for PublisherInterface message to mixed --- .../Magento/Framework/MessageQueue/PublisherInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php index 282d06dc143f4..10b2eaf347dd9 100644 --- a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php @@ -18,7 +18,7 @@ interface PublisherInterface * Publishes a message to a specific queue or exchange. * * @param string $topicName - * @param $data + * @param mixed $data * @return null|mixed * @throws \InvalidArgumentException If message is not formed properly * @since 103.0.0 From 460252f762bd9ee5af2fb880f4b3b242f471a193 Mon Sep 17 00:00:00 2001 From: Bas van Poppel <vanpoppel@redkiwi.nl> Date: Tue, 29 Sep 2020 16:59:43 +0200 Subject: [PATCH 0641/1013] Added copyright to test --- ...uleDiscountIsAppliedOnPackageValueForTableRateTest.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml index ef87696c37718..52d456bc6225d 100644 --- a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml @@ -1,4 +1,10 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?xml version="1.0" encoding="UTF-8"?><!-- + /** + * 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="SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest"> <annotations> From c55bccee9790c082daea29f374d76f5ecdf806fe Mon Sep 17 00:00:00 2001 From: Marjan <petkovski.marjan@gmail.com> Date: Tue, 29 Sep 2020 20:30:14 +0200 Subject: [PATCH 0642/1013] magento/magento2#29880: GraphQL categories and categoryList do not consider Category Permissions configuration PR review points --- .../Magento/CatalogGraphQl/Model/Category/CategoryFilter.php | 5 +++-- .../Category/CollectionProcessor/CatalogProcessor.php | 3 +++ .../DataProvider/Category/CollectionProcessorInterface.php | 2 ++ .../DataProvider/Category/CompositeCollectionProcessor.php | 4 +++- .../CatalogGraphQl/Model/Resolver/CategoriesQuery.php | 2 +- .../Magento/CatalogGraphQl/Model/Resolver/CategoryList.php | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php index 9d2a175b97fb5..4350b6dd85266 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -83,16 +83,17 @@ public function __construct( * * @param array $criteria * @param StoreInterface $store + * @param array $attributeNames * @param ContextInterface $context * @return int[] * @throws InputException */ - public function getResult(array $criteria, StoreInterface $store, ContextInterface $context) + public function getResult(array $criteria, StoreInterface $store, array $attributeNames, ContextInterface $context) { $searchCriteria = $this->searchCriteria->buildCriteria($criteria, $store); $collection = $this->categoryCollectionFactory->create(); $this->extensionAttributesJoinProcessor->process($collection); - $this->collectionProcessor->process($collection, $searchCriteria, $context); + $this->collectionProcessor->process($collection, $searchCriteria, $attributeNames, $context); /** @var CategorySearchResultsInterface $searchResult */ $categories = $this->categorySearchResultsFactory->create(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php index f40ae6210e442..c8f9ad5de008f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessor/CatalogProcessor.php @@ -37,12 +37,15 @@ public function __construct( * * @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, ContextInterface $context = null ): Collection { $this->collectionProcessor->process($searchCriteria, $collection); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php index a9e35c6f7970f..5e79064e9acfa 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CollectionProcessorInterface.php @@ -21,12 +21,14 @@ 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, ContextInterface $context = null ): Collection; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php index ce42280efec07..0ab76606f5dce 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories/DataProvider/Category/CompositeCollectionProcessor.php @@ -36,16 +36,18 @@ public function __construct(array $collectionProcessors = []) * * @param Collection $collection * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames * @param ContextInterface|null $context * @return Collection */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, + array $attributeNames, ContextInterface $context = null ): Collection { foreach ($this->collectionProcessors as $collectionProcessor) { - $collection = $collectionProcessor->process($collection, $searchCriteria, $context); + $collection = $collectionProcessor->process($collection, $searchCriteria, $attributeNames, $context); } return $collection; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php index eb5a2f6c90861..4d7ce13fd23cc 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoriesQuery.php @@ -70,7 +70,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value } try { - $filterResult = $this->categoryFilter->getResult($args, $store, $context); + $filterResult = $this->categoryFilter->getResult($args, $store, [], $context); } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php index bc474686bead1..13db03bb2766b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/CategoryList.php @@ -65,7 +65,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $args['filters']['ids'] = ['eq' => $store->getRootCategoryId()]; } try { - $filterResults = $this->categoryFilter->getResult($args, $store, $context); + $filterResults = $this->categoryFilter->getResult($args, $store, [], $context); $rootCategoryIds = $filterResults['category_ids']; } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage())); From 9f1bfa614d3b1e919a2a0a068cadd8c758c9b898 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 29 Sep 2020 14:38:02 -0500 Subject: [PATCH 0643/1013] MC-35996: "Port number must be configured" error in Advanced Reporting cron occurs even when "port" parameter is configured in env.php --- .../Analytics/ReportXml/ConnectionFactory.php | 55 +++++++----- .../Unit/ReportXml/ConnectionFactoryTest.php | 88 +++++++------------ app/code/Magento/Analytics/etc/di.xml | 5 ++ 3 files changed, 70 insertions(+), 78 deletions(-) diff --git a/app/code/Magento/Analytics/ReportXml/ConnectionFactory.php b/app/code/Magento/Analytics/ReportXml/ConnectionFactory.php index 432d011e593ec..4c280729ff75f 100644 --- a/app/code/Magento/Analytics/ReportXml/ConnectionFactory.php +++ b/app/code/Magento/Analytics/ReportXml/ConnectionFactory.php @@ -6,56 +6,65 @@ namespace Magento\Analytics\ReportXml; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ResourceConnection\ConfigInterface as ResourceConfigInterface; +use Magento\Framework\Config\ConfigOptionsListConstants; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Model\ResourceModel\Type\Db\ConnectionFactoryInterface; /** * Creates connection instance for export according to existing one + * * This connection does not use buffered statement, also this connection is not persistent */ class ConnectionFactory { /** - * @var ResourceConnection + * @var ResourceConfigInterface */ - private $resourceConnection; + private $resourceConfig; /** - * @var ObjectManagerInterface + * @var DeploymentConfig */ - private $objectManager; + private $deploymentConfig; /** - * @param ResourceConnection $resourceConnection - * @param ObjectManagerInterface $objectManager + * @var ConnectionFactoryInterface + */ + private $connectionFactory; + + /** + * @param ResourceConfigInterface $resourceConfig + * @param DeploymentConfig $deploymentConfig + * @param ConnectionFactoryInterface $connectionFactory */ public function __construct( - ResourceConnection $resourceConnection, - ObjectManagerInterface $objectManager + ResourceConfigInterface $resourceConfig, + DeploymentConfig $deploymentConfig, + ConnectionFactoryInterface $connectionFactory ) { - $this->resourceConnection = $resourceConnection; - $this->objectManager = $objectManager; + $this->resourceConfig = $resourceConfig; + $this->deploymentConfig = $deploymentConfig; + $this->connectionFactory = $connectionFactory; } /** * Creates one-time connection for export * - * @param string $connectionName + * @param string $resourceName * @return AdapterInterface */ - public function getConnection($connectionName) + public function getConnection($resourceName) { - $connection = $this->resourceConnection->getConnection($connectionName); - $connectionClassName = get_class($connection); - $configData = $connection->getConfig(); + $connectionName = $this->resourceConfig->getConnectionName($resourceName); + $configData = $this->deploymentConfig->get( + ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/' . $connectionName + ); $configData['use_buffered_query'] = false; unset($configData['persistent']); - return $this->objectManager->create( - $connectionClassName, - [ - 'config' => $configData - ] - ); + $connection = $this->connectionFactory->create($configData); + + return $connection; } } diff --git a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php index 16caaa380067f..9be5bea8d7d05 100644 --- a/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php +++ b/app/code/Magento/Analytics/Test/Unit/ReportXml/ConnectionFactoryTest.php @@ -8,40 +8,29 @@ namespace Magento\Analytics\Test\Unit\ReportXml; use Magento\Analytics\ReportXml\ConnectionFactory; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\ResourceConnection\ConfigInterface as ResourceConfigInterface; use Magento\Framework\DB\Adapter\AdapterInterface; -use Magento\Framework\DB\Adapter\Pdo\Mysql as MysqlPdoAdapter; -use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Framework\Model\ResourceModel\Type\Db\ConnectionFactoryInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class ConnectionFactoryTest extends TestCase { /** - * @var ResourceConnection|MockObject + * @var ResourceConfigInterface|MockObject */ - private $resourceConnectionMock; + private $resourceConfigMock; /** - * @var ObjectManagerInterface|MockObject + * @var DeploymentConfig|MockObject */ - private $objectManagerMock; + private $deploymentConfigMock; /** - * @var ConnectionFactory|MockObject + * @var ConnectionFactoryInterface|MockObject */ - private $connectionNewMock; - - /** - * @var AdapterInterface|MockObject - */ - private $connectionMock; - - /** - * @var ObjectManagerHelper - */ - private $objectManagerHelper; + private $connectionFactoryMock; /** * @var ConnectionFactory @@ -53,47 +42,36 @@ class ConnectionFactoryTest extends TestCase */ protected function setUp(): void { - $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); - - $this->objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); - - $this->connectionMock = $this->createMock(MysqlPdoAdapter::class); - - $this->connectionNewMock = $this->createMock(MysqlPdoAdapter::class); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->connectionFactory = $this->objectManagerHelper->getObject( - ConnectionFactory::class, - [ - 'resourceConnection' => $this->resourceConnectionMock, - 'objectManager' => $this->objectManagerMock, - ] + $this->resourceConfigMock = $this->createMock(ResourceConfigInterface::class); + $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); + $this->connectionFactoryMock = $this->createMock(ConnectionFactoryInterface::class); + + $this->connectionFactory = new ConnectionFactory( + $this->resourceConfigMock, + $this->deploymentConfigMock, + $this->connectionFactoryMock ); } public function testGetConnection() { - $connectionName = 'read'; - - $this->resourceConnectionMock - ->expects($this->once()) - ->method('getConnection') - ->with($connectionName) - ->willReturn($this->connectionMock); - - $this->connectionMock - ->expects($this->once()) - ->method('getConfig') - ->with() - ->willReturn(['persistent' => 1]); - - $this->objectManagerMock - ->expects($this->once()) + $resourceName = 'default'; + + $this->resourceConfigMock->expects($this->once()) + ->method('getConnectionName') + ->with($resourceName) + ->willReturn('default'); + $this->deploymentConfigMock->expects($this->once()) + ->method('get') + ->with('db/connection/default') + ->willReturn(['host' => 'localhost', 'port' => 3306, 'persistent' => true]); + $connectionMock = $this->createMock(AdapterInterface::class); + $this->connectionFactoryMock->expects($this->once()) ->method('create') - ->with(get_class($this->connectionMock), ['config' => ['use_buffered_query' => false]]) - ->willReturn($this->connectionNewMock); + ->with(['host' => 'localhost', 'port' => 3306, 'use_buffered_query' => false]) + ->willReturn($connectionMock); - $this->assertSame($this->connectionNewMock, $this->connectionFactory->getConnection($connectionName)); + $connection = $this->connectionFactory->getConnection($resourceName); + $this->assertSame($connectionMock, $connection); } } diff --git a/app/code/Magento/Analytics/etc/di.xml b/app/code/Magento/Analytics/etc/di.xml index 8d8ce4f83a605..0a57676b5fb8f 100644 --- a/app/code/Magento/Analytics/etc/di.xml +++ b/app/code/Magento/Analytics/etc/di.xml @@ -266,4 +266,9 @@ </argument> </arguments> </type> + <type name="Magento\Analytics\ReportXml\ConnectionFactory"> + <arguments> + <argument name="connectionFactory" xsi:type="object">Magento\Framework\Model\ResourceModel\Type\Db\ConnectionFactory</argument> + </arguments> + </type> </config> From 29b379f81a4db2e5f504a331ce3d4e58d286df16 Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Tue, 29 Sep 2020 16:17:06 -0500 Subject: [PATCH 0644/1013] MC-37727: Filter attributes labels are not translated --- .../AttributeOptionProvider.php | 9 ++- .../Catalog/ProductAttributeStoreTest.php | 61 +++++++++++++++++++ ...red_navigation_attribute_store_options.php | 6 +- ...ation_attribute_store_options_rollback.php | 6 -- 4 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreTest.php diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index 140659abfbfe6..c1ba036e542db 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -64,6 +64,13 @@ public function getOptions(array $optionIds, ?int $storeId, array $attributeCode 'attribute_label' => 'a.frontend_label', ] ) + ->joinLeft( + ['attribute_label' => $this->resourceConnection->getTableName('eav_attribute_label')], + "a.attribute_id = attribute_label.attribute_id AND attribute_label.store_id = {$storeId}", + [ + 'attribute_store_label' => 'attribute_label.value', + ] + ) ->joinLeft( ['options' => $this->resourceConnection->getTableName('eav_attribute_option')], 'a.attribute_id = options.attribute_id', @@ -119,7 +126,7 @@ private function formatResult(\Magento\Framework\DB\Select $select): array $result[$option['attribute_code']] = [ 'attribute_id' => $option['attribute_id'], 'attribute_code' => $option['attribute_code'], - 'attribute_label' => $option['attribute_label'], + 'attribute_label' => $option['attribute_store_label'] ? $option['attribute_store_label'] : $option['attribute_label'], 'options' => [], ]; } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreTest.php new file mode 100644 index 0000000000000..8cb7cec1b9a12 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreTest.php @@ -0,0 +1,61 @@ +<?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\Framework\Exception\LocalizedException; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductAttributeStoreTest extends GraphQlAbstract +{ + /** + * Test that custom attribute 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('Test Configurable Default Store'); + $this->attributeLabelTest('Test Configurable Test Store', ['Store' => 'test']); + } + + /** + * @param $expectedLabel + * @param array $headers + * @throws LocalizedException + * @throws Exception + */ + private function attributeLabelTest($expectedLabel, array $headers = []): void + { + $query = <<<QUERY +{ + products(search:"Simple", + pageSize: 3 + currentPage: 1 + ) + { + aggregations + { + attribute_code + label + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertNotEmpty($response['products']['aggregations']); + $attributes = $response['products']['aggregations']; + foreach ($attributes as $attribute) { + if ($attribute['attribute_code'] === 'test_configurable') { + $this->assertEquals($expectedLabel, $attribute['label']); + } + } + } +} 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 index c2ebfa4389ab2..69172f3edb34f 100644 --- 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 @@ -50,7 +50,11 @@ 'is_visible_on_front' => 1, 'used_in_product_listing' => 1, 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], + 'frontend_label' => [ + Store::DEFAULT_STORE_ID => 'Test Configurable Admin Store', + Store::DISTRO_STORE_ID => 'Test Configurable Default Store', + $store->getId() => 'Test Configurable Test Store' + ], 'backend_type' => 'int', 'option' => [ 'value' => ['option_0' => [ 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 index 6793051b5787b..60a8525dede24 100644 --- 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 @@ -24,12 +24,6 @@ } } -$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); From 9ef7eef2d7d6ec6ac5cca8722dd5b3f7cbf4ee40 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Tue, 29 Sep 2020 19:11:50 -0500 Subject: [PATCH 0645/1013] MC-37347: [OnPrem] Catalog Products Filter in 2.3.3 not working correctly --- .../Catalog/Ui/DataProvider/Product/ProductCollection.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php index ea8fc6f2d83b2..99391f92337b3 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php @@ -89,6 +89,8 @@ private function addAttributeToFilterAllStores(Attribute $attributeModel, array . $this->_getConditionSql("{$tableName}.value", $condition) . ') AND (' . $this->_getConditionSql("{$tableName}.attribute_id", $attributeId) + . ') AND (' + . $this->_getConditionSql("{$tableName}.store_id", $this->getDefaultStoreId()) . ')'; $selectExistsInAllStores = $this->getConnection()->select()->from($tableName); $this->getSelect()->exists($selectExistsInAllStores, $condition); From 10a0cedcfcf5cb00987e2128f5438b15d313c8b1 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Wed, 30 Sep 2020 10:08:46 +0300 Subject: [PATCH 0646/1013] MC-36405: Reorder is not working with custom options date with JavaScript Calendar enabled --- app/code/Magento/Catalog/Model/ProductOptionProcessor.php | 5 ++--- app/code/Magento/Sales/Model/AdminOrder/Create.php | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php index 443d740fa7970..db9f4de142956 100644 --- a/app/code/Magento/Catalog/Model/ProductOptionProcessor.php +++ b/app/code/Magento/Catalog/Model/ProductOptionProcessor.php @@ -10,10 +10,9 @@ use Magento\Catalog\Model\CustomOptions\CustomOptionFactory; use Magento\Framework\DataObject; use Magento\Framework\DataObject\Factory as DataObjectFactory; -use Magento\Framework\Serialize\Serializer\Json; /** - * Processor ofr product options + * Processor for product options */ class ProductOptionProcessor implements ProductOptionProcessorInterface { @@ -146,7 +145,7 @@ private function getUrlBuilder() } /** - * Returns date option value only with 'date_internal data + * Check if the option has a date_internal and date * * @param array $optionValue * @return bool diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 1f110e954d9c2..393d61b69bf22 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -667,8 +667,9 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q $productOptions = $orderItem->getProductOptions(); if ($productOptions !== null && !empty($productOptions['options'])) { $formattedOptions = []; + $useFrontendCalendar = $this->useFrontendCalendar(); foreach ($productOptions['options'] as $option) { - if (in_array($option['option_type'], ['date', 'date_time']) && $this->useFrontendCalendar()) { + if (in_array($option['option_type'], ['date', 'date_time']) && $useFrontendCalendar) { $product->setSkipCheckRequiredOption(false); break; } From 70ed61a4d88907a93440adca920b5b8fba699e33 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Wed, 30 Sep 2020 12:51:28 +0300 Subject: [PATCH 0647/1013] MC-36219: Incorrect order created date in shipment grid table. --- .../Shipping/Collection/ShipmentTest.php | 63 ++++++++++++++++--- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php index f6242432e2791..fabd2ef0021de 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Report/Shipping/Collection/ShipmentTest.php @@ -5,26 +5,35 @@ */ namespace Magento\Sales\Model\ResourceModel\Report\Shipping\Collection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Reports\Model\Item; +use Magento\Sales\Model\ResourceModel\Order\Shipment\Grid\Collection as ShipmentGridCollection; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * Integration tests for shipments reports collection which is used to obtain shipment reports by shipment date. */ -class ShipmentTest extends \PHPUnit\Framework\TestCase +class ShipmentTest extends TestCase { /** - * @var \Magento\Sales\Model\ResourceModel\Report\Shipping\Collection\Shipment + * @var Shipment */ private $collection; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); $this->collection = $this->objectManager->create( - \Magento\Sales\Model\ResourceModel\Report\Shipping\Collection\Shipment::class + Shipment::class ); $this->collection->setPeriod('day') ->setDateRange(null, null) @@ -43,11 +52,11 @@ public function testGetItems() $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); $order->loadByIncrementId('100000001'); $shipmentCreatedAt = $order->getShipmentsCollection()->getFirstItem()->getCreatedAt(); - /** @var \Magento\Framework\Stdlib\DateTime\DateTime $dateTime */ - $dateTime = $this->objectManager->create(\Magento\Framework\Stdlib\DateTime\DateTimeFactory::class) + /** @var DateTime $dateTime */ + $dateTime = $this->objectManager->create(DateTimeFactory::class) ->create(); - /** @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone */ - $timezone = $this->objectManager->create(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class); + /** @var TimezoneInterface $timezone */ + $timezone = $this->objectManager->create(TimezoneInterface::class); $shipmentCreatedAt = $timezone->formatDateTime( $shipmentCreatedAt, \IntlDateFormatter::SHORT, @@ -67,10 +76,44 @@ public function testGetItems() ], ]; $actualResult = []; - /** @var \Magento\Reports\Model\Item $reportItem */ + /** @var Item $reportItem */ foreach ($this->collection->getItems() as $reportItem) { $actualResult[] = array_intersect_key($reportItem->getData(), $expectedResult[0]); } $this->assertEquals($expectedResult, $actualResult); } + + /** + * Checks that order_created_at field does not change after sales_shipment_grid row update + * + * @magentoDataFixture Magento/Sales/_files/order_shipping.php + * @return void + */ + public function testOrderShipmentGridOrderCreatedAt(): void + { + $incrementId = '100000001'; + /** @var \Magento\Sales\Model\Order $order */ + $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); + $order->loadByIncrementId($incrementId); + /** @var ShipmentGridCollection $grid */ + $grid = $this->objectManager->get(ShipmentGridCollection::class); + $grid->getSelect() + ->where('order_increment_id', $incrementId); + $itemId = $grid->getFirstItem() + ->getEntityId(); + $connection = $grid->getResource() + ->getConnection(); + $tableName = $grid->getMainTable(); + $connection->update( + $tableName, + ['customer_name' => 'Test'], + $connection->quoteInto('entity_id = ?', $itemId) + ); + $updatedRow = $connection->select() + ->where('entity_id = ?', $itemId) + ->from($tableName, ['order_created_at']); + $orderCreatedAt = $connection->fetchOne($updatedRow); + + $this->assertEquals($order->getCreatedAt(), $orderCreatedAt); + } } From 1ca6ef5ee628d8329850ba93e90ff7ea2ae8d648 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Wed, 30 Sep 2020 12:51:49 +0300 Subject: [PATCH 0648/1013] MC-37718: Grouped product remains In Stock On Mass Update --- ...ateProductQtyAndStockStatusActionGroup.xml | 30 ++++ .../Data/ProductAttributeMassUpdateData.xml | 8 + ...dateAttributesAdvancedInventorySection.xml | 17 +++ .../Plugin/MassUpdateProductAttribute.php | 38 ++++- .../Model/Inventory/ParentItemProcessor.php | 143 +----------------- .../UpdateStockStatusGroupedProductTest.xml | 65 ++++++++ app/code/Magento/GroupedProduct/etc/di.xml | 7 + 7 files changed, 169 insertions(+), 139 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml new file mode 100644 index 0000000000000..fce287705b67c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.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"> + <!-- Update Product Name and Description attribute --> + <actionGroup name="AdminMassUpdateProductQtyAndStockStatusActionGroup"> + <arguments> + <argument name="attributes"/> + </arguments> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> + <waitForPageLoad stepKey="waitForUploadPage"/> + <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeAttributePageEditUrl"/> + <click selector="{{AdminUpdateAttributesAdvancedInventorySection.inventory}}" stepKey="openInvetoryTab"/> + <click selector="{{AdminUpdateAttributesAdvancedInventorySection.changeQty}}" stepKey="uncheckChangeQty"/> + <fillField selector="{{AdminUpdateAttributesAdvancedInventorySection.qty}}" userInput="{{attributes.qty}}" stepKey="fillFieldName"/> + <click selector="{{AdminUpdateAttributesAdvancedInventorySection.changeStockAvailability}}" stepKey="uncheckChangeStockAvailability"/> + <selectOption selector="{{AdminUpdateAttributesAdvancedInventorySection.stockAvailability}}" userInput="{{attributes.stockAvailability}}" stepKey="selectStatus"/> + <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="save"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitVisibleSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml index 99908f1c9df5f..22557972b991f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml @@ -12,4 +12,12 @@ <data key="name" unique="suffix">New Bundle Product Name</data> <data key="description" unique="suffix">This is the description</data> </entity> + <entity name="UpdateAttributeQtyAndStockToInStock" type="productAttributeMassUpdate"> + <data key="qty">10</data> + <data key="stockAvailability">In Stock</data> + </entity> + <entity name="UpdateAttributeQtyAndStockToOutOfStock" type="productAttributeMassUpdate"> + <data key="qty">0</data> + <data key="stockAvailability">Out of Stock</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml new file mode 100644 index 0000000000000..92dadbdd26c2d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.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="AdminUpdateAttributesAdvancedInventorySection"> + <element name="inventory" type="button" selector="#attributes_update_tabs_inventory"/> + <element name="changeQty" type="checkbox" selector="#inventory_qty_checkbox"/> + <element name="qty" type="input" selector="#inventory_qty"/> + <element name="changeStockAvailability" type="checkbox" selector="#inventory_stock_availability_checkbox"/> + <element name="stockAvailability" type="select" selector="//select[@name='inventory[is_in_stock]']"/> + </section> +</sections> diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index 334d2b22edbfa..b562171c95bff 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -5,11 +5,15 @@ */ namespace Magento\CatalogInventory\Plugin; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; +use Magento\Catalog\Model\Product; use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; /** - * MassUpdate product attribute. + * Around plugin for MassUpdate product attribute via product grid. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MassUpdateProductAttribute @@ -49,6 +53,15 @@ class MassUpdateProductAttribute */ private $messageManager; + /** + * @var ParentItemProcessorInterface[] + */ + private $parentItemProcessors; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; /** * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper @@ -57,6 +70,8 @@ class MassUpdateProductAttribute * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager + * @param ProductRepositoryInterface $productRepository + * @param ParentItemProcessorInterface[] $parentItemProcessors * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -66,7 +81,9 @@ public function __construct( \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository, \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, - \Magento\Framework\Message\ManagerInterface $messageManager + \Magento\Framework\Message\ManagerInterface $messageManager, + ProductRepositoryInterface $productRepository, + array $parentItemProcessors = [] ) { $this->stockIndexerProcessor = $stockIndexerProcessor; $this->dataObjectHelper = $dataObjectHelper; @@ -75,6 +92,8 @@ public function __construct( $this->stockConfiguration = $stockConfiguration; $this->attributeHelper = $attributeHelper; $this->messageManager = $messageManager; + $this->productRepository = $productRepository; + $this->parentItemProcessors = $parentItemProcessors; } /** @@ -145,6 +164,7 @@ private function addConfigSettings($inventoryData) private function updateInventoryInProducts($productIds, $websiteId, $inventoryData): void { foreach ($productIds as $productId) { + $product = $this->productRepository->getById($productId); $stockItemDo = $this->stockRegistry->getStockItem($productId, $websiteId); if (!$stockItemDo->getProductId()) { $inventoryData['product_id'] = $productId; @@ -153,7 +173,21 @@ private function updateInventoryInProducts($productIds, $websiteId, $inventoryDa $this->dataObjectHelper->populateWithArray($stockItemDo, $inventoryData, StockItemInterface::class); $stockItemDo->setItemId($stockItemId); $this->stockItemRepository->save($stockItemDo); + $this->processParents($product); } $this->stockIndexerProcessor->reindexList($productIds); } + + /** + * Process stock data for parent products + * + * @param Product $product + * @return void + */ + private function processParents(Product $product): void + { + foreach ($this->parentItemProcessors as $processor) { + $processor->process($product); + } + } } diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php index 0bb102f34dd2d..2d5113edd082e 100644 --- a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php @@ -7,17 +7,8 @@ namespace Magento\GroupedProduct\Model\Inventory; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\GroupedProduct\Model\Product\Type\Grouped; use Magento\Catalog\Api\Data\ProductInterface as Product; -use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface; -use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; -use Magento\CatalogInventory\Api\Data\StockItemInterface; -use Magento\GroupedProduct\Model\ResourceModel\Product\Link; -use Magento\Framework\App\ResourceConnection; /** * Process parent stock item for grouped product @@ -25,59 +16,17 @@ class ParentItemProcessor implements ParentItemProcessorInterface { /** - * @var Grouped + * @var ChangeParentStockStatus */ - private $groupedType; + private $changeParentStockStatus; /** - * @var StockItemRepositoryInterface - */ - private $stockItemRepository; - - /** - * @var StockConfigurationInterface - */ - private $stockConfiguration; - - /** - * @var StockItemCriteriaInterfaceFactory - */ - private $criteriaInterfaceFactory; - - /** - * Product metadata pool - * - * @var MetadataPool - */ - private $metadataPool; - - /** - * @var ResourceConnection - */ - private $resource; - - /** - * @param Grouped $groupedType - * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory - * @param StockItemRepositoryInterface $stockItemRepository - * @param StockConfigurationInterface $stockConfiguration - * @param ResourceConnection $resource - * @param MetadataPool $metadataPool + * @param ChangeParentStockStatus $changeParentStockStatus */ public function __construct( - Grouped $groupedType, - StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, - StockItemRepositoryInterface $stockItemRepository, - StockConfigurationInterface $stockConfiguration, - ResourceConnection $resource, - MetadataPool $metadataPool + ChangeParentStockStatus $changeParentStockStatus ) { - $this->groupedType = $groupedType; - $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; - $this->stockConfiguration = $stockConfiguration; - $this->stockItemRepository = $stockItemRepository; - $this->resource = $resource; - $this->metadataPool = $metadataPool; + $this->changeParentStockStatus = $changeParentStockStatus; } /** @@ -88,86 +37,6 @@ public function __construct( */ public function process(Product $product) { - $parentIds = $this->getParentEntityIdsByChild($product->getId()); - foreach ($parentIds as $productId) { - $this->processStockForParent((int)$productId); - } - } - - /** - * Change stock item for parent product depending on children stock items - * - * @param int $productId - * @return void - */ - private function processStockForParent(int $productId) - { - $criteria = $this->criteriaInterfaceFactory->create(); - $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); - $criteria->setProductsFilter($productId); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - $allItems = $stockItemCollection->getItems(); - if (empty($allItems)) { - return; - } - $parentStockItem = array_shift($allItems); - $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); - $criteria->setProductsFilter($groupedChildrenIds); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - $allItems = $stockItemCollection->getItems(); - - $groupedChildrenIsInStock = false; - - foreach ($allItems as $childItem) { - if ($childItem->getIsInStock() === true) { - $groupedChildrenIsInStock = true; - break; - } - } - - if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { - $parentStockItem->setIsInStock($groupedChildrenIsInStock); - $parentStockItem->setStockStatusChangedAuto(1); - $this->stockItemRepository->save($parentStockItem); - } - } - - /** - * Check is parent item should be updated - * - * @param StockItemInterface $parentStockItem - * @param bool $childrenIsInStock - * @return bool - */ - private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool - { - return $parentStockItem->getIsInStock() !== $childrenIsInStock && - ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); - } - - /** - * Retrieve parent ids array by child id - * - * @param int $childId - * @return string[] - */ - private function getParentEntityIdsByChild($childId) - { - $select = $this->resource->getConnection() - ->select() - ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) - ->join( - ['e' => $this->resource->getTableName('catalog_product_entity')], - 'e.' . - $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', - ['e.entity_id'] - ) - ->where('l.linked_product_id = ?', $childId) - ->where( - 'link_type_id = ?', - Link::LINK_TYPE_GROUPED - ); - - return $this->resource->getConnection()->fetchCol($select); + $this->changeParentStockStatus->execute((int)$product->getId()); } } diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml new file mode 100644 index 0000000000000..12d753c300ca5 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.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="UpdateStockStatusGroupedProductTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Create/Edit grouped product in Admin"/> + <title value="Stock status of grouped product after changing quantity of child product should be changed"/> + <description value="Change stock of grouped product after changing quantity of child product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38057"/> + <useCaseId value="MC-37718"/> + <group value="GroupedProduct"/> + </annotations> + <before> + <!--Create simple and grouped product--> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="ApiGroupedProduct" stepKey="createGroupedProduct"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <magentoCron stepKey="runCronIndex" groups="index"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + <!--Admin logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!--1.Open product grid page and choose "Update attributes" and set product stock status to "Out of Stock"--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridFirstTime"/> + <waitForPageLoad stepKey="waitForProductGridFirstTime"/> + <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="setProductToOutOfStock"> + <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> + </actionGroup> + <!--2.Run cron for updating stock status of parent product--> + <magentoCron stepKey="runAllCronJobs"/> + <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> + <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> + <argument name="productId" value="$$createGroupedProduct.id$$"/> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <!--4.Open product grid page choose "Update attributes" and set product stock status to "In Stock"--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridSecondTime"/> + <waitForPageLoad stepKey="waitForProductGridSecondTime"/> + <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="returnProductToInStock"> + <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> + </actionGroup> + <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> + <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> + <argument name="productId" value="$$createGroupedProduct.id$$"/> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/etc/di.xml b/app/code/Magento/GroupedProduct/etc/di.xml index d9534c6d3fe7d..924d2d1fc9669 100644 --- a/app/code/Magento/GroupedProduct/etc/di.xml +++ b/app/code/Magento/GroupedProduct/etc/di.xml @@ -112,4 +112,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute"> + <arguments> + <argument name="parentItemProcessorPool" xsi:type="array"> + <item name="grouped" xsi:type="object"> Magento\GroupedProduct\Model\Inventory\ParentItemProcessor</item> + </argument> + </arguments> + </type> </config> From 359f79c74573082d55d9e25cce64e4bf9cc794b7 Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Mon, 28 Sep 2020 15:08:21 -0500 Subject: [PATCH 0649/1013] MC-38003: [Magento Cloud] Indexer Performance - Possible Bug Found with Cache Clearing process - Flush indexer cache context after cleaning cache --- .../Model/Indexer/Category/Product.php | 2 +- .../Indexer/Model/Indexer/CacheCleaner.php | 1 + .../Indexer/Model/Processor/CleanCache.php | 27 +++- .../Test/Unit/Model/CacheContextTest.php | 63 +++++++-- .../Unit/Model/Indexer/CacheCleanerTest.php | 129 ++++++++++++++++++ .../Unit/Model/Processor/CleanCacheTest.php | 6 + .../GraphQl/Catalog/ProductSearchTest.php | 6 + .../Framework/Indexer/CacheContext.php | 13 +- 8 files changed, 228 insertions(+), 19 deletions(-) create mode 100644 app/code/Magento/Indexer/Test/Unit/Model/Indexer/CacheCleanerTest.php diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product.php index c18404bda1fc8..f5a8c33cfa6c9 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product.php @@ -64,8 +64,8 @@ public function __construct( */ public function execute($ids) { - $this->executeAction($ids); $this->registerEntities($ids); + $this->executeAction($ids); } /** diff --git a/app/code/Magento/Indexer/Model/Indexer/CacheCleaner.php b/app/code/Magento/Indexer/Model/Indexer/CacheCleaner.php index c75a3541ba9c3..1cf7142e07cac 100644 --- a/app/code/Magento/Indexer/Model/Indexer/CacheCleaner.php +++ b/app/code/Magento/Indexer/Model/Indexer/CacheCleaner.php @@ -95,6 +95,7 @@ private function cleanCache() $identities = $this->cacheContext->getIdentities(); if (!empty($identities)) { $this->appCache->clean($identities); + $this->cacheContext->flush(); } } } diff --git a/app/code/Magento/Indexer/Model/Processor/CleanCache.php b/app/code/Magento/Indexer/Model/Processor/CleanCache.php index 344fef6ef04ff..d7663171c8a65 100644 --- a/app/code/Magento/Indexer/Model/Processor/CleanCache.php +++ b/app/code/Magento/Indexer/Model/Processor/CleanCache.php @@ -5,8 +5,11 @@ */ namespace Magento\Indexer\Model\Processor; -use \Magento\Framework\App\CacheInterface; +use Magento\Framework\App\CacheInterface; +/** + * Clear cache after reindex + */ class CleanCache { /** @@ -46,9 +49,7 @@ public function __construct( public function afterUpdateMview(\Magento\Indexer\Model\Processor $subject) { $this->eventManager->dispatch('clean_cache_after_reindex', ['object' => $this->context]); - if (!empty($this->context->getIdentities())) { - $this->getCache()->clean($this->context->getIdentities()); - } + $this->cleanCache(); } /** @@ -61,9 +62,7 @@ public function afterUpdateMview(\Magento\Indexer\Model\Processor $subject) public function afterReindexAllInvalid(\Magento\Indexer\Model\Processor $subject) { $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->context]); - if (!empty($this->context->getIdentities())) { - $this->getCache()->clean($this->context->getIdentities()); - } + $this->cleanCache(); } /** @@ -79,4 +78,18 @@ private function getCache() } return $this->cache; } + + /** + * Clean cache. + * + * @return void + */ + private function cleanCache(): void + { + $identities = $this->context->getIdentities(); + if (!empty($identities)) { + $this->getCache()->clean($identities); + $this->context->flush(); + } + } } diff --git a/app/code/Magento/Indexer/Test/Unit/Model/CacheContextTest.php b/app/code/Magento/Indexer/Test/Unit/Model/CacheContextTest.php index d0a2dbfe9e8e4..fda36271e5d44 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/CacheContextTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/CacheContextTest.php @@ -10,6 +10,9 @@ use Magento\Framework\Indexer\CacheContext; use PHPUnit\Framework\TestCase; +/** + * Test indexer cache context + */ class CacheContextTest extends TestCase { /** @@ -38,20 +41,62 @@ public function testRegisterEntities() } /** - * test getIdentities + * Test getIdentities + * + * @param array $entities + * @param array $tags + * @param array $expected + * @dataProvider getIdentitiesDataProvider */ - public function testGetIdentities() + public function testGetIdentities(array $entities, array $tags = [], array $expected = []): void { - $expectedIdentities = [ - 'product_1', 'product_2', 'product_3', 'category_5', 'category_6', 'category_7', - ]; - $productTag = 'product'; - $categoryTag = 'category'; + foreach ($entities as $entity => $ids) { + $this->context->registerEntities($entity, $ids); + } + $this->context->registerTags($tags); + $this->assertEquals($expected, $this->context->getIdentities()); + } + + /** + * Test that flush() clears all data + */ + public function testFlush(): void + { + $productTag = 'cat_p'; + $categoryTag = 'cat_c'; + $additionalTags = ['cat_c_p']; $productIds = [1, 2, 3]; $categoryIds = [5, 6, 7]; $this->context->registerEntities($productTag, $productIds); $this->context->registerEntities($categoryTag, $categoryIds); - $actualIdentities = $this->context->getIdentities(); - $this->assertEquals($expectedIdentities, $actualIdentities); + $this->context->registerTags($additionalTags); + $this->assertNotEmpty($this->context->getIdentities()); + $this->context->flush(); + $this->assertEmpty($this->context->getIdentities()); + } + + /** + * @return array[] + */ + public function getIdentitiesDataProvider(): array + { + return [ + 'should return entities and tags' => [ + [ + 'cat_p' => [1, 2, 3], + 'cat_c' => [5, 6, 7] + ], + ['cat_c_p1', 'cat_c_p2'], + ['cat_p_1', 'cat_p_2', 'cat_p_3', 'cat_c_5', 'cat_c_6', 'cat_c_7', 'cat_c_p1', 'cat_c_p2'] + ], + 'should return unique values' => [ + [ + 'cat_p' => [1, 2, 3, 1, 3], + 'cat_c' => [5, 6, 7, 6] + ], + ['cat_c_p1', 'cat_c_p2'], + ['cat_p_1', 'cat_p_2', 'cat_p_3', 'cat_c_5', 'cat_c_6', 'cat_c_7', 'cat_c_p1', 'cat_c_p2'] + ] + ]; } } diff --git a/app/code/Magento/Indexer/Test/Unit/Model/Indexer/CacheCleanerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/Indexer/CacheCleanerTest.php new file mode 100644 index 0000000000000..0839dbfb13373 --- /dev/null +++ b/app/code/Magento/Indexer/Test/Unit/Model/Indexer/CacheCleanerTest.php @@ -0,0 +1,129 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Indexer\Test\Unit\Model\Indexer; + +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Event\Manager; +use Magento\Framework\Indexer\ActionInterface; +use Magento\Framework\Indexer\CacheContext; +use Magento\Indexer\Model\Indexer\CacheCleaner; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test cache cleaner plugin + */ +class CacheCleanerTest extends TestCase +{ + /** + * @var Manager|MockObject + */ + private $eventManager; + /** + * @var CacheContext|MockObject + */ + private $cacheContext; + /** + * @var CacheInterface|MockObject + */ + private $cache; + /** + * @var CacheCleaner + */ + private $model; + /** + * @var ActionInterface|MockObject + */ + private $action; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->action = $this->getMockForAbstractClass(ActionInterface::class); + $this->cacheContext = $this->createMock(CacheContext::class); + $this->eventManager = $this->createMock(Manager::class); + $this->cache = $this->getMockForAbstractClass(CacheInterface::class); + $this->model = new CacheCleaner( + $this->eventManager, + $this->cacheContext, + $this->cache + ); + } + + /** + * @param array $tags + * @param bool $isCacheClean + * @dataProvider cacheTagsDataProvider + */ + public function testAfterExecuteFull(array $tags, bool $isCacheClean = true): void + { + $this->expectCacheClean($tags, $isCacheClean); + $this->model->afterExecuteFull($this->action); + } + + /** + * @param array $tags + * @param bool $isCacheClean + * @dataProvider cacheTagsDataProvider + */ + public function testAfterExecuteList(array $tags, bool $isCacheClean = true): void + { + $this->expectCacheClean($tags, $isCacheClean); + $this->model->afterExecuteList($this->action); + } + + /** + * @param array $tags + * @param bool $isCacheClean + * @dataProvider cacheTagsDataProvider + */ + public function testAfterExecuteRow(array $tags, bool $isCacheClean = true): void + { + $this->expectCacheClean($tags, $isCacheClean); + $this->model->afterExecuteRow($this->action); + } + + /** + * @return array[] + */ + public function cacheTagsDataProvider(): array + { + return [ + [[], false], + [['cat_c_1', 'cat_c_2'], true] + ]; + } + + /** + * @param array $tags + * @param bool $isCacheClean + */ + private function expectCacheClean(array $tags, bool $isCacheClean = true): void + { + $this->eventManager->expects($this->once()) + ->method('dispatch') + ->with( + 'clean_cache_by_tags', + ['object' => $this->cacheContext] + ); + + $this->cacheContext->expects($this->atLeastOnce()) + ->method('getIdentities') + ->willReturn($tags); + + $this->cache->expects($this->exactly($isCacheClean ? 1 : 0)) + ->method('clean') + ->with($tags); + + $this->cacheContext->expects($this->exactly($isCacheClean ? 1 : 0)) + ->method('flush'); + } +} diff --git a/app/code/Magento/Indexer/Test/Unit/Model/Processor/CleanCacheTest.php b/app/code/Magento/Indexer/Test/Unit/Model/Processor/CleanCacheTest.php index a4b734f8ebc83..4c37f4c767fb9 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/Processor/CleanCacheTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/Processor/CleanCacheTest.php @@ -17,6 +17,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test cache clean plugin + */ class CleanCacheTest extends TestCase { /** @@ -103,6 +106,9 @@ public function testAfterUpdateMview() ->method('clean') ->with($tags); + $this->contextMock->expects($this->once()) + ->method('flush'); + $this->plugin->afterUpdateMview($this->subjectMock); } } 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 f755a1a1e0282..62620330f6e7b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\CategoryLinkManagement; +use Magento\Catalog\Model\Indexer\Product\Category\Processor; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Eav\Api\Data\AttributeOptionInterface; @@ -1170,6 +1171,11 @@ public function testSortByPosition() $category->setPostedProducts($productPositions); $category->save(); + // Reindex products from the result to invalidate query cache. + /** @var $indexer Processor */ + $indexer = Bootstrap::getObjectManager()->get(Processor::class); + $indexer->reindexList(array_keys($productPositions)); + $queryDesc = <<<QUERY { products(filter: {category_id: {eq: "$categoryId"}}, sort: {position: ASC}) { diff --git a/lib/internal/Magento/Framework/Indexer/CacheContext.php b/lib/internal/Magento/Framework/Indexer/CacheContext.php index 4a6964477ebd5..c0daeda23de9b 100644 --- a/lib/internal/Magento/Framework/Indexer/CacheContext.php +++ b/lib/internal/Magento/Framework/Indexer/CacheContext.php @@ -70,9 +70,18 @@ public function getIdentities() $identities = []; foreach ($this->entities as $cacheTag => $ids) { foreach ($ids as $id) { - $identities[] = $cacheTag . '_' . $id; + $identities[$cacheTag . '_' . $id] = true; } } - return array_merge($identities, array_unique($this->tags)); + return array_merge(array_keys($identities), array_unique($this->tags)); + } + + /** + * Clear context data + */ + public function flush(): void + { + $this->tags = []; + $this->entities = []; } } From 0d4e154c9532566c9ca5c9121992e03a778742a1 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Wed, 30 Sep 2020 16:13:58 +0300 Subject: [PATCH 0650/1013] MC-37718: Grouped product remains In Stock On Mass Update --- .../Inventory/ChangeParentStockStatus.php | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php b/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php new file mode 100644 index 0000000000000..bf1f6c1a5cf1a --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Inventory; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\GroupedProduct\Model\ResourceModel\Product\Link; + +/** + * Change stock status of grouped product by child product id + */ +class ChangeParentStockStatus +{ + /** + * @var Grouped + */ + private $groupedType; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * Product metadata pool + * + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param Grouped $groupedType + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + * @param ResourceConnection $resource + * @param MetadataPool $metadataPool + */ + public function __construct( + Grouped $groupedType, + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration, + ResourceConnection $resource, + MetadataPool $metadataPool + ) { + $this->groupedType = $groupedType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockConfiguration = $stockConfiguration; + $this->stockItemRepository = $stockItemRepository; + $this->resource = $resource; + $this->metadataPool = $metadataPool; + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + public function execute(int $productId): void + { + $parentIds = $this->getParentEntityIdsByChild($productId); + foreach ($parentIds as $productId) { + $this->changeParentStockStatus((int)$productId); + } + } + + /** + * Change stock status of grouped product + * + * @param int $productId + * @return void + */ + private function changeParentStockStatus(int $productId): void + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); + $criteria->setProductsFilter($groupedChildrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $groupedChildrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $groupedChildrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { + $parentStockItem->setIsInStock($groupedChildrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool + { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } + + /** + * Retrieve parent ids array by child id + * + * @param int $childId + * @return array + */ + private function getParentEntityIdsByChild(int $childId): array + { + $select = $this->resource->getConnection() + ->select() + ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) + ->join( + ['e' => $this->resource->getTableName('catalog_product_entity')], + 'e.' . + $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', + ['e.entity_id'] + ) + ->where('l.linked_product_id = ?', $childId) + ->where( + 'link_type_id = ?', + Link::LINK_TYPE_GROUPED + ); + + return $this->resource->getConnection()->fetchCol($select); + } +} From c36177cc506cab4884d791b5ee36dde8d75be7f5 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Wed, 30 Sep 2020 16:37:28 +0300 Subject: [PATCH 0651/1013] MC-36718: DOB issue: leading zero is missing for single digit day --- app/code/Magento/Customer/Block/Widget/Dob.php | 17 ++++++++++++++++- .../Customer/Test/Unit/Block/Widget/DobTest.php | 16 ++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index 90ce9ba210ed2..a475a1b1af93d 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -281,7 +281,7 @@ public function getHtmlExtraParams() */ public function getDateFormat() { - $dateFormat = $this->_localeDate->getDateFormatWithLongYear(); + $dateFormat = $this->setTwoDayPlaces($this->_localeDate->getDateFormatWithLongYear()); /** Escape RTL characters which are present in some locales and corrupt formatting */ $escapedDateFormat = preg_replace('/[^MmDdYy\/\.\-]/', '', $dateFormat); @@ -377,4 +377,19 @@ public function getFirstDay() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } + + /** + * Set 2 places for day value in format string + * + * @param string $format + * @return string + */ + private function setTwoDayPlaces(string $format): string + { + return preg_replace( + '/(?<!d)d(?!d)/', + 'dd', + $format + ); + } } diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 70232e955a86d..0e5eae1d47f1a 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -51,7 +51,7 @@ class DobTest extends TestCase const YEAR = '2014'; // Value of date('Y', strtotime(self::DATE)) - const DATE_FORMAT = 'M/d/y'; + const DATE_FORMAT = 'M/dd/y'; /** Constants used by Dob::setDateInput($code, $html) */ const DAY_HTML = @@ -356,11 +356,15 @@ public function getDateFormatDataProvider(): array [ 'ar_SA', preg_replace( - '/[^MmDdYy\/\.\-]/', - '', - (new DateFormatterFactory()) - ->create('ar_SA', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE) - ->getPattern() + '/(?<!d)d(?!d)/', + 'dd', + preg_replace( + '/[^MmDdYy\/\.\-]/', + '', + (new DateFormatterFactory()) + ->create('ar_SA', \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE) + ->getPattern() + ) ) ], [Resolver::DEFAULT_LOCALE, self::DATE_FORMAT], From 798c81a9530746bb2f748c9e4b28882bc2a66add Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Wed, 30 Sep 2020 16:44:18 +0300 Subject: [PATCH 0652/1013] resolved conflict --- .../StorefrontCartPriceRuleForBundleProductTest.xml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml index 3ddddc819df62..c5f4e8a07f622 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleForBundleProductTest.xml @@ -76,9 +76,7 @@ </actionGroup> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> - <argument name="indices" value=""/> - </actionGroup> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices" /> </before> <after> @@ -105,9 +103,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterDeleted"> - <argument name="indices" value=""/> - </actionGroup> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices2" /> </after> <!-- Create the rule --> @@ -142,8 +138,8 @@ <click selector="{{StorefrontBundledSection.addToCart}}" stepKey="clickCustomize"/> <!-- Select two products --> - <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.checkboxOptionLabel('$$createBundleOption1_1.sku$$','$$simpleProduct1.name$$')}}"/> - <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.checkboxOptionLabel('$$createBundleOption1_2.sku$$','$$simpleProduct3.name$$')}}"/> + <click stepKey="selectProduct1" selector="{{StorefrontBundledSection.productCheckbox('1','1')}}"/> + <click stepKey="selectProduct2" selector="{{StorefrontBundledSection.productCheckbox('2','1')}}"/> <!--Click "Add to Cart" button--> <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickAddBundleProductToCart"/> From 2c26f7a95b77c1f059c4e93bebae3b278a467ac7 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Wed, 30 Sep 2020 19:42:56 +0530 Subject: [PATCH 0653/1013] 30179: resetPassword mutation returns generic error - added API-functional test --- .../Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php | 2 +- .../testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php index a098325c820d6..1a1a67858ef47 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php @@ -118,7 +118,7 @@ public function resolve( $args['newPassword'] ); } catch (LocalizedException $e) { - throw new GraphQlInputException(__($e->getMessage()), $e); + throw new GraphQlInputException(__('The password must be at least 8 characters long, minimum of 3 different classes of characters: Lower Case, Upper Case, Digits, Special Characters.'), $e); } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php index 9c89b80433afd..ad3e0b613f401 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php @@ -156,7 +156,7 @@ public function testResetPasswordTokenEmptyValue() public function testResetPasswordTokenMismatched() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Cannot set the customer\'s password'); + $this->expectExceptionMessage('The password must be at least 8 characters long, minimum of 3 different classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); $query = <<<QUERY mutation { resetPassword ( From f692a937080862f3db77275c03b8f2edc27b45bb Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Wed, 30 Sep 2020 19:00:01 +0300 Subject: [PATCH 0654/1013] MC-37718: Grouped product remains In Stock On Mass Update --- ...pdateProductQtyAndStockStatusActionGroup.xml | 17 ++++++++++++++++- .../UpdateStockStatusGroupedProductTest.xml | 6 ++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml index fce287705b67c..4b5aca5050858 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml @@ -8,16 +8,28 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <!-- Update Product Name and Description attribute --> + <!--Update Product Name and Description attribute--> <actionGroup name="AdminMassUpdateProductQtyAndStockStatusActionGroup"> <arguments> <argument name="attributes"/> + <argument name="product"/> </arguments> + <!--Filter product in product grid--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageFirstTime"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <selectOption selector="{{AdminProductGridFilterSection.typeFilter}}" userInput="{{product.type_id}}" stepKey="selectionProductType"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <!--Select first product from grid and open mass action--> <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox"/> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> <waitForPageLoad stepKey="waitForUploadPage"/> <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeAttributePageEditUrl"/> + <!--Update inventory attributes and save--> <click selector="{{AdminUpdateAttributesAdvancedInventorySection.inventory}}" stepKey="openInvetoryTab"/> <click selector="{{AdminUpdateAttributesAdvancedInventorySection.changeQty}}" stepKey="uncheckChangeQty"/> <fillField selector="{{AdminUpdateAttributesAdvancedInventorySection.qty}}" userInput="{{attributes.qty}}" stepKey="fillFieldName"/> @@ -26,5 +38,8 @@ <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="save"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitVisibleSuccessMessage"/> <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeSuccessMessage"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageSecondTime"/> + <waitForPageLoad stepKey="waitForProductGridPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersAfterMassAction"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index 12d753c300ca5..cb9434029ff2e 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -38,10 +38,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> <!--1.Open product grid page and choose "Update attributes" and set product stock status to "Out of Stock"--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridFirstTime"/> - <waitForPageLoad stepKey="waitForProductGridFirstTime"/> <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="setProductToOutOfStock"> <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> + <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> <!--2.Run cron for updating stock status of parent product--> <magentoCron stepKey="runAllCronJobs"/> @@ -51,10 +50,9 @@ <argument name="stockStatus" value="Out of Stock"/> </actionGroup> <!--4.Open product grid page choose "Update attributes" and set product stock status to "In Stock"--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductGridSecondTime"/> - <waitForPageLoad stepKey="waitForProductGridSecondTime"/> <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="returnProductToInStock"> <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> + <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> From 777c7c720f3f2be1f5484ea0105dd22649a9b83d Mon Sep 17 00:00:00 2001 From: TuNa <ladiesman9x@gmail.com> Date: Wed, 30 Sep 2020 23:05:07 +0700 Subject: [PATCH 0655/1013] Fix missing escape url method up fix static tests --- .../templates/instance/edit/layout.phtml | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) 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 6dab476115cee..a3af1dd95e70c 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,11 +5,12 @@ */ /** @var \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout $block */ +/** @var \Magento\Framework\Escaper $escaper */ /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset class="fieldset"> - <legend class="legend"><span><?= $block->escapeHtml(__('Layout Updates')) ?></span></legend> + <legend class="legend"><span><?= $escaper->escapeHtml(__('Layout Updates')) ?></span></legend> <br /> <div class="widget-layout-updates"> <div id="page_group_container"></div> @@ -45,56 +46,56 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" 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 %>">'+ + '<div class="no-display {$escaper->escapeJs($container['code'])} group_container" '+ + 'id="{$escaper->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'])}]" />'+ + 'value="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}]" />'+ '<input disabled="disabled" type="hidden" '+ - 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][page_id]" '+ + 'name="widget_instance[<%- data.id %>][{$escaper->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'])}" />'+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][layout_handle]" '+ + 'value="{$escaper->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>{$escaper->escapeJs(__('%1', $container['label']))}</label></th>'+ + '<th><label>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->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]" '+ + 'id="all_{$escaper->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][for]" '+ 'value="all" checked="checked" /> '+ - '<label for="all_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ - '{$block->escapeJs(__('All'))}</label><br />'+ + '<label for="all_{$escaper->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$escaper->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]" '+ + 'id="specific_{$escaper->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][for]" '+ 'value="specific" /> '+ - '<label for="specific_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ - '{$block->escapeJs(__('Specific %1', $container['label']))}</label>'+ + '<label for="specific_{$escaper->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$escaper->escapeJs(__('Specific %1', $container['label']))}</label>'+ script; $scriptString1 = $secureRenderer->renderEventListenerAsTag( "onclick", "WidgetInstance.togglePageGroupChooser(this)", - "all_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + "all_" . $escaper->escapeJs($container['name']) . "_<%- data.id %>" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString1 = $secureRenderer->renderEventListenerAsTag( "onclick", "WidgetInstance.togglePageGroupChooser(this)", - "specific_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + "specific_" . $escaper->escapeJs($container['name']) . "_<%- data.id %>" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString .= <<<script '</td>'+ @@ -111,26 +112,30 @@ script; '</tr>'+ '</tbody>'+ '</table>'+ - '<div class="no-display chooser_container" id="{$block->escapeJs($container['name'])}_ids_<%- data.id %>">'+ + '<div class="no-display chooser_container" id="{$escaper->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'])}" />'+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][is_anchor_only]" '+ + 'value="{$escaper->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'])}" />'+ + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][product_type_id]" '+ + 'value="{$escaper->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" /> ' + + 'name="widget_instance[<%- data.id %>][{$escaper->escapeJs($container['name'])}][entities]" '+ + 'value="<%- data.{$escaper->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'))}" />' + + 'title="{$escaper->escapeJs(__('Open Chooser'))}">' + + '<img src="{$escaper->escapeJs( + $escaper->escapeUrl($block->getViewFileUrl('images/rule_chooser_trigger.gif')) + )}" '+ + 'alt="{$escaper->escapeJs(__('Open Chooser'))}" />' + '</a> ' + '<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'))}" />' + + 'title="{$escaper->escapeJs(__('Apply'))}">' + + '<img src="{$escaper->escapeJs( + $escaper->escapeUrl($block->getViewFileUrl('images/rule_component_apply.gif')) + )}" '+ + 'alt="{$escaper->escapeJs(__('Apply'))}" />' + '</a>' + '</p>'+ '<div class="chooser"></div>'+ @@ -141,19 +146,19 @@ 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" + WidgetInstance.displayEntityChooser('" .$escaper->escapeJs($container['code']) . + "', '" . $escaper->escapeJs($container['name']) . "_ids_<%- data.id %>')", + "div#" . $escaper->escapeJs($container['name']) . "_ids_<%- data.id %> a.widget-option-chooser" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString1 = $secureRenderer->renderEventListenerAsTag( 'onclick', "event.preventDefault(); - WidgetInstance.hideEntityChooser('" . $block->escapeJs($container['name']) . "_ids_<%- data.id %>')", + WidgetInstance.hideEntityChooser('" . $escaper->escapeJs($container['name']) . "_ids_<%- data.id %>')", "a#widget-apply-<%- data.id %>" ); - $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= "'" . $escaper->escapeJs($scriptString1) . "'+" . PHP_EOL; $scriptString .= <<<script '</div>'+ @@ -175,8 +180,8 @@ $scriptString .= <<<script '<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>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Template'))}</label></th>'+ '<th> </th>'+ '</tr>'+ '</thead>'+ @@ -208,9 +213,9 @@ $scriptString .= <<<script '<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>{$escaper->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ @@ -242,9 +247,9 @@ $scriptString .= <<<script '<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>{$escaper->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$escaper->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ @@ -412,10 +417,10 @@ var WidgetInstance = { additional = {}; } if (type == 'categories') { - additional.url = '{$block->escapeJs($block->getCategoriesChooserUrl())}'; + additional.url = '{$escaper->escapeJs($escaper->escapeUrl($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.url = '{$escaper->escapeJs($escaper->escapeUrl($block->getProductsChooserUrl()))}'; additional.post_parameters = \$H({'product_type_id':$(chooser).down('input.product_type_id').value}); } if (chooser && additional) { @@ -521,13 +526,13 @@ var WidgetInstance = { selected = ''; parameters = {}; if (type == 'block_reference') { - url = '{$block->escapeJs($block->getBlockChooserUrl())}'; + url = '{$escaper->escapeJs($escaper->escapeUrl($block->getBlockChooserUrl()))}'; if (additional.selectedBlock) { selected = additional.selectedBlock; } parameters.layout = value; } else if (type == 'block_template') { - url = '{$block->escapeJs($block->getTemplateChooserUrl())}'; + url = '{$escaper->escapeJs($escaper->escapeUrl($block->getTemplateChooserUrl()))}'; if (additional.selectedTemplate) { selected = additional.selectedTemplate; } From 80dc6c8c29f19165bae5fa4c18ab31ca2f37b3b2 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Wed, 30 Sep 2020 22:22:27 +0530 Subject: [PATCH 0656/1013] 30179: resetPassword mutation returns generic error - added API-functional test --- .../Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php | 2 +- .../testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php index 1a1a67858ef47..a098325c820d6 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/ResetPassword.php @@ -118,7 +118,7 @@ public function resolve( $args['newPassword'] ); } catch (LocalizedException $e) { - throw new GraphQlInputException(__('The password must be at least 8 characters long, minimum of 3 different classes of characters: Lower Case, Upper Case, Digits, Special Characters.'), $e); + throw new GraphQlInputException(__($e->getMessage()), $e); } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php index ad3e0b613f401..f097955a95664 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php @@ -156,7 +156,7 @@ public function testResetPasswordTokenEmptyValue() public function testResetPasswordTokenMismatched() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The password must be at least 8 characters long, minimum of 3 different classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); + $this->expectExceptionMessage('The password token is mismatched. Reset and try again'); $query = <<<QUERY mutation { resetPassword ( From 36969046e0cf1a2abeccad598cd9a84185cb1e5a Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Wed, 30 Sep 2020 21:10:31 +0300 Subject: [PATCH 0657/1013] MC-37718: Grouped product remains In Stock On Mass Update --- .../Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index cb9434029ff2e..bf071f568a376 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -43,7 +43,8 @@ <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> <!--2.Run cron for updating stock status of parent product--> - <magentoCron stepKey="runAllCronJobs"/> + <magentoCLI command="cron:run" stepKey="runCron"/> + <wait time="60" stepKey="waitForChanges"/> <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> From b12a471ed41e6f03ca2c1bb1316a17b5601c4859 Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Wed, 30 Sep 2020 14:41:27 -0500 Subject: [PATCH 0658/1013] MC-37934: Unexpected behavior of "Manage Shopping Cart" regarding "Products in the Comparison List" --- .../Magento/Reports/Model/ResourceModel/Event.php | 15 +++++++-------- .../Test/Unit/Model/ResourceModel/EventTest.php | 4 ++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Reports/Model/ResourceModel/Event.php b/app/code/Magento/Reports/Model/ResourceModel/Event.php index 1f621a3fde39d..a140c7732837e 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Event.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Event.php @@ -93,6 +93,7 @@ public function applyLogToCollection( $skipIds = [] ) { $idFieldName = $collection->getResource()->getIdFieldName(); + $predefinedStoreIds = ($collection->getStoreId() === null) ?: [$collection->getStoreId()]; $derivedSelect = $this->getConnection() ->select() @@ -103,7 +104,7 @@ public function applyLogToCollection( ->where('event_type_id = ?', (int) $eventTypeId) ->where('subject_id = ?', (int) $eventSubjectId) ->where('subtype = ?', (int) $subtype) - ->where('store_id IN(?)', $this->getCurrentStoreIds()) + ->where('store_id IN(?)', $this->getCurrentStoreIds($predefinedStoreIds)) ->group('object_id'); if ($skipIds) { @@ -132,13 +133,11 @@ public function getCurrentStoreIds(array $predefinedStoreIds = null) { $stores = []; // get all or specified stores - if ($this->_storeManager->getStore()->getId() == 0) { - if (null !== $predefinedStoreIds) { - $stores = $predefinedStoreIds; - } else { - foreach ($this->_storeManager->getStores() as $store) { - $stores[] = $store->getId(); - } + if ($predefinedStoreIds !== null) { + $stores = $predefinedStoreIds; + } else if ($this->_storeManager->getStore()->getId() == 0) { + foreach ($this->_storeManager->getStores() as $store) { + $stores[] = $store->getId(); } } else { // get all stores, required by configuration in current store scope diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php index adb31b52161f8..825ae9d709b9f 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php @@ -174,6 +174,10 @@ public function testApplyLogToCollection() ->expects($this->any()) ->method('getSelect') ->willReturn($collectionSelectMock); + $collectionMock + ->expects($this->any()) + ->method('getStoreId') + ->willReturn(1); $selectMock = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() From 4c4d410479ae10593ab366aed103d205be950662 Mon Sep 17 00:00:00 2001 From: Soumya Unnikrishnan <sunnikri@adobe.com> Date: Wed, 30 Sep 2020 16:13:59 -0500 Subject: [PATCH 0659/1013] MQE-2306: Release 3.1.1 Delivery MFTF tests clean up --- ...nCustomerAddressGridMainActionsSection.xml | 15 -------------- .../AdminCustomerGridMainActionsSection.xml | 3 ++- .../Mftf/Section/AdminDeleteRoleSection.xml | 20 ------------------- .../Mftf/Section/AdminDeleteUserSection.xml | 17 ---------------- .../AdminDeleteRoleSection.xml | 3 ++- .../AdminDeleteUserSection.xml | 2 +- 6 files changed, 5 insertions(+), 55 deletions(-) delete mode 100644 app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml delete mode 100644 app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml delete mode 100644 app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml deleted file mode 100644 index f226d49e3bf54..0000000000000 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerAddressGridMainActionsSection.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="AdminCustomerGridMainActionsSection"> - <element name="addNewAddress" type="button" selector=".add-new-address-button" timeout="30"/> - <element name="actions" type="text" selector=".admin__data-grid-header-row .action-select"/> - </section> -</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml index 8277cdd64928a..44ab653259b55 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerGridMainActionsSection.xml @@ -13,8 +13,9 @@ <element name="multicheck" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>label"/> <element name="multicheckTick" type="checkbox" selector="#container>div>div.admin__data-grid-wrap>table>thead>tr>th.data-grid-multicheck-cell>div>input"/> <element name="delete" type="button" selector="//*[contains(@class, 'admin__data-grid-header')]//span[contains(@class,'action-menu-item') and text()='Delete']"/> - <element name="actions" type="text" selector=".action-select"/> <element name="customerCheckbox" type="button" selector="//*[contains(text(),'{{arg}}')]/parent::td/preceding-sibling::td/label[@class='data-grid-checkbox-cell-inner']//input" parameterized="true"/> <element name="ok" type="button" selector="//button[@data-role='action']//span[text()='OK']"/> + <element name="addNewAddress" type="button" selector=".add-new-address-button" timeout="30"/> + <element name="actions" type="text" selector=".admin__data-grid-header-row .action-select"/> </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml deleted file mode 100644 index 6a0d0c9210396..0000000000000 --- a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="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"/> - </section> -</sections> - diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml deleted file mode 100644 index 21ca1cb36f988..0000000000000 --- a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteUserSection.xml +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="AdminDeleteUserSection"> - <element name="role" parameterized="true" selector="//td[contains(text(), '{{roleName}}')]" type="button"/> - <element name="password" selector="#user_current_password" type="input"/> - <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> - <element name="confirm" selector=".action-primary.action-accept" type="button"/> - </section> -</sections> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml index e369d037d28f6..dba7dd4cd520c 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminRoleGridSection/AdminDeleteRoleSection.xml @@ -9,7 +9,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminDeleteRoleSection"> <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> - <element name="role" parameterized="true" selector="//td[contains(text(), '{{args}}')]" 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/Section/AdminUserGridSection/AdminDeleteUserSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection/AdminDeleteUserSection.xml index d4718ca43d6cf..3937ee75c6b7d 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection/AdminDeleteUserSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminUserGridSection/AdminDeleteUserSection.xml @@ -11,7 +11,7 @@ <element name="theUser" selector="//td[contains(text(), '{{userName}}')]" type="button" parameterized="true"/> <element name="password" selector="#user_current_password" type="input"/> <element name="delete" selector="//button/span[contains(text(), 'Delete User')]" type="button"/> - <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> + <element name="confirm" selector=".action-primary.action-accept" type="button"/> <element name="role" parameterized="true" selector="//td[contains(text(), '{{args}}')]" type="button"/> </section> </sections> From a7180c216d9999bb0931d285765cb1389f9a09ce Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Wed, 30 Sep 2020 17:50:03 -0500 Subject: [PATCH 0660/1013] MC-37934: Unexpected behavior of "Manage Shopping Cart" regarding "Products in the Comparison List" --- .../Reports/Model/ResourceModel/Event.php | 2 +- .../customer_for_second_website_rollback.php | 4 +- ...cently_compared_product_second_website.php | 59 +++++++++++++++++++ ...mpared_product_second_website_rollback.php | 11 ++++ ...wed_product_by_customer_second_website.php | 53 +++++++++++++++++ ...ct_by_customer_second_website_rollback.php | 11 ++++ 6 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_second_website.php create mode 100644 dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_second_website_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website.php create mode 100644 dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website_rollback.php diff --git a/app/code/Magento/Reports/Model/ResourceModel/Event.php b/app/code/Magento/Reports/Model/ResourceModel/Event.php index a140c7732837e..4affe31b4b2fa 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Event.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Event.php @@ -135,7 +135,7 @@ public function getCurrentStoreIds(array $predefinedStoreIds = null) // get all or specified stores if ($predefinedStoreIds !== null) { $stores = $predefinedStoreIds; - } else if ($this->_storeManager->getStore()->getId() == 0) { + } elseif ($this->_storeManager->getStore()->getId() == 0) { foreach ($this->_storeManager->getStores() as $store) { $stores[] = $store->getId(); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_rollback.php index 4d8c524cf8f5f..b93e9e68dfc2c 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_rollback.php @@ -21,16 +21,16 @@ $customerRepository = $objectManager->get(CustomerRepositoryInterface::class); /** @var WebsiteRepositoryInterface $websiteRepository */ $websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); -$websiteId = $websiteRepository->get('test')->getId(); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); try { + $websiteId = $websiteRepository->get('test')->getId(); $customer = $customerRepository->get('customer@example.com', $websiteId); $customerRepository->delete($customer); } catch (NoSuchEntityException $e) { - //customer already deleted + //customer or website already deleted } $registry->unregister('isSecureArea'); diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_second_website.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_second_website.php new file mode 100644 index 0000000000000..3e05a9b05137a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_second_website.php @@ -0,0 +1,59 @@ +<?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; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_two_websites.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_for_second_website.php'); + +$objectManager = Bootstrap::getObjectManager(); +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$websiteId = $storeManager->getStore('fixture_second_store')->getWebsiteId(); +$customer = $customerRepository->get('customer@example.com', $websiteId); +/** @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'), +]; +$storeManager->setCurrentStore($storeManager->getStore('fixture_second_store')->getId()); + +try { + /** @var CategoryProductIndexer $indexer */ + $indexer = $objectManager->create(CategoryProductIndexer::class); + $indexer->executeFull(); + $config->setValue('reports/options/enabled', 1); + $config->setValue('reports/options/product_compare_enabled', 1); + /** @var Session $session */ + $session = $objectManager->get(Session::class); + $session->loginById($customer->getId()); + $session->setCustomerId($customer->getId()); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple-on-two-websites'); + $event = new DataObject(['product' => $product]); + /** @var CatalogProductCompareAddProductObserver $reportObserver */ + $reportObserver = $objectManager->get(CatalogProductCompareAddProductObserver::class); + $reportObserver->execute(new Observer(['event' => $event])); +} 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_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_second_website_rollback.php new file mode 100644 index 0000000000000..db53a60ed6787 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_second_website_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/product_two_websites_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_for_second_website_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website.php new file mode 100644 index 0000000000000..af0970666c6fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website.php @@ -0,0 +1,53 @@ +<?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; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_two_websites.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_for_second_website.php'); + +$objectManager = Bootstrap::getObjectManager(); +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$websiteId = $storeManager->getStore('fixture_second_store')->getWebsiteId(); +$customer = $customerRepository->get('customer@example.com', $websiteId); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originalValue = $config->getValue('reports/options/enabled'); +$storeManager->setCurrentStore($storeManager->getStore('fixture_second_store')->getId()); + +try { + /** @var CategoryProductIndexer $indexer */ + $indexer = $objectManager->create(CategoryProductIndexer::class); + $indexer->executeFull(); + $config->setValue('reports/options/enabled', 1); + /** @var Session $session */ + $session = $objectManager->get(Session::class); + $session->loginById($customer->getId()); + $session->setCustomerId($customer->getId()); + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get('simple-on-two-websites'); + $event = new DataObject(['product' => $product]); + /** @var CatalogProductViewObserver $reportObserver */ + $reportObserver = $objectManager->get(CatalogProductViewObserver::class); + $reportObserver->execute(new Observer(['event' => $event])); +} finally { + $session->logout(); + $config->setValue('reports/options/enabled', $originalValue); +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website_rollback.php new file mode 100644 index 0000000000000..db53a60ed6787 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_second_website_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/product_two_websites_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_for_second_website_rollback.php'); From 7f0eaa8615103b91fef062820fa5a762dc91f9e7 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Thu, 1 Oct 2020 12:50:52 +0530 Subject: [PATCH 0661/1013] 30179: resetPassword mutation returns generic error - added API-functional test --- .../GraphQl/Customer/ResetPasswordTest.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php index f097955a95664..254bf8c0fa97e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php @@ -192,6 +192,52 @@ public function testNewPasswordEmptyValue() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @throws NoSuchEntityException + * @throws Exception + * @throws LocalizedException + */ + public function testNewPasswordCheckMinLength() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The password needs at least 8 characters. Create a new password and try again'); + $query = <<<QUERY +mutation { + resetPassword ( + email: "{$this->getCustomerEmail()}" + resetPasswordToken: "{$this->getResetPasswordToken()}" + newPassword: "new_" + ) +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @throws NoSuchEntityException + * @throws Exception + * @throws LocalizedException + */ + public function testNewPasswordCheckCharactersStrenth() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Minimum of different classes of characters in password is 3. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); + $query = <<<QUERY +mutation { + resetPassword ( + email: "{$this->getCustomerEmail()}" + resetPasswordToken: "{$this->getResetPasswordToken()}" + newPassword: "new_password" + ) +} +QUERY; + $this->graphQlMutation($query); + } + /** * Check password reset for lock customer * From 9914179daa12e9a6175fc4125b2f92084cc04de2 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 1 Oct 2020 13:25:36 +0300 Subject: [PATCH 0662/1013] MC-37718: Grouped product remains In Stock On Mass Update --- .../Plugin/MassUpdateProductAttribute.php | 8 +++++--- .../Test/Mftf/Data/QueueConsumerData.xml | 15 +++++++++++++++ .../Test/UpdateStockStatusGroupedProductTest.xml | 10 ++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index b562171c95bff..ee5f16989ba37 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -3,11 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Plugin; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; -use Magento\Catalog\Model\Product; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; @@ -181,10 +183,10 @@ private function updateInventoryInProducts($productIds, $websiteId, $inventoryDa /** * Process stock data for parent products * - * @param Product $product + * @param ProductInterface $product * @return void */ - private function processParents(Product $product): void + private function processParents(ProductInterface $product): void { foreach ($this->parentItemProcessors as $processor) { $processor->process($product); diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml new file mode 100644 index 0000000000000..ef58de6cdc496 --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminInventoryMassUpdateConsumerData"> + <data key="consumerName">inventory.mass.update</data> + <data key="messageLimit">10</data> + </entity> +</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index bf071f568a376..42ffe88beb46e 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -27,7 +27,7 @@ <requiredEntity createDataKey="createGroupedProduct"/> <requiredEntity createDataKey="createFirstSimpleProduct"/> </createData> - <magentoCron stepKey="runCronIndex" groups="index"/> + <magentoCron groups="index" stepKey="runCronIndex"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -42,9 +42,11 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--2.Run cron for updating stock status of parent product--> - <magentoCLI command="cron:run" stepKey="runCron"/> - <wait time="60" stepKey="waitForChanges"/> + <!--2.Run cron for start consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> + <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> + </actionGroup> <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> From d6750b5890a4b4d5362723180c7da871ff814980 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 1 Oct 2020 15:16:47 +0300 Subject: [PATCH 0663/1013] MC-37718: Grouped product remains In Stock On Mass Update --- .../Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index 42ffe88beb46e..1d2d5333952ae 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -57,7 +57,12 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> + <!--5.Run cron for start consumer --> + <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueueSecond"> + <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> + <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> + </actionGroup> + <!--6.Check stock status of grouped product. Stock status should be "In Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> <argument name="stockStatus" value="In Stock"/> From 5f690367e221d8f2f1aaf773fcd6349cf4784318 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Thu, 1 Oct 2020 15:46:19 +0300 Subject: [PATCH 0664/1013] url escape test coverage --- .../Instance/Edit/Tab/Main/LayoutTest.php | 93 +++++++++++++------ 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php index 6b5829ee8bf28..2cda6710ce22b 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/Instance/Edit/Tab/Main/LayoutTest.php @@ -3,40 +3,53 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main; +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Framework\Escaper; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Widget\Model\Widget\Instance; +use PHPUnit\Framework\TestCase; + /** * @magentoAppArea adminhtml */ -class LayoutTest extends \PHPUnit\Framework\TestCase +class LayoutTest extends TestCase { /** - * @var \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Layout */ - protected $_block; + private $block; + /** + * @inheritDoc + */ protected function setUp(): void { - parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); - $this->_block = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock( - \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout::class, - '', - [ - 'data' => [ - 'widget_instance' => \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Widget\Model\Widget\Instance::class - ), + $this->block = $this->objectManager->get(LayoutInterface::class) + ->createBlock( + Layout::class, + '', + [ + 'data' => [ + 'widget_instance' => $this->objectManager->create(Instance::class), + ], ] - ] - ); - $this->_block->setLayout( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ) - ); + ); + $this->block->setLayout($this->objectManager->get(LayoutInterface::class)); } /** @@ -44,16 +57,12 @@ protected function setUp(): void */ public function testGetLayoutsChooser() { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\App\State::class - )->setAreaCode( - \Magento\Framework\App\Area::AREA_FRONTEND - ); - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\DesignInterface::class - )->setDefaultDesignTheme(); + $this->objectManager->get(State::class) + ->setAreaCode(Area::AREA_FRONTEND); + $this->objectManager->get(DesignInterface::class) + ->setDefaultDesignTheme(); - $actualHtml = $this->_block->getLayoutsChooser(); + $actualHtml = $this->block->getLayoutsChooser(); $this->assertStringStartsWith('<select ', $actualHtml); $this->assertStringEndsWith('</select>', $actualHtml); $this->assertStringContainsString('id="layout_handle"', $actualHtml); @@ -61,4 +70,28 @@ public function testGetLayoutsChooser() $this->assertGreaterThan(1, $optionCount, 'HTML select tag must provide options to choose from.'); $this->assertEquals($optionCount, substr_count($actualHtml, '</option>')); } + + /** + * Check that escapeUrl called from template + * + * @return void + */ + public function testToHtml(): void + { + $escaperMock = $this->createMock(Escaper::class); + $this->objectManager->addSharedInstance($escaperMock, Escaper::class); + + $escaperMock->expects($this->atLeast(6)) + ->method('escapeUrl'); + + $this->block->toHtml(); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->objectManager->removeSharedInstance(Escaper::class); + } } From 5c05c30ccedbf71a78fd30e742b50218c0cb0c46 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 1 Oct 2020 20:13:34 +0300 Subject: [PATCH 0665/1013] MC-37718: Grouped product remains In Stock On Mass Update --- .../Test/Mftf/Data/QueueConsumerData.xml | 15 --------------- .../Test/UpdateStockStatusGroupedProductTest.xml | 14 +++----------- 2 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml deleted file mode 100644 index ef58de6cdc496..0000000000000 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Data/QueueConsumerData.xml +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- - /** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ ---> - -<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> - <entity name="AdminInventoryMassUpdateConsumerData"> - <data key="consumerName">inventory.mass.update</data> - <data key="messageLimit">10</data> - </entity> -</entities> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml index 1d2d5333952ae..f39e18373893d 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -42,11 +42,8 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--2.Run cron for start consumer --> - <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueue"> - <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> - <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> - </actionGroup> + <!--2.Run cron for updating stock status of parent product--> + <magentoCron groups="index" stepKey="runCronIndex"/> <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> @@ -57,12 +54,7 @@ <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> <argument name="product" value="$$createFirstSimpleProduct$$"/> </actionGroup> - <!--5.Run cron for start consumer --> - <actionGroup ref="CliConsumerStartActionGroup" stepKey="startMessageQueueSecond"> - <argument name="consumerName" value="{{AdminInventoryMassUpdateConsumerData.consumerName}}"/> - <argument name="maxMessages" value="{{AdminInventoryMassUpdateConsumerData.messageLimit}}"/> - </actionGroup> - <!--6.Check stock status of grouped product. Stock status should be "In Stock"--> + <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> <argument name="productId" value="$$createGroupedProduct.id$$"/> <argument name="stockStatus" value="In Stock"/> From ac1042377517967d1e4e83036f070960032edc65 Mon Sep 17 00:00:00 2001 From: Prabhu Ram <pganapat@adobe.com> Date: Thu, 1 Oct 2020 15:39:31 -0500 Subject: [PATCH 0666/1013] - fix static failure --- .../Product/LayeredNavigation/AttributeOptionProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index c1ba036e542db..d46776bfe498e 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -126,7 +126,8 @@ private function formatResult(\Magento\Framework\DB\Select $select): array $result[$option['attribute_code']] = [ 'attribute_id' => $option['attribute_id'], 'attribute_code' => $option['attribute_code'], - 'attribute_label' => $option['attribute_store_label'] ? $option['attribute_store_label'] : $option['attribute_label'], + 'attribute_label' => $option['attribute_store_label'] + ? $option['attribute_store_label'] : $option['attribute_label'], 'options' => [], ]; } From 8833cd2a5d1b4f0044177f7c6f3da6d2b993c266 Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Thu, 1 Oct 2020 18:08:46 -0500 Subject: [PATCH 0667/1013] MC-37934: Unexpected behavior of "Manage Shopping Cart" regarding "Products in the Comparison List" --- .../Reports/Model/ResourceModel/Event.php | 4 ++- .../Unit/Model/ResourceModel/EventTest.php | 29 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Reports/Model/ResourceModel/Event.php b/app/code/Magento/Reports/Model/ResourceModel/Event.php index 4affe31b4b2fa..d27b8a03fecec 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Event.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Event.php @@ -93,7 +93,9 @@ public function applyLogToCollection( $skipIds = [] ) { $idFieldName = $collection->getResource()->getIdFieldName(); - $predefinedStoreIds = ($collection->getStoreId() === null) ?: [$collection->getStoreId()]; + $predefinedStoreIds = ($collection->getStoreId() === null) + ? null + : [$collection->getStoreId()]; $derivedSelect = $this->getConnection() ->select() diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php index 825ae9d709b9f..cca96deecfc55 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php @@ -134,9 +134,13 @@ public function testUpdateCustomerTypeWithType() } /** + * @dataProvider getApplyLogToCollectionDataProvider + * @param null|array $storeId + * @param null|array $storeIdSelect + * * @return void */ - public function testApplyLogToCollection() + public function testApplyLogToCollection($storeId, $storeIdSelect) { $derivedSelect = 'SELECT * FROM table'; $idFieldName = 'IdFieldName'; @@ -160,6 +164,7 @@ public function testApplyLogToCollection() ->willReturnSelf(); $collectionMock = $this->getMockBuilder(AbstractDb::class) + ->setMethods(['getResource', 'getIdFieldName', 'getSelect', 'getStoreId']) ->disableOriginalConstructor() ->getMock(); $collectionMock @@ -177,7 +182,7 @@ public function testApplyLogToCollection() $collectionMock ->expects($this->any()) ->method('getStoreId') - ->willReturn(1); + ->willReturn($storeId); $selectMock = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() @@ -199,6 +204,16 @@ public function testApplyLogToCollection() ->expects($this->any()) ->method('__toString') ->willReturn($derivedSelect); + $selectMock + ->expects($this->any()) + ->method('where') + ->withConsecutive([ + ['event_type_id = ?', 1], + ['subject_id = ?', 1], + ['subtype = ?', 1], + ['store_id IN(?)', $storeIdSelect] + ]) + ->willReturn($selectMock); $this->connectionMock ->expects($this->once()) @@ -213,6 +228,16 @@ public function testApplyLogToCollection() $this->event->applyLogToCollection($collectionMock, 1, 1, 1); } + /** + * @return array + */ + public function getApplyLogToCollectionDataProvider() + { + return [ + ['storeId' => 1, 'storeIdSelect' => [1]], + ['storeId' => null, 'storeIdSelect' => [1]], + ]; + } /** * @return void */ From 33b3e40684049a01b9b5ae4e75a1300b9833b2b1 Mon Sep 17 00:00:00 2001 From: Victor Rad <vrad@magento.com> Date: Thu, 1 Oct 2020 18:39:33 -0500 Subject: [PATCH 0668/1013] MC-37934: Unexpected behavior of "Manage Shopping Cart" regarding "Products in the Comparison List" --- .../Reports/Test/Unit/Model/ResourceModel/EventTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php index cca96deecfc55..bc064a434fa32 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/EventTest.php @@ -207,13 +207,12 @@ public function testApplyLogToCollection($storeId, $storeIdSelect) $selectMock ->expects($this->any()) ->method('where') - ->withConsecutive([ + ->willReturnMap([ ['event_type_id = ?', 1], ['subject_id = ?', 1], ['subtype = ?', 1], ['store_id IN(?)', $storeIdSelect] - ]) - ->willReturn($selectMock); + ]); $this->connectionMock ->expects($this->once()) From 2ee63de2e0ea9fd01d53b5a6dbf566d93fbbc399 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Thu, 1 Oct 2020 20:38:57 -0500 Subject: [PATCH 0669/1013] MC-37371: Wrong currency sign in Credit Memo grid - fixed - modified unit test --- .../Ui/Component/Listing/Column/PriceTest.php | 74 +++++++++++++++---- .../Ui/Component/Listing/Column/Price.php | 18 ++++- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/PriceTest.php b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/PriceTest.php index e3c1c0cc32a3f..4a9061c3f3c5c 100644 --- a/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/PriceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Ui/Component/Listing/Column/PriceTest.php @@ -12,6 +12,8 @@ use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponent\Processor; use Magento\Sales\Ui\Component\Listing\Column\Price; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -27,6 +29,11 @@ class PriceTest extends TestCase */ protected $currencyMock; + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -40,31 +47,45 @@ protected function setUp(): void ->setMethods(['load', 'format']) ->disableOriginalConstructor() ->getMock(); + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); $this->model = $objectManager->getObject( Price::class, - ['currency' => $this->currencyMock, 'context' => $contextMock] + ['currency' => $this->currencyMock, 'context' => $contextMock, 'storeManager' => $this->storeManagerMock] ); } - public function testPrepareDataSource() + /** + * @param $hasCurrency + * @param $dataSource + * @param $currencyCode + * @dataProvider testPrepareDataSourceDataProvider + */ + public function testPrepareDataSource($hasCurrency, $dataSource, $currencyCode) { $itemName = 'itemName'; $oldItemValue = 'oldItemValue'; $newItemValue = 'newItemValue'; - $dataSource = [ - 'data' => [ - 'items' => [ - [ - $itemName => $oldItemValue, - 'base_currency_code' => 'US' - ] - ] - ] - ]; + + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $currencyMock = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->getMock(); + $currencyMock->expects($hasCurrency ? $this->never() : $this->once()) + ->method('getCurrencyCode') + ->willReturn($currencyCode); + $this->storeManagerMock->expects($hasCurrency ? $this->never() : $this->once()) + ->method('getStore') + ->willReturn($store); + $store->expects($hasCurrency ? $this->never() : $this->once()) + ->method('getBaseCurrency') + ->willReturn($currencyMock); $this->currencyMock->expects($this->once()) ->method('load') - ->with($dataSource['data']['items'][0]['base_currency_code']) ->willReturnSelf(); $this->currencyMock->expects($this->once()) @@ -76,4 +97,31 @@ public function testPrepareDataSource() $dataSource = $this->model->prepareDataSource($dataSource); $this->assertEquals($newItemValue, $dataSource['data']['items'][0][$itemName]); } + + public function testPrepareDataSourceDataProvider() + { + $dataSource1 = [ + 'data' => [ + 'items' => [ + [ + 'itemName' => 'oldItemValue', + 'base_currency_code' => 'US' + ] + ] + ] + ]; + $dataSource2 = [ + 'data' => [ + 'items' => [ + [ + 'itemName' => 'oldItemValue' + ] + ] + ] + ]; + return [ + [true, $dataSource1, 'US'], + [false, $dataSource2, 'SAR'], + ]; + } } diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php index 8780ce10375ec..238d79766b59a 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php @@ -9,6 +9,7 @@ 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; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Directory\Model\Currency; @@ -28,6 +29,11 @@ class Price extends Column */ private $currency; + /** + * @var StoreManagerInterface|null + */ + private $storeManager; + /** * Constructor * @@ -37,6 +43,7 @@ class Price extends Column * @param array $components * @param array $data * @param Currency $currency + * @param StoreManagerInterface|null $storeManager */ public function __construct( ContextInterface $context, @@ -44,11 +51,14 @@ public function __construct( PriceCurrencyInterface $priceFormatter, array $components = [], array $data = [], - Currency $currency = null + Currency $currency = null, + StoreManagerInterface $storeManager = null ) { $this->priceFormatter = $priceFormatter; $this->currency = $currency ?: \Magento\Framework\App\ObjectManager::getInstance() ->create(Currency::class); + $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() + ->create(StoreManagerInterface::class); parent::__construct($context, $uiComponentFactory, $components, $data); } @@ -63,6 +73,12 @@ public function prepareDataSource(array $dataSource) if (isset($dataSource['data']['items'])) { foreach ($dataSource['data']['items'] as & $item) { $currencyCode = isset($item['base_currency_code']) ? $item['base_currency_code'] : null; + if (!$currencyCode) { + $store = $this->storeManager->getStore( + $this->context->getFilterParam('store_id', \Magento\Store\Model\Store::DEFAULT_STORE_ID) + ); + $currencyCode = $store->getBaseCurrency()->getCurrencyCode(); + } $basePurchaseCurrency = $this->currency->load($currencyCode); $item[$this->getData('name')] = $basePurchaseCurrency ->format($item[$this->getData('name')], [], false); From 9e48f91c7f3e67b8d49195fd2f6a3b3e03bc7025 Mon Sep 17 00:00:00 2001 From: Tu Nguyen <tuna@ecommage.com> Date: Mon, 24 Aug 2020 17:51:26 +0700 Subject: [PATCH 0670/1013] Allow specify font type when declare custom font --- lib/web/css/source/lib/_typography.less | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/web/css/source/lib/_typography.less b/lib/web/css/source/lib/_typography.less index db4eaaf584f4a..67a4df192c1f6 100644 --- a/lib/web/css/source/lib/_typography.less +++ b/lib/web/css/source/lib/_typography.less @@ -13,7 +13,8 @@ @font-format: false, @font-weight: normal, @font-style: normal, - @font-display: auto + @font-display: auto, + @font-type: false ) when (@font-format = false) { @font-face { font-family: @family-name; @@ -25,17 +26,23 @@ } } +// When need specify font format also should define font type include +// The available types for @font-type are 'woff', 'woff2', 'truetype', 'opentype', 'embedded-opentype', and 'svg' +// Enclose it in single quotes +// _____________________________________________ + .lib-font-face( @family-name, @font-path, @font-format: false, @font-weight: normal, @font-style: normal, - @font-display: auto -) when not (@font-format = false) { + @font-display: auto, + @font-type: false +) when not (@font-format = false) and not (@font-type = false) { @font-face { font-family: @family-name; - src: url('@{font-path}.@{font-format}') format(@font-format); + src: url('@{font-path}.@{font-format}') format(@font-type); font-weight: @font-weight; font-style: @font-style; font-display: @font-display; @@ -569,3 +576,4 @@ .lib-typography-code(); .lib-typography-blockquote(); } + From f9f3725e1326a375cecbd8fd1c70125d35adeaba Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Fri, 2 Oct 2020 13:56:37 +0300 Subject: [PATCH 0671/1013] magento/magento2#29196: Avoids endless loop of indexers being marked as invalid. --- .../Console/Command/IndexerReindexCommand.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php index 6abc2b242a9d1..ad286bcd1f81e 100644 --- a/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php +++ b/app/code/Magento/Indexer/Console/Command/IndexerReindexCommand.php @@ -64,7 +64,7 @@ public function __construct( ) { $this->indexerRegistry = $indexerRegistry; $this->dependencyInfoProvider = $dependencyInfoProvider; - $this->makeSharedValid = $makeSharedValid ?: ObjectManager::getInstance()->get(MakeSharedIndexValid::class); + $this->makeSharedValid = $makeSharedValid; parent::__construct($objectManagerFactory); } @@ -99,7 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output) // Skip indexers having shared index that was already complete if (!in_array($sharedIndex, $this->sharedIndexesComplete)) { $indexer->reindexAll(); - if (!empty($sharedIndex) && $this->makeSharedValid->execute($sharedIndex)) { + if (!empty($sharedIndex) && $this->getMakeSharedValid()->execute($sharedIndex)) { $this->sharedIndexesComplete[] = $sharedIndex; } } @@ -253,4 +253,18 @@ private function getDependencyInfoProvider() } return $this->dependencyInfoProvider; } + + /** + * Get MakeSharedIndexValid processor. + * + * @return MakeSharedIndexValid + */ + private function getMakeSharedValid(): MakeSharedIndexValid + { + if (!$this->makeSharedValid) { + $this->makeSharedValid = $this->getObjectManager()->get(MakeSharedIndexValid::class); + } + + return $this->makeSharedValid; + } } From ce1263da089d5b8ca28bc8689480092db0924d33 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec <wojtek@mediotype.com> Date: Fri, 2 Oct 2020 13:12:36 +0200 Subject: [PATCH 0672/1013] Fix exception which is thrown on search results page --- app/code/Magento/Search/Model/SynonymAnalyzer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index eea6a950d7ce5..5b6e04b8321fe 100644 --- a/app/code/Magento/Search/Model/SynonymAnalyzer.php +++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php @@ -137,6 +137,7 @@ private function getSearchPattern(array $words): string $patterns = []; for ($lastItem = count($words); $lastItem > 0; $lastItem--) { $phrase = implode("\s+", \array_slice($words, 0, $lastItem)); + $phrase = preg_quote($phrase, '/'); $patterns[] = '^' . $phrase . ','; $patterns[] = ',' . $phrase . ','; $patterns[] = ',' . $phrase . '$'; From b1de332f0e5823379e2f049a38c10f4f38b412a8 Mon Sep 17 00:00:00 2001 From: Kate Kyzyma <kate@atwix.com> Date: Fri, 2 Oct 2020 14:13:54 +0300 Subject: [PATCH 0673/1013] Refactoring the test --- .../AdminOpenCmsBlocksGridActionGroup.xml | 19 +++++ ...inPressAddNewCmsBlockButtonActionGroup.xml | 19 +++++ ...dminPressSaveCmsBlockButtonActionGroup.xml | 19 +++++ ...dminSelectCMSBlockStoreViewActionGroup.xml | 17 ++++ ...AssertAdminProperUrlIsShownActionGroup.xml | 21 +++++ .../Test/Mftf/Test/CheckStaticBlocksTest.xml | 78 +++++++++++-------- 6 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressAddNewCmsBlockButtonActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressSaveCmsBlockButtonActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminProperUrlIsShownActionGroup.xml diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.xml new file mode 100644 index 0000000000000..fca85651f7fda --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.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="AdminOpenCmsBlocksGridActionGroup"> + <annotations> + <description>Goes to the Cms Blocks grid page.</description> + </annotations> + + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSBlocksGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> +</actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressAddNewCmsBlockButtonActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressAddNewCmsBlockButtonActionGroup.xml new file mode 100644 index 0000000000000..17fc1cd7bdc50 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressAddNewCmsBlockButtonActionGroup.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="AdminPressAddNewCmsBlockButtonActionGroup"> + <annotations> + <description>Press Add new block button on Cms Blocks gid page</description> + </annotations> + + <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="clickOnAddNewBlockButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressSaveCmsBlockButtonActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressSaveCmsBlockButtonActionGroup.xml new file mode 100644 index 0000000000000..a45e5ed6b9fbf --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminPressSaveCmsBlockButtonActionGroup.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="AdminPressSaveCmsBlockButtonActionGroup"> + <annotations> + <description>Press save button on Cms Block page</description> + </annotations> + + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="clickOnSaveBlock"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.xml new file mode 100644 index 0000000000000..8c543e29c1ed7 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.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="AdminSelectCMSBlockStoreViewActionGroup"> + <arguments> + <argument name="storeViewName" type="string"/> + </arguments> + + <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="{{storeViewName}}" stepKey="selectStoreView" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminProperUrlIsShownActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminProperUrlIsShownActionGroup.xml new file mode 100644 index 0000000000000..fb97c9656aca2 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminProperUrlIsShownActionGroup.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="AssertAdminProperUrlIsShownActionGroup"> + <annotations> + <description>Assert current page has proper URL</description> + </annotations> + <arguments> + <argument name="target_path" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{target_path}}" stepKey="seePropertUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml index eba7812e29a0c..490e11d3cc751 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -21,8 +21,7 @@ </annotations> <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> - + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> <argument name="newWebsiteName" value="{{customWebsite.name}}"/> <argument name="websiteCode" value="{{customWebsite.code}}"/> @@ -39,49 +38,60 @@ </before> <after> - <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> + <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="deleteCMSBlock"/> + <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="deleteSecondCMSBlock"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Go to Cms blocks page--> - <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> - <seeInCurrentUrl url="cms/block/" stepKey="VerifyPageIsOpened"/> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="navigateToCMSBlocksGrid"/> + <actionGroup ref="AssertAdminProperUrlIsShownActionGroup" stepKey="verifyPageIsOpened"> + <argument name="target_path" value="cms/block/"/> + </actionGroup> + <!--Click to create new block--> - <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> - <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened"/> + <actionGroup ref="AdminPressAddNewCmsBlockButtonActionGroup" stepKey="clickOnAddNewBlockButton"/> + <actionGroup ref="AssertAdminProperUrlIsShownActionGroup" stepKey="verifyNewCmsBlockPageIsOpened"> + <argument name="target_path" value="cms/block/new"/> + </actionGroup> <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent"/> - <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock"/> - <waitForPageLoad stepKey="waitForPageLoad3"/> - <see userInput="You saved the block." stepKey="VerifyBlockIsSaved"/> - <!--Click to go back and add new block--> - <click selector="{{BlockNewPagePageActionsSection.back}}" stepKey="ClickToGoBack"/> - <waitForPageLoad stepKey="waitForPageLoad4"/> - <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock1"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> - <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened1"/> + <actionGroup ref="AdminPressSaveCmsBlockButtonActionGroup" stepKey="saveCmsBlock"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="You saved the block."/> + </actionGroup> + <!--Add new BLock with the same data--> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="openCmsBlocksGrid"/> + <actionGroup ref="AdminPressAddNewCmsBlockButtonActionGroup" stepKey="pressAddNewBlockButton"/> + <actionGroup ref="AssertAdminProperUrlIsShownActionGroup" stepKey="assertNewCmsBlockPageIsOpened"> + <argument name="target_path" value="cms/block/new"/> + </actionGroup> <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent1"/> - <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock1"/> - <waitForPageLoad stepKey="waitForPageLoad6"/> - <!--Verify that corresponding message is displayed--> - <see userInput="A block identifier with the same properties already exists in the selected store." stepKey="VerifyBlockIsSaved1"/> - <!--Click to go back and add new block--> - <click selector="{{BlockNewPagePageActionsSection.back}}" stepKey="ClickToGoBack1"/> - <waitForPageLoad stepKey="waitForPageLoad7"/> - <click selector="{{BlockPageActionsSection.addNewBlock}}" stepKey="ClickToAddNewBlock2"/> - <waitForPageLoad stepKey="waitForPageLoad8"/> - <seeInCurrentUrl url="cms/block/new" stepKey="VerifyNewBlockPageIsOpened2"/> + <actionGroup ref="AdminPressSaveCmsBlockButtonActionGroup" stepKey="clickOnSaveButton"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertErrorMessage"> + <argument name="messageType" value="error"/> + <argument name="message" value="A block identifier with the same properties already exists in the selected store."/> + </actionGroup> + <!--Add new BLock with the same data for another store view--> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="goToCmsBlocksGrid"/> + <actionGroup ref="AdminPressAddNewCmsBlockButtonActionGroup" stepKey="clickToAddNewButton"/> + <actionGroup ref="AssertAdminProperUrlIsShownActionGroup" stepKey="confirmNewCmsBlockPageIsOpened"> + <argument name="target_path" value="cms/block/new"/> + </actionGroup> <actionGroup ref="FillOutBlockContent" stepKey="FillOutBlockContent2"/> - <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="Default Store View" stepKey="selectDefaultStoreView" /> - <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="{{customStore.name}}" stepKey="selectSecondStoreView1" /> - <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock2"/> - <waitForPageLoad stepKey="waitForPageLoad9"/> - <see userInput="You saved the block." stepKey="VerifyBlockIsSaved2"/> + + <actionGroup ref="AdminSelectCMSBlockStoreViewActionGroup" stepKey="selectCustomStoreView"> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + + <actionGroup ref="AdminPressSaveCmsBlockButtonActionGroup" stepKey="saveNewCmsBlock"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="verifyBlockIsSaved"> + <argument name="message" value="You saved the block."/> + </actionGroup> </test> </tests> From ccec05add626879219b858485d8c06aeecc1a145 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec <wojtek@mediotype.com> Date: Fri, 2 Oct 2020 13:27:15 +0200 Subject: [PATCH 0674/1013] Modify unit test to cover for buggy use case --- .../Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php index 8751c8a4f3ec0..9e6d087f72f99 100644 --- a/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/SynonymAnalyzerTest.php @@ -49,9 +49,9 @@ protected function setUp(): void */ public function testGetSynonymsForPhrase() { - $phrase = 'Elizabeth is the british queen'; + $phrase = 'Elizabeth/Angela is the british queen'; $expected = [ - 0 => [ 0 => "Elizabeth" ], + 0 => [ 0 => "Elizabeth/Angela" ], 1 => [ 0 => "is" ], 2 => [ 0 => "the" ], 3 => [ 0 => "british", 1 => "english" ], From d59ad58d924f50350908ae504bbff46ebdef59d6 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 2 Oct 2020 15:50:04 +0300 Subject: [PATCH 0675/1013] MC-38048: Incorrect default country displayed on shipping page when store view is changed in cart. Part 2 --- .../Checkout/view/frontend/web/js/view/shipping.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 646e6156ec646..4e82c05f0385a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -121,7 +121,13 @@ define([ ); } checkoutProvider.on('shippingAddress', function (shippingAddrsData) { - checkoutData.setShippingAddressFromData(shippingAddrsData); + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + if (quote.shippingAddress().countryId !== shippingAddrsData.country_id && + (shippingAddrsData.postcode || shippingAddrsData.region_id) + ) { + checkoutData.setShippingAddressFromData(shippingAddrsData); + } + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers }); shippingRatesValidator.initFields(fieldsetName); }); From 08c9f33f0c56df5fb12634a2a452f94aeeee0187 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 2 Oct 2020 16:18:08 +0300 Subject: [PATCH 0676/1013] fix tests --- ...minAdvancedReportingPageUrlActionGroup.xml | 2 +- .../Test/Unit/Model/ReportUrlProviderTest.php | 59 +++++-------------- .../Controller/Adminhtml/Reports/ShowTest.php | 2 +- 3 files changed, 16 insertions(+), 47 deletions(-) diff --git a/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml index 51d77228c8dcf..ac4fca843a36b 100644 --- a/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml +++ b/app/code/Magento/Analytics/Test/Mftf/ActionGroup/AssertAdminAdvancedReportingPageUrlActionGroup.xml @@ -15,6 +15,6 @@ <switchToNextTab stepKey="switchToNewTab"/> <waitForPageLoad stepKey="waitForAdvancedReportingPageLoad"/> - <seeInCurrentUrl url="advancedreporting.rjmetrics.com/report" stepKey="seeAssertAdvancedReportingPageUrl"/> + <seeInCurrentUrl url="reports/advanced-reporting" stepKey="seeAssertAdvancedReportingPageUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php index 40e350515d491..fd13028b92d90 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportUrlProviderTest.php @@ -43,25 +43,10 @@ class ReportUrlProviderTest extends TestCase */ private $flagManagerMock; - /** - * @var ObjectManagerHelper - */ - private $objectManagerHelper; - /** * @var ReportUrlProvider */ - private $reportUrlProvider; - - /** - * @var string - */ - private $urlReportConfigPath = 'path/url/report'; - - /** - * @var string - */ - private $urlReportDocConfigPath = 'analytics/url/documentation'; + private $model; /** * @return void @@ -76,27 +61,15 @@ protected function setUp(): void $this->flagManagerMock = $this->createMock(FlagManager::class); - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->reportUrlProvider = $this->objectManagerHelper->getObject( - ReportUrlProvider::class, - [ - 'config' => $this->configMock, - 'analyticsToken' => $this->analyticsTokenMock, - 'otpRequest' => $this->otpRequestMock, - 'flagManager' => $this->flagManagerMock, - 'urlReportConfigPath' => $this->urlReportConfigPath, - ] - ); + $objectManagerHelper = new ObjectManagerHelper($this); - $this->reportUrlProvider = $this->objectManagerHelper->getObject( + $this->model = $objectManagerHelper->getObject( ReportUrlProvider::class, [ 'config' => $this->configMock, 'analyticsToken' => $this->analyticsTokenMock, 'otpRequest' => $this->otpRequestMock, 'flagManager' => $this->flagManagerMock, - 'urlReportDocConfigPath' => $this->urlReportDocConfigPath, ] ); } @@ -104,11 +77,12 @@ protected function setUp(): void /** * @param bool $isTokenExist * @param string|null $otp If null OTP was not received. + * @param string $configPath + * @return void * - * @throws SubscriptionUpdateException * @dataProvider getUrlDataProvider */ - public function testGetUrl($isTokenExist, $otp) + public function testGetUrl(bool $isTokenExist, ?string $otp, string $configPath): void { $reportUrl = 'https://example.com/report'; $url = ''; @@ -116,12 +90,7 @@ public function testGetUrl($isTokenExist, $otp) $this->configMock ->expects($this->once()) ->method('getValue') - ->with($this->urlReportConfigPath) - ->willReturn($reportUrl); - $this->configMock - ->expects($this->once()) - ->method('getValue') - ->with($this->urlReportDocConfigPath) + ->with($configPath) ->willReturn($reportUrl); $this->analyticsTokenMock ->expects($this->once()) @@ -136,24 +105,24 @@ public function testGetUrl($isTokenExist, $otp) if ($isTokenExist && $otp) { $url = $reportUrl . '?' . http_build_query(['otp' => $otp], '', '&'); } - $this->assertSame($url ?: $reportUrl, $this->reportUrlProvider->getUrl()); + + $this->assertSame($url ?: $reportUrl, $this->model->getUrl()); } /** * @return array */ - public function getUrlDataProvider() + public function getUrlDataProvider(): array { return [ - 'TokenDoesNotExist' => [false, null], - 'TokenExistAndOtpEmpty' => [true, null], - 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab'], + 'TokenDoesNotExist' => [false, null, 'analytics/url/documentation'], + 'TokenExistAndOtpEmpty' => [true, null, 'analytics/url/report'], + 'TokenExistAndOtpValid' => [true, '249e6b658877bde2a77bc4ab', 'analytics/url/report'], ]; } /** * @return void - * @throws SubscriptionUpdateException */ public function testGetUrlWhenSubscriptionUpdateRunning() { @@ -163,6 +132,6 @@ public function testGetUrlWhenSubscriptionUpdateRunning() ->with(SubscriptionUpdateHandler::PREVIOUS_BASE_URL_FLAG_CODE) ->willReturn('http://store.com'); $this->expectException(SubscriptionUpdateException::class); - $this->reportUrlProvider->getUrl(); + $this->model->getUrl(); } } diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php index f492e2cd09570..779cd24791b8c 100644 --- a/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php +++ b/dev/tests/integration/testsuite/Magento/Analytics/Controller/Adminhtml/Reports/ShowTest.php @@ -14,7 +14,7 @@ */ class ShowTest extends AbstractBackendController { - private const REPORT_HOST = 'advancedreporting.rjmetrics.com'; + private const REPORT_HOST = 'docs.magento.com'; /** * @inheritDoc */ From 48216f3e13d04b8755b70f8b9b1399d87a33ab91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szubert?= <bartlomiejszubert@gmail.com> Date: Mon, 28 Sep 2020 23:52:32 +0200 Subject: [PATCH 0677/1013] Fix #11175 - i18n:collect-phrases -m can't find many important magento phrases --- .../Setup/Module/I18n/Parser/Adapter/Html.php | 30 ++++++++-- .../Module/I18n/Parser/Adapter/HtmlTest.php | 56 ++++++++++--------- .../I18n/Parser/Adapter/_files/email.html | 2 + 3 files changed, 57 insertions(+), 31 deletions(-) diff --git a/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php b/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php index cf38fd70884f3..ec62ab8b84482 100644 --- a/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php +++ b/setup/src/Magento/Setup/Module/I18n/Parser/Adapter/Html.php @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Setup\Module\I18n\Parser\Adapter; +use Exception; use Magento\Email\Model\Template\Filter; /** @@ -16,17 +19,30 @@ class Html extends AbstractAdapter * Covers * <span><!-- ko i18n: 'Next'--><!-- /ko --></span> * <th class="col col-method" data-bind="i18n: 'Select Method'"></th> + * @deprecated Not used anymore because of newly introduced constant + * @see self::HTML_REGEX_LIST */ const HTML_FILTER = "/i18n:\s?'(?<value>[^'\\\\]*(?:\\\\.[^'\\\\]*)*)'/i"; + private const HTML_REGEX_LIST = [ + // <span><!-- ko i18n: 'Next'--><!-- /ko --></span> + // <th class="col col-method" data-bind="i18n: 'Select Method'"></th> + "/i18n:\s?'(?<value>[^'\\\\]*(?:\\\\.[^'\\\\]*)*)'/i", + // <translate args="'System Messages'"/> + // <span translate="'Examples'"></span> + "/translate( args|)=\"'(?<value>[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)'\"/i" + ]; + /** * @inheritdoc */ protected function _parse() { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $data = file_get_contents($this->_file); if ($data === false) { - throw new \Exception('Failed to load file from disk.'); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception('Failed to load file from disk.'); } $results = []; @@ -37,15 +53,19 @@ protected function _parse() if (preg_match(Filter::TRANS_DIRECTIVE_REGEX, $results[$i][2], $directive) !== 1) { continue; } + $quote = $directive[1]; $this->_addPhrase($quote . $directive[2] . $quote); } } - preg_match_all(self::HTML_FILTER, $data, $results, PREG_SET_ORDER); - for ($i = 0, $count = count($results); $i < $count; $i++) { - if (!empty($results[$i]['value'])) { - $this->_addPhrase($results[$i]['value']); + foreach (self::HTML_REGEX_LIST as $regex) { + preg_match_all($regex, $data, $results, PREG_SET_ORDER); + + for ($i = 0, $count = count($results); $i < $count; $i++) { + if (!empty($results[$i]['value'])) { + $this->_addPhrase($results[$i]['value']); + } } } } diff --git a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php index 15c442e9bac98..d7a2f0b4a9397 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/HtmlTest.php @@ -7,33 +7,25 @@ namespace Magento\Setup\Test\Unit\Module\I18n\Parser\Adapter; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Setup\Module\I18n\Parser\Adapter\Html; use PHPUnit\Framework\TestCase; class HtmlTest extends TestCase { /** - * @var string - */ - protected $_testFile; - - /** - * @var int + * @var Html */ - protected $_stringsCount; + private $model; /** - * @var Html + * @var string */ - protected $_adapter; + private $testFile; protected function setUp(): void { - $this->_testFile = str_replace('\\', '/', realpath(dirname(__FILE__))) . '/_files/email.html'; - $this->_stringsCount = count(file($this->_testFile)); - - $this->_adapter = (new ObjectManager($this))->getObject(Html::class); + $this->testFile = str_replace('\\', '/', realpath(__DIR__)) . '/_files/email.html'; + $this->model = new Html(); } public function testParse() @@ -41,68 +33,80 @@ public function testParse() $expectedResult = [ [ 'phrase' => 'Phrase 1', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '\'', ], [ 'phrase' => 'Phrase 2 with %a_lot of extra info for the brilliant %customer_name.', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '"', ], [ 'phrase' => 'This is test data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data at right side of attr', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is \\\' test \\\' data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is \\" test \\" data', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data with a quote after', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => 'This is test data with space after ', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => '\\\'', - 'file' => $this->_testFile, + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], [ 'phrase' => '\\\\\\\\ ', - 'file' => $this->_testFile, + 'file' => $this->testFile, + 'line' => '', + 'quote' => '', + ], + [ + 'phrase' => 'This is test content in translate tag', + 'file' => $this->testFile, + 'line' => '', + 'quote' => '', + ], + [ + 'phrase' => 'This is test content in translate attribute', + 'file' => $this->testFile, 'line' => '', 'quote' => '', ], ]; - $this->_adapter->parse($this->_testFile); + $this->model->parse($this->testFile); - $this->assertEquals($expectedResult, $this->_adapter->getPhrases()); + $this->assertEquals($expectedResult, $this->model->getPhrases()); } } diff --git a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html index 90579b48a07b5..f5603768ef306 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html +++ b/setup/src/Magento/Setup/Test/Unit/Module/I18n/Parser/Adapter/_files/email.html @@ -29,5 +29,7 @@ <label data-bind="i18n: ''"></label> <label data-bind="i18n: '\''"></label> <label data-bind="i18n: '\\\\ '"></label> + <span><translate args="'This is test content in translate tag'" /></span> + <span translate="'This is test content in translate attribute'"></span> </body> </html> From c0a9c422053ab85c4f3f77bb337196764c4ed149 Mon Sep 17 00:00:00 2001 From: naitsirch <naitsirch@e.mail.de> Date: Fri, 2 Oct 2020 16:15:49 +0200 Subject: [PATCH 0678/1013] Fix array to string conversion error when saving row system config with defaults This PR solves issue #30314. The backend model that is designed to handle complex values serializes non-scalar values before they are persisted to database. But when the old value is loaded from config defaults (defined in config.xml) for comparison, the default value is fetched as array. When the array is casted to string it results in an array to string conversion error. This issue is fixed by serializing the default value coming from the config. --- .../Model/Config/Backend/Serialized.php | 23 ++++++++++++++ .../Model/Config/Backend/SerializedTest.php | 30 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/app/code/Magento/Config/Model/Config/Backend/Serialized.php b/app/code/Magento/Config/Model/Config/Backend/Serialized.php index 6e0b6275db836..88fdae8908797 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Serialized.php +++ b/app/code/Magento/Config/Model/Config/Backend/Serialized.php @@ -5,6 +5,7 @@ */ namespace Magento\Config\Model\Config\Backend; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\Serializer\Json; @@ -84,4 +85,26 @@ public function beforeSave() parent::beforeSave(); return $this; } + + /** + * Get old value from existing config + * + * @return string + */ + public function getOldValue() + { + // If the value is retrieved from defaults defined in config.xml + // it may be returned as an array. + $value = $this->_config->getValue( + $this->getPath(), + $this->getScope() ?: ScopeConfigInterface::SCOPE_TYPE_DEFAULT, + $this->getScopeCode() + ); + + if (is_array($value)) { + return $this->serializer->serialize($value); + } + + return (string)$value; + } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php index c509b515b3112..90a381cff714b 100644 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php +++ b/app/code/Magento/Config/Test/Unit/Model/Config/Backend/SerializedTest.php @@ -8,6 +8,7 @@ namespace Magento\Config\Test\Unit\Model\Config\Backend; use Magento\Config\Model\Config\Backend\Serialized; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\Model\Context; use Magento\Framework\Serialize\Serializer\Json; @@ -27,11 +28,14 @@ class SerializedTest extends TestCase /** @var LoggerInterface|MockObject */ private $loggerMock; + private $scopeConfigMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); $this->serializerMock = $this->createMock(Json::class); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); $contextMock = $this->createMock(Context::class); $eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); $contextMock->method('getEventDispatcher') @@ -43,6 +47,7 @@ protected function setUp(): void [ 'serializer' => $this->serializerMock, 'context' => $contextMock, + 'config' => $this->scopeConfigMock, ] ); } @@ -135,4 +140,29 @@ public function beforeSaveDataProvider() ] ]; } + + /** + * If a config value is not available in core_confid_data the defaults are + * loaded from the config.xml file. Those defaults may be arrays. + * The Serialized backend model has to override its parent + * getOldValue function, to prevent an array to string conversion error + * and serialize those values. + */ + public function testGetOldValueWithNonScalarDefaultValue(): void + { + $value = [ + ['foo' => '1', 'bar' => '2'], + ]; + $serializedValue = \json_encode($value); + + $this->scopeConfigMock->method('getValue')->willReturn($value); + $this->serializerMock->method('serialize')->willReturn($serializedValue); + + $this->serializedConfig->setData('value', $serializedValue); + + $oldValue = $this->serializedConfig->getOldValue(); + + $this->assertIsString($oldValue, 'Default value from the config is not serialized.'); + $this->assertSame($serializedValue, $oldValue); + } } From 1ee63d4bfd031d2fbba27ef104f85cfe1f136e9a Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Thu, 1 Oct 2020 18:10:20 -0500 Subject: [PATCH 0679/1013] MC-38114: Custom address attribute Multiselect are not correctly displayed on checkout and order page - Fix multiple select option IDs are displayed instead of option labels on checkout page --- .../view/frontend/web/js/checkout-data.js | 7 ++-- .../frontend/web/js/view/billing-address.js | 37 ++++++++++++++++--- .../address-renderer/default.js | 37 ++++++++++++++++--- .../address-renderer/default.js | 37 ++++++++++++++++--- 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js index 1858ce946fb07..5c51fbb01f873 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/checkout-data.js @@ -11,8 +11,9 @@ define([ 'jquery', 'Magento_Customer/js/customer-data', + 'mageUtils', 'jquery/jquery-storageapi' -], function ($, storage) { +], function ($, storage, utils) { 'use strict'; var cacheKey = 'checkout-data', @@ -88,7 +89,7 @@ define([ setShippingAddressFromData: function (data) { var obj = getData(); - obj.shippingAddressFromData = data; + obj.shippingAddressFromData = utils.filterFormData(data); saveData(obj); }, @@ -193,7 +194,7 @@ define([ setBillingAddressFromData: function (data) { var obj = getData(); - obj.billingAddressFromData = data; + obj.billingAddressFromData = utils.filterFormData(data); saveData(obj); }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js index f850386890470..127aa6ef01f55 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/billing-address.js @@ -245,7 +245,7 @@ function ( * @returns {*} */ getCustomAttributeLabel: function (attribute) { - var resultAttribute; + var label; if (typeof attribute === 'string') { return attribute; @@ -255,13 +255,40 @@ function ( return attribute.label; } - if (typeof this.source.get('customAttributes') !== 'undefined') { - resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { - value: attribute.value + if (_.isArray(attribute.value)) { + label = _.map(attribute.value, function (value) { + return this.getCustomAttributeOptionLabel(attribute['attribute_code'], value) || value; + }, this).join(', '); + } else { + label = this.getCustomAttributeOptionLabel(attribute['attribute_code'], attribute.value); + } + + return label || attribute.value; + }, + + /** + * Get option label for given attribute code and option ID + * + * @param {String} attributeCode + * @param {String} value + * @returns {String|null} + */ + getCustomAttributeOptionLabel: function (attributeCode, value) { + var option, + label, + options = this.source.get('customAttributes') || {}; + + if (options[attributeCode]) { + option = _.findWhere(options[attributeCode], { + value: value }); + + if (option) { + label = option.label; + } } - return resultAttribute && resultAttribute.label || attribute.value; + return label; } }); }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js index 1f8cc90fe1622..3a4f34c26e5d7 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-address/address-renderer/default.js @@ -55,7 +55,7 @@ define([ * @returns {*} */ getCustomAttributeLabel: function (attribute) { - var resultAttribute; + var label; if (typeof attribute === 'string') { return attribute; @@ -65,13 +65,40 @@ define([ return attribute.label; } - if (typeof this.source.get('customAttributes') !== 'undefined') { - resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { - value: attribute.value + if (_.isArray(attribute.value)) { + label = _.map(attribute.value, function (value) { + return this.getCustomAttributeOptionLabel(attribute['attribute_code'], value) || value; + }, this).join(', '); + } else { + label = this.getCustomAttributeOptionLabel(attribute['attribute_code'], attribute.value); + } + + return label || attribute.value; + }, + + /** + * Get option label for given attribute code and option ID + * + * @param {String} attributeCode + * @param {String} value + * @returns {String|null} + */ + getCustomAttributeOptionLabel: function (attributeCode, value) { + var option, + label, + options = this.source.get('customAttributes') || {}; + + if (options[attributeCode]) { + option = _.findWhere(options[attributeCode], { + value: value }); + + if (option) { + label = option.label; + } } - return resultAttribute && resultAttribute.label || attribute.value; + return label; }, /** Set selected customer shipping address */ diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js index 6ec9fde554dc2..03591c95e46cb 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping-information/address-renderer/default.js @@ -32,7 +32,7 @@ define([ * @returns {*} */ getCustomAttributeLabel: function (attribute) { - var resultAttribute; + var label; if (typeof attribute === 'string') { return attribute; @@ -42,13 +42,40 @@ define([ return attribute.label; } - if (typeof this.source.get('customAttributes') !== 'undefined') { - resultAttribute = _.findWhere(this.source.get('customAttributes')[attribute['attribute_code']], { - value: attribute.value + if (_.isArray(attribute.value)) { + label = _.map(attribute.value, function (value) { + return this.getCustomAttributeOptionLabel(attribute['attribute_code'], value) || value; + }, this).join(', '); + } else { + label = this.getCustomAttributeOptionLabel(attribute['attribute_code'], attribute.value); + } + + return label || attribute.value; + }, + + /** + * Get option label for given attribute code and option ID + * + * @param {String} attributeCode + * @param {String} value + * @returns {String|null} + */ + getCustomAttributeOptionLabel: function (attributeCode, value) { + var option, + label, + options = this.source.get('customAttributes') || {}; + + if (options[attributeCode]) { + option = _.findWhere(options[attributeCode], { + value: value }); + + if (option) { + label = option.label; + } } - return resultAttribute && resultAttribute.label || attribute.value; + return label; } }); }); From 1e9001bef877b07550c6259b17ca4b4bb5a947c2 Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Fri, 2 Oct 2020 15:41:39 +0100 Subject: [PATCH 0680/1013] Added test coverage --- .../js/form/element/file-uploader.test.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js index 46916054a29be..445f94d6a48a9 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js @@ -358,6 +358,27 @@ define([ }); }); + describe('onFail handler', function () { + it('it logs responseText and status', function () { + spyOn(console, 'error'); + + var fakeEvent = { + target: document.createElement('input') + }, + data = { + jqXHR: { + responseText: 'Failed', + status: '500' + } + }; + + component.onFail(fakeEvent, data); + expect(console.error).toHaveBeenCalledWith(data.jqXHR.responseText); + expect(console.error).toHaveBeenCalledWith(data.jqXHR.status); + expect(console.error).toHaveBeenCalledTimes(2); + }); + }); + describe('aggregateError method', function () { it('should append onto aggregatedErrors array when called', function () { spyOn(component.aggregatedErrors, 'push'); From d45646954ac770058d081d408bddfa778f8b1c0f Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Fri, 2 Oct 2020 17:57:03 +0300 Subject: [PATCH 0681/1013] MC-38048: Incorrect default country displayed on shipping page when store view is changed in cart. Part 2 --- .../Magento/Checkout/view/frontend/web/js/view/shipping.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 4e82c05f0385a..084c8ad59a8da 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -122,9 +122,7 @@ define([ } checkoutProvider.on('shippingAddress', function (shippingAddrsData) { //jscs:disable requireCamelCaseOrUpperCaseIdentifiers - if (quote.shippingAddress().countryId !== shippingAddrsData.country_id && - (shippingAddrsData.postcode || shippingAddrsData.region_id) - ) { + if (shippingAddrsData.street && shippingAddrsData.street[0].length > 0) { checkoutData.setShippingAddressFromData(shippingAddrsData); } //jscs:enable requireCamelCaseOrUpperCaseIdentifiers From 2588d77c4076b34a816e86bc6bf8f61ecd8c4672 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Fri, 2 Oct 2020 18:05:58 +0300 Subject: [PATCH 0682/1013] MC-35771: Datepicker/calendar control does not use the store locale --- .../Magento/Customer/Block/Widget/Dob.php | 51 ++++++++++++++++++- .../view/frontend/templates/widget/dob.phtml | 19 +++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Customer/Block/Widget/Dob.php b/app/code/Magento/Customer/Block/Widget/Dob.php index 90ce9ba210ed2..1ca15981b8370 100644 --- a/app/code/Magento/Customer/Block/Widget/Dob.php +++ b/app/code/Magento/Customer/Block/Widget/Dob.php @@ -7,11 +7,16 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Framework\Api\ArrayObjectSearch; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\EncoderInterface; +use Magento\Framework\Locale\Bundle\DataBundle; +use Magento\Framework\Locale\ResolverInterface; /** * Customer date of birth attribute block * * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Dob extends AbstractWidget { @@ -39,6 +44,18 @@ class Dob extends AbstractWidget */ protected $filterFactory; + /** + * JSON Encoder + * + * @var EncoderInterface + */ + private $encoder; + + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Customer\Helper\Address $addressHelper @@ -46,6 +63,8 @@ class Dob extends AbstractWidget * @param \Magento\Framework\View\Element\Html\Date $dateElement * @param \Magento\Framework\Data\Form\FilterFactory $filterFactory * @param array $data + * @param EncoderInterface|null $encoder + * @param ResolverInterface|null $localeResolver */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -53,10 +72,14 @@ public function __construct( CustomerMetadataInterface $customerMetadata, \Magento\Framework\View\Element\Html\Date $dateElement, \Magento\Framework\Data\Form\FilterFactory $filterFactory, - array $data = [] + array $data = [], + ?EncoderInterface $encoder = null, + ?ResolverInterface $localeResolver = null ) { $this->dateElement = $dateElement; $this->filterFactory = $filterFactory; + $this->encoder = $encoder ?? ObjectManager::getInstance()->get(EncoderInterface::class); + $this->localeResolver = $localeResolver ?? ObjectManager::getInstance()->get(ResolverInterface::class); parent::__construct($context, $addressHelper, $customerMetadata, $data); } @@ -377,4 +400,30 @@ public function getFirstDay() \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); } + + /** + * Get translated calendar config json formatted + * + * @return string + */ + public function getTranslatedCalendarConfigJson(): string + { + $localeData = (new DataBundle())->get($this->localeResolver->getLocale()); + $monthsData = $localeData['calendar']['gregorian']['monthNames']; + $daysData = $localeData['calendar']['gregorian']['dayNames']; + + return $this->encoder->encode( + [ + 'closeText' => __('Done'), + 'prevText' => __('Prev'), + 'nextText' => __('Next'), + 'currentText' => __('Today'), + 'monthNames' => array_values(iterator_to_array($monthsData['format']['wide'])), + 'monthNamesShort' => array_values(iterator_to_array($monthsData['format']['abbreviated'])), + 'dayNames' => array_values(iterator_to_array($daysData['format']['wide'])), + 'dayNamesShort' => array_values(iterator_to_array($daysData['format']['abbreviated'])), + 'dayNamesMin' => array_values(iterator_to_array($daysData['format']['short'])), + ] + ); + } } diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index 3c2f970faadee..fee577abadbf9 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -27,10 +27,12 @@ $fieldCssClass = 'field date field-' . $block->getHtmlId(); $fieldCssClass .= $block->isRequired() ? ' required' : ''; ?> <div class="<?= $block->escapeHtmlAttr($fieldCssClass) ?>"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"><span><?= $block->escapeHtml($block->getStoreLabel('dob')) ?></span></label> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"> + <span><?= $block->escapeHtml($block->getStoreLabel('dob')) ?></span> + </label> <div class="control customer-dob"> <?= $block->getFieldHtml() ?> - <?php if ($_message = $block->getAdditionalDescription()) : ?> + <?php if ($_message = $block->getAdditionalDescription()): ?> <div class="note"><?= $block->escapeHtml($_message) ?></div> <?php endif; ?> </div> @@ -42,4 +44,15 @@ $fieldCssClass .= $block->isRequired() ? ' required' : ''; "Magento_Customer/js/validation": {} } } - </script> +</script> + +<script> + require([ + 'jquery', + 'jquery-ui-modules/datepicker' + ], function ( $ ) { + $.extend(true, $, { + calendarConfig: <?= $block->escapeJs($block->getTranslatedCalendarConfigJson()); ?> + }); + }); +</script> From 0767ff7e811257a6707ccfee5cef31e03973f800 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Fri, 2 Oct 2020 13:08:39 -0500 Subject: [PATCH 0683/1013] MC-37371: Wrong currency sign in Credit Memo grid - failed static class description --- app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php index 238d79766b59a..6720b1646cc9a 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php @@ -16,6 +16,8 @@ /** * Class Price + * + * UiComponent class for Price format column */ class Price extends Column { From 9804ad90d9b7d1ada7fe89a782dcec49dc892914 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Fri, 2 Oct 2020 16:42:47 -0500 Subject: [PATCH 0684/1013] [AWS S3] MC-37479: Support by Magento Content Design (#6179) * MC-37479: Support by Magento Content Design --- app/code/Magento/AwsS3/Driver/AwsS3.php | 209 ++++++----- .../Magento/AwsS3/Driver/AwsS3Factory.php | 45 ++- app/code/Magento/AwsS3/Model/Config.php | 36 +- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 327 ++++++++++++++++++ .../Command/RemoteStorageEnableCommand.php | 90 +++++ .../RemoteStorage/Driver/DriverPool.php | 47 ++- app/code/Magento/RemoteStorage/Filesystem.php | 108 ++++++ .../Magento/RemoteStorage/Model/Config.php | 44 ++- .../Model/Config/Source/FileStorage.php | 37 -- .../RemoteStorage/Model/Filesystem.php | 55 --- .../Magento/RemoteStorage/Plugin/Scope.php | 63 ++++ .../Magento/RemoteStorage/Plugin/Sitemap.php | 23 +- .../Test/Unit/Model/ConfigTest.php | 43 --- .../RemoteStorage/etc/adminhtml/di.xml | 19 - .../RemoteStorage/etc/adminhtml/system.xml | 20 -- app/code/Magento/RemoteStorage/etc/di.xml | 42 ++- app/code/Magento/RemoteStorage/etc/module.xml | 1 + .../Theme/Model/Design/Backend/File.php | 2 +- .../Magento/Framework/File/Uploader.php | 25 +- 19 files changed, 903 insertions(+), 333 deletions(-) create mode 100644 app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php create mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php create mode 100644 app/code/Magento/RemoteStorage/Filesystem.php delete mode 100644 app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php delete mode 100644 app/code/Magento/RemoteStorage/Model/Filesystem.php create mode 100644 app/code/Magento/RemoteStorage/Plugin/Scope.php delete mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php delete mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/di.xml delete mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/system.xml diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 602e81ec480ff..a224d9d6ce0ef 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -7,7 +7,6 @@ namespace Magento\AwsS3\Driver; -use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; @@ -18,10 +17,10 @@ */ class AwsS3 implements DriverInterface { - public const S3 = 'aws-s3'; + public const TYPE_DIR = 'dir'; + public const TYPE_FILE = 'file'; - private const TYPE_DIR = 'dir'; - private const TYPE_FILE = 'file'; + private const CONFIG = ['ACL' => 'public-read']; /** * @var AwsS3Adapter @@ -34,27 +33,11 @@ class AwsS3 implements DriverInterface private $streams = []; /** - * @param string $region - * @param string $bucket - * @param string|null $key - * @param string|null $secret + * @param AwsS3Adapter $adapter */ - public function __construct(string $region, string $bucket, string $key = null, string $secret = null) + public function __construct(AwsS3Adapter $adapter) { - $config = [ - 'region' => $region, - 'version' => 'latest' - ]; - - if ($key && $secret) { - $config['credentials'] = [ - 'key' => $key, - 'secret' => $secret, - ]; - } - - $client = new S3Client($config); - $this->adapter = new AwsS3Adapter($client, $bucket); + $this->adapter = $adapter; } /** @@ -74,7 +57,7 @@ public function __destruct() */ public function fileGetContents($path, $flag = null, $context = null): string { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (isset($this->streams[$path])) { //phpcs:disable @@ -82,7 +65,7 @@ public function fileGetContents($path, $flag = null, $context = null): string //phpcs:enable } - return $this->adapter->read($path)['contents']; + return $this->adapter->read($path)['contents'] ?? ''; } /** @@ -94,7 +77,7 @@ public function isExists($path): bool return true; } - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (!$path || $path === '/') { return true; @@ -120,7 +103,26 @@ public function createDirectory($path, $permissions = 0777): bool return true; } - $path = $this->getRelativePath('', $path); + return $this->createDirectoryRecursively( + $this->normalizeRelativePath($path) + ); + } + + /** + * Created directory recursively. + * + * @param string $path + * @return bool + * @throws FileSystemException + */ + private function createDirectoryRecursively(string $path): bool + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $parentDir = dirname($path); + + while (!$this->isDirectory($parentDir)) { + $this->createDirectoryRecursively($parentDir); + } return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config([])); } @@ -130,10 +132,10 @@ public function createDirectory($path, $permissions = 0777): bool */ public function copy($source, $destination, DriverInterface $targetDriver = null): bool { - $source = $this->getRelativePath('', $source); - $destination = $this->getRelativePath('', $destination); - - return $this->adapter->copy($source, $destination); + return $this->adapter->copy( + $this->normalizeRelativePath($source), + $this->normalizeRelativePath($destination) + ); } /** @@ -141,9 +143,9 @@ public function copy($source, $destination, DriverInterface $targetDriver = null */ public function deleteFile($path): bool { - $path = $this->getRelativePath('', $path); - - return $this->adapter->delete($path); + return $this->adapter->delete( + $this->normalizeRelativePath($path) + ); } /** @@ -151,9 +153,9 @@ public function deleteFile($path): bool */ public function deleteDirectory($path): bool { - $path = $this->getRelativePath('', $path); - - return $this->adapter->deleteDir($path); + return $this->adapter->deleteDir( + $this->normalizeRelativePath($path) + ); } /** @@ -161,9 +163,9 @@ public function deleteDirectory($path): bool */ public function filePutContents($path, $content, $mode = null, $context = null): int { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); - return $this->adapter->write($path, $content, new Config(['ACL' => 'public-read']))['size']; + return $this->adapter->write($path, $content, new Config(self::CONFIG))['size']; } /** @@ -171,9 +173,10 @@ public function filePutContents($path, $content, $mode = null, $context = null): */ public function readDirectoryRecursively($path = null): array { - $path = $this->getRelativePath('', $path); - - return $this->adapter->listContents($path, true); + return $this->adapter->listContents( + $this->normalizeRelativePath($path), + true + ); } /** @@ -181,9 +184,10 @@ public function readDirectoryRecursively($path = null): array */ public function readDirectory($path): array { - $path = $this->getRelativePath('', $path); - - return $this->adapter->listContents($path, false); + return $this->adapter->listContents( + $this->normalizeRelativePath($path), + false + ); } /** @@ -191,7 +195,9 @@ public function readDirectory($path): array */ public function getRealPathSafety($path) { - return '/'; + return $this->normalizeAbsolutePath( + $this->normalizeRelativePath($path) + ); } /** @@ -199,19 +205,52 @@ public function getRealPathSafety($path) */ public function getAbsolutePath($basePath, $path, $scheme = null) { - $path = $this->getRelativePath($basePath, $path); + if ($basePath && $path && 0 === strpos($path, $basePath)) { + return $this->normalizeAbsolutePath( + $this->normalizeRelativePath($path) + ); + } - if ($path === '/') { - $path = ''; + if ($basePath && $basePath !== '/') { + return $basePath . ltrim((string)$path, '/'); } - if ($basePath !== '/') { - $path = $basePath . $path; + return $this->normalizeAbsolutePath($path); + } + + /** + * Resolves absolute path. + * + * @param string $path Relative path + * @return string Absolute path + */ + private function normalizeAbsolutePath(string $path = '.'): string + { + $path = ltrim($path, '/'); + + if (!$path) { + $path = '.'; } - $path = $path ?: '.'; + return $this->adapter->getClient()->getObjectUrl( + $this->adapter->getBucket(), + $this->adapter->applyPathPrefix($path) + ); + } - return $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), $path); + /** + * Resolves relative path. + * + * @param string $path Absolute path + * @return string Relative path + */ + private function normalizeRelativePath(string $path): string + { + return str_replace( + $this->normalizeAbsolutePath(), + '', + $path + ); } /** @@ -227,11 +266,11 @@ public function isReadable($path): bool */ public function isFile($path): bool { - if ($path === '/') { + if (!$path || $path === '/') { return false; } - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); $path = rtrim($path, '/'); return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_FILE; @@ -242,11 +281,11 @@ public function isFile($path): bool */ public function isDirectory($path): bool { - if ($path === '/') { + if (in_array($path, ['.', '/'], true)) { return true; } - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (!$path || $path === '/') { return true; @@ -262,23 +301,14 @@ public function isDirectory($path): bool */ public function getRelativePath($basePath, $path = null): string { - $relativePath = str_replace( - $this->adapter->getClient()->getObjectUrl($this->adapter->getBucket(), '.'), - '', - $path - ); + $basePath = $this->normalizeAbsolutePath($basePath); + $absolutePath = $this->normalizeAbsolutePath((string)$path); - if ($basePath && $basePath !== '/') { - $relativePath = str_replace($basePath, '', $relativePath); + if ($basePath === $absolutePath . '/' || strpos($absolutePath, $basePath) === 0) { + return ltrim(substr($absolutePath, strlen($basePath)), '/'); } - $relativePath = ltrim($relativePath, '/'); - - if (!$relativePath) { - $relativePath = '/'; - } - - return $relativePath; + return ltrim($path, '/'); } /** @@ -286,7 +316,8 @@ public function getRelativePath($basePath, $path = null): string */ public function getParentDirectory($path): string { - return '/'; + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return dirname($this->normalizeAbsolutePath($path)); } /** @@ -294,7 +325,7 @@ public function getParentDirectory($path): string */ public function getRealPath($path) { - return $this->getAbsolutePath('', $path); + return $this->normalizeAbsolutePath($path); } /** @@ -302,10 +333,10 @@ public function getRealPath($path) */ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null): bool { - $oldPath = $this->getRelativePath('', $oldPath); - $newPath = $this->getRelativePath('', $newPath); - - return $this->adapter->rename($oldPath, $newPath); + return $this->adapter->rename( + $this->normalizeRelativePath($oldPath), + $this->normalizeRelativePath($newPath) + ); } /** @@ -313,7 +344,7 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) */ public function stat($path): array { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); $metaInfo = $this->adapter->getMetadata($path); if (!$metaInfo) { @@ -336,6 +367,7 @@ public function stat($path): array 'type' => $metaInfo['type'], 'mtime' => $metaInfo['timestamp'], 'disposition' => null, + 'mimetype' => $metaInfo['mimetype'] ]; } @@ -368,7 +400,7 @@ public function changePermissions($path, $permissions): bool */ public function changePermissionsRecursively($path, $dirPermissions, $filePermissions): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return true; } /** @@ -376,7 +408,13 @@ public function changePermissionsRecursively($path, $dirPermissions, $filePermis */ public function touch($path, $modificationTime = null) { - return true; + $path = $this->normalizeRelativePath($path); + + $content = $this->adapter->has($path) ? + $this->adapter->read($path)['contents'] + : ''; + + return (bool)$this->adapter->write($path, $content, new Config([])); } /** @@ -478,13 +516,15 @@ public function fileWrite($resource, $data) { //phpcs:disable $resourcePath = stream_get_meta_data($resource)['uri']; + //phpcs:enable foreach ($this->streams as $stream) { + //phpcs:disable if (stream_get_meta_data($stream)['uri'] === $resourcePath) { return fwrite($stream, $data); } + //phpcs:enable } - //phpcs:enable return false; } @@ -496,10 +536,12 @@ public function fileClose($resource): bool { //phpcs:disable $resourcePath = stream_get_meta_data($resource)['uri']; + //phpcs:enable foreach ($this->streams as $path => $stream) { + //phpcs:disable if (stream_get_meta_data($stream)['uri'] === $resourcePath) { - $this->adapter->writeStream($path, $resource, new Config(['ACL' => 'public-read'])); + $this->adapter->writeStream($path, $resource, new Config(self::CONFIG)); // Remove path from streams after unset($this->streams[$path]); @@ -507,7 +549,6 @@ public function fileClose($resource): bool return fclose($stream); } } - //phpcs:enable return false; } @@ -517,7 +558,7 @@ public function fileClose($resource): bool */ public function fileOpen($path, $mode) { - $path = $this->getRelativePath('', $path); + $path = $this->normalizeRelativePath($path); if (!isset($this->streams[$path])) { $this->streams[$path] = tmpfile(); diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index e71c3a84d3ce5..6ab8f93fa6e2b 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -7,9 +7,11 @@ namespace Magento\AwsS3\Driver; +use Aws\S3\S3Client; +use League\Flysystem\AwsS3v3\AwsS3Adapter; use Magento\AwsS3\Model\Config; - use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\RemoteStorage\Driver\DriverFactoryInterface; /** @@ -17,16 +19,23 @@ */ class AwsS3Factory implements DriverFactoryInterface { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** * @var Config */ private $config; /** + * @param ObjectManagerInterface $objectManager * @param Config $config */ - public function __construct(Config $config) + public function __construct(ObjectManagerInterface $objectManager, Config $config) { + $this->objectManager = $objectManager; $this->config = $config; } @@ -37,11 +46,33 @@ public function __construct(Config $config) */ public function create(): DriverInterface { - return new AwsS3( - $this->config->getRegion(), - $this->config->getBucket(), - $this->config->getAccessKey(), - $this->config->getSecretKey() + $config = [ + 'region' => $this->config->getRegion(), + 'version' => 'latest' + ]; + + $key = $this->config->getAccessKey(); + $secret = $this->config->getSecretKey(); + + if ($key && $secret) { + $config['credentials'] = [ + 'key' => $key, + 'secret' => $secret, + ]; + } + + return $this->objectManager->create( + AwsS3::class, + [ + 'adapter' => $this->objectManager->create( + AwsS3Adapter::class, + [ + 'client' => $this->objectManager->create(S3Client::class, ['args' => $config]), + 'bucket' => $this->config->getBucket(), + 'prefix' => $this->config->getPrefix() + ] + ) + ] ); } } diff --git a/app/code/Magento/AwsS3/Model/Config.php b/app/code/Magento/AwsS3/Model/Config.php index 00cd5b36740e6..f4e19edd4eec3 100644 --- a/app/code/Magento/AwsS3/Model/Config.php +++ b/app/code/Magento/AwsS3/Model/Config.php @@ -7,28 +7,28 @@ namespace Magento\AwsS3\Model; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\DeploymentConfig; /** * Configuration for AWS S3. */ class Config { - public const PATH_DRIVER = 'system/file_system/driver'; - public const PATH_REGION = 'system/file_system/region'; - public const PATH_BUCKET = 'system/file_system/bucket'; - public const PATH_ACCESS_KEY = 'system/file_system/access_key'; - public const PATH_SECRET_KEY = 'system/file_system/secret_key'; + public const PATH_REGION = 'remote_storage/region'; + public const PATH_BUCKET = 'remote_storage/bucket'; + public const PATH_ACCESS_KEY = 'remote_storage/access_key'; + public const PATH_SECRET_KEY = 'remote_storage/secret_key'; + public const PATH_PREFIX = 'remote_storage/prefix'; /** - * @var ScopeConfigInterface + * @var DeploymentConfig */ private $config; /** - * @param ScopeConfigInterface $config + * @param DeploymentConfig $config */ - public function __construct(ScopeConfigInterface $config) + public function __construct(DeploymentConfig $config) { $this->config = $config; } @@ -40,7 +40,7 @@ public function __construct(ScopeConfigInterface $config) */ public function getRegion(): string { - return (string)$this->config->getValue(self::PATH_REGION); + return (string)$this->config->get(self::PATH_REGION); } /** @@ -50,7 +50,7 @@ public function getRegion(): string */ public function getBucket(): string { - return (string)$this->config->getValue(self::PATH_BUCKET); + return (string)$this->config->get(self::PATH_BUCKET); } /** @@ -60,7 +60,7 @@ public function getBucket(): string */ public function getAccessKey(): string { - return (string)$this->config->getValue(self::PATH_ACCESS_KEY); + return (string)$this->config->get(self::PATH_ACCESS_KEY); } /** @@ -70,6 +70,16 @@ public function getAccessKey(): string */ public function getSecretKey(): string { - return (string)$this->config->getValue(self::PATH_SECRET_KEY); + return (string)$this->config->get(self::PATH_SECRET_KEY); + } + + /** + * Retrieves prefix. + * + * @return string + */ + public function getPrefix(): string + { + return (string)$this->config->get(self::PATH_PREFIX, ''); } } diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php new file mode 100644 index 0000000000000..5ddc4811230ec --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -0,0 +1,327 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Test\Unit\Driver; + +use Aws\S3\S3ClientInterface; +use League\Flysystem\AwsS3v3\AwsS3Adapter; +use Magento\AwsS3\Driver\AwsS3; +use Magento\Framework\Exception\FileSystemException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @see AwsS3 + */ +class AwsS3Test extends TestCase +{ + private const URL = 'https://test.s3.amazonaws.com/'; + + /** + * @var AwsS3 + */ + private $driver; + + /** + * @var AwsS3Adapter|MockObject + */ + private $adapterMock; + + /** + * @var S3ClientInterface|MockObject + */ + private $clientMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->adapterMock = $this->createMock(AwsS3Adapter::class); + $this->clientMock = $this->getMockForAbstractClass(S3ClientInterface::class); + + $this->adapterMock->method('applyPathPrefix') + ->willReturnArgument(0); + $this->adapterMock->method('getBucket') + ->willReturn('test'); + $this->adapterMock->method('getClient') + ->willReturn($this->clientMock); + $this->clientMock->method('getObjectUrl') + ->willReturnCallback(function (string $bucket, string $path) { + if ($path === '.') { + $path = ''; + } + + return self::URL . $path; + }); + + $this->driver = new AwsS3( + $this->adapterMock + ); + } + + /** + * @param string|null $basePath + * @param string|null $path + * @param string $expected + * + * @dataProvider getAbsolutePathDataProvider + */ + public function testGetAbsolutePath($basePath, $path, string $expected): void + { + self::assertSame($expected, $this->driver->getAbsolutePath($basePath, $path)); + } + + /** + * @return array + */ + public function getAbsolutePathDataProvider(): array + { + return [ + [ + null, + 'test.png', + self::URL . 'test.png' + ], + [ + self::URL . 'test/test.png', + null, + self::URL . 'test/test.png' + ], + [ + '', + 'test.png', + self::URL . 'test.png' + ], + [ + '', + '/test/test.png', + self::URL . 'test/test.png' + ], + [ + self::URL . 'test/test.png', + self::URL . 'test/test.png', + self::URL . 'test/test.png' + ], + [ + self::URL . 'test/', + 'test.txt', + self::URL . 'test/test.txt' + ], + [ + self::URL . 'media/', + '/catalog/test.png', + self::URL . 'media/catalog/test.png' + ] + ]; + } + + /** + * @param string $basePath + * @param string $path + * @param string $expected + * + * @dataProvider getRelativePathDataProvider + */ + public function testGetRelativePath(string $basePath, string $path, string $expected): void + { + self::assertSame($expected, $this->driver->getRelativePath($basePath, $path)); + } + + /** + * @return array + */ + public function getRelativePathDataProvider(): array + { + return [ + [ + '', + 'test/test.txt', + 'test/test.txt' + ], + [ + '', + '/test/test.txt', + 'test/test.txt' + ], + [ + self::URL, + self::URL . 'test/test.txt', + 'test/test.txt' + ], + + ]; + } + + /** + * @param string $path + * @param string $normalizedPath + * @param bool $has + * @param array $metadata + * @param bool $expected + * @throws FileSystemException + * + * @dataProvider isDirectoryDataProvider + */ + public function testIsDirectory( + string $path, + string $normalizedPath, + bool $has, + array $metadata, + bool $expected + ): void { + $this->adapterMock->method('has') + ->with($normalizedPath) + ->willReturn($has); + $this->adapterMock->method('getMetadata') + ->with($normalizedPath) + ->willReturn($metadata); + + self::assertSame($expected, $this->driver->isDirectory($path)); + } + + /** + * @return array + */ + public function isDirectoryDataProvider(): array + { + return [ + [ + 'some_directory/', + 'some_directory/', + false, + [], + false + ], + [ + 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + true + ], + [ + self::URL . 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + true + ], + [ + self::URL . 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + false + ], + [ + '', + '', + true, + [], + true + ], + [ + '/', + '', + true, + [], + true + ], + ]; + } + + /** + * @param string $path + * @param string $normalizedPath + * @param bool $has + * @param array $metadata + * @param bool $expected + * @throws FileSystemException + * + * @dataProvider isFileDataProvider + */ + public function testIsFile( + string $path, + string $normalizedPath, + bool $has, + array $metadata, + bool $expected + ): void { + $this->adapterMock->method('has') + ->with($normalizedPath) + ->willReturn($has); + $this->adapterMock->method('getMetadata') + ->with($normalizedPath) + ->willReturn($metadata); + + self::assertSame($expected, $this->driver->isFile($path)); + } + + /** + * @return array + */ + public function isFileDataProvider(): array + { + return [ + [ + 'some_file.txt', + 'some_file.txt', + false, + [], + false + ], + [ + 'some_file.txt/', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + true + ], + [ + self::URL . 'some_file.txt', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + true + ], + [ + self::URL . 'some_file.txt/', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + false + ], + [ + '', + '', + false, + [], + false + ], + [ + '/', + '', + false, + [], + false + ] + ]; + } +} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php new file mode 100644 index 0000000000000..cad5ecf314da2 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Console\Command; + +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Exception\FileSystemException; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Remote storage configuration enablement. + */ +class RemoteStorageEnableCommand extends Command +{ + private const NAME = 'remote-storage:enable'; + private const ARG_DRIVER = 'driver'; + private const OPTION_BUCKET = 'bucket'; + private const OPTION_REGION = 'region'; + private const OPTION_ACCESS_KEY = 'access-key'; + private const OPTION_SECRET_KEY = 'secret-key'; + private const OPTION_PREFIX = 'prefix'; + private const OPTION_IS_PUBLIC = 'is-public'; + + /** + * @var Writer + */ + private $writer; + + /** + * @param Writer $writer + */ + public function __construct(Writer $writer) + { + $this->writer = $writer; + + parent::__construct(); + } + + /** + * @inheritDoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Enable remote storage integration') + ->addArgument(self::ARG_DRIVER, InputArgument::REQUIRED, 'Remote driver') + ->addOption(self::OPTION_BUCKET, null, InputOption::VALUE_REQUIRED, 'Bucket') + ->addOption(self::OPTION_REGION, null, InputOption::VALUE_REQUIRED, 'Region') + ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_REQUIRED, 'Access key') + ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_REQUIRED, 'Secret key') + ->addOption(self::OPTION_PREFIX, null, InputOption::VALUE_REQUIRED, 'Prefix', '') + ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_NONE, 'Is public'); + } + + /** + * Executes command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return void + * @throws FileSystemException + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $this->writer->saveConfig([ + ConfigFilePool::APP_ENV => [ + 'remote_storage' => [ + 'driver' => (string)$input->getArgument(self::ARG_DRIVER), + 'bucket' => (string)$input->getOption(self::OPTION_BUCKET), + 'region' => (string)$input->getOption(self::OPTION_REGION), + 'access_key' => (string)$input->getOption(self::OPTION_ACCESS_KEY), + 'secret_key' => (string)$input->getOption(self::OPTION_SECRET_KEY), + 'prefix' => (string)$input->getOption(self::OPTION_PREFIX), + 'is_public' => (bool)$input->getOption(self::OPTION_IS_PUBLIC) + ] + ] + ], true); + + $output->writeln('<info>Config was saved.</info>'); + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index 11a49147f4f19..4c2c834f0d776 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -7,24 +7,22 @@ namespace Magento\RemoteStorage\Driver; -use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool as BaseDriverPool; use Magento\Framework\Filesystem\DriverPoolInterface; +use Magento\RemoteStorage\Model\Config; /** * The remote driver pool. */ class DriverPool implements DriverPoolInterface { - public const PATH_DRIVER = 'system/file_system/driver'; + public const PATH_DRIVER = 'remote_storage/driver'; + public const PATH_IS_PUBLIC = 'remote_storage/is_public'; public const REMOTE = 'remote'; - /** - * @var ScopeConfigInterface - */ - private $config; - /** * @var DriverPool */ @@ -40,12 +38,17 @@ class DriverPool implements DriverPoolInterface */ private $remotePool; + /** + * @var Config + */ + private $config; + /** * @param BaseDriverPool $driverPool - * @param ScopeConfigInterface $config + * @param Config $config * @param array $remotePool */ - public function __construct(BaseDriverPool $driverPool, ScopeConfigInterface $config, array $remotePool = []) + public function __construct(BaseDriverPool $driverPool, Config $config, array $remotePool = []) { $this->driverPool = $driverPool; $this->config = $config; @@ -53,20 +56,30 @@ public function __construct(BaseDriverPool $driverPool, ScopeConfigInterface $co } /** - * @inheritDoc + * Retrieves remote driver. + * + * @param string $code + * @return DriverInterface + * + * @throws RuntimeException + * @throws FileSystemException */ public function getDriver($code = self::REMOTE): DriverInterface { - $driver = $this->config->getValue('system/file_system/driver'); + if ($code === self::REMOTE) { + if (isset($this->pool[$code])) { + return $this->pool[$code]; + } - if (isset($this->pool[$code])) { - return $this->pool[$code]; - } + $driver = $this->config->getDriver(); + + if ($driver && isset($this->remotePool[$driver])) { + return $this->pool[$code] = $this->remotePool[$driver]->create(); + } - if ($driver && $driver !== BaseDriverPool::FILE) { - return $this->pool[$code] = $this->remotePool[$driver]->create(); + throw new RuntimeException(__('Remote driver is not available.')); } - return $this->pool[$code] = $this->driverPool->getDriver($code); + return $this->driverPool->getDriver($code); } } diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php new file mode 100644 index 0000000000000..1594e53392b76 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage; + +use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Filesystem as BaseFilesystem; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; + +/** + * Filesystem implementation for remote storage. + */ +class Filesystem extends BaseFilesystem +{ + /** + * @var bool + */ + private $isEnabled; + + /** + * @var array + */ + private $directoryCodes; + + /** + * @var DriverPool + */ + private $driverPool; + + /** + * @param BaseFilesystem\DirectoryList $directoryList + * @param ReadFactory $readFactory + * @param WriteFactory $writeFactory + * @param Config $config + * @param DriverPool $driverPool + * @param array $directoryCodes + */ + public function __construct( + BaseFilesystem\DirectoryList $directoryList, + ReadFactory $readFactory, + WriteFactory $writeFactory, + Config $config, + DriverPool $driverPool, + array $directoryCodes = [] + ) { + $this->isEnabled = $config->isEnabled(); + $this->driverPool = $driverPool; + $this->directoryCodes = $directoryCodes; + + parent::__construct($directoryList, $readFactory, $writeFactory); + } + + /** + * @inheritDoc + */ + public function getDirectoryRead($directoryCode, $driverCode = DriverPool::REMOTE) + { + $hasCode = !$this->directoryCodes || in_array($directoryCode, $this->directoryCodes, true); + + if ($driverCode === DriverPool::REMOTE && $hasCode && $this->isEnabled) { + $code = $directoryCode . '_' . $driverCode; + + if (!array_key_exists($code, $this->readInstances)) { + $uri = $this->getUri($directoryCode) ?: '/'; + + $this->readInstances[$code] = $this->readFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $uri), + $driverCode + ); + } + + return $this->readInstances[$code]; + } + + return parent::getDirectoryRead($directoryCode); + } + + /** + * @inheritDoc + */ + public function getDirectoryWrite($directoryCode, $driverCode = DriverPool::REMOTE) + { + $hasCode = !$this->directoryCodes || in_array($directoryCode, $this->directoryCodes, true); + + if ($driverCode === DriverPool::REMOTE && $hasCode && $this->isEnabled) { + $code = $directoryCode . '_' . $driverCode; + + if (!array_key_exists($code, $this->writeInstances)) { + $uri = $this->getUri($directoryCode) ?: '/'; + + $this->writeInstances[$code] = $this->writeFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $uri), + $driverCode + ); + } + + return $this->writeInstances[$code]; + } + + return parent::getDirectoryWrite($directoryCode); + } +} diff --git a/app/code/Magento/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php index b49c647ab6894..164d94cefddee 100644 --- a/app/code/Magento/RemoteStorage/Model/Config.php +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -7,8 +7,10 @@ namespace Magento\RemoteStorage\Model; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\RemoteStorage\Driver\DriverPool; /** * Configuration for remote storage. @@ -16,27 +18,51 @@ class Config { /** - * @var ScopeConfigInterface + * @var DeploymentConfig */ - private $scopeConfig; + private $config; /** - * @param ScopeConfigInterface $scopeConfig + * @param DeploymentConfig $config */ - public function __construct(ScopeConfigInterface $scopeConfig) + public function __construct(DeploymentConfig $config) { - $this->scopeConfig = $scopeConfig; + $this->config = $config; + } + + /** + * Retrieve driver name. + * + * @return string|null + * @throws FileSystemException + * @throws RuntimeException + */ + public function getDriver(): ?string + { + return $this->config->get(DriverPool::PATH_DRIVER, null); } /** * Check if remote FS is enabled. * * @return bool + * @throws FileSystemException + * @throws RuntimeException */ public function isEnabled(): bool { - $driver = $this->scopeConfig->getValue('system/file_system/driver'); + return $this->config->get(DriverPool::PATH_DRIVER) !== null; + } - return $driver && $driver !== DriverPool::FILE; + /** + * Use remote URL for public URLs. + * + * @return bool + * @throws FileSystemException + * @throws RuntimeException + */ + public function isPublic(): bool + { + return (bool)$this->config->get(DriverPool::PATH_IS_PUBLIC, false); } } diff --git a/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php b/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php deleted file mode 100644 index 4972cdda18d9b..0000000000000 --- a/app/code/Magento/RemoteStorage/Model/Config/Source/FileStorage.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Model\Config\Source; - -use Magento\Framework\Data\OptionSourceInterface; - -/** - * Provides a list of supported file storages. - */ -class FileStorage implements OptionSourceInterface -{ - /** - * @var array - */ - private $options; - - /** - * @param array $options - */ - public function __construct(array $options = []) - { - $this->options = $options; - } - - /** - * @inheritDoc - */ - public function toOptionArray(): array - { - return $this->options; - } -} diff --git a/app/code/Magento/RemoteStorage/Model/Filesystem.php b/app/code/Magento/RemoteStorage/Model/Filesystem.php deleted file mode 100644 index 040ee005cd57d..0000000000000 --- a/app/code/Magento/RemoteStorage/Model/Filesystem.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Model; - -use Magento\Framework\Filesystem\DirectoryList; -use Magento\Framework\Filesystem\Directory\ReadFactory; -use Magento\Framework\Filesystem\Directory\WriteFactory; - -/** - * Filesystem implementation for remote storage. - */ -class Filesystem extends \Magento\Framework\Filesystem -{ - /** - * @var bool - */ - private $isEnabled; - - /** - * @param DirectoryList $directoryList - * @param ReadFactory $readFactory - * @param WriteFactory $writeFactory - * @param Config $config - */ - public function __construct( - DirectoryList $directoryList, - ReadFactory $readFactory, - WriteFactory $writeFactory, - Config $config - ) { - $this->isEnabled = $config->isEnabled(); - - parent::__construct($directoryList, $readFactory, $writeFactory); - } - - /** - * Gets URL path by code. - * - * @param string $code - * @return string - */ - protected function getDirPath($code): string - { - if ($this->isEnabled) { - return $this->directoryList->getUrlPath($code) ?: '/'; - } - - return parent::getDirPath($code); - } -} diff --git a/app/code/Magento/RemoteStorage/Plugin/Scope.php b/app/code/Magento/RemoteStorage/Plugin/Scope.php new file mode 100644 index 0000000000000..ab723fa1d0c19 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Scope.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Url\ScopeInterface; +use Magento\Framework\UrlInterface; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Filesystem; + +/** + * Modifies the base URL. + */ +class Scope +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var bool + */ + private $isEnabled; + + /** + * @param Config $config + * @param Filesystem $filesystem + */ + public function __construct(Config $config, Filesystem $filesystem) + { + $this->isEnabled = $config->isEnabled() && $config->isPublic(); + $this->filesystem = $filesystem; + } + + /** + * Modifies the base URL. + * + * @param ScopeInterface $subject + * @param string $result + * @param string $type + * @return string + * @throws ValidatorException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetBaseUrl(ScopeInterface $subject, string $result, string $type = ''): string + { + if ($type === UrlInterface::URL_TYPE_MEDIA && $this->isEnabled) { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA, DriverPool::REMOTE) + ->getAbsolutePath(); + } + + return $result; + } +} diff --git a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php index e84f216ba996c..2e93949b40fce 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php +++ b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php @@ -7,7 +7,9 @@ namespace Magento\RemoteStorage\Plugin; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Filesystem; use Magento\RemoteStorage\Model\Config; use Magento\Sitemap\Model\Sitemap as BaseSitemap; @@ -17,23 +19,23 @@ class Sitemap { /** - * @var Config + * @var Filesystem */ - private $config; + private $filesystem; /** - * @var DriverPool + * @var bool */ - private $driverPool; + private $isEnabled; /** - * @param DriverPool $driverPool + * @param Filesystem $filesystem * @param Config $config */ - public function __construct(DriverPool $driverPool, Config $config) + public function __construct(Filesystem $filesystem, Config $config) { - $this->driverPool = $driverPool; - $this->config = $config; + $this->filesystem = $filesystem; + $this->isEnabled = $config->isEnabled(); } /** @@ -53,10 +55,11 @@ public function afterGetSitemapUrl( string $sitemapPath, string $sitemapFileName ): string { - if ($this->config->isEnabled()) { + if ($this->isEnabled) { $path = trim($sitemapPath . $sitemapFileName, '/'); - return $this->driverPool->getDriver()->getAbsolutePath('', $path); + return $this->filesystem->getDirectoryRead(DirectoryList::ROOT, DriverPool::REMOTE) + ->getAbsolutePath($path); } return $result; diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php deleted file mode 100644 index 1e121c1d1dda1..0000000000000 --- a/app/code/Magento/RemoteStorage/Test/Unit/Model/ConfigTest.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Test\Unit\Model; - -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\RemoteStorage\Driver\DriverPool; -use Magento\RemoteStorage\Model\Config; -use PHPUnit\Framework\TestCase; - -/** - * @see Config - */ -class ConfigTest extends TestCase -{ - /** - * @var Config - */ - private $model; - - /** - * @inheritDoc - */ - protected function setUp(): void - { - $configMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $configMock->method('getValue') - ->willReturnMap([ - [DriverPool::PATH_DRIVER, ScopeConfigInterface::SCOPE_TYPE_DEFAULT, null, DriverPool::REMOTE], - ]); - - $this->model = new Config($configMock); - } - - public function testIsEnabled(): void - { - self::assertTrue($this->model->isEnabled()); - } -} diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml deleted file mode 100644 index 1437f0636ac03..0000000000000 --- a/app/code/Magento/RemoteStorage/etc/adminhtml/di.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?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\RemoteStorage\Model\Config\Source\FileStorage"> - <arguments> - <argument name="options" xsi:type="array"> - <item name="file" xsi:type="array"> - <item name="value" xsi:type="const">Magento\Framework\Filesystem\DriverPool::FILE</item> - <item name="label" xsi:type="string" translate="true">File System</item> - </item> - </argument> - </arguments> - </type> -</config> diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml deleted file mode 100644 index aa5865c099dc5..0000000000000 --- a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?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="file_system" translate="label" type="text" sortOrder="850" showInDefault="1"> - <label>Storage Configuration</label> - <field id="driver" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>General Storage</label> - <source_model>Magento\RemoteStorage\Model\Config\Source\FileStorage</source_model> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index 9bc960691d034..a43a6160c554f 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -16,36 +16,56 @@ <argument name="driverPool" xsi:type="object">Magento\RemoteStorage\Driver\DriverPool</argument> </arguments> </virtualType> - <virtualType name="remoteFilesystem" type="Magento\RemoteStorage\Model\Filesystem"> + <type name="Magento\RemoteStorage\Filesystem"> <arguments> <argument name="writeFactory" xsi:type="object">remoteWriteFactory</argument> <argument name="readFactory" xsi:type="object">remoteReadFactory</argument> </arguments> + </type> + <virtualType name="customRemoteFilesystem" type="Magento\RemoteStorage\Filesystem"> + <arguments> + <argument name="directoryCodes" xsi:type="array"> + <item name="media" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::MEDIA</item> + </argument> + </arguments> </virtualType> + <virtualType name="fullRemoteFilesystem" type="Magento\RemoteStorage\Filesystem" /> + <preference for="Magento\Framework\Filesystem" type="customRemoteFilesystem"/> + <type name="Magento\Framework\Filesystem\Directory\TargetDirectory"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + <argument name="driverCode" xsi:type="const">Magento\RemoteStorage\Driver\DriverPool::REMOTE</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Model\Sitemap"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + <plugin name="remote_sitemap" type="Magento\RemoteStorage\Plugin\Sitemap" /> + </type> <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Save"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> <type name="Magento\Sitemap\Block\Adminhtml\Grid\Renderer\Link"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> - <type name="Magento\Sitemap\Model\Sitemap"> - <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> - </arguments> - <plugin name="remote_sitemap" type="Magento\RemoteStorage\Plugin\Sitemap" /> + <type name="Magento\Framework\Url\ScopeInterface"> + <plugin name="remote_url" type="Magento\RemoteStorage\Plugin\Scope" /> </type> - <type name="Magento\Framework\Filesystem\Directory\TargetDirectory"> + <type name="Magento\Framework\Console\CommandListInterface"> <arguments> - <argument name="filesystem" xsi:type="object">remoteFilesystem</argument> + <argument name="commands" xsi:type="array"> + <item name="remoteStorageEnable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageEnableCommand</item> + </argument> </arguments> </type> </config> diff --git a/app/code/Magento/RemoteStorage/etc/module.xml b/app/code/Magento/RemoteStorage/etc/module.xml index cc9f2e7328292..6c1b7f0b05a34 100644 --- a/app/code/Magento/RemoteStorage/etc/module.xml +++ b/app/code/Magento/RemoteStorage/etc/module.xml @@ -10,6 +10,7 @@ <sequence> <module name="Magento_Backend"/> <module name="Magento_Sitemap"/> + <module name="Magento_Store"/> </sequence> </module> </config> diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index 143889364781f..3ef113fa63fa7 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -160,7 +160,7 @@ public function afterLoad() 'size' => is_array($stat) ? $stat['size'] : 0, //phpcs:ignore Magento2.Functions.DiscouragedFunction 'name' => basename($value), - 'type' => $this->getMimeType($fileName), + 'type' => $stat['mimetype'] ?? $this->getMimeType($fileName), 'exists' => true, ] ]; diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index b5929f8ce7a08..b944ceb94628b 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -311,7 +311,10 @@ private function validateDestination(string $destinationFolder): void { if ($this->_allowCreateFolders) { $this->createDestinationFolder($destinationFolder); - } elseif (!$this->getFileDriver()->isWritable($destinationFolder)) { + } elseif (!$this->getTargetDirectory() + ->getDirectoryWrite(DirectoryList::ROOT) + ->isWritable($destinationFolder) + ) { throw new FileSystemException(__('Destination folder is not writable or does not exists.')); } } @@ -334,13 +337,19 @@ protected function chmod($file) * * @param string $tmpPath * @param string $destPath - * @return bool|void + * @return bool + * @throws FileSystemException */ protected function _moveFile($tmpPath, $destPath) { - $rootPath = $this->getDocumentRoot()->getPath(); - $destPath = str_replace($this->getDirectoryList()->getPath($rootPath), '', $destPath); - $directory = $this->getTargetDirectory()->getDirectoryWrite($rootPath); + $rootCode = $this->getDocumentRoot()->getPath(); + + if (strpos($destPath, $this->getDirectoryList()->getPath($rootCode)) !== 0) { + $rootCode = DirectoryList::ROOT; + } + + $destPath = str_replace($this->getDirectoryList()->getPath($rootCode), '', $destPath); + $directory = $this->getTargetDirectory()->getDirectoryWrite($rootCode); return $this->getFileDriver()->rename( $tmpPath, @@ -745,8 +754,10 @@ private function createDestinationFolder(string $destinationFolder) $destinationFolder = substr($destinationFolder, 0, -1); } - if (!$this->getFileDriver()->isDirectory($destinationFolder)) { - $result = $this->getFileDriver()->createDirectory($destinationFolder); + $rootDirectory = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + + if (!$rootDirectory->isDirectory($destinationFolder)) { + $result = $rootDirectory->getDriver()->createDirectory($destinationFolder); if (!$result) { throw new FileSystemException(__('Unable to create directory %1.', $destinationFolder)); } From 00a03e958a24a2d7872b4510be8544acef6af813 Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Sat, 3 Oct 2020 10:03:32 +0200 Subject: [PATCH 0685/1013] Fixes after CR --- .../Config/QueueConfigItem/DataMapperTest.php | 33 ++++++++++--------- .../Config/QueueConfigItem/DataMapper.php | 12 +++---- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php index 5870e7cd80e67..62581ad13a84b 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php @@ -6,7 +6,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Framework\MessageQueue\Test\Unit\Topology\Config\QueueConfigItem; @@ -23,17 +22,17 @@ class DataMapperTest extends TestCase /** * @var Data|MockObject */ - private $configData; + private $configDataMock; /** * @var CommunicationConfig|MockObject */ - private $communicationConfig; + private $communicationConfigMock; /** * @var ResponseQueueNameBuilder|MockObject */ - private $queueNameBuilder; + private $queueNameBuilderMock; /** * @var DataMapper @@ -45,10 +44,14 @@ class DataMapperTest extends TestCase */ protected function setUp(): void { - $this->configData = $this->createMock(Data::class); - $this->communicationConfig = $this->createMock(CommunicationConfig::class); - $this->queueNameBuilder = $this->createMock(ResponseQueueNameBuilder::class); - $this->model = new DataMapper($this->configData, $this->communicationConfig, $this->queueNameBuilder); + $this->configDataMock = $this->createMock(Data::class); + $this->communicationConfigMock = $this->createMock(CommunicationConfig::class); + $this->queueNameBuilderMock = $this->createMock(ResponseQueueNameBuilder::class); + $this->model = new DataMapper( + $this->configDataMock, + $this->communicationConfigMock, + $this->queueNameBuilderMock + ); } /** @@ -112,11 +115,11 @@ public function testGetMappedData(): void ['topic02', ['name' => 'topic02', 'is_synchronous' => false]], ]; - $this->communicationConfig->expects($this->exactly(2)) + $this->communicationConfigMock->expects($this->exactly(2)) ->method('getTopic') ->willReturnMap($communicationMap); - $this->configData->expects($this->once())->method('get')->willReturn($data); - $this->queueNameBuilder->expects($this->once()) + $this->configDataMock->expects($this->once())->method('get')->willReturn($data); + $this->queueNameBuilderMock->expects($this->once()) ->method('getQueueName') ->with('topic01') ->willReturn('responseQueue.topic01'); @@ -218,15 +221,15 @@ public function testGetMappedDataForWildcard(): void 'topic08.part2.some.test' => ['name' => 'topic08.part2.some.test', 'is_synchronous' => true], ]; - $this->communicationConfig->expects($this->once()) + $this->communicationConfigMock->expects($this->once()) ->method('getTopic') ->with('topic01') ->willReturn(['name' => 'topic01', 'is_synchronous' => true]); - $this->communicationConfig->expects($this->any()) + $this->communicationConfigMock->expects($this->any()) ->method('getTopics') ->willReturn($communicationData); - $this->configData->expects($this->once())->method('get')->willReturn($data); - $this->queueNameBuilder->expects($this->any()) + $this->configDataMock->expects($this->once())->method('get')->willReturn($data); + $this->queueNameBuilderMock->expects($this->any()) ->method('getQueueName') ->willReturnCallback(function ($value) { return 'responseQueue.' . $value; diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php index 627dca68d14a4..d48fa637fd885 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php @@ -73,12 +73,12 @@ public function getMappedData(): array foreach ($exchange['bindings'] as $binding) { if ($binding['destinationType'] === 'queue') { $queueItems = $this->createQueueItems( - (string) $binding['destination'], - (string) $binding['topic'], - (array) $binding['arguments'], - (string) $connection + (string)$binding['destination'], + (string)$binding['topic'], + (array)$binding['arguments'], + (string)$connection ); - $this->mappedData += $queueItems; + $this->mappedData = array_merge($this->mappedData, $queueItems); } } } @@ -141,7 +141,7 @@ private function isSynchronousTopic(string $topicName): bool { try { $topic = $this->communicationConfig->getTopic($topicName); - return (bool) $topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + return (bool)$topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; } catch (LocalizedException $exception) { throw new LocalizedException(new Phrase('Error while checking if topic is synchronous')); } From cff5e3afb691d39b28de0a1786e1729c6faf4c97 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Mon, 5 Oct 2020 10:41:19 +0530 Subject: [PATCH 0686/1013] 30179: resetPassword mutation returns generic error - fixed typo error --- .../testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php index 254bf8c0fa97e..be6513c75b081 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php @@ -222,7 +222,7 @@ public function testNewPasswordCheckMinLength() * @throws Exception * @throws LocalizedException */ - public function testNewPasswordCheckCharactersStrenth() + public function testNewPasswordCheckCharactersStrength() { $this->expectException(\Exception::class); $this->expectExceptionMessage('Minimum of different classes of characters in password is 3. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); From 9d99f32192345b3a1d1c2ed1a7096e686585cedb Mon Sep 17 00:00:00 2001 From: "taras.gamanov" <engcom-vendorworker-hotel@adobe.com> Date: Mon, 5 Oct 2020 09:54:23 +0300 Subject: [PATCH 0687/1013] Code refactoring --- .../Magento/Ui/base/js/form/element/file-uploader.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js index 445f94d6a48a9..ba5ad61cfe310 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/file-uploader.test.js @@ -360,8 +360,6 @@ define([ describe('onFail handler', function () { it('it logs responseText and status', function () { - spyOn(console, 'error'); - var fakeEvent = { target: document.createElement('input') }, @@ -372,6 +370,8 @@ define([ } }; + spyOn(console, 'error'); + component.onFail(fakeEvent, data); expect(console.error).toHaveBeenCalledWith(data.jqXHR.responseText); expect(console.error).toHaveBeenCalledWith(data.jqXHR.status); From 47c21ab6bf8eed1f6dac3dbdb53019a698ba01ab Mon Sep 17 00:00:00 2001 From: Alin Alexandru <alin.alexandru@innobyte.com> Date: Mon, 5 Oct 2020 10:40:03 +0300 Subject: [PATCH 0688/1013] Allow to cache search results --- app/code/Magento/PageCache/etc/varnish4.vcl | 4 ++-- app/code/Magento/PageCache/etc/varnish5.vcl | 4 ++-- app/code/Magento/PageCache/etc/varnish6.vcl | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/PageCache/etc/varnish4.vcl b/app/code/Magento/PageCache/etc/varnish4.vcl index f5e25ce36e973..12c69b82ebada 100644 --- a/app/code/Magento/PageCache/etc/varnish4.vcl +++ b/app/code/Magento/PageCache/etc/varnish4.vcl @@ -57,8 +57,8 @@ sub vcl_recv { return (pass); } - # Bypass shopping cart, checkout and search requests - if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") { + # Bypass shopping cart and checkout + if (req.url ~ "/checkout") { return (pass); } diff --git a/app/code/Magento/PageCache/etc/varnish5.vcl b/app/code/Magento/PageCache/etc/varnish5.vcl index 92bb3394486fc..355f358f58ed6 100644 --- a/app/code/Magento/PageCache/etc/varnish5.vcl +++ b/app/code/Magento/PageCache/etc/varnish5.vcl @@ -58,8 +58,8 @@ sub vcl_recv { return (pass); } - # Bypass shopping cart, checkout and search requests - if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") { + # Bypass shopping cart and checkout + if (req.url ~ "/checkout") { return (pass); } diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl index b23bec4c45fb8..427faafc84d2c 100644 --- a/app/code/Magento/PageCache/etc/varnish6.vcl +++ b/app/code/Magento/PageCache/etc/varnish6.vcl @@ -62,8 +62,8 @@ sub vcl_recv { return (pass); } - # Bypass shopping cart, checkout and search requests - if (req.url ~ "/checkout" || req.url ~ "/catalogsearch") { + # Bypass shopping cart and checkout + if (req.url ~ "/checkout") { return (pass); } From 0c508ef7d05fc63c7345bf30a1e0b51fabb2be04 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 5 Oct 2020 11:04:42 +0300 Subject: [PATCH 0689/1013] MC-35771: Datepicker/calendar control does not use the store locale --- .../Test/Unit/Block/Widget/DobTest.php | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 70232e955a86d..1f5a6145e0eea 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -17,6 +17,7 @@ use Magento\Framework\Data\Form\FilterFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Json\EncoderInterface; use Magento\Framework\Locale\Resolver; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Stdlib\DateTime\Intl\DateFormatterFactory; @@ -91,6 +92,11 @@ class DobTest extends TestCase */ private $_locale; + /** + * @var EncoderInterface + */ + private $encoder; + /** * @inheritDoc */ @@ -157,12 +163,16 @@ function () use ($timezone, $localeResolver) { } ); + $this->encoder = $this->getMockForAbstractClass(EncoderInterface::class); + $this->_block = new Dob( $this->context, $this->createMock(Address::class), $this->customerMetadata, $this->createMock(Date::class), - $this->filterFactory + $this->filterFactory, + [], + $this->encoder ); } @@ -598,4 +608,51 @@ public function testGetHtmlExtraParamsWithRequiredOption() $this->_block->getHtmlExtraParams() ); } + + /** + * Tests getTranslatedCalendarConfigJson() + * + * @param array $expectedArray + * @param string $expectedJson + * @dataProvider getTranslatedCalendarConfigJsonDataProvider + * @return void + */ + public function testGetTranslatedCalendarConfigJson(array $expectedArray, string $expectedJson): void + { + $this->encoder->expects($this->once()) + ->method('encode') + ->with($expectedArray) + ->willReturn($expectedJson); + + $this->assertEquals( + $expectedJson, + $this->_block->getTranslatedCalendarConfigJson() + ); + } + + /** + * Provider for testGetTranslatedCalendarConfigJson + * + * @return array + */ + public function getTranslatedCalendarConfigJsonDataProvider() + { + return [ + [ + 'expectedArray' => [ + 'closeText' => 'Done', + 'prevText' => 'Prev', + 'nextText' => 'Next', + 'currentText' => 'Today', + 'monthNames' => ['January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December'], + 'monthNamesShort' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + 'dayNames' => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + 'dayNamesShort' => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + 'dayNamesMin' => ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], + ], + 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["January","February","March","April","May","June","July","August","September","October","November","December"],"monthNamesShort":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"dayNames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"dayNamesShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"dayNamesMin":["Su","Mo","Tu","We","Th","Fr","Sa"]}' + ], + ]; + } } From e36b4d0f906081ba776cc15546504fef85a90b9a Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 5 Oct 2020 11:41:30 +0300 Subject: [PATCH 0690/1013] MC-35771: Datepicker/calendar control does not use the store locale --- .../Test/Unit/Block/Widget/DobTest.php | 35 ++++++++++++++++--- .../view/frontend/templates/widget/dob.phtml | 27 +++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 1f5a6145e0eea..020dbbbe4ff13 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -97,6 +97,11 @@ class DobTest extends TestCase */ private $encoder; + /** + * @var ResolverInterface + */ + private $localeResolver; + /** * @inheritDoc */ @@ -116,14 +121,15 @@ protected function setUp(): void $cache->expects($this->any())->method('getFrontend')->willReturn($frontendCache); $objectManager = new ObjectManager($this); - $localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); - $localeResolver->expects($this->any()) + $this->localeResolver = $this->getMockForAbstractClass(ResolverInterface::class); + $this->localeResolver->expects($this->any()) ->method('getLocale') ->willReturnCallback( function () { return $this->_locale; } ); + $localeResolver = $this->localeResolver; $timezone = $objectManager->getObject( Timezone::class, ['localeResolver' => $localeResolver, 'dateFormatterFactory' => new DateFormatterFactory()] @@ -172,7 +178,8 @@ function () use ($timezone, $localeResolver) { $this->createMock(Date::class), $this->filterFactory, [], - $this->encoder + $this->encoder, + $this->localeResolver ); } @@ -612,13 +619,16 @@ public function testGetHtmlExtraParamsWithRequiredOption() /** * Tests getTranslatedCalendarConfigJson() * + * @param string $locale * @param array $expectedArray * @param string $expectedJson * @dataProvider getTranslatedCalendarConfigJsonDataProvider * @return void */ - public function testGetTranslatedCalendarConfigJson(array $expectedArray, string $expectedJson): void + public function testGetTranslatedCalendarConfigJson(string $locale, array $expectedArray, string $expectedJson): void { + $this->_locale = $locale; + $this->encoder->expects($this->once()) ->method('encode') ->with($expectedArray) @@ -639,6 +649,7 @@ public function getTranslatedCalendarConfigJsonDataProvider() { return [ [ + 'locale' => 'en_US', 'expectedArray' => [ 'closeText' => 'Done', 'prevText' => 'Prev', @@ -653,6 +664,22 @@ public function getTranslatedCalendarConfigJsonDataProvider() ], 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["January","February","March","April","May","June","July","August","September","October","November","December"],"monthNamesShort":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"dayNames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"dayNamesShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"dayNamesMin":["Su","Mo","Tu","We","Th","Fr","Sa"]}' ], + [ + 'locale' => 'de_DE', + 'expectedArray' => [ + 'closeText' => 'Done', + 'prevText' => 'Prev', + 'nextText' => 'Next', + 'currentText' => 'Today', + 'monthNames' => ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], + 'monthNamesShort' => ['Jan.', 'Feb.', 'März', 'Apr.', 'Mai', 'Juni', 'Juli', 'Aug.', 'Sept.', 'Okt.', 'Nov.', 'Dez.'], + 'dayNames' => ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + 'dayNamesShort' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], + 'dayNamesMin' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], + ], + 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["Januar","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],"monthNamesShort":["Jan.","Feb.","M\u00e4rz","Apr.","Mai","Juni","Juli","Aug.","Sept.","Okt.","Nov.","Dez."],"dayNames":["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],"dayNamesShort":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],"dayNamesMin":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."]}' + ], ]; } } diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index fee577abadbf9..8a584938b1548 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ /** @var \Magento\Customer\Block\Widget\Dob $block */ /* @@ -23,6 +24,7 @@ NOTE: Regarding styles - if we leave it this way, we'll move it to boxes.css. Al automatically using block input parameters. */ +$translatedCalendarConfigJson = $block->getTranslatedCalendarConfigJson(); $fieldCssClass = 'field date field-' . $block->getHtmlId(); $fieldCssClass .= $block->isRequired() ? ' required' : ''; ?> @@ -46,13 +48,20 @@ $fieldCssClass .= $block->isRequired() ? ' required' : ''; } </script> -<script> - require([ - 'jquery', - 'jquery-ui-modules/datepicker' - ], function ( $ ) { - $.extend(true, $, { - calendarConfig: <?= $block->escapeJs($block->getTranslatedCalendarConfigJson()); ?> - }); +<?php $scriptString = <<<script + +require([ + 'jquery', + 'jquery-ui-modules/datepicker' +], function($){ + +//<![CDATA[ + $.extend(true, $, { + calendarConfig: {$translatedCalendarConfigJson} }); -</script> +//]]> + +}); +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> From a7ee62f69f6f5c3ba6cce9d1b647188145ee7b01 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Mon, 5 Oct 2020 12:17:32 +0300 Subject: [PATCH 0691/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Product/View/Options/AbstractOptions.php | 9 +- .../Magento/Catalog/Model/Product/Option.php | 8 +- .../Model/Product/Option/Type/DefaultType.php | 6 +- .../Model/Product/Option/Type/Select.php | 15 +-- .../CalculateCustomOptionCatalogRule.php | 4 +- .../Unit/Model/Product/Option/ValueTest.php | 8 +- app/code/Magento/CatalogRule/Model/Rule.php | 1 + ...RuleForSimpleProductAndFixedMethodTest.xml | 4 +- ...eForSimpleProductWithCustomOptionsTest.xml | 4 +- ...impleProductWithSelectFixedMethodTest.xml} | 57 +++++----- ...orSimpleProductsWithCustomOptionsTest.xml} | 103 ++++++++++-------- 11 files changed, 119 insertions(+), 100 deletions(-) rename app/code/Magento/CatalogRule/Test/Mftf/Test/{ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml => StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml} (67%) rename app/code/Magento/CatalogRule/Test/Mftf/Test/{ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml => StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml} (56%) 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 2093ac68e321f..de92546a8dd88 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -60,7 +60,7 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper * @param \Magento\Catalog\Helper\Data $catalogData * @param array $data - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -174,14 +174,15 @@ protected function _formatPrice($value, $flag = true) $priceStr = $sign; $customOptionPrice = $this->getProduct()->getPriceInfo()->getPrice('custom_option_price'); + $isPercent = (bool) $value['is_percent']; - if (!$value['is_percent']) { + if (!$isPercent) { $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( $this->getProduct(), (float)$value['pricing_value'], - (bool)$value['is_percent'] + $isPercent ); - if ($catalogPriceValue!==null) { + if ($catalogPriceValue !== null) { $value['pricing_value'] = $catalogPriceValue; } } diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 4b8fd5d1a602a..44d6fb04b01b0 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -146,7 +146,7 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory * @param array $optionGroups * @param array $optionTypesToGroups - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -474,19 +474,21 @@ public function afterSave() */ public function getPrice($flag = false) { - if ($flag && $this->getPriceType() == self::$typePercent) { + if ($flag && $this->getPriceType() === self::$typePercent) { $price = $this->calculateCustomOptionCatalogRule->execute( $this->getProduct(), (float)$this->getData(self::KEY_PRICE), $this->getPriceType() === Value::TYPE_PERCENT ); - if ($price == null) { + if ($price === null) { $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); } 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 86b3a72f73ec0..e819f36b5cf7d 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -73,7 +73,7 @@ class DefaultType extends \Magento\Framework\DataObject * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param array $data - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, @@ -357,12 +357,12 @@ public function getOptionPrice($optionValue, $basePrice) (float)$option->getPrice(), $option->getPriceType() === Value::TYPE_PERCENT ); - if ($catalogPriceValue!==null) { + if ($catalogPriceValue !== null) { return $catalogPriceValue; } else { return $this->_getChargeableOptionPrice( $option->getPrice(), - $option->getPriceType() == 'percent', + $option->getPriceType() === Value::TYPE_PERCENT, $basePrice ); } 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 435b1629d0c85..580ef7689ff4e 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -10,6 +10,7 @@ use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Catalog\Model\Product\Option; /** * Catalog product option select type @@ -52,7 +53,7 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType * @param \Magento\Framework\Escaper $escaper * @param array $data * @param array $singleSelectionTypes - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, @@ -350,24 +351,24 @@ protected function _isSingleSelection() /** * Returns calculated price of option * - * @param \Magento\Catalog\Model\Product\Option $option - * @param \Magento\Catalog\Model\Product\Option\Value $result + * @param Option $option + * @param Option\Value $result * @param float $basePrice - * @return float|null + * @return float */ - protected function getCalculatedOptionValue($option, $result, $basePrice) + protected function getCalculatedOptionValue(Option $option, Value $result, float $basePrice) : float { $catalogPriceValue = $this->calculateCustomOptionCatalogRule->execute( $option->getProduct(), (float)$result->getPrice(), $result->getPriceType() === Value::TYPE_PERCENT ); - if ($catalogPriceValue!==null) { + if ($catalogPriceValue !== null) { $optionCalculatedValue = $catalogPriceValue; } else { $optionCalculatedValue = $this->_getChargeableOptionPrice( $result->getPrice(), - $result->getPriceType() == 'percent', + $result->getPriceType() === Value::TYPE_PERCENT, $basePrice ); } diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php index 2f7156bc70dbf..1090867aa51a5 100644 --- a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php +++ b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php @@ -27,7 +27,6 @@ class CalculateCustomOptionCatalogRule private $priceModifier; /** - * CalculateCustomOptionCatalogRule constructor. * @param PriceCurrencyInterface $priceCurrency * @param PriceModifierInterface $priceModifier */ @@ -51,7 +50,7 @@ public function execute( Product $product, float $optionPriceValue, bool $isPercent - ) { + ): ?float { $regularPrice = (float)$product->getPriceInfo() ->getPrice(RegularPrice::PRICE_CODE) ->getValue(); @@ -69,6 +68,7 @@ public function execute( $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; return $this->priceCurrency->convertAndRound($finalOptionPrice); } + return null; } 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 d604e41018c07..e084a8cbbde27 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 @@ -32,14 +32,14 @@ class ValueTest extends TestCase /** * @var CalculateCustomOptionCatalogRule|MockObject */ - private $CalculateCustomOptionCatalogRule; + private $calculateCustomOptionCatalogRule; protected function setUp(): void { $mockedResource = $this->getMockedResource(); $mockedCollectionFactory = $this->getMockedValueCollectionFactory(); - $this->CalculateCustomOptionCatalogRule = $this->createMock( + $this->calculateCustomOptionCatalogRule = $this->createMock( CalculateCustomOptionCatalogRule::class ); @@ -49,7 +49,7 @@ protected function setUp(): void [ 'resource' => $mockedResource, 'valueCollectionFactory' => $mockedCollectionFactory, - 'CalculateCustomOptionCatalogRule' => $this->CalculateCustomOptionCatalogRule + 'CalculateCustomOptionCatalogRule' => $this->calculateCustomOptionCatalogRule ] ); $this->model->setOption($this->getMockedOption()); @@ -77,7 +77,7 @@ public function testGetPrice() $this->assertEquals($price, $this->model->getPrice(false)); $percentPrice = 100.0; - $this->CalculateCustomOptionCatalogRule->expects($this->atLeastOnce()) + $this->calculateCustomOptionCatalogRule->expects($this->atLeastOnce()) ->method('execute') ->willReturn($percentPrice); $this->assertEquals($percentPrice, $this->model->getPrice(true)); diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index bcd01dae96e81..c9d912f500ebd 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -898,6 +898,7 @@ public function getIdentities() /** * Clear price rules cache. + * @return void; */ public function clearPriceRulesData(): void { diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml index 43f8decb874cb..ece8dc4bacf28 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductAndFixedMethodTest" deprecated="Use ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead"> + <test name="ApplyCatalogRuleForSimpleProductAndFixedMethodTest" deprecated="Use StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> @@ -18,7 +18,7 @@ <group value="CatalogRule"/> <group value="mtf_migrated"/> <skip> - <issueId value="DEPRECATED">Use ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead</issueId> + <issueId value="DEPRECATED">Use StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest instead</issueId> </skip> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index 31aed3f71608f..45e97f179a11f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductWithCustomOptionsTest" deprecated="Use ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead"> + <test name="ApplyCatalogRuleForSimpleProductWithCustomOptionsTest" deprecated="Use StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> @@ -18,7 +18,7 @@ <group value="CatalogRule"/> <group value="mtf_migrated"/> <skip> - <issueId value="DEPRECATED">Use ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead</issueId> + <issueId value="DEPRECATED">Use StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest instead</issueId> </skip> </annotations> <before> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml similarity index 67% rename from app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml rename to app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index 0a6f5c5f104b4..cb1e20aa2b227 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest"> + <test name="StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> @@ -17,6 +17,7 @@ <testCaseId value="MC-14771"/> <group value="catalogRule"/> <group value="mtf_migrated"/> + <group value="catalog"/> </annotations> <before> <!-- Create category --> @@ -29,13 +30,16 @@ </createData> <!-- Update all products to have custom options --> - <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateProductWithOptions1"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateFirstProductWithOptions"/> <!-- Login as Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Clear all catalog price rules and reindex before test --> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete products and category --> @@ -43,31 +47,28 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete the catalog price rule --> - <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> - <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> - <argument name="name" value="{{CatalogRuleByFixed.name}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> - </actionGroup> - + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <!-- 1. Begin creating a new catalog price rule --> - <actionGroup ref="NewCatalogPriceRuleByUIWithConditionIsCategoryActionGroup" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> - <argument name ="categoryId" value="$createCategory.id$"/> - <argument name ="catalogRule" value="CatalogRuleByFixed"/> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="startCreatingFirstPriceRule"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForFirstPriceRule"> + <argument name="groups" value="'NOT LOGGED IN'"/> </actionGroup> - - <!-- Select not logged in customer group --> - <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> - - <!-- Save and apply the new catalog price rule --> - <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> - <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="waitForSave"/> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="createCatalogPriceRule"> + <argument name="conditionValue" value="$createCategory.id$"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="apply" value="by_fixed"/> + <argument name="discountAmount" value="12.3"/> + </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <!-- Navigate to category on store front --> - <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToStorefrontCategoryPage"/> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$createCategory$"/> + </actionGroup> <!-- Check product 1 name on store front category page --> <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> @@ -87,8 +88,10 @@ <argument name="productNumber" value="1"/> </actionGroup> - <!-- Navigate to product 1 on store front --> - <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> + <!-- Navigate to product on store front --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage1"> + <argument name="productUrlKey" value="$createProduct1.custom_attributes[url_key]$"/> + </actionGroup> <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> <actionGroup ref="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> @@ -99,8 +102,8 @@ </actionGroup> <!-- Add product 1 to cart --> - <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct1ToCart"> - <argument name="productQty" value="1"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="productName" value="$createProduct1.name$"/> </actionGroup> <!-- Assert sub total on mini shopping cart --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml similarity index 56% rename from app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml rename to app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml index 2944b76434330..4cfbae45b4dcc 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -7,7 +7,7 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="ApplyCatalogRuleForSimpleProductsWithCustomOptionsTest"> + <test name="StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest"> <annotations> <features value="CatalogRule"/> <stories value="Apply catalog price rule"/> @@ -17,6 +17,7 @@ <testCaseId value="MC-14769"/> <group value="catalogRule"/> <group value="mtf_migrated"/> + <group value="catalog"/> </annotations> <before> <!-- Login as Admin --> @@ -39,6 +40,7 @@ <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProductWithOptions2"/> <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProductWithOptions3"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> </before> <after> <!-- Delete products and category --> @@ -48,65 +50,71 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete the catalog price rule --> - <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> - <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> - <argument name="name" value="{{_defaultCatalogRule.name}}"/> - <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> - </actionGroup> - - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAdterTest"> - <argument name="indices" value=""/> - </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTest"> - <argument name="tags" value=""/> - </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndices"/> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- 1. Begin creating a new catalog price rule --> - <actionGroup ref="NewCatalogPriceRuleByUIWithConditionIsCategoryActionGroup" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> - <argument name ="categoryId" value="$createCategory.id$"/> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="startCreatingFirstPriceRule"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForFirstPriceRule"> + <argument name="groups" value="'NOT LOGGED IN'"/> </actionGroup> - - <!-- Select not logged in customer group --> - <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> - - <!-- Save and apply the new catalog price rule --> - <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> - <click selector="{{AdminNewCatalogPriceRule.save}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="waitForSave"/> - - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="createCatalogPriceRule"> + <argument name="conditionValue" value="$createCategory.id$"/> </actionGroup> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> - <argument name="tags" value=""/> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="apply" value="by_percent"/> + <argument name="discountAmount" value="10"/> </actionGroup> + <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <!-- Navigate to category on store front --> - <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToStorefrontCategoryPage"> + <argument name="category" value="$createCategory$"/> + </actionGroup> <!-- Check product 1 price on store front category page --> - <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$51.10" stepKey="storefrontProduct1Price"/> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct1Price"> + <argument name="productName" value="$createProduct1.name$"/> + <argument name="productPrice" value="$51.10"/> + </actionGroup> <!-- Check product 1 regular price on store front category page --> - <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct1.name$)}}" userInput="$56.78" stepKey="storefrontProduct1RegularPrice"/> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct1RegularPrice"> + <argument name="productName" value="$createProduct1.name$"/> + <argument name="productPrice" value="$56.78"/> + </actionGroup> <!-- Check product 2 price on store front category page --> - <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$51.10" stepKey="storefrontProduct2Price"/> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct2Price"> + <argument name="productName" value="$createProduct2.name$"/> + <argument name="productPrice" value="$51.10"/> + </actionGroup> <!-- Check product 2 regular price on store front category page --> - <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct2.name$)}}" userInput="$56.78" stepKey="storefrontProduct2RegularPrice"/> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct2RegularPrice"> + <argument name="productName" value="$createProduct2.name$"/> + <argument name="productPrice" value="$56.78"/> + </actionGroup> <!-- Check product 3 price on store front category page --> - <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$51.10" stepKey="storefrontProduct3Price"/> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct3Price"> + <argument name="productName" value="$createProduct3.name$"/> + <argument name="productPrice" value="$51.10"/> + </actionGroup> <!-- Check product 3 regular price on store front category page --> - <see selector="{{StorefrontCategoryProductSection.ProductInfoByName($createProduct3.name$)}}" userInput="$56.78" stepKey="storefrontProduct3RegularPrice"/> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct3RegularPrice"> + <argument name="productName" value="$createProduct3.name$"/> + <argument name="productPrice" value="$56.78"/> + </actionGroup> <!-- Navigate to product 1 on store front --> - <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage1"/> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage1"> + <argument name="productUrlKey" value="$createProduct1.custom_attributes[url_key]$"/> + </actionGroup> <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> @@ -116,12 +124,14 @@ </actionGroup> <!-- Add product 1 to cart --> - <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct1ToCart"> - <argument name="productQty" value="1"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> + <argument name="productName" value="$createProduct1.name$"/> </actionGroup> <!-- Navigate to product 2 on store front --> - <amOnPage url="{{StorefrontProductPage.url($createProduct1.name$)}}" stepKey="goToProductPage2"/> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage2"> + <argument name="productUrlKey" value="$createProduct2.custom_attributes[url_key]$"/> + </actionGroup> <!-- Assert regular and special price after selecting ProductOptionValueDropdown3 --> <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices2"> @@ -131,16 +141,18 @@ </actionGroup> <!-- Add product 2 to cart --> - <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct2ToCart"> - <argument name="productQty" value="1"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> + <argument name="productName" value="$createProduct2.name$"/> </actionGroup> <!-- Navigate to product 3 on store front --> - <amOnPage url="{{StorefrontProductPage.url($createProduct3.name$)}}" stepKey="goToProductPage3"/> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage3"> + <argument name="productUrlKey" value="$createProduct3.custom_attributes[url_key]$"/> + </actionGroup> <!-- Add product 3 to cart with no custom option --> - <actionGroup ref="StorefrontAddProductToCartWithQtyActionGroup" stepKey="cartAddSimpleProduct3ToCart"> - <argument name="productQty" value="1"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage3"> + <argument name="productName" value="$createProduct3.name$"/> </actionGroup> <!-- Assert sub total on mini shopping cart --> @@ -149,8 +161,7 @@ </actionGroup> <!-- Navigate to checkout shipping page --> - <amOnPage stepKey="navigateToShippingPage" url="{{CheckoutShippingPage.url}}"/> - <waitForPageLoad stepKey="waitFoCheckoutShippingPageLoad"/> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="onCheckout"/> <!-- Fill Shipping information --> <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillOrderShippingInfo"> From 8edca5fb3f6d49914dd7f11a9d418b2c2864bbe9 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 5 Oct 2020 13:20:14 +0300 Subject: [PATCH 0692/1013] MC-35771: Datepicker/calendar control does not use the store locale --- .../Customer/Test/Unit/Block/Widget/DobTest.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php index 020dbbbe4ff13..bb2f350ae60a1 100644 --- a/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php +++ b/app/code/Magento/Customer/Test/Unit/Block/Widget/DobTest.php @@ -625,8 +625,11 @@ public function testGetHtmlExtraParamsWithRequiredOption() * @dataProvider getTranslatedCalendarConfigJsonDataProvider * @return void */ - public function testGetTranslatedCalendarConfigJson(string $locale, array $expectedArray, string $expectedJson): void - { + public function testGetTranslatedCalendarConfigJson( + string $locale, + array $expectedArray, + string $expectedJson + ): void { $this->_locale = $locale; $this->encoder->expects($this->once()) @@ -657,12 +660,15 @@ public function getTranslatedCalendarConfigJsonDataProvider() 'currentText' => 'Today', 'monthNames' => ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], - 'monthNamesShort' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + 'monthNamesShort' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 'dayNames' => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 'dayNamesShort' => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 'dayNamesMin' => ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], ], + // phpcs:disable Generic.Files.LineLength.TooLong 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["January","February","March","April","May","June","July","August","September","October","November","December"],"monthNamesShort":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"dayNames":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"dayNamesShort":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"dayNamesMin":["Su","Mo","Tu","We","Th","Fr","Sa"]}' + // phpcs:enable Generic.Files.LineLength.TooLong ], [ 'locale' => 'de_DE', @@ -673,12 +679,15 @@ public function getTranslatedCalendarConfigJsonDataProvider() 'currentText' => 'Today', 'monthNames' => ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], - 'monthNamesShort' => ['Jan.', 'Feb.', 'März', 'Apr.', 'Mai', 'Juni', 'Juli', 'Aug.', 'Sept.', 'Okt.', 'Nov.', 'Dez.'], + 'monthNamesShort' => ['Jan.', 'Feb.', 'März', 'Apr.', 'Mai', 'Juni', + 'Juli', 'Aug.', 'Sept.', 'Okt.', 'Nov.', 'Dez.'], 'dayNames' => ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], 'dayNamesShort' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], 'dayNamesMin' => ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'], ], + // phpcs:disable Generic.Files.LineLength.TooLong 'expectedJson' => '{"closeText":"Done","prevText":"Prev","nextText":"Next","currentText":"Today","monthNames":["Januar","Februar","M\u00e4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],"monthNamesShort":["Jan.","Feb.","M\u00e4rz","Apr.","Mai","Juni","Juli","Aug.","Sept.","Okt.","Nov.","Dez."],"dayNames":["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],"dayNamesShort":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],"dayNamesMin":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."]}' + // phpcs:enable Generic.Files.LineLength.TooLong ], ]; } From c722790487631c0ce78d753d2dfe9d97c7606a18 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Mon, 5 Oct 2020 13:54:39 +0300 Subject: [PATCH 0693/1013] magento/magento2#30062: Feature/formatting json yaml. --- .editorconfig | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0452085768830..1a9acd92fc0fc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,8 +14,5 @@ trim_trailing_whitespace = false [*.{yml,yaml,json}] indent_size = 2 -[composer.{json,lock}] -indent_size = 4 - -[auth.json] +[{composer, auth}.json] indent_size = 4 From 58ad4b063ae27a5f6140df43f9f7377b93ad4d5f Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Mon, 5 Oct 2020 14:21:57 +0300 Subject: [PATCH 0694/1013] MC-37564: Create automated test for "Delete CMS Block" --- .../Controller/Adminhtml/Block/DeleteTest.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php new file mode 100644 index 0000000000000..5b763d0ec5e59 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Controller\Adminhtml\Block; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Checks that cms block can be successfully deleted + * + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class DeleteTest extends AbstractBackendController +{ + /** @var GetBlockByIdentifierInterface */ + private $getBlockByIdentifierInterface; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var CollectionFactory */ + private $collectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->getBlockByIdentifierInterface = $this->_objectManager->get(GetBlockByIdentifierInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->collectionFactory = $this->_objectManager->get(CollectionFactory::class); + } + + /** + * @magentoDataFixture Magento/Cms/_files/block_default_store.php + * + * @return void + */ + public function testDeleteBlock(): void + { + $defaultStoreId = (int)$this->storeManager->getStore('default')->getId(); + $blockId = $this->getBlockByIdentifierInterface->execute('default_store_block', $defaultStoreId)->getId(); + $this->getRequest()->setMethod(Http::METHOD_POST) + ->setParams(['block_id' => $blockId]); + $this->dispatch('backend/cms/block/delete'); + $this->assertSessionMessages( + $this->containsEqual((string)__('You deleted the block.')), + MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('cms/block/index')); + $collection = $this->collectionFactory->getReport('cms_block_listing_data_source'); + $this->assertNull($collection->getItemByColumnValue(BlockInterface::IDENTIFIER, 'default_store_block')); + } +} From 3dd1fbd51cfb424d71d53f414c49bb5c587974de Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Mon, 5 Oct 2020 15:35:12 +0300 Subject: [PATCH 0695/1013] MC-38048: Incorrect default country displayed on shipping page when store view is changed in cart. Part 2 --- ...eckoutDifferentDefaultCountryPerStoreTest.xml | 16 ++++++++++++++-- .../view/frontend/web/js/view/shipping.js | 4 +--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml index c4c70cef81b0b..e6a5f37c764fe 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/CheckoutDifferentDefaultCountryPerStoreTest.xml @@ -39,10 +39,10 @@ </after> <!-- Open product and add product to cart--> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> - <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> </actionGroup> <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> - <argument name="product" value="$$createProduct$$"/> + <argument name="product" value="$createProduct$"/> <argument name="productCount" value="1"/> </actionGroup> <!-- Go to cart --> @@ -59,5 +59,17 @@ <actualResult type="const">$grabCountry</actualResult> <expectedResult type="string">{{DE_Address_Berlin_Not_Default_Address.country_id}}</expectedResult> </assertEquals> + <!-- Go to cart --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="returnToCartPage"/> + <!-- Switch to default store view --> + <actionGroup ref="StorefrontSwitchDefaultStoreViewActionGroup" stepKey="switchToDefaultStoreView"/> + <!-- Go to checkout page --> + <actionGroup ref="OpenStoreFrontCheckoutShippingPageActionGroup" stepKey="proceedToCheckoutWithDefaultStore"/> + <!-- Grab country code from checkout page and assert value with default country for default store view --> + <grabValueFrom selector="{{CheckoutShippingSection.country}}" stepKey="grabDefaultStoreCountry"/> + <assertEquals stepKey="assertDefaultCountryValue"> + <actualResult type="const">$grabDefaultStoreCountry</actualResult> + <expectedResult type="string">{{US_Address_TX.country_id}}</expectedResult> + </assertEquals> </test> </tests> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index 084c8ad59a8da..f6590e60a9589 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -121,11 +121,9 @@ define([ ); } checkoutProvider.on('shippingAddress', function (shippingAddrsData) { - //jscs:disable requireCamelCaseOrUpperCaseIdentifiers - if (shippingAddrsData.street && shippingAddrsData.street[0].length > 0) { + if (!_.isEmpty(shippingAddrsData.street[0])) { checkoutData.setShippingAddressFromData(shippingAddrsData); } - //jscs:enable requireCamelCaseOrUpperCaseIdentifiers }); shippingRatesValidator.initFields(fieldsetName); }); From 970d1793bc408ad99cf959197c77dec424f4cefc Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec <wojtek@mediotype.com> Date: Mon, 5 Oct 2020 14:36:14 +0200 Subject: [PATCH 0696/1013] Add missing line break before docblock tags --- app/code/Magento/Search/Model/SynonymAnalyzer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index 5b6e04b8321fe..06d5c1eda0631 100644 --- a/app/code/Magento/Search/Model/SynonymAnalyzer.php +++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php @@ -42,6 +42,7 @@ public function __construct(SynonymReader $synReader) * 3 => [ 0 => "british", 1 => "english" ], * 4 => [ 0 => "queen", 1 => "monarch" ] * ] + * * @param string $phrase * @return array * @throws \Magento\Framework\Exception\LocalizedException From 5347b598cc3455210369b80770cbc9780162c043 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Mon, 5 Oct 2020 15:50:23 +0300 Subject: [PATCH 0697/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- app/code/Magento/CatalogRule/Model/Rule.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index c9d912f500ebd..2d92192368960 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -898,6 +898,7 @@ public function getIdentities() /** * Clear price rules cache. + * * @return void; */ public function clearPriceRulesData(): void From ef5d73479bd166ed599b70fc36855cff5f43d7f2 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Mon, 5 Oct 2020 16:13:35 +0300 Subject: [PATCH 0698/1013] MC-37564: Create automated test for "Delete CMS Block" --- .../Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php index 5b763d0ec5e59..eaff6d386fe64 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php @@ -19,7 +19,7 @@ * Checks that cms block can be successfully deleted * * @magentoAppArea adminhtml - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ class DeleteTest extends AbstractBackendController { From 75cc0b4620442484a92d3045af8a8c56ecb3cc0b Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Mon, 5 Oct 2020 16:40:43 +0300 Subject: [PATCH 0699/1013] magento/magento2#29165: Add a head.additional block to adminhtml layout. --- app/code/Magento/Backend/view/adminhtml/layout/default.xml | 4 ++-- .../Backend/view/adminhtml/templates/page/container.phtml | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/Backend/view/adminhtml/templates/page/container.phtml diff --git a/app/code/Magento/Backend/view/adminhtml/layout/default.xml b/app/code/Magento/Backend/view/adminhtml/layout/default.xml index 2b086791c5523..fc0cfe54af0ef 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/default.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/default.xml @@ -17,7 +17,7 @@ <body> <attribute name="id" value="html-body"/> <block name="require.js" class="Magento\Backend\Block\Page\RequireJs" template="Magento_Backend::page/js/require_js.phtml"/> - <block class="Magento\Framework\View\Element\Text\ListText" name="head.additional"/> + <block class="Magento\Framework\View\Element\Template" name="head.additional" template="Magento_Backend::page/container.phtml"/> <referenceContainer name="global.notices"> <block class="Magento\Backend\Block\Page\Notices" name="global_notices" as="global_notices" template="Magento_Backend::page/notices.phtml"/> </referenceContainer> @@ -71,7 +71,7 @@ <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/templates/page/container.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/container.phtml new file mode 100644 index 0000000000000..6da55e4f8f8b1 --- /dev/null +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/container.phtml @@ -0,0 +1,7 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +?> +<?= $block->getChildHtml(); ?> From b4ee601db5141316c0d268f83e95f32d4e341162 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Mon, 5 Oct 2020 17:39:47 +0300 Subject: [PATCH 0700/1013] MC-38048: Incorrect default country displayed on shipping page when store view is changed in cart. Part 2 --- app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js index f6590e60a9589..2a52b64647749 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/shipping.js @@ -121,7 +121,7 @@ define([ ); } checkoutProvider.on('shippingAddress', function (shippingAddrsData) { - if (!_.isEmpty(shippingAddrsData.street[0])) { + if (shippingAddrsData.street && !_.isEmpty(shippingAddrsData.street[0])) { checkoutData.setShippingAddressFromData(shippingAddrsData); } }); From 9d5b7de049c37b98cf472e144888f460fc7ed5f0 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Mon, 5 Oct 2020 17:47:51 +0300 Subject: [PATCH 0701/1013] MC-37564: Create automated test for "Delete CMS Block" --- .../Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php index eaff6d386fe64..ab3eda8cf4e9f 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/Block/DeleteTest.php @@ -24,7 +24,7 @@ class DeleteTest extends AbstractBackendController { /** @var GetBlockByIdentifierInterface */ - private $getBlockByIdentifierInterface; + private $getBlockByIdentifier; /** @var StoreManagerInterface */ private $storeManager; @@ -39,7 +39,7 @@ protected function setUp(): void { parent::setUp(); - $this->getBlockByIdentifierInterface = $this->_objectManager->get(GetBlockByIdentifierInterface::class); + $this->getBlockByIdentifier = $this->_objectManager->get(GetBlockByIdentifierInterface::class); $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); $this->collectionFactory = $this->_objectManager->get(CollectionFactory::class); } @@ -52,7 +52,7 @@ protected function setUp(): void public function testDeleteBlock(): void { $defaultStoreId = (int)$this->storeManager->getStore('default')->getId(); - $blockId = $this->getBlockByIdentifierInterface->execute('default_store_block', $defaultStoreId)->getId(); + $blockId = $this->getBlockByIdentifier->execute('default_store_block', $defaultStoreId)->getId(); $this->getRequest()->setMethod(Http::METHOD_POST) ->setParams(['block_id' => $blockId]); $this->dispatch('backend/cms/block/delete'); From 5f02b3bf976c5e8ee3b9f18373340f2d268a70df Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Mon, 5 Oct 2020 18:10:46 +0300 Subject: [PATCH 0702/1013] MC-37496: Create automated test for "Reorder Order from Admin for Offline Payment Methods" --- .../ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml | 4 ++-- .../Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml index a084e858d5f46..d6d5c9e7315d9 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminAssertOrderShippingMethodActionGroup.xml @@ -13,8 +13,8 @@ <description>Assert that shipping method and shipping price is present for the order.</description> </annotations> <arguments> - <argument name="shippingMethod" type="string" defaultValue="Flat Rate - Fixed"/> - <argument name="shippingPrice" type="string"/> + <argument name="shippingMethod" type="string" defaultValue="{{flatRateTitleDefault.value}} - {{flatRateNameDefault.value}}"/> + <argument name="shippingPrice" type="string" defaultValue="$5.00"/> </arguments> <see selector="{{AdminOrderShippingInformationSection.shippingMethod}}" userInput="{{shippingMethod}}" stepKey="seeShippingMethod"/> <see selector="{{AdminOrderShippingInformationSection.shippingPrice}}" userInput="{{shippingPrice}}" stepKey="seeShippingMethodPrice"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml index 6767ee25b21ed..874164fdcdcf0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderOrderWithOfflinePaymentMethodTest.xml @@ -55,7 +55,7 @@ <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorderButton"/> <actionGroup ref="AdminOrderClickSubmitOrderActionGroup" stepKey="submitReorder"/> <actionGroup ref="VerifyCreatedOrderInformationActionGroup" stepKey="verifyCreatedOrderInformation"/> - <actionGroup ref="AssertOrderAddressInformationActionGroup" stepKey="verifyOrderInformation"> + <actionGroup ref="AssertOrderAddressInformationActionGroup" stepKey="verifyOrderAddressInformation"> <argument name="customer" value="$createCustomer$"/> <argument name="shippingAddress" value="ShippingAddressTX"/> <argument name="billingAddress" value="BillingAddressTX"/> From 38c4a3e01ebc3dd0d18286efba20d03373deb830 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec <wojtek@mediotype.com> Date: Mon, 5 Oct 2020 19:29:15 +0200 Subject: [PATCH 0703/1013] Change implementation to ensure one of current use cases is not broken. Add case with slash in regression integration test. --- app/code/Magento/Search/Model/SynonymAnalyzer.php | 4 +++- .../testsuite/Magento/Search/Model/SynonymAnalyzerTest.php | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index 06d5c1eda0631..b022f47efebd5 100644 --- a/app/code/Magento/Search/Model/SynonymAnalyzer.php +++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php @@ -137,8 +137,10 @@ private function getSearchPattern(array $words): string { $patterns = []; for ($lastItem = count($words); $lastItem > 0; $lastItem--) { + $words = array_map(function ($word) { + return preg_quote($word, '/'); + }, $words); $phrase = implode("\s+", \array_slice($words, 0, $lastItem)); - $phrase = preg_quote($phrase, '/'); $patterns[] = '^' . $phrase . ','; $patterns[] = ',' . $phrase . ','; $patterns[] = ',' . $phrase . '$'; diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php index 173cdf8a64703..2531f9c60f070 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/SynonymAnalyzerTest.php @@ -78,6 +78,10 @@ public static function loadGetSynonymsForPhraseDataProvider() 'phrase' => 'schlicht', 'expectedResult' => [['schlicht', 'natürlich']] ], + 'withSlashInSearchPhrase' => [ + 'phrase' => 'orange hill/peak', + 'expectedResult' => [['orange', 'magento'], ['hill/peak']] + ], ]; } From 8e7db568c98a3a9191d6a2af59cc2167d5dfc584 Mon Sep 17 00:00:00 2001 From: Oleksandr Dubovyk <odubovyk@magento.com> Date: Mon, 5 Oct 2020 14:06:27 -0500 Subject: [PATCH 0704/1013] MC-37371: Wrong currency sign in Credit Memo grid - changed from Create to Get in constructor --- .../Sales/Ui/Component/Listing/Column/Price.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php b/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php index 6720b1646cc9a..4ffb6f98447c7 100644 --- a/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php +++ b/app/code/Magento/Sales/Ui/Component/Listing/Column/Price.php @@ -7,6 +7,7 @@ namespace Magento\Sales\Ui\Component\Listing\Column; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\UiComponent\ContextInterface; use Magento\Framework\View\Element\UiComponentFactory; use Magento\Store\Model\StoreManagerInterface; @@ -45,7 +46,7 @@ class Price extends Column * @param array $components * @param array $data * @param Currency $currency - * @param StoreManagerInterface|null $storeManager + * @param StoreManagerInterface $storeManager */ public function __construct( ContextInterface $context, @@ -57,10 +58,10 @@ public function __construct( StoreManagerInterface $storeManager = null ) { $this->priceFormatter = $priceFormatter; - $this->currency = $currency ?: \Magento\Framework\App\ObjectManager::getInstance() - ->create(Currency::class); - $this->storeManager = $storeManager ?: \Magento\Framework\App\ObjectManager::getInstance() - ->create(StoreManagerInterface::class); + $this->currency = $currency ?: ObjectManager::getInstance() + ->get(Currency::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance() + ->get(StoreManagerInterface::class); parent::__construct($context, $uiComponentFactory, $components, $data); } From 7a662f5ee991e5768c1dccb30bcaace3891c6f35 Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 5 Oct 2020 17:21:01 -0500 Subject: [PATCH 0705/1013] MC-37347: [OnPrem] Catalog Products Filter in 2.3.3 not working correctly --- .../Product/ProductCollection.php | 70 ------------------- 1 file changed, 70 deletions(-) diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php index 99391f92337b3..f4334bc25efd8 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductCollection.php @@ -5,16 +5,10 @@ */ namespace Magento\Catalog\Ui\DataProvider\Product; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -use Magento\Framework\Exception\LocalizedException; -use Magento\Eav\Model\Entity\Attribute\AttributeInterface; - /** * Collection which is used for rendering product list in the backend. * * Used for product grid and customizes behavior of the default Product collection for grid needs. - * - * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class ProductCollection extends \Magento\Catalog\Model\ResourceModel\Product\Collection { @@ -31,68 +25,4 @@ protected function _productLimitationJoinPrice() $this->_productLimitationFilters->setUsePriceIndex(false); return $this->_productLimitationPrice(true); } - - /** - * Add attribute filter to collection - * - * @param AttributeInterface|integer|string|array $attribute - * @param null|string|array $condition - * @param string $joinType - * @return $this - * @throws LocalizedException - */ - public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') - { - $storeId = (int)$this->getStoreId(); - if ($attribute === 'is_saleable' - || is_array($attribute) - || $storeId !== $this->getDefaultStoreId() - ) { - return parent::addAttributeToFilter($attribute, $condition, $joinType); - } - - if ($attribute instanceof AttributeInterface) { - $attributeModel = $attribute; - } else { - $attributeModel = $this->getEntity()->getAttribute($attribute); - if ($attributeModel === false) { - throw new LocalizedException( - __('Invalid attribute identifier for filter (%1)', get_class($attribute)) - ); - } - } - - if ($attributeModel->isScopeGlobal() || $attributeModel->getBackend()->isStatic()) { - return parent::addAttributeToFilter($attribute, $condition, $joinType); - } - - $this->addAttributeToFilterAllStores($attributeModel, $condition); - - return $this; - } - - /** - * Add attribute to filter by all stores - * - * @param Attribute $attributeModel - * @param array $condition - * @return void - */ - private function addAttributeToFilterAllStores(Attribute $attributeModel, array $condition): void - { - $tableName = $this->getTable($attributeModel->getBackendTable()); - $entity = $this->getEntity(); - $fKey = 'e.' . $this->getEntityPkName($entity); - $pKey = $tableName . '.' . $this->getEntityPkName($entity); - $attributeId = $attributeModel->getAttributeId(); - $condition = "({$pKey} = {$fKey}) AND (" - . $this->_getConditionSql("{$tableName}.value", $condition) - . ') AND (' - . $this->_getConditionSql("{$tableName}.attribute_id", $attributeId) - . ') AND (' - . $this->_getConditionSql("{$tableName}.store_id", $this->getDefaultStoreId()) - . ')'; - $selectExistsInAllStores = $this->getConnection()->select()->from($tableName); - $this->getSelect()->exists($selectExistsInAllStores, $condition); - } } From c6c7df6be07ff11a18aea6fc89fd9cf8727c8fea Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Mon, 5 Oct 2020 17:41:32 -0500 Subject: [PATCH 0706/1013] MC-37347: [OnPrem] Catalog Products Filter in 2.3.3 not working correctly --- .../Test/AdminFilterByNameByStoreViewOnProductGridTest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml index eb9fe693f8b3b..c6f3c69a2aa48 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml @@ -15,8 +15,8 @@ <title value="Product grid filtering by store view level attribute"/> <description value="Verify that products grid can be filtered on all store view level by attribute"/> <severity value="MAJOR"/> - <testCaseId value="MAGETWO-98755"/> - <useCaseId value="MAGETWO-98335"/> + <testCaseId value="MC-28534"/> + <useCaseId value="MC-37347"/> <group value="catalog"/> </annotations> <before> From 88f6101fe15eda61471822169670b744914fdf40 Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec <wojtek@mediotype.com> Date: Tue, 6 Oct 2020 07:37:29 +0200 Subject: [PATCH 0707/1013] Fix incorrect line indent --- app/code/Magento/Search/Model/SynonymAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Search/Model/SynonymAnalyzer.php b/app/code/Magento/Search/Model/SynonymAnalyzer.php index b022f47efebd5..16d0b0b4ddcd9 100644 --- a/app/code/Magento/Search/Model/SynonymAnalyzer.php +++ b/app/code/Magento/Search/Model/SynonymAnalyzer.php @@ -138,7 +138,7 @@ private function getSearchPattern(array $words): string $patterns = []; for ($lastItem = count($words); $lastItem > 0; $lastItem--) { $words = array_map(function ($word) { - return preg_quote($word, '/'); + return preg_quote($word, '/'); }, $words); $phrase = implode("\s+", \array_slice($words, 0, $lastItem)); $patterns[] = '^' . $phrase . ','; From 67b9b6a5d723e95ed31d9d1be8658fd44daffd69 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 6 Oct 2020 09:35:09 +0300 Subject: [PATCH 0708/1013] MC-37718: Grouped product remains In Stock On Mass Update --- .../Plugin/MassUpdateProductAttribute.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index ee5f16989ba37..2dd47eae16959 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -58,7 +58,7 @@ class MassUpdateProductAttribute /** * @var ParentItemProcessorInterface[] */ - private $parentItemProcessors; + private $parentItemProcessorPool; /** * @var ProductRepositoryInterface @@ -73,7 +73,7 @@ class MassUpdateProductAttribute * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager * @param ProductRepositoryInterface $productRepository - * @param ParentItemProcessorInterface[] $parentItemProcessors + * @param ParentItemProcessorInterface[] $parentItemProcessorPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -85,7 +85,7 @@ public function __construct( \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, \Magento\Framework\Message\ManagerInterface $messageManager, ProductRepositoryInterface $productRepository, - array $parentItemProcessors = [] + array $parentItemProcessorPool = [] ) { $this->stockIndexerProcessor = $stockIndexerProcessor; $this->dataObjectHelper = $dataObjectHelper; @@ -95,7 +95,7 @@ public function __construct( $this->attributeHelper = $attributeHelper; $this->messageManager = $messageManager; $this->productRepository = $productRepository; - $this->parentItemProcessors = $parentItemProcessors; + $this->parentItemProcessorPool = $parentItemProcessorPool; } /** @@ -188,7 +188,7 @@ private function updateInventoryInProducts($productIds, $websiteId, $inventoryDa */ private function processParents(ProductInterface $product): void { - foreach ($this->parentItemProcessors as $processor) { + foreach ($this->parentItemProcessorPool as $processor) { $processor->process($product); } } From e7cc0e39e8dea0981c5f219cbdc7df25755b7d03 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 6 Oct 2020 10:43:01 +0300 Subject: [PATCH 0709/1013] MC-35771: Datepicker/calendar control does not use the store locale --- .../Magento/Customer/view/frontend/templates/widget/dob.phtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml index 8a584938b1548..da1c85cce9856 100644 --- a/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/widget/dob.phtml @@ -48,7 +48,7 @@ $fieldCssClass .= $block->isRequired() ? ' required' : ''; } </script> -<?php $scriptString = <<<script +<?php $scriptString = <<<code require([ 'jquery', @@ -62,6 +62,6 @@ require([ //]]> }); -script; +code; ?> <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> From 0e3b1dc678b071767857eecb7d18695c0d7cc11b Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 6 Oct 2020 11:48:35 +0300 Subject: [PATCH 0710/1013] MC-38197: [Magento Cloud] Error when updating carts - fatal error Call to a member function getValue() on null in module-configurable-product CartItemProcessor.php --- .../Model/Quote/Item/CartItemProcessor.php | 45 ++++++++++--------- .../Quote/Item/CartItemProcessorTest.php | 23 +++++++++- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php index 56993ecec1fbf..814c1e971ea33 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php @@ -82,31 +82,34 @@ public function convertToBuyRequest(CartItemInterface $cartItem) */ public function processOptions(CartItemInterface $cartItem) { - $attributesOption = $cartItem->getProduct()->getCustomOption('attributes'); - $selectedConfigurableOptions = $this->serializer->unserialize($attributesOption->getValue()); + $attributesOption = $cartItem->getProduct() + ->getCustomOption('attributes'); + if ($attributesOption) { + $selectedConfigurableOptions = $this->serializer->unserialize($attributesOption->getValue()); - if (is_array($selectedConfigurableOptions)) { - $configurableOptions = []; - foreach ($selectedConfigurableOptions as $optionId => $optionValue) { - /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $option */ - $option = $this->itemOptionValueFactory->create(); - $option->setOptionId($optionId); - $option->setOptionValue($optionValue); - $configurableOptions[] = $option; - } + if (is_array($selectedConfigurableOptions)) { + $configurableOptions = []; + foreach ($selectedConfigurableOptions as $optionId => $optionValue) { + /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $option */ + $option = $this->itemOptionValueFactory->create(); + $option->setOptionId($optionId); + $option->setOptionValue($optionValue); + $configurableOptions[] = $option; + } - $productOption = $cartItem->getProductOption() - ? $cartItem->getProductOption() - : $this->productOptionFactory->create(); + $productOption = $cartItem->getProductOption() + ? $cartItem->getProductOption() + : $this->productOptionFactory->create(); - /** @var \Magento\Quote\Api\Data\ProductOptionExtensionInterface $extensibleAttribute */ - $extensibleAttribute = $productOption->getExtensionAttributes() - ? $productOption->getExtensionAttributes() - : $this->extensionFactory->create(); + /** @var \Magento\Quote\Api\Data\ProductOptionExtensionInterface $extensibleAttribute */ + $extensibleAttribute = $productOption->getExtensionAttributes() + ? $productOption->getExtensionAttributes() + : $this->extensionFactory->create(); - $extensibleAttribute->setConfigurableItemOptions($configurableOptions); - $productOption->setExtensionAttributes($extensibleAttribute); - $cartItem->setProductOption($productOption); + $extensibleAttribute->setConfigurableItemOptions($configurableOptions); + $productOption->setExtensionAttributes($extensibleAttribute); + $cartItem->setProductOption($productOption); + } } return $cartItem; } diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php index 10f5b1cbb344a..cd68e1dcfce24 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Quote/Item/CartItemProcessorTest.php @@ -59,7 +59,7 @@ class CartItemProcessorTest extends TestCase */ private $productOptionExtensionAttributes; - /** @var \PHPUnit\Framework\MockObject\MockObject */ + /** @var MockObject */ private $serializer; protected function setUp(): void @@ -263,4 +263,25 @@ public function testProcessProductOptionsIfOptionsExist() $this->assertEquals($cartItemMock, $this->model->processOptions($cartItemMock)); } + + /** + * Checks processOptions method with the empty custom option + * + * @return void + */ + public function testProcessProductWithEmptyOption(): void + { + $customOption = $this->createMock(Option::class); + $productMock = $this->createMock(Product::class); + $productMock->method('getCustomOption') + ->with('attributes') + ->willReturn(null); + $customOption->expects($this->never()) + ->method('getValue'); + $cartItemMock = $this->createPartialMock(Item::class, ['getProduct']); + $cartItemMock->expects($this->once()) + ->method('getProduct') + ->willReturn($productMock); + $this->assertEquals($cartItemMock, $this->model->processOptions($cartItemMock)); + } } From 0df8611881eaca5a2ad15da85504996414b326b5 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Tue, 6 Oct 2020 12:06:34 +0300 Subject: [PATCH 0711/1013] MC-36965: Create automated test for "[ES] Search with Layered Navigation and different types of attribute products. --- ...orefrontAssertAppliedFilterActionGroup.xml | 22 ++++ ...tegoryPageByAttributeOptionActionGroup.xml | 23 ++++ .../StorefrontLayeredNavigationSection.xml | 2 + ...opdownAttributeInLayeredNavigationTest.xml | 102 ++++++++++++++++++ .../Block/Navigation/AbstractFiltersTest.php | 98 ++++++++++++++++- .../Navigation/Category/BooleanFilterTest.php | 52 +++++++++ .../Category/MultiselectFilterTest.php | 77 ++++++++++++- .../Navigation/Category/PriceFilterTest.php | 64 ++++++++++- .../Navigation/Category/SelectFilterTest.php | 46 ++++++++ .../Navigation/Search/BooleanFilterTest.php | 19 ++++ .../Search/MultiselectFilterTest.php | 19 ++++ .../Navigation/Search/PriceFilterTest.php | 15 +++ .../Navigation/Search/SelectFilterTest.php | 19 ++++ .../Category/SwatchTextFilterTest.php | 46 ++++++++ .../Category/SwatchVisualFilterTest.php | 46 ++++++++ .../Search/SwatchTextFilterTest.php | 19 ++++ .../Search/SwatchVisualFilterTest.php | 19 ++++ 17 files changed, 685 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml create mode 100644 app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml create mode 100644 app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml new file mode 100644 index 0000000000000..faf33a6649d50 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.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="StorefrontAssertAppliedFilterActionGroup"> + <annotations> + <description>Asserts applied filter label and value on category page is layered navigation block.</description> + </annotations> + <arguments> + <argument name="attributeLabel" type="string" defaultValue=""/> + <argument name="attributeOptionLabel" type="string" defaultValue=""/> + </arguments> + <see selector="{{StorefrontLayeredNavigationSection.appliedFilterLabel('1')}}" userInput="{{attributeLabel}}" stepKey="seeAppliedFilterLabel"/> + <see selector="{{StorefrontLayeredNavigationSection.appliedFilterValue('1')}}" userInput="{{attributeOptionLabel}}" stepKey="seeAppliedFilterValue"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml new file mode 100644 index 0000000000000..e7f9a766014e6 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.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="StorefrontFilterCategoryPageByAttributeOptionActionGroup"> + <annotations> + <description>Filters category page by given filterable attribute and attribute option is layered navigation block.</description> + </annotations> + <arguments> + <argument name="attributeLabel" type="string" defaultValue=""/> + <argument name="attributeOptionLabel" type="string" defaultValue=""/> + </arguments> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" stepKey="waitForFilterVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandFilter"/> + <click selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel(attributeOptionLabel)}}" stepKey="clickOnOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml index d3a3005c296b2..71799bfbfee1f 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml @@ -9,5 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontLayeredNavigationSection"> <element name="shoppingOptionsByName" type="button" selector="//*[text()='Shopping Options']/..//*[contains(text(),'{{arg}}')]" parameterized="true"/> + <element name="appliedFilterLabel" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-label" parameterized="true" timeout="30"/> + <element name="appliedFilterValue" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-value" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml new file mode 100644 index 0000000000000..72b5a5bbc75c3 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.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="StorefrontDropdownAttributeInLayeredNavigationTest"> + <annotations> + <features value="LayeredNavigation"/> + <stories value="Product attributes in Layered Navigation"/> + <title value="[ES] Search with Layered Navigation and different types of attribute products."/> + <description value="Filtering by dropdown attribute in Layered navigation"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28963"/> + <group value="layeredNavigation"/> + <group value="catalog"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="dropdownProductAttribute" stepKey="createDropdownProductAttribute"/> + <createData entity="productAttributeOption" stepKey="firstDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="secondDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondDropdownProductAttributeOption"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </getData> + <createData entity="AddToDefaultSet" stepKey="AddDropdownProductAttributeToAttributeSet"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createFirstProduct"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + <requiredEntity createDataKey="getFirstDropdownProductAttributeOption"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createSecondProduct"> + <requiredEntity createDataKey="createDropdownProductAttribute"/> + <requiredEntity createDataKey="getSecondDropdownProductAttributeOption"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createDropdownProductAttribute" stepKey="deleteDropdownProductAttribute"/> + </after> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategory"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertFirstAttributeOptionPresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getFirstDropdownProductAttributeOption.label$"/> + <argument name="attributeOptionPosition" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertSecondAttributeOptionPresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getSecondDropdownProductAttributeOption.label$"/> + <argument name="attributeOptionPosition" value="2"/> + </actionGroup> + <actionGroup ref="StorefrontFilterCategoryPageByAttributeOptionActionGroup" stepKey="filterCategoryByFirstOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getFirstDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertAppliedFilterActionGroup" stepKey="assertFilterByFirstOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getFirstDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="assertFirstProductOnCatalogPage"> + <argument name="productName" value="$createFirstProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup" stepKey="assertSecondProductIsMissingOnCatalogPage"> + <argument name="productName" value="$createSecondProduct.name$"/> + </actionGroup> + <click selector="{{StorefrontCategorySidebarSection.removeFilter}}" stepKey="removeSideBarFilter"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="StorefrontFilterCategoryPageByAttributeOptionActionGroup" stepKey="filterCategoryBySecondOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getSecondDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertAppliedFilterActionGroup" stepKey="assertFilterBySecondOption"> + <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getSecondDropdownProductAttributeOption.label$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductIsPresentOnCategoryPageActionGroup" stepKey="assertSecondProductOnCatalogPage"> + <argument name="productName" value="$createSecondProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductIsMissingInCategoryProductsPageActionGroup" stepKey="assertFirstProductIsMissingOnCatalogPage"> + <argument name="productName" value="$createFirstProduct.name$"/> + </actionGroup> + </test> +</tests> diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php index b949a68ed4c1c..df542dd622864 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php @@ -21,8 +21,8 @@ use Magento\Framework\Search\Request\Config; use Magento\Framework\View\LayoutInterface; use Magento\LayeredNavigation\Block\Navigation; -use Magento\LayeredNavigation\Block\Navigation\Search as SearchNavigationBlock; use Magento\LayeredNavigation\Block\Navigation\Category as CategoryNavigationBlock; +use Magento\LayeredNavigation\Block\Navigation\Search as SearchNavigationBlock; use Magento\Search\Model\Search; use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; @@ -124,6 +124,38 @@ protected function getCategoryFiltersAndAssert( } } + /** + * Tests getFilters method from navigation block layer state on category page. + * + * @param array $products + * @param array $expectation + * @param string $categoryName + * @param string|null $filterValue + * @param int $productsCount + * @return void + */ + protected function getCategoryActiveFiltersAndAssert( + array $products, + array $expectation, + string $categoryName, + string $filterValue, + int $productsCount + ): void { + $this->updateAttribute(['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS]); + $this->updateProducts($products, $this->getAttributeCode()); + $this->clearInstanceAndReindexSearch(); + $this->navigationBlock->getRequest()->setParams($this->getRequestParams($filterValue)); + $this->navigationBlock->getLayer()->setCurrentCategory( + $this->loadCategory($categoryName, Store::DEFAULT_STORE_ID) + ); + $this->navigationBlock->setLayout($this->layout); + $activeFilters = $this->navigationBlock->getLayer()->getState()->getFilters(); + $this->assertCount(1, $activeFilters); + $currentFilter = reset($activeFilters); + $this->assertActiveFilter($expectation, $currentFilter); + $this->assertEquals($productsCount, $this->navigationBlock->getLayer()->getProductCollection()->getSize()); + } + /** * Tests getFilters method from navigation block on search page. * @@ -152,6 +184,37 @@ protected function getSearchFiltersAndAssert( } } + /** + * Tests getFilters method from navigation block layer state on search page. + * + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + protected function getSearchActiveFiltersAndAssert( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->updateAttribute( + ['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS, 'is_filterable_in_search' => 1] + ); + $this->updateProducts($products, $this->getAttributeCode()); + $this->clearInstanceAndReindexSearch(); + $this->navigationBlock->getRequest()->setParams( + array_merge($this->getRequestParams($filterValue), ['q' => $this->getSearchString()]) + ); + $this->navigationBlock->setLayout($this->layout); + $activeFilters = $this->navigationBlock->getLayer()->getState()->getFilters(); + $this->assertCount(1, $activeFilters); + $currentFilter = reset($activeFilters); + $this->assertActiveFilter($expectation, $currentFilter); + $this->assertEquals($productsCount, $this->navigationBlock->getLayer()->getProductCollection()->getSize()); + } + /** * Returns filter with specified attribute. * @@ -303,4 +366,37 @@ protected function getSearchString(): string { return 'Simple Product'; } + + /** + * Adds params for filtering. + * + * @param string $filterValue + * @return array + */ + protected function getRequestParams(string $filterValue): array + { + $attribute = $this->attributeRepository->get($this->getAttributeCode()); + $filterValue = $attribute->usesSource() + ? $attribute->getSource()->getOptionId($filterValue) + : $filterValue; + + return [$this->getAttributeCode() => $filterValue]; + } + + /** + * Asserts active filter data. + * + * @param array $expectation + * @param Item $currentFilter + * @return void + */ + protected function assertActiveFilter(array $expectation, Item $currentFilter): void + { + $this->assertEquals($expectation['label'], $currentFilter->getData('label')); + $this->assertEquals($expectation['count'], $currentFilter->getData('count')); + $this->assertEquals( + $this->getAttributeCode(), + $currentFilter->getFilter()->getData('attribute_model')->getAttributeCode() + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php index 24787bc3c4ca8..52c2c6ea6f66e 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/BooleanFilterTest.php @@ -69,6 +69,58 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_boolean_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'selected_yes_option_in_all_products' => [ + 'products_data' => ['simple1000' => 'Yes', 'simple1001' => 'Yes'], + 'expectation' => ['label' => 'Yes', 'count' => 0], + 'filter_value' => 'Yes', + 'products_count' => 2, + ], + 'selected_yes_option_in_one_product' => [ + 'products_data' => ['simple1000' => 'Yes', 'simple1001' => 'No'], + 'expectation' => ['label' => 'Yes', 'count' => 0], + 'filter_value' => 'Yes', + 'products_count' => 1, + ], + 'selected_no_option_in_all_products' => [ + 'products_data' => ['simple1000' => 'No', 'simple1001' => 'No'], + 'expectation' => ['label' => 'No', 'count' => 0], + 'filter_value' => 'No', + 'products_count' => 2, + ], + 'selected_no_option_in_one_product' => [ + 'products_data' => ['simple1000' => 'Yes', 'simple1001' => 'No'], + 'expectation' => ['label' => 'No', 'count' => 0], + 'filter_value' => 'No', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php index f8391c60a30cf..abc8fa9201eba 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultiselectFilterTest.php @@ -7,9 +7,10 @@ namespace Magento\LayeredNavigation\Block\Navigation\Category; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; use Magento\Catalog\Model\Layer\Resolver; use Magento\LayeredNavigation\Block\Navigation\AbstractFiltersTest; -use Magento\Catalog\Model\Layer\Filter\AbstractFilter; +use Magento\Store\Model\Store; /** * Provides tests for custom multiselect filter in navigation block on category page. @@ -72,6 +73,58 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/multiselect_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 1'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_two_options' => [ + 'products_data' => ['simple1000' => 'Option 1,Option 2', 'simple1001' => 'Option 1,Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 2, + ], + 'filter_by_second_option_in_products_with_hybrid_options' => [ + 'products_data' => ['simple1000' => 'Option 1,Option 2', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 2, + ], + ]; + } + /** * @inheritdoc */ @@ -87,4 +140,26 @@ protected function getAttributeCode(): string { return 'multiselect_attribute'; } + + /** + * @inheritdoc + */ + protected function updateProducts( + array $products, + string $attributeCode, + int $storeId = Store::DEFAULT_STORE_ID + ): void { + $attribute = $this->attributeRepository->get($attributeCode); + + foreach ($products as $productSku => $stringValue) { + $product = $this->productRepository->get($productSku, false, $storeId, true); + $values = explode(',', $stringValue); + $productValue = []; + foreach ($values as $value) { + $productValue[] = $attribute->usesSource() ? $attribute->getSource()->getOptionId($value) : $value; + } + $product->addData([$attribute->getAttributeCode() => implode(',', $productValue)]); + $this->productRepository->save($product); + } + } } 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 97928463620f4..db3bcb10c8364 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 @@ -14,7 +14,6 @@ use Magento\Catalog\Model\Layer\Filter\AbstractFilter; use Magento\Catalog\Model\Layer\Filter\Item; use Magento\Store\Model\ScopeInterface as StoreScope; -use Magento\Store\Model\Store; /** * Provides price filter tests with different price ranges calculation in navigation block on category page. @@ -163,6 +162,56 @@ public function getFiltersDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @dataProvider getActiveFiltersDataProvider + * @param array $config + * @param array $products + * @param array $expectation + * @param string $filterValue + * @return void + */ + public function testGetActiveFilters(array $config, array $products, array $expectation, string $filterValue): void + { + $this->applyCatalogConfig($config); + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, 1); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getActiveFiltersDataProvider(): array + { + return [ + 'auto_calculation' => [ + 'config' => ['catalog/layered_navigation/price_range_calculation' => 'auto'], + 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], + 'expectation' => ['label' => '$10.00 - $19.99', 'count' => 0], + 'filter_value' => '10-20', + ], + 'improved_calculation' => [ + 'config' => [ + 'catalog/layered_navigation/price_range_calculation' => 'improved', + 'catalog/layered_navigation/interval_division_limit' => 3, + ], + 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], + 'expectation' => ['label' => '$0.00 - $19.99', 'count' => 0], + 'filter_value' => '0-20', + ], + 'manual_calculation' => [ + 'config' => [ + 'catalog/layered_navigation/price_range_calculation' => 'manual', + 'catalog/layered_navigation/price_range_step' => 10, + 'catalog/layered_navigation/price_range_max_intervals' => 10, + ], + 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 30.00], + 'expectation' => ['label' => '$10.00 - $19.99', 'count' => 0], + 'filter_value' => '10-20', + ], + ]; + } + /** * @inheritdoc */ @@ -209,4 +258,17 @@ protected function applyCatalogConfig(array $config): void $this->scopeConfig->setValue($path, $value, StoreScope::SCOPE_STORE, ScopeInterface::SCOPE_DEFAULT); } } + + /** + * @inheritdoc + */ + protected function assertActiveFilter(array $expectation, Item $currentFilter): void + { + $this->assertEquals($expectation['label'], strip_tags((string)$currentFilter->getData('label'))); + $this->assertEquals($expectation['count'], $currentFilter->getData('count')); + $this->assertEquals( + $this->getAttributeCode(), + $currentFilter->getFilter()->getData('attribute_model')->getAttributeCode() + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php index e2278239be242..014438b4906bd 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/SelectFilterTest.php @@ -71,6 +71,52 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_dropdown_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 1'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php index 8f03ae3eed229..664b54ed92161 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/BooleanFilterTest.php @@ -71,6 +71,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_boolean_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php index 9220a81f507ee..1d5d57fcddddd 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultiselectFilterTest.php @@ -74,6 +74,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Catalog/_files/multiselect_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php index d9ac02b2bff11..6f7d040d1ad9c 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/PriceFilterTest.php @@ -41,6 +41,21 @@ public function testGetFilters(array $config, array $products, array $expectatio ); } + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @dataProvider getActiveFiltersDataProvider + * @param array $config + * @param array $products + * @param array $expectation + * @param string $filterValue + * @return void + */ + public function testGetActiveFilters(array $config, array $products, array $expectation, string $filterValue): void + { + $this->applyCatalogConfig($config); + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, 1); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php index d44994de7e31c..2133ab445b8f9 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/SelectFilterTest.php @@ -72,6 +72,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Catalog/_files/product_dropdown_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php index a56c13ca92f2f..57d8e72712a61 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchTextFilterTest.php @@ -71,6 +71,52 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_text_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 1'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 1', 'count' => 0], + 'filter_value' => 'Option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'Option 1', 'simple1001' => 'Option 2'], + 'expectation' => ['label' => 'Option 2', 'count' => 0], + 'filter_value' => 'Option 2', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php index 9860e5a78c436..a254d4953f2d3 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Category/SwatchVisualFilterTest.php @@ -71,6 +71,52 @@ public function getFiltersWithCustomAttributeDataProvider(): array ]; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_visual_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getCategoryActiveFiltersAndAssert($products, $expectation, 'Category 999', $filterValue, $productsCount); + } + + /** + * @return array + */ + public function getActiveFiltersWithCustomAttributeDataProvider(): array + { + return [ + 'filter_by_first_option_in_products_with_first_option' => [ + 'products_data' => ['simple1000' => 'option 1', 'simple1001' => 'option 1'], + 'expectation' => ['label' => 'option 1', 'count' => 0], + 'filter_value' => 'option 1', + 'products_count' => 2, + ], + 'filter_by_first_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'option 1', 'simple1001' => 'option 2'], + 'expectation' => ['label' => 'option 1', 'count' => 0], + 'filter_value' => 'option 1', + 'products_count' => 1, + ], + 'filter_by_second_option_in_products_with_different_options' => [ + 'products_data' => ['simple1000' => 'option 1', 'simple1001' => 'option 2'], + 'expectation' => ['label' => 'option 2', 'count' => 0], + 'filter_value' => 'option 2', + 'products_count' => 1, + ], + ]; + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php index 83867453a98ea..f38a22e5249e7 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchTextFilterTest.php @@ -72,6 +72,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_text_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php index 47c7b09f2eb85..a82e637bc77fc 100644 --- a/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/SwatchesLayeredNavigation/Block/Navigation/Search/SwatchVisualFilterTest.php @@ -72,6 +72,25 @@ public function getFiltersWithCustomAttributeDataProvider(): array return $dataProvider; } + /** + * @magentoDataFixture Magento/Swatches/_files/product_visual_swatch_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_different_price_products.php + * @dataProvider getActiveFiltersWithCustomAttributeDataProvider + * @param array $products + * @param array $expectation + * @param string $filterValue + * @param int $productsCount + * @return void + */ + public function testGetActiveFiltersWithCustomAttribute( + array $products, + array $expectation, + string $filterValue, + int $productsCount + ): void { + $this->getSearchActiveFiltersAndAssert($products, $expectation, $filterValue, $productsCount); + } + /** * @inheritdoc */ From 544296e3172e9343e1c87a8502220dff64efa89b Mon Sep 17 00:00:00 2001 From: engcom-Kilo <mikola.malevanec@transoftgroup.com> Date: Tue, 6 Oct 2020 15:32:48 +0300 Subject: [PATCH 0712/1013] MC-37666: Incorrect Customer TAX Class saved with Quote when VAT Validation used on Guest orders --- .../Magento/Quote/Model/QuoteManagement.php | 4 +- .../Quote/Model/QuoteManagementTest.php | 57 +++++++++++++++++++ .../_files/guest_quote_with_addresses.php | 5 +- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Quote/Model/QuoteManagement.php b/app/code/Magento/Quote/Model/QuoteManagement.php index b0aef022dcd25..1d4b8feba07f5 100644 --- a/app/code/Magento/Quote/Model/QuoteManagement.php +++ b/app/code/Magento/Quote/Model/QuoteManagement.php @@ -8,6 +8,7 @@ namespace Magento\Quote\Model; use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Api\Data\GroupInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\ManagerInterface as EventManager; use Magento\Framework\Exception\CouldNotSaveException; @@ -396,7 +397,8 @@ public function placeOrder($cartId, PaymentInterface $paymentMethod = null) } } $quote->setCustomerIsGuest(true); - $quote->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID); + $groupId = $quote->getCustomer()->getGroupId() ?: GroupInterface::NOT_LOGGED_IN_ID; + $quote->setCustomerGroupId($groupId); } $remoteAddress = $this->remoteAddress->getRemoteAddress(); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php index facb4879650b1..26ae82120b2c7 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php @@ -10,10 +10,15 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Type; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\Vat; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\StateException; use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Observer\Frontend\Quote\Address\CollectTotalsObserver; +use Magento\Quote\Observer\Frontend\Quote\Address\VatValidator; use Magento\Sales\Api\OrderManagementInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Store\Model\StoreManagerInterface; @@ -21,6 +26,7 @@ use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * Class for testing QuoteManagement model @@ -106,6 +112,28 @@ public function testSubmit(): void } } + /** + * Verify guest customer place order with auto-group assigment. + * + * @magentoDataFixture Magento/Sales/_files/guest_quote_with_addresses.php + * + * @magentoConfigFixture default_store customer/create_account/auto_group_assign 1 + * @magentoConfigFixture default_store customer/create_account/tax_calculation_address_type shipping + * @magentoConfigFixture default_store customer/create_account/viv_intra_union_group 2 + * @magentoConfigFixture default_store customer/create_account/viv_on_each_transaction 1 + * + * @return void + */ + public function testSubmitGuestCustomer(): void + { + $this->mockVatValidation(); + $quote = $this->getQuoteByReservedOrderId->execute('guest_quote'); + $this->cartManagement->placeOrder($quote->getId()); + $quoteAfterOrderPlaced = $this->getQuoteByReservedOrderId->execute('guest_quote'); + self::assertEquals(2, $quoteAfterOrderPlaced->getCustomerGroupId()); + self::assertEquals(3, $quoteAfterOrderPlaced->getCustomerTaxClassId()); + } + /** * Tries to create order with product that has child items and one of them was deleted. * @@ -231,4 +259,33 @@ private function makeProductOutOfStock(string $sku): void $stockItem->setIsInStock(false); $this->productRepository->save($product); } + + /** + * Makes customer vat validator 'check vat number' response successful. + * + * @return void + */ + private function mockVatValidation(): void + { + $vatMock = $this->getMockBuilder(Vat::class) + ->setConstructorArgs( + [ + 'scopeConfig' => $this->objectManager->get(ScopeConfigInterface::class), + 'logger' => $this->objectManager->get(LoggerInterface::class), + ] + ) + ->onlyMethods(['checkVatNumber']) + ->getMock(); + $gatewayResponse = new DataObject([ + 'is_valid' => true, + 'request_date' => 'testData', + 'request_identifier' => 'testRequestIdentifier', + 'request_success' => true, + ]); + $vatMock->method('checkVatNumber')->willReturn($gatewayResponse); + $this->objectManager->removeSharedInstance(CollectTotalsObserver::class); + $this->objectManager->removeSharedInstance(VatValidator::class); + $this->objectManager->removeSharedInstance(Vat::class); + $this->objectManager->addSharedInstance($vatMock, Vat::class); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php index d613b60c1d52f..019b3114e04c8 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/guest_quote_with_addresses.php @@ -40,14 +40,14 @@ $addressData = [ 'telephone' => 3234676, 'postcode' => 47676, - 'country_id' => 'US', + 'country_id' => 'DE', 'city' => 'CityX', 'street' => ['Black str, 48'], 'lastname' => 'Smith', 'firstname' => 'John', + 'vat_id' => 12345, 'address_type' => 'shipping', 'email' => 'some_email@mail.com', - 'region_id' => 1, ]; $billingAddress = $objectManager->create( @@ -66,6 +66,7 @@ $quote->setCustomerIsGuest(true) ->setStoreId($store->getId()) ->setReservedOrderId('guest_quote') + ->setCheckoutMethod('guest') ->setBillingAddress($billingAddress) ->setShippingAddress($shippingAddress) ->addProduct($product); From e01c6efe19991779e7b411dd6e8bdbe8922c1254 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Tue, 6 Oct 2020 16:15:52 +0300 Subject: [PATCH 0713/1013] MC-36965: Create automated test for "[ES] Search with Layered Navigation and different types of attribute products. --- .../StorefrontCategorySidebarSection.xml | 2 +- .../ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml | 4 ++-- ...refrontFilterCategoryPageByAttributeOptionActionGroup.xml | 5 +++-- .../StorefrontLayeredNavigationSection.xml | 4 ++-- .../StorefrontDropdownAttributeInLayeredNavigationTest.xml | 3 +-- 5 files changed, 9 insertions(+), 9 deletions(-) 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 5ec493aef0cea..f1c3d0c5ec67a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml @@ -14,7 +14,7 @@ <element name="filterOption" type="text" selector=".filter-options-content .item"/> <element name="optionQty" type="text" selector=".filter-options-content .item .count"/> <element name="filterOptionByLabel" type="button" selector=" div.filter-options-item div[data-option-label='{{optionLabel}}']" parameterized="true"/> - <element name="removeFilter" type="button" selector="div.filter-current .remove"/> + <element name="removeFilter" type="button" selector="div.filter-current .remove" timeout="30"/> <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"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml index faf33a6649d50..652423df37f85 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml @@ -13,8 +13,8 @@ <description>Asserts applied filter label and value on category page is layered navigation block.</description> </annotations> <arguments> - <argument name="attributeLabel" type="string" defaultValue=""/> - <argument name="attributeOptionLabel" type="string" defaultValue=""/> + <argument name="attributeLabel" type="string"/> + <argument name="attributeOptionLabel" type="string"/> </arguments> <see selector="{{StorefrontLayeredNavigationSection.appliedFilterLabel('1')}}" userInput="{{attributeLabel}}" stepKey="seeAppliedFilterLabel"/> <see selector="{{StorefrontLayeredNavigationSection.appliedFilterValue('1')}}" userInput="{{attributeOptionLabel}}" stepKey="seeAppliedFilterValue"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml index e7f9a766014e6..63d16b821a4be 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml @@ -13,11 +13,12 @@ <description>Filters category page by given filterable attribute and attribute option is layered navigation block.</description> </annotations> <arguments> - <argument name="attributeLabel" type="string" defaultValue=""/> - <argument name="attributeOptionLabel" type="string" defaultValue=""/> + <argument name="attributeLabel" type="string"/> + <argument name="attributeOptionLabel" type="string"/> </arguments> <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" stepKey="waitForFilterVisible"/> <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandFilter"/> <click selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel(attributeOptionLabel)}}" stepKey="clickOnOption"/> + <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml index 71799bfbfee1f..d8a103116ef06 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Section/LayeredNavigationSection/StorefrontLayeredNavigationSection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="StorefrontLayeredNavigationSection"> <element name="shoppingOptionsByName" type="button" selector="//*[text()='Shopping Options']/..//*[contains(text(),'{{arg}}')]" parameterized="true"/> - <element name="appliedFilterLabel" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-label" parameterized="true" timeout="30"/> - <element name="appliedFilterValue" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-value" parameterized="true" timeout="30"/> + <element name="appliedFilterLabel" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-label" parameterized="true"/> + <element name="appliedFilterValue" type="text" selector=".filter-current .items > li.item:nth-of-type({{position}}) > span.filter-value" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml index 72b5a5bbc75c3..5d9d732089ea8 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml @@ -15,7 +15,7 @@ <title value="[ES] Search with Layered Navigation and different types of attribute products."/> <description value="Filtering by dropdown attribute in Layered navigation"/> <severity value="CRITICAL"/> - <testCaseId value="MC-28963"/> + <testCaseId value="MC-36326"/> <group value="layeredNavigation"/> <group value="catalog"/> <group value="SearchEngineElasticsearch"/> @@ -83,7 +83,6 @@ <argument name="productName" value="$createSecondProduct.name$"/> </actionGroup> <click selector="{{StorefrontCategorySidebarSection.removeFilter}}" stepKey="removeSideBarFilter"/> - <waitForPageLoad stepKey="waitForPageLoad"/> <actionGroup ref="StorefrontFilterCategoryPageByAttributeOptionActionGroup" stepKey="filterCategoryBySecondOption"> <argument name="attributeLabel" value="$createDropdownProductAttribute.attribute[frontend_labels][0][label]$"/> <argument name="attributeOptionLabel" value="$getSecondDropdownProductAttributeOption.label$"/> From fc8590177de2a810f9864995e7292c7c5e367481 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 6 Oct 2020 16:27:37 +0300 Subject: [PATCH 0714/1013] MC-38197: [Magento Cloud] Error when updating carts - fatal error Call to a member function getValue() on null in module-configurable-product CartItemProcessor.php --- .../Model/Quote/Item/CartItemProcessor.php | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php index 814c1e971ea33..75592efc52dca 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php +++ b/app/code/Magento/ConfigurableProduct/Model/Quote/Item/CartItemProcessor.php @@ -5,6 +5,8 @@ */ namespace Magento\ConfigurableProduct\Model\Quote\Item; +use Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface; +use Magento\Quote\Api\Data\ProductOptionExtensionInterface; use Magento\Quote\Model\Quote\Item\CartItemProcessorInterface; use Magento\Quote\Api\Data\CartItemInterface; use Magento\Framework\Serialize\Serializer\Json; @@ -64,7 +66,7 @@ public function __construct( public function convertToBuyRequest(CartItemInterface $cartItem) { if ($cartItem->getProductOption() && $cartItem->getProductOption()->getExtensionAttributes()) { - /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $options */ + /** @var ConfigurableItemOptionValueInterface $options */ $options = $cartItem->getProductOption()->getExtensionAttributes()->getConfigurableItemOptions(); if (is_array($options)) { $requestData = []; @@ -84,33 +86,35 @@ public function processOptions(CartItemInterface $cartItem) { $attributesOption = $cartItem->getProduct() ->getCustomOption('attributes'); - if ($attributesOption) { - $selectedConfigurableOptions = $this->serializer->unserialize($attributesOption->getValue()); + if (!$attributesOption) { + return $cartItem; + } + $selectedConfigurableOptions = $this->serializer->unserialize($attributesOption->getValue()); - if (is_array($selectedConfigurableOptions)) { - $configurableOptions = []; - foreach ($selectedConfigurableOptions as $optionId => $optionValue) { - /** @var \Magento\ConfigurableProduct\Api\Data\ConfigurableItemOptionValueInterface $option */ - $option = $this->itemOptionValueFactory->create(); - $option->setOptionId($optionId); - $option->setOptionValue($optionValue); - $configurableOptions[] = $option; - } + if (is_array($selectedConfigurableOptions)) { + $configurableOptions = []; + foreach ($selectedConfigurableOptions as $optionId => $optionValue) { + /** @var ConfigurableItemOptionValueInterface $option */ + $option = $this->itemOptionValueFactory->create(); + $option->setOptionId($optionId); + $option->setOptionValue($optionValue); + $configurableOptions[] = $option; + } - $productOption = $cartItem->getProductOption() - ? $cartItem->getProductOption() - : $this->productOptionFactory->create(); + $productOption = $cartItem->getProductOption() + ? $cartItem->getProductOption() + : $this->productOptionFactory->create(); - /** @var \Magento\Quote\Api\Data\ProductOptionExtensionInterface $extensibleAttribute */ - $extensibleAttribute = $productOption->getExtensionAttributes() - ? $productOption->getExtensionAttributes() - : $this->extensionFactory->create(); + /** @var ProductOptionExtensionInterface $extensibleAttribute */ + $extensibleAttribute = $productOption->getExtensionAttributes() + ? $productOption->getExtensionAttributes() + : $this->extensionFactory->create(); - $extensibleAttribute->setConfigurableItemOptions($configurableOptions); - $productOption->setExtensionAttributes($extensibleAttribute); - $cartItem->setProductOption($productOption); - } + $extensibleAttribute->setConfigurableItemOptions($configurableOptions); + $productOption->setExtensionAttributes($extensibleAttribute); + $cartItem->setProductOption($productOption); } + return $cartItem; } } From 0bbfb66a936ecf657cebc896679f845049920788 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Tue, 6 Oct 2020 17:40:33 +0300 Subject: [PATCH 0715/1013] MC-37098: Create automated test for "Check functionality of RabbitMQ" --- .../Section/AdminSystemMessagesSection.xml | 2 + .../Section/AdminBulkDetailModalSection.xml | 16 +++++ ...MassUpdateProductAttributesActionGroup.xml | 20 +++++++ ...roductsMassAttributesUpdateActionGroup.xml | 18 ++++++ .../AdminEditProductAttributesSection.xml | 2 + ...ProductAttributeUpdateAddedToQueueTest.xml | 60 +++++++++++++++++++ 6 files changed, 118 insertions(+) create mode 100644 app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailModalSection.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml diff --git a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml index e3b2ea7e24c83..53a73446a29d1 100644 --- a/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml +++ b/app/code/Magento/AdminNotification/Test/Mftf/Section/AdminSystemMessagesSection.xml @@ -15,5 +15,7 @@ <element name="success" type="text" selector="#system_messages div.message-success"/> <element name="warning" type="text" selector="#system_messages div.message-warning"/> <element name="notice" type="text" selector="#system_messages div.message-notice"/> + <element name="info" type="text" selector="#system_messages div.message-info"/> + <element name="viewDetailsLink" type="button" selector="//div[contains(@class, 'message-system-short')]/a[contains(text(), 'View Details')]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailModalSection.xml b/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailModalSection.xml new file mode 100644 index 0000000000000..0ef6a8981172b --- /dev/null +++ b/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailModalSection.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="AdminBulkDetailsModalSection"> + <element name="descriptionValue" type="text" selector="//aside//div[@data-index='description']//span[@name='description']"/> + <element name="summaryValue" type="text" selector="//aside//div[@data-index='summary']//span[@name='summary']" /> + <element name="startTimeValue" type="text" selector="//aside//div[@data-index='start_time']//span[@name='start_time']" /> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml new file mode 100644 index 0000000000000..81a576e9ea3f1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.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="AdminClickMassUpdateProductAttributesActionGroup"> + <annotations> + <description>Clicks on 'Update attributes' on product grid page.</description> + </annotations> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> + <waitForPageLoad stepKey="waitForBulkUpdatePage"/> + <seeInCurrentUrl url="catalog/product_action_attribute/edit/" stepKey="seeInUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml new file mode 100644 index 0000000000000..b794528135858 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.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="AdminSaveProductsMassAttributesUpdateActionGroup"> + <annotations> + <description>Clicks on 'Save' button on products mass attributes update page.</description> + </annotations> + <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" time="60" stepKey="waitForSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml index ad4ab57f8de2c..d7dc38a4a88f6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminEditProductAttributesSection.xml @@ -23,5 +23,7 @@ <element name="defaultLabel" type="text" selector="//td[contains(text(), '{{attributeName}}')]/following-sibling::td[contains(@class, 'col-frontend_label')]" parameterized="true"/> <element name="formByStoreId" type="block" selector="//form[contains(@action,'store/{{store_id}}')]" parameterized="true"/> <element name="tabButton" type="text" selector="#product_attribute_tabs a[title='{{tabName}}']" parameterized="true"/> + <element name="attributeShortDescription" type="text" selector="#short_description"/> + <element name="changeAttributeShortDescriptionToggle" type="checkbox" selector="#toggle_short_description"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml new file mode 100644 index 0000000000000..728f9dd7d68ca --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.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="AdminMassProductAttributeUpdateAddedToQueueTest"> + <annotations> + <features value="Catalog"/> + <stories value="Mass update product attributes"/> + <title value="Check functionality of RabbitMQ"/> + <description value="Mass product attribute update task should be added to the queue"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28990"/> + <useCaseId value="MC-29179"/> + <group value="catalog"/> + <group value="asynchronousOperations"/> + <group value="productAttributes"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="createFirstProduct"/> + <createData entity="ApiProductWithDescription" stepKey="createSecondProduct"/> + <createData entity="ApiProductNameWithNoSpaces" stepKey="createThirdProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> + <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> + <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> + <argument name="keyword" value="api-simple-product"/> + </actionGroup> + <actionGroup ref="SortProductsByIdDescendingActionGroup" stepKey="sortProductsByIdDescending"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="selectThirdProduct"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="selectSecondProduct"/> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('3')}}" stepKey="selectFirstProduct"/> + <actionGroup ref="AdminClickMassUpdateProductAttributesActionGroup" stepKey="goToUpdateProductAttributesPage"/> + <checkOption selector="{{AdminEditProductAttributesSection.changeAttributeShortDescriptionToggle}}" stepKey="toggleToChangeShortDescription"/> + <fillField selector="{{AdminEditProductAttributesSection.attributeShortDescription}}" userInput="Test Update" stepKey="fillShortDescriptionField"/> + <actionGroup ref="AdminSaveProductsMassAttributesUpdateActionGroup" stepKey="saveMassAttributeUpdate"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeAttributeUpdateSuccessMsg"/> + <see selector="{{AdminSystemMessagesSection.info}}" userInput="Task "Update attributes for 3 selected products": 1 item(s) have been scheduled for update." stepKey="seeInfoMessage"/> + <click selector="{{AdminSystemMessagesSection.viewDetailsLink}}" stepKey="seeDetails"/> + <see selector="{{AdminBulkDetailsModalSection.descriptionValue}}" userInput="Update attributes for 3 selected products" stepKey="seeDescription"/> + <see selector="{{AdminBulkDetailsModalSection.summaryValue}}" userInput="Pending, in queue..." stepKey="seeSummary"/> + <grabTextFrom selector="{{AdminBulkDetailsModalSection.startTimeValue}}" stepKey="grabStartTimeValue"/> + <assertRegExp stepKey="assertStartTime"> + <expectedResult type="string">/\d{1,2}\/\d{2}\/\d{4}\s\d{1,2}:\d{2}:\d{2}\s(AM|PM)/</expectedResult> + <actualResult type="variable">grabStartTimeValue</actualResult> + </assertRegExp> + </test> +</tests> From f892998e79884862daba328a5c29878d099ac06f Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Fri, 2 Oct 2020 12:34:11 -0500 Subject: [PATCH 0716/1013] MC-37217: Incorrect currency symbol in orders report - Adding test file --- .../Grid/Column/Renderer/Currency.php | 138 +++++++- .../Grid/Column/Renderer/CurrencyTest.php | 300 ++++++++++++++++++ app/code/Magento/Reports/composer.json | 3 +- 3 files changed, 435 insertions(+), 6 deletions(-) create mode 100644 app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php index f22b3e7bb963b..1ebfd64b2f37c 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/Column/Renderer/Currency.php @@ -3,36 +3,164 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Reports\Block\Adminhtml\Grid\Column\Renderer; +use Magento\Backend\Block\Widget\Grid\Column\Renderer\Currency as BackendCurrency; +use Magento\Backend\Block\Context; +use Magento\Directory\Model\Currency\DefaultLocator; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Locale\CurrencyInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Zend_Currency_Exception; + /** * Adminhtml grid item renderer currency * * @author Magento Core Team <core@magentocommerce.com> * @api * @since 100.0.2 + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Currency extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Currency +class Currency extends BackendCurrency { + /** + * @var CurrencyFactory + */ + private $currencyFactory; + + /** + * @param Context $context + * @param StoreManagerInterface $storeManager + * @param DefaultLocator $currencyLocator + * @param CurrencyFactory $currencyFactory + * @param CurrencyInterface $localeCurrency + * @param array $data + */ + public function __construct( + Context $context, + StoreManagerInterface $storeManager, + DefaultLocator $currencyLocator, + CurrencyFactory $currencyFactory, + CurrencyInterface $localeCurrency, + array $data = [] + ) { + parent::__construct( + $context, + $storeManager, + $currencyLocator, + $currencyFactory, + $localeCurrency, + $data + ); + $this->currencyFactory = $currencyFactory; + } + /** * Renders grid column * - * @param \Magento\Framework\DataObject $row + * @param DataObject $row * @return string + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws Zend_Currency_Exception */ - public function render(\Magento\Framework\DataObject $row) + public function render(DataObject $row) { $data = $row->getData($this->getColumn()->getIndex()); - $currencyCode = $this->_getCurrencyCode($row); + $currencyCode = $this->getStoreCurrencyCode($row); if (!$currencyCode) { return $data; } - $data = (float)$data * $this->_getRate($row); + $rate = $this->getStoreCurrencyRate($currencyCode, $row); + + $data = (float)$data * $rate; $data = sprintf("%f", $data); $data = $this->_localeCurrency->getCurrency($currencyCode)->toCurrency($data); return $data; } + + /** + * Get admin currency code + * + * @return string + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function getAdminCurrencyCode(): string + { + $adminWebsiteId = (int) $this->_storeManager + ->getStore(Store::ADMIN_CODE) + ->getWebsiteId(); + return (string) $this->_storeManager + ->getWebsite($adminWebsiteId) + ->getBaseCurrencyCode(); + } + + /** + * Get store currency code + * + * @param DataObject $row + * @return string + * @throws NoSuchEntityException + */ + private function getStoreCurrencyCode(DataObject $row): string + { + $catalogPriceScope = $this->getCatalogPriceScope(); + $storeId = $this->_request->getParam('store_ids'); + if ($catalogPriceScope != 0 && !empty($storeId)) { + $currencyCode = $this->_storeManager->getStore($storeId)->getBaseCurrencyCode(); + } elseif ($catalogPriceScope != 0) { + $currencyCode = $this->_currencyLocator->getDefaultCurrency($this->_request); + } else { + $currencyCode = $this->_getCurrencyCode($row); + } + return $currencyCode; + } + + /** + * Get store currency rate + * + * @param string $currencyCode + * @param DataObject $row + * @return float + * @throws LocalizedException + * @throws NoSuchEntityException + */ + private function getStoreCurrencyRate(string $currencyCode, DataObject $row): float + { + $catalogPriceScope = $this->getCatalogPriceScope(); + $adminCurrencyCode = $this->getAdminCurrencyCode(); + + if (($catalogPriceScope != 0 + && $adminCurrencyCode !== $currencyCode)) { + $storeCurrency = $this->currencyFactory->create()->load($adminCurrencyCode); + $currencyRate = $storeCurrency->getRate($currencyCode); + } else { + $currencyRate = $this->_getRate($row); + } + return (float) $currencyRate; + } + + /** + * Get catalog price scope from the admin config + * + * @return int + */ + private function getCatalogPriceScope(): int + { + return (int) $this->_scopeConfig->getValue( + Store::XML_PATH_PRICE_SCOPE, + ScopeInterface::SCOPE_WEBSITE + ); + } } diff --git a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php new file mode 100644 index 0000000000000..f071bffe57c1e --- /dev/null +++ b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/CurrencyTest.php @@ -0,0 +1,300 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Test\Unit\Block\Adminhtml\Grid\Column\Renderer; + +use Magento\Backend\Block\Widget\Grid\Column; +use Magento\Directory\Model\Currency as CurrencyModel; +use Magento\Directory\Model\Currency\DefaultLocator; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Locale\CurrencyInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Reports\Block\Adminhtml\Grid\Column\Renderer\Currency; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Zend_Currency; +use Zend_Currency_Exception; + +/** + * Test for class Currency. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CurrencyTest extends TestCase +{ + /** + * @var Currency|MockObject + */ + private $model; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var DefaultLocator|MockObject + */ + private $currencyLocatorMock; + + /** + * @var CurrencyInterface|MockObject + */ + private $localeCurrencyMock; + + /** + * @var Column|MockObject + */ + private $gridColumnMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var StoreInterface|MockObject + */ + private $storeMock; + + /** + * @var WebsiteInterface|MockObject + */ + private $websiteMock; + + /** + * @var DataObject + */ + private $row; + + /** + * @var CurrencyModel|MockObject + */ + private $currencyMock; + + /** + * @var MockObject + */ + private $backendCurrencyMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + $this->scopeConfigMock = $this->getMockForAbstractClass( + ScopeConfigInterface::class, + [], + '', + true, + true, + true, + ['getValue'] + ); + + $this->storeManagerMock = $this->getMockForAbstractClass( + StoreManagerInterface::class, + [], + '', + true, + true, + true, + ['getStore', 'getWebsite'] + ); + + $this->storeMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + true, + true, + true, + ['getWebsiteId', 'getCurrentCurrencyCode'] + ); + + $this->websiteMock = $this->getMockForAbstractClass( + WebsiteInterface::class, + [], + '', + true, + true, + true, + ['getBaseCurrencyCode'] + ); + + $this->currencyLocatorMock = $this->getMockBuilder(DefaultLocator::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->currencyMock = $this->createMock(CurrencyModel::class); + $this->currencyMock->expects($this->any())->method('load')->willReturnSelf(); + + $currencyFactoryMock = $this->createPartialMock(CurrencyFactory::class, ['create']); + $currencyFactoryMock->expects($this->any())->method('create')->willReturn($this->currencyMock); + + $this->backendCurrencyMock = $this->getMockBuilder(Currency::class) + ->disableOriginalConstructor() + ->getMock(); + $this->localeCurrencyMock = $this->getMockForAbstractClass( + CurrencyInterface::class, + [], + '', + true, + true, + true, + ['getCurrency'] + ); + + $this->gridColumnMock = $this->getMockBuilder(Column::class) + ->addMethods(['getIndex']) + ->disableOriginalConstructor() + ->getMock(); + $this->model = $objectManager->getObject( + Currency::class, + [ + '_scopeConfig' => $this->scopeConfigMock, + 'storeManager' => $this->storeManagerMock, + 'currencyLocator' => $this->currencyLocatorMock, + 'currencyFactory' => $currencyFactoryMock, + 'localeCurrency' => $this->localeCurrencyMock + ] + ); + $this->model->setColumn($this->gridColumnMock); + } + + /** + * Test render function which converts store currency based on price scope settings + * + * @param float $rate + * @param string $columnIndex + * @param int $catalogPriceScope + * @param int $adminWebsiteId + * @param string $adminCurrencyCode + * @param string $storeCurrencyCode + * @param float $adminOrderAmount + * @param float $convertedAmount + * @throws LocalizedException + * @throws NoSuchEntityException + * @throws Zend_Currency_Exception + * @dataProvider getCurrencyDataProvider + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testRender( + float $rate, + string $columnIndex, + int $catalogPriceScope, + int $adminWebsiteId, + string $adminCurrencyCode, + string $storeCurrencyCode, + float $adminOrderAmount, + float $convertedAmount + ): void { + $this->row = new DataObject( + [ + $columnIndex => $adminOrderAmount, + 'rate' => $rate + ] + ); + $this->backendCurrencyMock + ->expects($this->any()) + ->method('getColumn') + ->willReturn($this->gridColumnMock); + $this->gridColumnMock + ->expects($this->any()) + ->method('getIndex') + ->willReturn($columnIndex); + $this->currencyMock + ->expects($this->any()) + ->method('getRate') + ->willReturn($rate); + $this->scopeConfigMock + ->expects($this->any()) + ->method('getValue') + ->willReturn($catalogPriceScope); + $this->storeManagerMock + ->expects($this->any()) + ->method('getStore') + ->willReturn($this->storeMock); + $this->storeMock + ->expects($this->any()) + ->method('getWebsiteId') + ->willReturn($adminWebsiteId); + $this->storeManagerMock + ->expects($this->any()) + ->method('getWebsite') + ->with($adminWebsiteId) + ->willReturn($this->websiteMock); + $this->websiteMock + ->expects($this->any()) + ->method('getBaseCurrencyCode') + ->willReturn($adminCurrencyCode); + $this->currencyLocatorMock + ->expects($this->any()) + ->method('getDefaultCurrency') + ->willReturn($storeCurrencyCode); + $currLocaleMock = $this->createMock(Zend_Currency::class); + $currLocaleMock + ->expects($this->any()) + ->method('toCurrency') + ->willReturn($convertedAmount); + $this->localeCurrencyMock + ->expects($this->any()) + ->method('getCurrency') + ->with($storeCurrencyCode) + ->willReturn($currLocaleMock); + $actualAmount = $this->model->render($this->row); + $this->assertEquals($convertedAmount, $actualAmount); + } + + /** + * DataProvider for testRender. + * + * @return array + */ + public function getCurrencyDataProvider(): array + { + return [ + 'rate conversion with same admin and storefront rate' => [ + 'rate' => 1.00, + 'columnIndex' => 'total_income_amount', + 'catalogPriceScope' => 1, + 'adminWebsiteId' => 1, + 'adminCurrencyCode' => 'EUR', + 'storeCurrencyCode' => 'EUR', + 'adminOrderAmount' => 105.00, + 'convertedAmount' => 105.00 + ], + 'rate conversion with different admin and storefront rate' => [ + 'rate' => 1.4150, + 'columnIndex' => 'total_income_amount', + 'catalogPriceScope' => 1, + 'adminWebsiteId' => 1, + 'adminCurrencyCode' => 'USD', + 'storeCurrencyCode' => 'EUR', + 'adminOrderAmount' => 105.00, + 'convertedAmount' => 148.575 + ] + ]; + } + + protected function tearDown(): void + { + unset($this->scopeConfigMock); + unset($this->storeManagerMock); + unset($this->currencyLocatorMock); + unset($this->localeCurrencyMock); + unset($this->websiteMock); + unset($this->storeMock); + unset($this->currencyMock); + unset($this->backendCurrencyMock); + unset($this->gridColumnMock); + } +} diff --git a/app/code/Magento/Reports/composer.json b/app/code/Magento/Reports/composer.json index f1fe6c1e2c83a..df535ae28b135 100644 --- a/app/code/Magento/Reports/composer.json +++ b/app/code/Magento/Reports/composer.json @@ -22,7 +22,8 @@ "magento/module-store": "*", "magento/module-tax": "*", "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/module-wishlist": "*", + "magento/module-directory": "*" }, "type": "magento2-module", "license": [ From efc313151eb866c1e981fb05f9941969a19fadea Mon Sep 17 00:00:00 2001 From: Munkh-Ulzii Balidar <mbalidar@comwrap.com> Date: Thu, 10 Sep 2020 15:19:04 +0200 Subject: [PATCH 0717/1013] 29251 Configurable Options Selection :: New GraphQL Query #29251 29978 add code refactoring 29978 add more code refactoring 29251 fix test issues 29251 fix static test 29251 add code refactoring 29251 add backward compatible class 29251 add refactoring, fix media gallery 29251 fix static test 29251 fix empty options 29251 fix media gallery 29251 fix web-api test 29251 add debug in failing web-api test 29251 remove debug 29251 add debug in failing web-api test 29251 add debug in failing web-api test 29251 add variant data provider 29251 clean up code 29251 clean up code --- .../Model/Options/Collection.php | 78 +++- .../Model/Options/DataProvider/Variant.php | 67 +++ .../Model/Options/Metadata.php | 173 ++++++++ .../Model/Options/SelectionUidFormatter.php | 60 +++ .../Resolver/OptionsSelectionMetadata.php | 49 +++ .../Model/Resolver/SelectionMediaGallery.php | 45 ++ .../Model/Resolver/Variant/Variant.php | 34 ++ .../ConfigurableProductGraphQl/composer.json | 2 + .../etc/graphql/di.xml | 8 + .../ConfigurableProductGraphQl/etc/module.xml | 2 + .../etc/schema.graphqls | 14 + ...nfigurableOptionsSelectionMetadataTest.php | 410 ++++++++++++++++++ ...oducts_with_two_attributes_combination.php | 188 ++++++++ ...th_two_attributes_combination_rollback.php | 84 ++++ 14 files changed, 1213 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php create mode 100644 app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Variant.php create mode 100644 dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination_rollback.php diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index 5e3666407a383..c5c66a194503a 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -10,10 +10,14 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Model\ProductRepository; +use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection as AttributeCollection; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; /** @@ -31,11 +35,36 @@ class Collection */ private $productFactory; + /** + * @var ProductRepository + */ + private $productRepository; + /** * @var MetadataPool */ private $metadataPool; + /** + * @var Data + */ + private $configurableProductHelper; + + /** + * @var Metadata + */ + private $optionsMetadata; + + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + /** * @var int[] */ @@ -49,16 +78,32 @@ class Collection /** * @param CollectionFactory $attributeCollectionFactory * @param ProductFactory $productFactory + * @param ProductRepository $productRepository * @param MetadataPool $metadataPool + * @param Data $configurableProductHelper + * @param Metadata $optionsMetadata + * @param SelectionUidFormatter $selectionUidFormatter + * @param SearchCriteriaBuilder $searchCriteriaBuilder */ public function __construct( CollectionFactory $attributeCollectionFactory, ProductFactory $productFactory, - MetadataPool $metadataPool + ProductRepository $productRepository, + MetadataPool $metadataPool, + Data $configurableProductHelper, + Metadata $optionsMetadata, + SelectionUidFormatter $selectionUidFormatter, + SearchCriteriaBuilder $searchCriteriaBuilder ) { $this->attributeCollectionFactory = $attributeCollectionFactory; $this->productFactory = $productFactory; + $this->productRepository = $productRepository; $this->metadataPool = $metadataPool; + $this->configurableProductHelper = $configurableProductHelper; + $this->optionsMetadata = $optionsMetadata; + $this->selectionUidFormatter = $selectionUidFormatter; + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?? + ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -111,6 +156,8 @@ private function fetch() : array $attributeCollection->setProductFilter($product); } + $products = $this->getProducts($this->productIds); + /** @var Attribute $attribute */ foreach ($attributeCollection->getItems() as $attribute) { $productId = (int)$attribute->getProductId(); @@ -128,8 +175,37 @@ private function fetch() : array $this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options']; $this->attributeMap[$productId][$attribute->getId()]['label'] = $attribute->getProductAttribute()->getStoreLabel(); + + if (isset($products[$productId])) { + $options = $this->configurableProductHelper->getOptions( + $products[$productId], + $this->optionsMetadata->getAllowProducts($products[$productId]) + ); + foreach ($attributeData['options'] as $index => $value) { + $this->attributeMap[$productId][$attribute->getId()]['values'][$index]['uid'] + = $this->selectionUidFormatter->encode((int)$attribute->getId(), (int)$value['value_index']); + $this->attributeMap[$productId][$attribute->getId()]['values'][$index] + ['is_available_for_selection'] = + isset($options[$attribute['attribute_id']][$value['value_index']]) + && $options[$attribute['attribute_id']][$value['value_index']]; + } + } } return $this->attributeMap; } + + /** + * Load products by entity ids + * + * @param int[] $productIds + * @return ProductInterface[] + */ + private function getProducts($productIds) + { + $this->searchCriteriaBuilder->addFilter('entity_id', $productIds, 'in'); + $searchCriteria = $this->searchCriteriaBuilder->create(); + $products = $this->productRepository->getList($searchCriteria)->getItems(); + return $products; + } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.php new file mode 100644 index 0000000000000..80fbdc76bacb3 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/DataProvider/Variant.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\Options\DataProvider; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Model\ResourceModel\Stock\StatusFactory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; + +/** + * Retrieve child products + */ +class Variant +{ + /** + * @var Configurable + */ + private $configurableType; + + /** + * @var StatusFactory + */ + private $stockStatusFactory; + + /** + * @param Configurable $configurableType + * @param StatusFactory $stockStatusFactory + */ + public function __construct( + Configurable $configurableType, + StatusFactory $stockStatusFactory + ) { + $this->configurableType = $configurableType; + $this->stockStatusFactory = $stockStatusFactory; + } + + /** + * Load available child products by parent + * + * @param ProductInterface $product + * @return ProductInterface[] + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function getSalableVariantsByParent(ProductInterface $product) + { + $collection = $this->configurableType->getUsedProductCollection($product); + $collection + ->addAttributeToSelect('*') + ->addFilterByRequiredOptions(); + $collection->addMediaGalleryData(); + $collection->addTierPriceData(); + + $stockFlag = 'has_stock_status_filter'; + if (!$collection->hasFlag($stockFlag)) { + $stockStatusResource = $this->stockStatusFactory->create(); + $stockStatusResource->addStockDataToCollection($collection, true); + $collection->setFlag($stockFlag, true); + } + $collection->clear(); + + return $collection->getItems(); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php new file mode 100644 index 0000000000000..9fa6e4f23fa56 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Metadata.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Options; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\ConfigurableProduct\Helper\Data; +use Magento\ConfigurableProductGraphQl\Model\Options\DataProvider\Variant; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Retrieve metadata for configurable option selection. + */ +class Metadata +{ + /** + * @var Data + */ + private $configurableProductHelper; + + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var Variant + */ + private $variant; + + /** + * @param Data $configurableProductHelper + * @param SelectionUidFormatter $selectionUidFormatter + * @param ProductRepositoryInterface $productRepository + * @param Variant $variant + */ + public function __construct( + Data $configurableProductHelper, + SelectionUidFormatter $selectionUidFormatter, + ProductRepositoryInterface $productRepository, + Variant $variant + ) { + $this->configurableProductHelper = $configurableProductHelper; + $this->selectionUidFormatter = $selectionUidFormatter; + $this->productRepository = $productRepository; + $this->variant = $variant; + } + + /** + * Load available selections from configurable options. + * + * @param ProductInterface $product + * @param array $selectedOptionsUid + * @return array + * @throws NoSuchEntityException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getAvailableSelections( + ProductInterface $product, + array $selectedOptionsUid + ): array { + $options = $this->configurableProductHelper->getOptions($product, $this->getAllowProducts($product)); + $selectedOptions = $this->selectionUidFormatter->extract($selectedOptionsUid); + $attributeCodes = $this->getAttributeCodes($product); + $availableSelections = $availableProducts = $variantData = []; + + if (isset($options['index']) && $options['index']) { + foreach ($options['index'] as $productId => $productOptions) { + if (!empty($selectedOptions) && !$this->hasProductRequiredOptions($selectedOptions, $productOptions)) { + continue; + } + + $availableProducts[] = $productId; + foreach ($productOptions as $attributeId => $optionIndex) { + $uid = $this->selectionUidFormatter->encode($attributeId, (int)$optionIndex); + + if (isset($availableSelections[$attributeId]['option_value_uids']) + && in_array($uid, $availableSelections[$attributeId]['option_value_uids']) + ) { + continue; + } + $availableSelections[$attributeId]['option_value_uids'][] = $uid; + $availableSelections[$attributeId]['attribute_code'] = $attributeCodes[$attributeId]; + } + + if ($this->hasSelectionProduct($selectedOptions, $productOptions)) { + $variantProduct = $this->productRepository->getById($productId); + $variantData = $variantProduct->getData(); + $variantData['model'] = $variantProduct; + } + } + } + + return [ + 'options_available_for_selection' => $availableSelections, + 'variant' => $variantData, + 'availableSelectionProducts' => array_unique($availableProducts), + 'product' => $product + ]; + } + + /** + * Get allowed products. + * + * @param ProductInterface $product + * @return ProductInterface[] + */ + public function getAllowProducts(ProductInterface $product): array + { + return $this->variant->getSalableVariantsByParent($product) ?? []; + } + + /** + * Check if a product has the selected options. + * + * @param array $requiredOptions + * @param array $productOptions + * @return bool + */ + private function hasProductRequiredOptions($requiredOptions, $productOptions): bool + { + $result = true; + foreach ($requiredOptions as $attributeId => $optionIndex) { + if (!isset($productOptions[$attributeId]) || !$productOptions[$attributeId] + || $optionIndex != $productOptions[$attributeId] + ) { + $result = false; + break; + } + } + + return $result; + } + + /** + * Check if selected options match a product. + * + * @param array $requiredOptions + * @param array $productOptions + * @return bool + */ + private function hasSelectionProduct($requiredOptions, $productOptions): bool + { + return $this->hasProductRequiredOptions($productOptions, $requiredOptions); + } + + /** + * Retrieve attribute codes + * + * @param ProductInterface $product + * @return string[] + */ + private function getAttributeCodes(ProductInterface $product): array + { + $allowedAttributes = $this->configurableProductHelper->getAllowAttributes($product); + $attributeCodes = []; + foreach ($allowedAttributes as $attribute) { + $attributeCodes[$attribute->getAttributeId()] = $attribute->getProductAttribute()->getAttributeCode(); + } + + return $attributeCodes; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php new file mode 100644 index 0000000000000..1d13ad75489a1 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/SelectionUidFormatter.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProductGraphQl\Model\Options; + +/** + * Handle option selection uid. + */ +class SelectionUidFormatter +{ + /** + * Prefix of uid for encoding + */ + private const UID_PREFIX = 'configurable'; + + /** + * Separator of uid for encoding + */ + private const UID_SEPARATOR = '/'; + + /** + * Create uid and encode. + * + * @param int $attributeId + * @param int $indexId + * @return string + */ + public function encode(int $attributeId, int $indexId): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode(implode(self::UID_SEPARATOR, [ + self::UID_PREFIX, + $attributeId, + $indexId + ])); + } + + /** + * Retrieve attribute and option index from uid. Array key is the id of attribute and value is the index of option + * + * @param string $selectionUids + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function extract(array $selectionUids): array + { + $attributeOption = []; + foreach ($selectionUids as $uid) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = explode(self::UID_SEPARATOR, base64_decode($uid)); + if (count($optionData) == 3) { + $attributeOption[(int)$optionData[1]] = (int)$optionData[2]; + } + } + + return $attributeOption; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php new file mode 100644 index 0000000000000..f7d5a96ad2aba --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/OptionsSelectionMetadata.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProductGraphQl\Model\Resolver; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\ConfigurableProductGraphQl\Model\Options\Metadata; +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; + +/** + * Resolver class for option selection metadata. + */ +class OptionsSelectionMetadata implements ResolverInterface +{ + /** + * @var Metadata + */ + private $configurableSelectionMetadata; + + /** + * @param Metadata $configurableSelectionMetadata + */ + public function __construct( + Metadata $configurableSelectionMetadata + ) { + $this->configurableSelectionMetadata = $configurableSelectionMetadata; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + + $selectedOptions = $args['selectedConfigurableOptionValues'] ?? []; + /** @var ProductInterface $product */ + $product = $value['model']; + + return $this->configurableSelectionMetadata->getAvailableSelections($product, $selectedOptions); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php new file mode 100644 index 0000000000000..7b3ddc4ac1417 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/SelectionMediaGallery.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProductGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver class for media gallery of child products. + */ +class SelectionMediaGallery implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['product']) || !$value['product']) { + return null; + } + + $product = $value['product']; + $availableSelectionProducts = $value['availableSelectionProducts']; + $mediaGalleryEntries = []; + $usedProducts = $product->getTypeInstance()->getUsedProducts($product, null); + foreach ($usedProducts as $usedProduct) { + if (in_array($usedProduct->getId(), $availableSelectionProducts)) { + foreach ($usedProduct->getMediaGalleryEntries() ?? [] as $key => $entry) { + $index = $usedProduct->getId() . '_' . $key; + $mediaGalleryEntries[$index] = $entry->getData(); + $mediaGalleryEntries[$index]['model'] = $usedProduct; + if ($entry->getExtensionAttributes() && $entry->getExtensionAttributes()->getVideoContent()) { + $mediaGalleryEntries[$index]['video_content'] + = $entry->getExtensionAttributes()->getVideoContent()->getData(); + } + } + } + } + return $mediaGalleryEntries; + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Variant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Variant.php new file mode 100644 index 0000000000000..625c31a2680c8 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Variant.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Variant; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Resolver class for product variant. + */ +class Variant implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (isset($value['variant']['model']) && $value['variant']['model']) { + return + array_merge( + $value['variant']['model']->getData(), + [ + 'model' => $value['variant']['model'] + ] + ); + } else { + return null; + } + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index 295efb65b1978..73e134e1522a4 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -6,10 +6,12 @@ "php": "~7.3.0||~7.4.0", "magento/module-catalog": "*", "magento/module-configurable-product": "*", + "magento/module-eav": "*", "magento/module-graph-ql": "*", "magento/module-catalog-graph-ql": "*", "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", + "magento/module-catalog-inventory": "*", "magento/framework": "*" }, "license": [ diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index 808ca62f7e149..dc672b02e2f96 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -43,4 +43,12 @@ </argument> </arguments> </type> + + <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> + <plugin name="used_products_cache_graphql" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> + </type> + + <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> + <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder_GraphQl" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> + </type> </config> diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml index f249a417f1046..36b6fd40eea15 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml @@ -10,8 +10,10 @@ <sequence> <module name="Magento_Catalog"/> <module name="Magento_ConfigurableProduct"/> + <module name="Magento_Eav"/> <module name="Magento_GraphQl"/> <module name="Magento_CatalogGraphQl"/> + <module name="Magento_CatalogInventory"/> <module name="Magento_QuoteGraphQl"/> </sequence> </module> diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 257bca11fb5b7..88d0f8e212acf 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -7,6 +7,7 @@ type Mutation { type ConfigurableProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "ConfigurableProduct defines basic features of a configurable product and its simple product variants") { variants: [ConfigurableVariant] @doc(description: "An array of variants of products") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\ConfigurableVariant") configurable_options: [ConfigurableProductOptions] @doc(description: "An array of linked simple product items") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Options") + configurable_options_selection_metadata(selectedConfigurableOptionValues: [ID!]): ConfigurableOptionsSelectionMetadata @doc(description: "Metadata for the specified configurable options selection") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\OptionsSelectionMetadata") } type ConfigurableVariant @doc(description: "An array containing all the simple product variants of a configurable product") { @@ -34,6 +35,8 @@ type ConfigurableProductOptions @doc(description: "ConfigurableProductOptions de } type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOptionsValues contains the index number assigned to a configurable product option") { + uid: ID! + is_available_for_selection: Boolean! value_index: Int @doc(description: "A unique index number assigned to the configurable product option") label: String @doc(description: "The label of the product") default_label: String @doc(description: "The label of the product on the default store") @@ -73,3 +76,14 @@ type ConfigurableWishlistItem implements WishlistItemInterface @doc(description: child_sku: String! @doc(description: "The SKU of the simple product corresponding to a set of selected configurable options") @resolver(class: "\\Magento\\ConfigurableProductGraphQl\\Model\\Wishlist\\ChildSku") configurable_options: [SelectedConfigurableOption!] @resolver(class: "\\Magento\\ConfigurableProductGraphQl\\Model\\Wishlist\\ConfigurableOptions") @doc (description: "An array of selected configurable options") } + +type ConfigurableOptionsSelectionMetadata @doc(description: "Metadata corresponding to the configurable options selection.") { + options_available_for_selection: [ConfigurableOptionAvailableForSelection!] @doc(description: "Configurable options available for further selection based on current selection.") + media_gallery: [MediaGalleryInterface!] @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\SelectionMediaGallery") @doc(description: "Product images and videos corresponding to the specified configurable options selection.") + variant: SimpleProduct @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Variant") @doc(description: "Variant represented by the specified configurable options selection. It is expected to be null, until selections are made for each configurable option.") +} + +type ConfigurableOptionAvailableForSelection @doc(description: "Configurable option available for further selection based on current selection.") { + option_value_uids: [ID!]! @doc(description: "Configurable option values available for further selection.") + attribute_code: String! @doc(description: "Attribute code that uniquely identifies configurable option.") +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php new file mode 100644 index 0000000000000..f0e4df50794a3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/ConfigurableOptionsSelectionMetadataTest.php @@ -0,0 +1,410 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Magento\ConfigurableProductGraphQl\Model\Options\SelectionUidFormatter; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test configurable product option selection. + */ +class ConfigurableOptionsSelectionMetadataTest extends GraphQlAbstract +{ + /** + * @var AttributeRepository + */ + private $attributeRepository; + + /** + * @var SelectionUidFormatter + */ + private $selectionUidFormatter; + + private $firstConfigurableAttribute = null; + + private $secondConfigurableAttribute = null; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepository::class); + $this->selectionUidFormatter = Bootstrap::getObjectManager()->create(SelectionUidFormatter::class); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testWithoutSelectedOption() + { + $query = <<<QUERY +{ + products(filter:{ + sku: {eq: "configurable_12345"} + }) + { + items + { + id + sku + name + description { + html + } + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: [] + ) { + options_available_for_selection { + option_value_uids + attribute_code + } + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(1, count($response['products']['items'])); + $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'])); + $this->assertEquals(4, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][0]['option_value_uids'])); + $this->assertEquals(4, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][1]['option_value_uids'])); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testSelectedFirstAttributeFirstOption() + { + $attribute = $this->getFirstConfigurableAttribute(); + $options = $attribute->getOptions(); + $firstOptionUid = $this->selectionUidFormatter->encode( + (int)$attribute->getAttributeId(), + (int)$options[1]->getValue() + ); + $query = <<<QUERY +{ + products(filter:{ + sku: {eq: "configurable_12345"} + }) + { + items + { + id + sku + name + description { + html + } + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["{$firstOptionUid}"] + ) { + options_available_for_selection { + option_value_uids + attribute_code + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertEquals(1, count($response['products']['items'])); + $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'])); + $this->assertEquals(1, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][0]['option_value_uids'])); + $this->assertEquals($firstOptionUid, $response['products']['items'][0] + ['configurable_options_selection_metadata']['options_available_for_selection'][0]['option_value_uids'][0]); + $this->assertEquals(4, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][1]['option_value_uids'])); + + $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); + $this->assertAvailableOptionUids( + $this->getSecondConfigurableAttribute()->getAttributeId(), + $secondAttributeOptions, + $response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][1]['option_value_uids'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testSelectedFirstAttributeLastOption() + { + $attribute = $this->getFirstConfigurableAttribute(); + $options = $attribute->getOptions(); + $lastOptionUid = $this->selectionUidFormatter->encode( + (int)$attribute->getAttributeId(), + (int)$options[4]->getValue() + ); + $query = <<<QUERY +{ + products(filter:{ + sku: {eq: "configurable_12345"} + }) + { + items + { + id + sku + name + description { + html + } + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["{$lastOptionUid}"] + ) { + options_available_for_selection { + option_value_uids + attribute_code + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlQuery($query); + $this->assertEquals(1, count($response['products']['items'])); + $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'])); + $this->assertEquals(1, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][0]['option_value_uids'])); + $this->assertEquals($lastOptionUid, $response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][0]['option_value_uids'][0]); + $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][1]['option_value_uids'])); + $secondAttributeOptions = $this->getSecondConfigurableAttribute()->getOptions(); + unset($secondAttributeOptions[0]); + unset($secondAttributeOptions[1]); + unset($secondAttributeOptions[2]); + $this->assertAvailableOptionUids( + $this->getSecondConfigurableAttribute()->getAttributeId(), + $secondAttributeOptions, + $response['products']['items'][0]['configurable_options_selection_metadata'] + ['options_available_for_selection'][1]['option_value_uids'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testSelectedVariant() + { + $firstAttribute = $this->getFirstConfigurableAttribute(); + $firstOptions = $firstAttribute->getOptions(); + $firstAttributeFirstOptionUid = $this->selectionUidFormatter->encode( + (int)$firstAttribute->getAttributeId(), + (int)$firstOptions[1]->getValue() + ); + $secodnAttribute = $this->getSecondConfigurableAttribute(); + $secondOptions = $secodnAttribute->getOptions(); + $secondAttributeFirstOptionUid = $this->selectionUidFormatter->encode( + (int)$secodnAttribute->getAttributeId(), + (int)$secondOptions[1]->getValue() + ); + $query = <<<QUERY +{ + products(filter:{ + sku: {eq: "configurable_12345"} + }) + { + items + { + id + sku + name + description { + html + } + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["{$firstAttributeFirstOptionUid}", "{$secondAttributeFirstOptionUid}"] + ) { + options_available_for_selection { + option_value_uids + } + variant { + id + sku + name + } + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(1, count($response['products']['items'])); + $this->assertNotNull($response['products']['items'][0]['configurable_options_selection_metadata'] + ['variant']); + $this->assertEquals( + 'simple_' . $firstOptions[1]->getValue() . '_' . $secondOptions[1]->getValue(), + $response['products']['items'][0]['configurable_options_selection_metadata'] + ['variant']['sku'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testMediaGalleryForAll() + { + $query = <<<QUERY +{ + products(filter:{ + sku: {eq: "configurable_12345"} + }) + { + items + { + id + sku + name + description { + html + } + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: [] + ) { + options_available_for_selection { + option_value_uids + attribute_code + } + media_gallery { + url + } + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(1, count($response['products']['items'])); + $this->assertEquals(14, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['media_gallery'])); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php + */ + public function testMediaGalleryWithSelection() + { + $attribute = $this->getFirstConfigurableAttribute(); + $options = $attribute->getOptions(); + $lastOptionUid = $this->selectionUidFormatter->encode( + (int)$attribute->getAttributeId(), + (int)$options[4]->getValue() + ); + $query = <<<QUERY +{ + products(filter:{ + sku: {eq: "configurable_12345"} + }) + { + items + { + id + sku + name + description { + html + } + ... on ConfigurableProduct { + configurable_options_selection_metadata( + selectedConfigurableOptionValues: ["$lastOptionUid"] + ) { + options_available_for_selection { + option_value_uids + attribute_code + } + media_gallery { + url + } + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $this->assertEquals(1, count($response['products']['items'])); + $this->assertEquals(2, count($response['products']['items'][0]['configurable_options_selection_metadata'] + ['media_gallery'])); + } + + /** + * Assert option uid. + * + * @param $attributeId + * @param $expectedOptions + * @param $selectedOptions + */ + private function assertAvailableOptionUids($attributeId, $expectedOptions, $selectedOptions) + { + unset($expectedOptions[0]); + foreach ($expectedOptions as $option) { + $this->assertContains( + $this->selectionUidFormatter->encode((int)$attributeId, (int)$option->getValue()), + $selectedOptions + ); + } + } + + /** + * Get first configurable attribute. + * + * @return AttributeInterface + * @throws NoSuchEntityException + */ + private function getFirstConfigurableAttribute() + { + if (!$this->firstConfigurableAttribute) { + $attributeCode = 'test_configurable_first'; + $this->firstConfigurableAttribute = $this->attributeRepository->get('catalog_product', $attributeCode); + } + + return $this->firstConfigurableAttribute; + } + + /** + * Get second configurable attribute. + * + * @return AttributeInterface + * @throws NoSuchEntityException + */ + private function getSecondConfigurableAttribute() + { + if (!$this->secondConfigurableAttribute) { + $attributeCode = 'test_configurable_second'; + $this->secondConfigurableAttribute = $this->attributeRepository->get('catalog_product', $attributeCode); + } + + return $this->secondConfigurableAttribute; + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php new file mode 100644 index 0000000000000..24e6010275bac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +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\Framework\Api\Data\ImageContentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_first.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_second.php' +); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager() + ->get(ProductRepositoryInterface::class); + +/** @var $installer CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); + +/** @var \Magento\Eav\Model\Config $eavConfig */ +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$firstAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first'); +$secondAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_second'); + +/* Create simple products per each option value*/ +/** @var AttributeOptionInterface[] $firstAttributeOptions */ +$firstAttributeOptions = $firstAttribute->getOptions(); +/** @var AttributeOptionInterface[] $secondAttributeOptions */ +$secondAttributeOptions = $secondAttribute->getOptions(); + +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); +$associatedProductIds = []; +$firstAttributeValues = []; +$secondAttributeValues = []; +$testImagePath = __DIR__ . '/magento_image.jpg'; + +array_shift($firstAttributeOptions); +array_shift($secondAttributeOptions); +foreach ($firstAttributeOptions as $i => $firstAttributeOption) { + $firstAttributeValues[] = [ + 'label' => 'test first ' . $firstAttributeOption->getValue(), + 'attribute_id' => $firstAttribute->getId(), + 'value_index' => $firstAttributeOption->getValue(), + ]; + foreach ($secondAttributeOptions as $j => $secondAttributeOption) { + if ($i == 3 && in_array($j, [0, 1])) { + $qty = 0; + $isInStock = 0; + } else { + $qty = 100; + $isInStock = 1; + } + $product = Bootstrap::getObjectManager()->create(Product::class); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName( + 'Configurable Option ' . $firstAttributeOption->getLabel() . '-' . $secondAttributeOption->getLabel() + ) + ->setSku('simple_' . $firstAttributeOption->getValue() . '_' . $secondAttributeOption->getValue()) + ->setPrice($firstAttributeOption->getValue() + $secondAttributeOption->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + ['use_config_manage_stock' => 1, 'qty' => $qty, 'is_qty_decimal' => 0, 'is_in_stock' => $isInStock] + ) + ->setImage('/m/a/magento_image.jpg') + ->setSmallImage('/m/a/magento_image.jpg') + ->setThumbnail('/m/a/magento_image.jpg') + ->setData( + 'media_gallery', + [ + 'images' => [ + [ + 'file' => '/m/a/magento_image.jpg', + 'position' => 1, + 'label' => 'Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image', + 'content' => [ + 'data' => [ + ImageContentInterface::BASE64_ENCODED_DATA => base64_encode( + file_get_contents($testImagePath) + ), + ImageContentInterface::NAME => 'simple_' . $firstAttributeOption->getValue() . + '_' . $secondAttributeOption->getValue() . "_1.jpg", + ImageContentInterface::TYPE => "image/jpeg" + ] + ] + ], + ] + ] + ); + $customAttributes = [ + $firstAttribute->getAttributeCode() => $firstAttributeOption->getValue(), + $secondAttribute->getAttributeCode() => $secondAttributeOption->getValue() + ]; + foreach ($customAttributes as $attributeCode => $attributeValue) { + $product->setCustomAttributes($customAttributes); + } + $product = $productRepository->save($product); + $associatedProductIds[] = $product->getId(); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(Item::class); + $stockItem->load($product->getId(), 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($product->getId()); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty($qty); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock($isInStock); + $stockItem->save(); + + $secondAttributeValues[$j] = [ + 'label' => 'test second ' . $firstAttributeOption->getValue() . $secondAttributeOption->getValue(), + 'attribute_id' => $secondAttribute->getId(), + 'value_index' => $secondAttributeOption->getValue(), + ]; + } + +} + +$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor->reindexList($associatedProductIds, true); + +/** @var $product Product */ +$product = Bootstrap::getObjectManager()->create(Product::class); + +/** @var Factory $optionsFactory */ +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); + +$configurableAttributesData = [ + [ + 'attribute_id' => $firstAttribute->getId(), + 'code' => $firstAttribute->getAttributeCode(), + 'label' => $firstAttribute->getStoreLabel(), + 'position' => '0', + 'values' => $firstAttributeValues, + ], + [ + 'attribute_id' => $secondAttribute->getId(), + 'code' => $secondAttribute->getAttributeCode(), + 'label' => $secondAttribute->getStoreLabel(), + 'position' => '1', + 'values' => $secondAttributeValues, + ], +]; + +$configurableOptions = $optionsFactory->create($configurableAttributesData); +$firstAttributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); + +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); + +$product->setExtensionAttributes($extensionConfigurableAttributes); + +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($firstAttributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product 12345') + ->setSku('configurable_12345') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); +$productRepository->cleanCache(); +$product = $productRepository->save($product); + +$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor->reindexRow($product->getId(), true); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination_rollback.php new file mode 100644 index 0000000000000..b5527b8484a19 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products_with_two_attributes_combination_rollback.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Setup\CategorySetup; +use Magento\CatalogInventory\Model\Stock\Status; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$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); + +/** @var $installer CategorySetup */ +$installer = $objectManager->create(CategorySetup::class); + +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); +$firstAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first'); +$secondAttribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_second'); + +/** @var AttributeOptionInterface[] $firstAttributeOptions */ +$firstAttributeOptions = $firstAttribute->getOptions(); +/** @var AttributeOptionInterface[] $secondAttributeOptions */ +$secondAttributeOptions = $secondAttribute->getOptions(); + +array_shift($firstAttributeOptions); +array_shift($secondAttributeOptions); +foreach ($firstAttributeOptions as $i => $firstAttributeOption) { + foreach ($secondAttributeOptions as $j => $secondAttributeOption) { + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $objectManager->get(ProductRepositoryInterface::class); + try { + //delete child product + $sku = 'simple_' . $firstAttributeOption->getValue() . '_' . $secondAttributeOption->getValue(); + $product = $productRepository->get($sku, true); + $stockStatus = $objectManager->create(Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + $productRepository->delete($product); + } catch (NoSuchEntityException $e) { + //Product already removed + } + } +} + +//delete configurable product +try { + $product = $productRepository->get('configurable_12345', true); + $stockStatus = $objectManager->create(Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //Product already removed +} +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_second_rollback.php' +); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Catalog/_files/product_image_rollback.php' +); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From d8023289262ecadfd3e2dbbe6ac3b5d012f5123a Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Mon, 5 Oct 2020 23:07:07 +0200 Subject: [PATCH 0718/1013] Another fix after CR --- .../Config/QueueConfigItem/DataMapperTest.php | 4 ++-- .../Topology/Config/QueueConfigItem/DataMapper.php | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php index 62581ad13a84b..46ea82b887db6 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php @@ -1,12 +1,12 @@ <?php -declare(strict_types=1); - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\MessageQueue\Test\Unit\Topology\Config\QueueConfigItem; use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php index d48fa637fd885..912aa4a6b0fb1 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php @@ -1,11 +1,12 @@ <?php -declare(strict_types=1); - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\MessageQueue\Topology\Config\QueueConfigItem; use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; @@ -67,7 +68,7 @@ public function __construct( public function getMappedData(): array { if (null === $this->mappedData) { - $this->mappedData = []; + $mappedData = []; foreach ($this->configData->get() as $exchange) { $connection = $exchange['connection']; foreach ($exchange['bindings'] as $binding) { @@ -78,10 +79,11 @@ public function getMappedData(): array (array)$binding['arguments'], (string)$connection ); - $this->mappedData = array_merge($this->mappedData, $queueItems); + $mappedData[] = $queueItems; } } } + $this->mappedData = array_merge([], ...$mappedData); } return $this->mappedData; @@ -158,7 +160,7 @@ private function matchSynchronousTopics(string $wildcard): array $topicDefinitions = array_filter( $this->communicationConfig->getTopics(), function ($item) { - return (bool) $item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + return (bool)$item[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; } ); From bf014eb0e19b5f2f4a977aea3af9cb21e136fb1c Mon Sep 17 00:00:00 2001 From: Munkh-Ulzii Balidar <mbalidar@comwrap.com> Date: Tue, 6 Oct 2020 23:03:27 +0200 Subject: [PATCH 0719/1013] 29251 remove redundant dependency --- app/code/Magento/ConfigurableProductGraphQl/composer.json | 1 - app/code/Magento/ConfigurableProductGraphQl/etc/module.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index 73e134e1522a4..a6e1d1c822435 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -6,7 +6,6 @@ "php": "~7.3.0||~7.4.0", "magento/module-catalog": "*", "magento/module-configurable-product": "*", - "magento/module-eav": "*", "magento/module-graph-ql": "*", "magento/module-catalog-graph-ql": "*", "magento/module-quote": "*", diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml index 36b6fd40eea15..3aa1658c9388d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/module.xml @@ -10,7 +10,6 @@ <sequence> <module name="Magento_Catalog"/> <module name="Magento_ConfigurableProduct"/> - <module name="Magento_Eav"/> <module name="Magento_GraphQl"/> <module name="Magento_CatalogGraphQl"/> <module name="Magento_CatalogInventory"/> From e15188899de53d1794b094eb37dad4995a16af0c Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Wed, 7 Oct 2020 10:34:45 +0300 Subject: [PATCH 0720/1013] MC-37100: Create automated test for "Create customer, when admin interface locale is en_GB" --- .../Controller/Adminhtml/Index/SaveTest.php | 63 +++++++++++++++++- .../Form/Element/DataType/DateTest.php | 65 +++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Ui/Component/Form/Element/DataType/DateTest.php diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php index 2ec87f758b812..375a1d493b20a 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php @@ -10,10 +10,12 @@ use Magento\Backend\Model\Session; use Magento\Customer\Api\CustomerNameGenerationInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Data\Customer as CustomerData; use Magento\Customer\Model\EmailNotification; use Magento\Framework\App\Area; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Mail\Template\TransportBuilder; use Magento\Framework\Mail\TransportInterface; use Magento\Framework\Message\MessageInterface; @@ -54,6 +56,12 @@ class SaveTest extends AbstractBackendController /** @var StoreManagerInterface */ private $storeManager; + /** @var ResolverInterface */ + private $localeResolver; + + /** @var CustomerInterface */ + private $customer; + /** * @inheritdoc */ @@ -65,6 +73,19 @@ protected function setUp(): void $this->subscriberFactory = $this->_objectManager->get(SubscriberFactory::class); $this->session = $this->_objectManager->get(Session::class); $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->localeResolver = $this->_objectManager->get(ResolverInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->customer instanceof CustomerInterface) { + $this->customerRepository->delete($this->customer); + } + + parent::tearDown(); } /** @@ -418,6 +439,43 @@ public function testCreateSameEmailFormatDateError(): void $this->assertRedirect($this->stringContains($this->baseControllerUrl . 'new/key/')); } + /** + * @return void + */ + public function testCreateCustomerByAdminWithLocaleGB(): void + { + $this->localeResolver->setLocale('en_GB'); + $postData = array_replace_recursive( + $this->getDefaultCustomerData(), + [ + 'customer' => [ + CustomerData::DOB => '24/10/1990', + ], + ] + ); + $expectedData = array_replace_recursive( + $postData, + [ + 'customer' => [ + CustomerData::DOB => '1990-10-24', + CustomerData::GENDER => '0', + ], + ] + ); + unset($expectedData['customer']['sendemail_store_id']); + $this->dispatchCustomerSave($postData); + $this->assertSessionMessages( + $this->equalTo([(string)__('You saved the customer.')]), + MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains($this->baseControllerUrl . 'index/key/')); + $this->assertCustomerData( + $postData['customer'][CustomerData::EMAIL], + (int)$postData['customer'][CustomerData::WEBSITE_ID], + $expectedData + ); + } + /** * Default values for customer creation * @@ -496,9 +554,8 @@ private function assertCustomerData( int $customerWebsiteId, array $expectedData ): void { - /** @var CustomerData $customerData */ - $customerData = $this->customerRepository->get($customerEmail, $customerWebsiteId); - $actualCustomerArray = $customerData->__toArray(); + $this->customer = $this->customerRepository->get($customerEmail, $customerWebsiteId); + $actualCustomerArray = $this->customer->__toArray(); foreach ($expectedData['customer'] as $key => $expectedValue) { $this->assertEquals( $expectedValue, diff --git a/dev/tests/integration/testsuite/Magento/Ui/Component/Form/Element/DataType/DateTest.php b/dev/tests/integration/testsuite/Magento/Ui/Component/Form/Element/DataType/DateTest.php new file mode 100644 index 0000000000000..779c9c955a62e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Component/Form/Element/DataType/DateTest.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Ui\Component\Form\Element\DataType; + +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for date component. + */ +class DateTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var DateFactory */ + private $dateFactory; + + /** @var ResolverInterface */ + private $localeResolver; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->dateFactory = $this->objectManager->get(DateFactory::class); + $this->localeResolver = $this->objectManager->get(ResolverInterface::class); + } + + /** + * @dataProvider localeDataProvider + * + * @param string $locale + * @param string $dateFormat + * @return void + */ + public function testDateFormat(string $locale, string $dateFormat): void + { + $this->localeResolver->setLocale($locale); + $date = $this->dateFactory->create(); + $date->prepare(); + $this->assertEquals($dateFormat, $date->getData('config')['options']['dateFormat']); + } + + /** + * @return array + */ + public function localeDataProvider(): array + { + return [ + ['en_GB', 'dd/MM/y'], ['en_US', 'M/d/yy'], + ]; + } +} From ccc83b21a25aee282be830891b5708017e992211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szubert?= <bartlomiejszubert@gmail.com> Date: Wed, 7 Oct 2020 10:03:28 +0200 Subject: [PATCH 0721/1013] Fix #30296 - Wrong ip value in sendfriend_log table --- .../Model/ResourceModel/SendFriend.php | 10 +++--- .../SendFriend/Model/SendFriendTest.php | 9 +++-- .../HTTP/PhpEnvironment/RemoteAddress.php | 10 +++--- .../Unit/PhpEnvironment/RemoteAddressTest.php | 33 +++++++------------ 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php index 618d941f7047e..468fb2ed1af66 100644 --- a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php @@ -6,10 +6,6 @@ namespace Magento\SendFriend\Model\ResourceModel; /** - * SendFriend Log Resource Model - * - * @author Magento Core Team <core@magentocommerce.com> - * * @api * @since 100.0.2 */ @@ -32,6 +28,7 @@ protected function _construct() * @param int $ip * @param int $startTime * @param int $websiteId + * * @return int * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -58,14 +55,16 @@ public function getSendCount($object, $ip, $startTime, $websiteId = null) * @param int $ip * @param int $startTime * @param int $websiteId + * * @return $this */ public function addSendItem($ip, $startTime, $websiteId) { $this->getConnection()->insert( $this->getMainTable(), - ['ip' => ip2long($ip), 'time' => $startTime, 'website_id' => $websiteId] + ['ip' => $ip, 'time' => $startTime, 'website_id' => $websiteId] ); + return $this; } @@ -73,6 +72,7 @@ public function addSendItem($ip, $startTime, $websiteId) * Delete Old logs * * @param int $time + * * @return $this */ public function deleteLogsBefore($time) diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php index 7013346fd76e2..9117f088e6b8d 100644 --- a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php @@ -13,7 +13,7 @@ use Magento\SendFriend\Helper\Data as SendFriendHelper; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; -use Zend\Stdlib\Parameters; +use Laminas\Stdlib\Parameters; /** * Class checks send friend model behavior @@ -55,6 +55,7 @@ protected function setUp(): void * @param array $sender * @param array $recipients * @param string|bool $expectedResult + * * @return void */ public function testValidate(array $sender, array $recipients, $expectedResult): void @@ -185,11 +186,11 @@ public function testisExceedLimitByCookies(): void * @magentoDataFixture Magento/SendFriend/_files/sendfriend_log_record_half_hour_before.php * * @magentoDbIsolation disabled + * * @return void */ public function testisExceedLimitByIp(): void { - $this->markTestSkipped('Blocked by MC-31968'); $parameters = $this->objectManager->create(Parameters::class); $parameters->set('REMOTE_ADDR', '127.0.0.1'); $this->request->setServer($parameters); @@ -197,10 +198,11 @@ public function testisExceedLimitByIp(): void } /** - * Check result + * Check test result * * @param array|bool $expectedResult * @param array|bool $result + * * @return void */ private function checkResult($expectedResult, $result): void @@ -217,6 +219,7 @@ private function checkResult($expectedResult, $result): void * * @param array $sender * @param array $recipients + * * @return void */ private function prepareData(array $sender, array $recipients): void diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php index dfe4b759e85be..c505c82789f81 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/RemoteAddress.php @@ -120,7 +120,7 @@ function (string $ip) { public function getRemoteAddress(bool $ipToLong = false) { if ($this->remoteAddress !== null) { - return $this->remoteAddress; + return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } $remoteAddress = $this->readAddress(); @@ -135,11 +135,11 @@ public function getRemoteAddress(bool $ipToLong = false) $this->remoteAddress = false; return false; - } else { - $this->remoteAddress = $remoteAddress; - - return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } + + $this->remoteAddress = $remoteAddress; + + return $ipToLong ? ip2long($this->remoteAddress) : $this->remoteAddress; } /** diff --git a/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php b/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php index 25f665ed70e84..20aafb797ce0e 100644 --- a/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php +++ b/lib/internal/Magento/Framework/HTTP/Test/Unit/PhpEnvironment/RemoteAddressTest.php @@ -9,13 +9,10 @@ use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test for - * * @see RemoteAddress */ class RemoteAddressTest extends TestCase @@ -23,24 +20,17 @@ class RemoteAddressTest extends TestCase /** * @var MockObject|HttpRequest */ - protected $_request; - - /** - * @var ObjectManager - */ - protected $_objectManager; + private $requestMock; /** * @inheritdoc */ protected function setUp(): void { - $this->_request = $this->getMockBuilder(HttpRequest::class) + $this->requestMock = $this->getMockBuilder(HttpRequest::class) ->disableOriginalConstructor() - ->setMethods(['getServer']) + ->onlyMethods(['getServer']) ->getMock(); - - $this->_objectManager = new ObjectManager($this); } /** @@ -49,6 +39,7 @@ protected function setUp(): void * @param string|bool $expected * @param bool $ipToLong * @param string[]|null $trustedProxies + * * @return void * @dataProvider getRemoteAddressProvider */ @@ -59,18 +50,16 @@ public function testGetRemoteAddress( bool $ipToLong, array $trustedProxies = null ): void { - $remoteAddress = $this->_objectManager->getObject( - RemoteAddress::class, - [ - 'httpRequest' => $this->_request, - 'alternativeHeaders' => $alternativeHeaders, - 'trustedProxies' => $trustedProxies, - ] + $remoteAddress = new RemoteAddress( + $this->requestMock, + $alternativeHeaders, + $trustedProxies ); - $this->_request->expects($this->any()) - ->method('getServer') + $this->requestMock->method('getServer') ->willReturnMap($serverValueMap); + // Check twice to verify if internal variable is cached correctly + $this->assertEquals($expected, $remoteAddress->getRemoteAddress($ipToLong)); $this->assertEquals($expected, $remoteAddress->getRemoteAddress($ipToLong)); } From 926ad62a1f953a0b06c49c0595957fe6d1d93670 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 7 Oct 2020 11:32:17 +0300 Subject: [PATCH 0722/1013] MC-37098: Create automated test for "Check functionality of RabbitMQ" --- ...ulkDetailModalSection.xml => AdminBulkDetailsModalSection.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/code/Magento/AsynchronousOperations/Test/Mftf/Section/{AdminBulkDetailModalSection.xml => AdminBulkDetailsModalSection.xml} (100%) diff --git a/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailModalSection.xml b/app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailsModalSection.xml similarity index 100% rename from app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailModalSection.xml rename to app/code/Magento/AsynchronousOperations/Test/Mftf/Section/AdminBulkDetailsModalSection.xml From e9e951d639b0f8eee1454385eaa24f628b4428c5 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Wed, 7 Oct 2020 11:36:45 +0300 Subject: [PATCH 0723/1013] MC-37102: Create automated test for "Create customer, with 2 websites and with different allowed countries" --- .../CreateAccountWithAddressTest.php | 113 ++++++++++++++++++ .../Model/Address/CreateAddressTest.php | 17 +++ 2 files changed, 130 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountWithAddressTest.php diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountWithAddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountWithAddressTest.php new file mode 100644 index 0000000000000..351c84680389b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagement/CreateAccountWithAddressTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\AccountManagement; + +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\AddressInterfaceFactory; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for creation customer with address via customer account management service. + * + * @magentoDbIsolation enabled + */ +class CreateAccountWithAddressTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var AccountManagementInterface */ + private $accountManagement; + + /** @var CustomerInterfaceFactory */ + private $customerFactory; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var AddressInterfaceFactory */ + private $addressFactory; + + /** @var CustomerInterface */ + private $customer; + + /** @var Registry */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->accountManagement = $this->objectManager->get(AccountManagementInterface::class); + $this->customerFactory = $this->objectManager->get(CustomerInterfaceFactory::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->addressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->customer instanceof CustomerInterface) { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($this->customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + } + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoConfigFixture default_store general/country/allow BD,BB,AF + * @magentoConfigFixture fixture_second_store_store general/country/allow AS,BM + * @return void + */ + public function testCreateNewCustomerWithAddress(): void + { + $availableCountry = 'BD'; + $address = $this->addressFactory->create(); + $address->setCountryId($availableCountry) + ->setPostcode('75477') + ->setRegionId(1) + ->setStreet(['Green str, 67']) + ->setTelephone('3468676') + ->setCity('CityM') + ->setFirstname('John') + ->setLastname('Smith') + ->setIsDefaultShipping(true) + ->setIsDefaultBilling(true); + $customerEntity = $this->customerFactory->create(); + $customerEntity->setEmail('test@example.com') + ->setFirstname('John') + ->setLastname('Smith') + ->setStoreId(1); + $customerEntity->setAddresses([$address]); + $this->customer = $this->accountManagement->createAccount($customerEntity); + $this->assertCount(1, $this->customer->getAddresses(), 'The available address wasn\'t saved.'); + $this->assertSame( + $availableCountry, + $this->customer->getAddresses()[0]->getCountryId(), + 'The address was saved with disallowed country.' + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php index eb638eeb329aa..79f8b1466d8d3 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Address/CreateAddressTest.php @@ -424,6 +424,23 @@ public function testAddressCreatedWithGroupAssignByVatIdWithError(): void $this->assertEquals(2, $this->getCustomerGroupId('customer5@example.com')); } + /** + * @magentoDataFixture Magento/Customer/_files/customer_no_address.php + * @magentoDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @magentoConfigFixture default_store general/country/allow BD,BB,AF + * @magentoConfigFixture fixture_second_store_store general/country/allow AS,BM + * + * @return void + */ + public function testCreateAvailableAddress(): void + { + $countryId = 'BB'; + $addressData = array_merge(self::STATIC_CUSTOMER_ADDRESS_DATA, [AddressInterface::COUNTRY_ID => $countryId]); + $customer = $this->customerRepository->get('customer5@example.com'); + $address = $this->createAddress((int)$customer->getId(), $addressData); + $this->assertSame($countryId, $address->getCountryId()); + } + /** * Create customer address with provided address data. * From cc51b773ebe44a252fc8889c3fe45dba29455b2c Mon Sep 17 00:00:00 2001 From: Namrata Vora <namrata@seepossible.com> Date: Wed, 7 Oct 2020 14:11:17 +0530 Subject: [PATCH 0724/1013] Added correct block class for frontend viewModel reference example. --- lib/internal/Magento/Framework/View/Element/Template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/View/Element/Template.php b/lib/internal/Magento/Framework/View/Element/Template.php index 53355203213fa..e9f164ca2fd14 100644 --- a/lib/internal/Magento/Framework/View/Element/Template.php +++ b/lib/internal/Magento/Framework/View/Element/Template.php @@ -19,7 +19,7 @@ * custom view models in block arguments in layout handle file. * * Example: - * <block name="my.block" class="Magento\Backend\Block\Template" template="My_Module::template.phtml" > + * <block name="my.block" class="Magento\Framework\View\Element\Template" template="My_Module::template.phtml" > * <arguments> * <argument name="viewModel" xsi:type="object">My\Module\ViewModel\Custom</argument> * </arguments> From ac843d37b7a9878f124f50c209a24aca997e40fd Mon Sep 17 00:00:00 2001 From: engcom-Kilo <mikola.malevanec@transoftgroup.com> Date: Wed, 7 Oct 2020 09:53:38 +0300 Subject: [PATCH 0725/1013] MC-37666: Incorrect Customer TAX Class saved with Quote when VAT Validation used on Guest orders. Fix unit test. --- .../Quote/Test/Unit/Model/QuoteManagementTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php index ea758f7ce34f3..4197af2f2848a 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteManagementTest.php @@ -247,6 +247,7 @@ protected function setUp(): void 'getPayment', 'setCheckoutMethod', 'setCustomerIsGuest', + 'getCustomer', 'getId' ] ) @@ -799,6 +800,12 @@ public function testPlaceOrderIfCustomerIsGuest() $this->quoteMock->expects($this->once()) ->method('getCheckoutMethod') ->willReturn(Onepage::METHOD_GUEST); + $customerMock = $this->getMockBuilder(Customer::class) + ->disableOriginalConstructor() + ->getMock(); + $this->quoteMock->expects($this->once()) + ->method('getCustomer') + ->willReturn($customerMock); $this->quoteMock->expects($this->once())->method('setCustomerId')->with(null)->willReturnSelf(); $this->quoteMock->expects($this->once())->method('setCustomerEmail')->with($email)->willReturnSelf(); @@ -866,6 +873,9 @@ public function testPlaceOrderIfCustomerIsGuest() $this->assertEquals($orderId, $service->placeOrder($cartId)); } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ public function testPlaceOrder() { $cartId = 323; From 284bacd472bd42edac83a7497a7335f921c58906 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Wed, 7 Oct 2020 12:27:58 +0300 Subject: [PATCH 0726/1013] MC-37493: Create automated test for "Create Invoice for Offline Payment Methods with Async Notification" --- .../Model/InvoiceEmailSenderHandlerTest.php | 147 +++++++-------- .../Order/Email/Sender/InvoiceSenderTest.php | 168 ++++++++++-------- .../_files/invoice_in_email_send_queue.php | 34 ++++ .../invoice_in_email_send_queue_rollback.php | 10 ++ 4 files changed, 219 insertions(+), 140 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php index 4e33dc398d7ad..0e3148e9ce4ac 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php @@ -7,97 +7,104 @@ namespace Magento\Sales\Model; -use Magento\Config\Model\Config; -use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Store\Model\ScopeInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceSearchResultInterface; +use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; +use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Spi\InvoiceResourceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use PHPUnit\Framework\TestCase; -class InvoiceEmailSenderHandlerTest extends \PHPUnit\Framework\TestCase +/** + * Checks sending emails to customers after creation/modification of invoice. + * + * @see \Magento\Sales\Model\EmailSenderHandler + */ +class InvoiceEmailSenderHandlerTest extends TestCase { - /** - * @var \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection - */ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var InvoiceSearchResultInterface */ private $entityCollection; + /** @var EmailSenderHandler */ + private $emailSenderHandler; + + /** @var InvoiceIdentity */ + private $invoiceIdentity; + + /** @var InvoiceSender */ + private $invoiceSender; + + /** @var InvoiceResourceInterface */ + private $entityResource; + + /** @var TransportBuilderMock */ + private $transportBuilderMock; + /** - * @var \Magento\Sales\Model\EmailSenderHandler + * @inheritdoc */ - private $emailSender; - protected function setUp(): void { - /** @var \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity $invoiceIdentity */ - $invoiceIdentity = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order\Email\Container\InvoiceIdentity::class - ); - /** @var \Magento\Sales\Model\Order\Email\Sender\InvoiceSender $invoiceSender */ - $invoiceSender = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create( - \Magento\Sales\Model\Order\Email\Sender\InvoiceSender::class, - [ - 'identityContainer' => $invoiceIdentity, - ] - ); - $entityResource = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Sales\Model\ResourceModel\Order\Invoice::class); - $this->entityCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\ResourceModel\Order\Invoice\Collection::class - ); - $this->emailSender = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\EmailSenderHandler::class, + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->invoiceIdentity = $this->objectManager->get(InvoiceIdentity::class); + $this->invoiceSender = $this->objectManager->get(InvoiceSender::class); + $this->entityResource = $this->objectManager->get(InvoiceResourceInterface::class); + $this->entityCollection = $this->objectManager->create(InvoiceSearchResultInterface::class); + $this->emailSenderHandler = $this->objectManager->create( + EmailSenderHandler::class, [ - 'emailSender' => $invoiceSender, - 'entityResource' => $entityResource, + 'emailSender' => $this->invoiceSender, + 'entityResource' => $this->entityResource, 'entityCollection' => $this->entityCollection, - 'identityContainer' => $invoiceIdentity, + 'identityContainer' => $this->invoiceIdentity, ] ); + $this->transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); } /** - * @magentoAppIsolation enabled - * @magentoDbIsolation disabled - * @magentoDataFixture Magento/Sales/_files/invoice_list_different_stores.php + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Sales/_files/invoice_list_different_stores.php + * @magentoConfigFixture default/sales_email/general/async_sending 1 + * @magentoConfigFixture fixture_second_store_store sales_email/invoice/enabled 0 + * @return void */ - public function testInvoiceEmailSenderExecute() + public function testInvoiceEmailSenderExecute(): void { - $expectedResult = 1; - - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** @var Config $defConfig */ - $defConfig = $objectManager->create(Config::class); - $defConfig->setScope(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); - $defConfig->setDataByPath('sales_email/general/async_sending', 1); - $defConfig->save(); - - /** @var Config $storeConfig */ - $storeConfig = $objectManager->create(Config::class); - $storeConfig->setScope(ScopeInterface::SCOPE_STORES); - $storeConfig->setStore('fixture_second_store'); - $storeConfig->setDataByPath('sales_email/invoice/enabled', 0); - $storeConfig->save(); - - $sendCollection = clone $this->entityCollection; - $sendCollection->addFieldToFilter('send_email', ['eq' => 1]); - $sendCollection->addFieldToFilter('email_sent', ['null' => true]); - - $this->emailSender->sendEmails(); - - $this->assertCount($expectedResult, $sendCollection->getItems()); + $invoiceCollection = clone $this->entityCollection; + $invoiceCollection->addFieldToFilter('send_email', ['eq' => 1]); + $invoiceCollection->addFieldToFilter(InvoiceInterface::EMAIL_SENT, ['null' => true]); + $this->emailSenderHandler->sendEmails(); + $this->assertEquals(1, $invoiceCollection->getTotalCount()); } /** - * @inheritdoc - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Exception + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Sales/_files/invoice_in_email_send_queue.php + * @magentoConfigFixture default/sales_email/general/async_sending 1 + * @return void */ - protected function tearDown(): void + public function testSendEmailsCheckEmailReceived(): void { - /** @var \Magento\Config\Model\Config $defConfig */ - $defConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Config\Model\Config::class); - $defConfig->setScope(\Magento\Framework\App\Config\ScopeConfigInterface::SCOPE_TYPE_DEFAULT); - $defConfig->setDataByPath('sales_email/general/async_sending', 0); - $defConfig->save(); + $invoiceCollection = clone $this->entityCollection; + $this->emailSenderHandler->sendEmails(); + /** @var Invoice $invoice */ + $invoice = $invoiceCollection->getFirstItem(); + $this->assertNotNull($invoice->getId()); + $message = $this->transportBuilderMock->getSentMessage(); + $this->assertNotNull($message, 'The message is expected to be received'); + $subject = __('Invoice for your %1 order', $invoice->getStore()->getFrontendName())->render(); + $this->assertEquals($message->getSubject(), $subject); + $this->assertStringContainsString( + "Your Invoice #{$invoice->getIncrementId()} for Order #{$invoice->getOrder()->getIncrementId()}", + $message->getBody()->getParts()[0]->getRawContent() + ); } } 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 55af8e9d2ee62..a22ab000cb697 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 @@ -9,60 +9,83 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Framework\App\Area; +use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceInterfaceFactory; +use Magento\Sales\Api\Data\OrderInterfaceFactory; 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 Magento\TestFramework\Mail\Template\TransportBuilderMock; use PHPUnit\Framework\TestCase; +/** + * Checks the sending of order invoice email to the customer. + * + * @see \Magento\Sales\Model\Order\Email\Sender\InvoiceSender + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ 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 - */ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CustomerRepository */ private $customerRepository; + /** @var InvoiceSender */ + private $invoiceSender; + + /** @var TransportBuilderMock */ + private $transportBuilderMock; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var InvoiceInterfaceFactory */ + private $invoiceFactory; + + /** @var InvoiceIdentity */ + private $invoiceIdentity; + /** - * @inheritDoc + * @inheritdoc */ protected function setUp(): void { parent::setUp(); - $this->customerRepository = Bootstrap::getObjectManager() - ->get(CustomerRepositoryInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->invoiceSender = $this->objectManager->get(InvoiceSender::class); + $this->transportBuilderMock = $this->objectManager->get(TransportBuilderMock::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->invoiceFactory = $this->objectManager->get(InvoiceInterfaceFactory::class); + $this->invoiceIdentity = $this->objectManager->get(InvoiceIdentity::class); } /** * @magentoDataFixture Magento/Sales/_files/order.php + * @return void */ - public function testSend() + public function testSend(): void { - Bootstrap::getInstance() - ->loadArea(Area::AREA_FRONTEND); - $order = Bootstrap::getObjectManager() - ->create(Order::class); - $order->loadByIncrementId('100000001'); + Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); + $order = $this->getOrder('100000001'); $order->setCustomerEmail('customer@example.com'); - - $invoice = Bootstrap::getObjectManager()->create( - Invoice::class - ); - $invoice->setOrder($order); + $invoice = $this->createInvoice($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); + $result = $this->invoiceSender->send($invoice, true); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); @@ -76,22 +99,20 @@ public function testSend() * * @magentoDataFixture Magento/Sales/_files/order_with_customer.php * @magentoAppArea frontend + * @return void */ - public function testSendWhenCustomerEmailWasModified() + public function testSendWhenCustomerEmailWasModified(): void { $customer = $this->customerRepository->getById(1); $customer->setEmail(self::NEW_CUSTOMER_EMAIL); $this->customerRepository->save($customer); - - $order = $this->createOrder(); + $order = $this->getOrder('100000001'); $invoice = $this->createInvoice($order); - $invoiceIdentity = $this->createInvoiceEntity(); - $invoiceSender = $this->createInvoiceSender($invoiceIdentity); $this->assertEmpty($invoice->getEmailSent()); - $result = $invoiceSender->send($invoice, true); + $result = $this->invoiceSender->send($invoice, true); - $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } @@ -101,18 +122,17 @@ public function testSendWhenCustomerEmailWasModified() * * @magentoDataFixture Magento/Sales/_files/order_with_customer.php * @magentoAppArea frontend + * @return void */ - public function testSendWhenCustomerEmailWasNotModified() + public function testSendWhenCustomerEmailWasNotModified(): void { - $order = $this->createOrder(); + $order = $this->getOrder('100000001'); $invoice = $this->createInvoice($order); - $invoiceIdentity = $this->createInvoiceEntity(); - $invoiceSender = $this->createInvoiceSender($invoiceIdentity); $this->assertEmpty($invoice->getEmailSent()); - $result = $invoiceSender->send($invoice, true); + $result = $this->invoiceSender->send($invoice, true); - $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } @@ -122,59 +142,67 @@ public function testSendWhenCustomerEmailWasNotModified() * * @magentoDataFixture Magento/Sales/_files/order.php * @magentoAppArea frontend + * @return void */ - public function testSendWithoutCustomer() + public function testSendWithoutCustomer(): void { - $order = $this->createOrder(); + $order = $this->getOrder('100000001'); $invoice = $this->createInvoice($order); - /** @var InvoiceIdentity $invoiceIdentity */ - $invoiceIdentity = $this->createInvoiceEntity(); - /** @var InvoiceSender $invoiceSender */ - $invoiceSender = $this->createInvoiceSender($invoiceIdentity); - $this->assertEmpty($invoice->getEmailSent()); - $result = $invoiceSender->send($invoice, true); + $result = $this->invoiceSender->send($invoice, true); - $this->assertEquals(self::ORDER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertEquals(self::ORDER_EMAIL, $this->invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } - private function createInvoice(Order $order): Invoice + /** + * @magentoDataFixture Magento/Sales/_files/invoice.php + * @magentoConfigFixture default/sales_email/general/async_sending 1 + * @return void + */ + public function testSendWithAsyncSendingEnabled(): void { - $invoice = Bootstrap::getObjectManager()->create( - Invoice::class + $order = $this->getOrder('100000001'); + /** @var Invoice $invoice */ + $invoice = $order->getInvoiceCollection() + ->addAttributeToFilter(InvoiceInterface::ORDER_ID, $order->getID()) + ->getFirstItem(); + $result = $this->invoiceSender->send($invoice); + $this->assertFalse($result); + $invoice = $order->getInvoiceCollection()->clear()->getFirstItem(); + $this->assertEmpty($invoice->getEmailSent()); + $this->assertEquals('1', $invoice->getSendEmail()); + $this->assertNull( + $this->transportBuilderMock->getSentMessage(), + 'The message is not expected to be received.' ); - $invoice->setOrder($order); - - return $invoice; } - private function createOrder(): Order + /** + * Create invoice and set order + * + * @param Order $order + * @return Invoice + */ + private function createInvoice(Order $order): Invoice { - $order = Bootstrap::getObjectManager() - ->create(Order::class); - $order->loadByIncrementId('100000001'); - - return $order; - } + /** @var Invoice $invoice */ + $invoice = $this->invoiceFactory->create(); + $invoice->setOrder($order); - private function createInvoiceEntity(): InvoiceIdentity - { - return Bootstrap::getObjectManager()->create( - InvoiceIdentity::class - ); + return $invoice; } - private function createInvoiceSender(InvoiceIdentity $invoiceIdentity): InvoiceSender + /** + * Get order by increment_id + * + * @param string $incrementId + * @return Order + */ + private function getOrder(string $incrementId): Order { - return Bootstrap::getObjectManager() - ->create( - InvoiceSender::class, - [ - 'identityContainer' => $invoiceIdentity, - ] - ); + return $this->orderFactory->create()->loadByIncrementId($incrementId); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue.php b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue.php new file mode 100644 index 0000000000000..c90e39e086659 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue.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\DB\Transaction; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\InvoiceManagementInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Service\InvoiceService; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var OrderInterfaceFactory $orderFactory */ +$orderFactory = $objectManager->get(OrderInterfaceFactory::class); +/** @var InvoiceService $invoiceService */ +$invoiceService = $objectManager->get(InvoiceManagementInterface::class); +/** @var Transaction $transactionSave */ +$transactionSave = $objectManager->get(Transaction::class); +/** @var Order $order */ +$order = $orderFactory->create()->loadByIncrementId('100000001'); + +$invoice = $invoiceService->prepareInvoice($order); +$invoice->register(); +$invoice->setSendEmail(true); +//$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue_rollback.php new file mode 100644 index 0000000000000..dc455c3cb2c49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue_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'); From a77560cf42ef1bd4f3811a419c22e0f9fb0fe705 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 7 Oct 2020 13:22:51 +0300 Subject: [PATCH 0727/1013] MC-37098: Create automated test for "Check functionality of RabbitMQ" --- .../AdminClickMassUpdateProductAttributesActionGroup.xml | 3 +-- .../AdminSaveProductsMassAttributesUpdateActionGroup.xml | 1 + .../Catalog/Test/Mftf/Section/AdminProductGridSection.xml | 4 ++-- .../Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml | 2 -- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml index 81a576e9ea3f1..90cc7666eb92f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminClickMassUpdateProductAttributesActionGroup.xml @@ -10,11 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminClickMassUpdateProductAttributesActionGroup"> <annotations> - <description>Clicks on 'Update attributes' on product grid page.</description> + <description>Clicks on 'Update attributes' from dropdown actions list on product grid page. Products should be selected via mass action before</description> </annotations> <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> - <waitForPageLoad stepKey="waitForBulkUpdatePage"/> <seeInCurrentUrl url="catalog/product_action_attribute/edit/" stepKey="seeInUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml index b794528135858..811eb6ee5f1f7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductsMassAttributesUpdateActionGroup.xml @@ -14,5 +14,6 @@ </annotations> <click selector="{{AdminEditProductAttributesSection.Save}}" stepKey="save"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" time="60" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="assertSuccessMessage"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml index 540db609f550b..8f2b789639e7f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductGridSection.xml @@ -26,8 +26,8 @@ <element name="productGridHeaderCell" type="text" selector="//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]" parameterized="true"/> <element name="multicheckDropdown" type="button" selector="div[data-role='grid-wrapper'] th.data-grid-multicheck-cell button.action-multicheck-toggle"/> <element name="multicheckOption" type="button" selector="//div[@data-role='grid-wrapper']//th[contains(@class, data-grid-multicheck-cell)]//li//span[text() = '{{label}}']" parameterized="true"/> - <element name="bulkActionDropdown" type="button" selector="div.admin__data-grid-header-row.row div.action-select-wrap button.action-select"/> - <element name="bulkActionOption" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-select-wrap')]//ul/li/span[text() = '{{label}}']" parameterized="true"/> + <element name="bulkActionDropdown" type="button" selector="div.admin__data-grid-header-row.row div.action-select-wrap button.action-select" timeout="30"/> + <element name="bulkActionOption" type="button" selector="//div[contains(@class,'admin__data-grid-header-row') and contains(@class, 'row')]//div[contains(@class, 'action-select-wrap')]//ul/li/span[text() = '{{label}}']" parameterized="true" timeout="30"/> <element name="productGridXRowYColumnButton" type="input" selector="table.data-grid tr.data-row:nth-child({{row}}) td:nth-child({{column}})" parameterized="true" timeout="30"/> <element name="table" type="text" selector="#container > div > div.admin__data-grid-wrap > table"/> <element name="firstRow" type="button" selector="tr.data-row:nth-of-type(1)"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml index 728f9dd7d68ca..dc34607f2a771 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductAttributeUpdateAddedToQueueTest.xml @@ -19,7 +19,6 @@ <useCaseId value="MC-29179"/> <group value="catalog"/> <group value="asynchronousOperations"/> - <group value="productAttributes"/> </annotations> <before> <createData entity="ApiProductWithDescription" stepKey="createFirstProduct"/> @@ -46,7 +45,6 @@ <checkOption selector="{{AdminEditProductAttributesSection.changeAttributeShortDescriptionToggle}}" stepKey="toggleToChangeShortDescription"/> <fillField selector="{{AdminEditProductAttributesSection.attributeShortDescription}}" userInput="Test Update" stepKey="fillShortDescriptionField"/> <actionGroup ref="AdminSaveProductsMassAttributesUpdateActionGroup" stepKey="saveMassAttributeUpdate"/> - <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeAttributeUpdateSuccessMsg"/> <see selector="{{AdminSystemMessagesSection.info}}" userInput="Task "Update attributes for 3 selected products": 1 item(s) have been scheduled for update." stepKey="seeInfoMessage"/> <click selector="{{AdminSystemMessagesSection.viewDetailsLink}}" stepKey="seeDetails"/> <see selector="{{AdminBulkDetailsModalSection.descriptionValue}}" userInput="Update attributes for 3 selected products" stepKey="seeDescription"/> From bebc574b270bb6dc1d49217ab99898b0757bd0fb Mon Sep 17 00:00:00 2001 From: Dmitry Tsymbal <d.tsymbal@atwix.com> Date: Wed, 7 Oct 2020 13:50:30 +0300 Subject: [PATCH 0728/1013] Admin Delete CMS Block Test --- ...AdminDeleteCMSBlockFromGridActionGroup.xml | 21 +++++++++ .../AdminOpenCMSBlocksGridActionGroup.xml | 19 ++++++++ ...hCMSBlockInGridByIdentifierActionGroup.xml | 20 ++++++++ ...ertAdminCMSBlockIsNotInGridActionGroup.xml | 17 +++++++ .../Mftf/Section/AdminBlockGridSection.xml | 1 + .../Mftf/Section/BlockPageActionsSection.xml | 2 + .../Mftf/Test/AdminDeleteCmsBlockTest.xml | 47 +++++++++++++++++++ 7 files changed, 127 insertions(+) create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml new file mode 100644 index 0000000000000..a61f565bac2bc --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.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="AdminDeleteCMSBlockFromGridActionGroup"> + <arguments> + <argument name="identifier" type="entity"/> + </arguments> + <click selector="{{BlockPageActionsSection.select(identifier)}}" stepKey="clickSelect"/> + <click selector="{{BlockPageActionsSection.delete(identifier)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{BlockPageActionsSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{BlockPageActionsSection.deleteConfirm}}" stepKey="clickOkButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml new file mode 100644 index 0000000000000..18e7e5fb52615 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.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="AdminOpenCMSBlocksGridActionGroup"> + <annotations> + <description>Navigate to the Admin Blocks Grid page.</description> + </annotations> + + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSBlocksGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml new file mode 100644 index 0000000000000..1099cd7e753c9 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.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="AdminSearchCMSBlockInGridByIdentifierActionGroup"> + <arguments> + <argument name="identifier" type="string"/> + </arguments> + <click selector="{{BlockPageActionsSection.FilterBtn}}" stepKey="clickFilterButton"/> + <fillField selector="{{BlockPageActionsSection.URLKey}}" userInput="{{identifier}}" stepKey="fillIdentifierField"/> + <click selector="{{BlockPageActionsSection.ApplyFiltersBtn}}" stepKey="clickApplyFiltersButton"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml new file mode 100644 index 0000000000000..1b5a5301eda1b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.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="AssertAdminCMSBlockIsNotInGridActionGroup"> + <arguments> + <argument name="identifier" type="entity"/> + </arguments> + <dontSee userInput="{{identifier}}" selector="{{AdminBlockGridSection.gridDataRow}}" stepKey="dontSeeCmsBlockInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml index ab15570a01f40..a9c9a5943529c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml @@ -14,5 +14,6 @@ <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> + <element name="gridDataRow" type="input" selector=".data-row .data-grid-cell-content"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml index ac9c66fe82c74..529000dc44c3a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml @@ -20,5 +20,7 @@ <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"/> + <element name="delete" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" parameterized="true"/> + <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml new file mode 100644 index 0000000000000..4274973796b64 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.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="AdminDeleteCmsBlockTest"> + <annotations> + <features value="Cms"/> + <stories value="CMS Blocks Deleting"/> + <title value="Admin should be able to delete CMS block from grid"/> + <description value="Admin should be able to delete CMS block from grid"/> + <group value="Cms"/> + <severity value="MINOR"/> + </annotations> + <before> + <createData entity="_defaultBlock" stepKey="createCMSBlock"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenCMSBlocksGridActionGroup" stepKey="navigateToCmsBlocksGrid"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridSearchFilters"/> + <actionGroup ref="AdminSearchCMSBlockInGridByIdentifierActionGroup" stepKey="findCreatedCmsBlock"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AdminDeleteCMSBlockFromGridActionGroup" stepKey="deleteCmsBlockFromGrid"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="You deleted the block."/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridSearchFiltersAfterBlockDeleting"/> + <actionGroup ref="AdminSearchCMSBlockInGridByIdentifierActionGroup" stepKey="searchDeletedCmsBlock"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCMSBlockIsNotInGridActionGroup" stepKey="assertDeletedCMSBlockIsNotInGrid"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + </test> +</tests> From a67ad11ab890759cfe0591b730c8e21117d3088e Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Wed, 7 Oct 2020 14:15:53 +0300 Subject: [PATCH 0729/1013] MC-36965: Create automated test for "[ES] Search with Layered Navigation and different types of attribute products. --- .../StorefrontCategorySidebarSection.xml | 4 ++-- .../ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml | 2 +- ...orefrontFilterCategoryPageByAttributeOptionActionGroup.xml | 3 +-- .../StorefrontDropdownAttributeInLayeredNavigationTest.xml | 2 ++ 4 files changed, 6 insertions(+), 5 deletions(-) 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 f1c3d0c5ec67a..848035b911aab 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml @@ -17,7 +17,7 @@ <element name="removeFilter" type="button" selector="div.filter-current .remove" timeout="30"/> <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"/> + <element name="enabledFilterOptionItemByLabel" type="text" selector="//div[@class='filter-options']//li[@class='item']//a[contains(text(), '{{optionLabel}}')]" parameterized="true" timeout="30"/> + <element name="disabledFilterOptionItemByLabel" type="text" selector="//div[@class='filter-options']//li[@class='item' and contains(text(), '{{optionLabel}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml index 652423df37f85..92fea20a83157 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontAssertAppliedFilterActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontAssertAppliedFilterActionGroup"> <annotations> - <description>Asserts applied filter label and value on category page is layered navigation block.</description> + <description>Asserts applied filter label and value on storefront category page.</description> </annotations> <arguments> <argument name="attributeLabel" type="string"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml index 63d16b821a4be..f6fe2b20185e6 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/ActionGroup/StorefrontFilterCategoryPageByAttributeOptionActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontFilterCategoryPageByAttributeOptionActionGroup"> <annotations> - <description>Filters category page by given filterable attribute and attribute option is layered navigation block.</description> + <description>Filters storefront category page by given filterable attribute and attribute option.</description> </annotations> <arguments> <argument name="attributeLabel" type="string"/> @@ -19,6 +19,5 @@ <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" stepKey="waitForFilterVisible"/> <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle(attributeLabel)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandFilter"/> <click selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel(attributeOptionLabel)}}" stepKey="clickOnOption"/> - <waitForPageLoad stepKey="waitForPageLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml index 5d9d732089ea8..0cd115d3febeb 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontDropdownAttributeInLayeredNavigationTest.xml @@ -48,12 +48,14 @@ <requiredEntity createDataKey="getSecondDropdownProductAttributeOption"/> <requiredEntity createDataKey="createCategory"/> </createData> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createDropdownProductAttribute" stepKey="deleteDropdownProductAttribute"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategory"> <argument name="category" value="$createCategory$"/> From d1aaa1e7bed1ad20262075a1d06322ca57bd82eb Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Wed, 7 Oct 2020 14:31:50 +0300 Subject: [PATCH 0730/1013] MC-38031: Checkout with Multiple Addresses - Review Page does not follow the configured total sort order --- .../Multishipping/Block/Checkout/Overview.php | 37 +++++++++++++- .../Test/Unit/Block/Checkout/OverviewTest.php | 50 ++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 1ea2dc2618778..ef6dae2182f10 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -10,6 +10,8 @@ use Magento\Quote\Model\Quote\Address; use Magento\Checkout\Helper\Data as CheckoutHelper; use Magento\Framework\App\ObjectManager; +use Magento\Quote\Model\Quote\Address\Total\Collector; +use Magento\Store\Model\ScopeInterface; /** * Multishipping checkout overview information @@ -430,7 +432,7 @@ public function getBillingAddressTotals() public function renderTotals($totals, $colspan = null) { //check if the shipment is multi shipment - $totals = $this->getMultishippingTotals($totals); + $totals = $this->sortTotals($this->getMultishippingTotals($totals)); if ($colspan === null) { $colspan = 3; @@ -481,4 +483,37 @@ protected function _getRowItemRenderer($type) } return $renderer; } + + /** + * Sort total information based on configuration settings. + * + * @param array $totals + * @return array + */ + private function sortTotals($totals): array + { + $sortedTotals = []; + $sorts = $this->_scopeConfig->getValue( + Collector::XML_PATH_SALES_TOTALS_SORT, + ScopeInterface::SCOPE_STORES + ); + + foreach ($sorts as $code => $sortOrder) { + $sorted[$sortOrder] = $code; + } + ksort($sorted); + + foreach ($sorted as $code) { + if (isset($totals[$code])) { + $sortedTotals[$code] = $totals[$code]; + } + } + + $notSorted = array_diff(array_keys($totals), array_keys($sortedTotals)); + foreach ($notSorted as $code) { + $sortedTotals[$code] = $totals[$code]; + } + + return $sortedTotals; + } } diff --git a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php index 7da77030f308a..2d044afd32c70 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Block/Checkout/OverviewTest.php @@ -8,6 +8,7 @@ namespace Magento\Multishipping\Test\Unit\Block\Checkout; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\UrlInterface; @@ -67,6 +68,11 @@ class OverviewTest extends TestCase */ private $urlBuilderMock; + /** + * @var MockObject + */ + private $scopeConfigMock; + protected function setUp(): void { $objectManager = new ObjectManager($this); @@ -85,6 +91,7 @@ protected function setUp(): void $this->createMock(Multishipping::class); $this->quoteMock = $this->createMock(Quote::class); $this->urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); + $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->model = $objectManager->getObject( Overview::class, [ @@ -92,7 +99,8 @@ protected function setUp(): void 'totalsCollector' => $this->totalsCollectorMock, 'totalsReader' => $this->totalsReaderMock, 'multishipping' => $this->checkoutMock, - 'urlBuilder' => $this->urlBuilderMock + 'urlBuilder' => $this->urlBuilderMock, + '_scopeConfig' => $this->scopeConfigMock ] ); } @@ -187,4 +195,44 @@ public function testGetVirtualProductEditUrl() $this->urlBuilderMock->expects($this->once())->method('getUrl')->with('checkout/cart', [])->willReturn($url); $this->assertEquals($url, $this->model->getVirtualProductEditUrl()); } + + /** + * Test sort total information + * + * @return void + */ + public function testSortCollectors(): void + { + $sorts = [ + 'discount' => 40, + 'subtotal' => 10, + 'tax' => 20, + 'shipping' => 30, + ]; + + $this->scopeConfigMock->method('getValue') + ->with('sales/totals_sort', 'stores') + ->willReturn($sorts); + + $totalsNotSorted = [ + 'subtotal' => [], + 'shipping' => [], + 'tax' => [], + ]; + + $totalsExpected = [ + 'subtotal' => [], + 'tax' => [], + 'shipping' => [], + ]; + + $method = new \ReflectionMethod($this->model, 'sortTotals'); + $method->setAccessible(true); + $result = $method->invoke($this->model, $totalsNotSorted); + + $this->assertEquals( + $totalsExpected, + $result + ); + } } From 99d9d4b22992a048247d99738090e62fad650b90 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Wed, 7 Oct 2020 14:32:07 +0300 Subject: [PATCH 0731/1013] MC-37100: Create automated test for "Create customer, when admin interface locale is en_GB" --- .../Customer/Controller/Adminhtml/Index/SaveTest.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php index 375a1d493b20a..33635d3678726 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Adminhtml/Index/SaveTest.php @@ -8,11 +8,13 @@ namespace Magento\Customer\Controller\Adminhtml\Index; use Magento\Backend\Model\Session; +use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Api\CustomerNameGenerationInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Model\Data\Customer as CustomerData; use Magento\Customer\Model\EmailNotification; +use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\Framework\App\Area; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Locale\ResolverInterface; @@ -23,6 +25,7 @@ use Magento\Newsletter\Model\SubscriberFactory; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractBackendController; use PHPUnit\Framework\MockObject\MockObject; @@ -458,14 +461,13 @@ public function testCreateCustomerByAdminWithLocaleGB(): void [ 'customer' => [ CustomerData::DOB => '1990-10-24', - CustomerData::GENDER => '0', ], ] ); unset($expectedData['customer']['sendemail_store_id']); $this->dispatchCustomerSave($postData); $this->assertSessionMessages( - $this->equalTo([(string)__('You saved the customer.')]), + $this->containsEqual((string)__('You saved the customer.')), MessageInterface::TYPE_SUCCESS ); $this->assertRedirect($this->stringContains($this->baseControllerUrl . 'index/key/')); @@ -496,7 +498,8 @@ private function getDefaultCustomerData(): array CustomerData::EMAIL => 'janedoe' . uniqid() . '@example.com', CustomerData::DOB => '01/01/2000', CustomerData::TAXVAT => '121212', - CustomerData::GENDER => 'Male', + CustomerData::GENDER => Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class) + ->get(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, 'gender')->getSource()->getOptionId('Male'), 'sendemail_store_id' => '1', ] ]; @@ -516,7 +519,6 @@ private function getExpectedCustomerData(array $defaultCustomerData): array [ 'customer' => [ CustomerData::DOB => '2000-01-01', - CustomerData::GENDER => '0', CustomerData::STORE_ID => 1, CustomerData::CREATED_IN => 'Default Store View', ], From 73c6045330654bbf075538a8ea1ca341ad56f5f8 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Wed, 7 Oct 2020 15:14:48 +0300 Subject: [PATCH 0732/1013] MC-38031: Checkout with Multiple Addresses - Review Page does not follow the configured total sort order --- app/code/Magento/Multishipping/Block/Checkout/Overview.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index ef6dae2182f10..3d6d4b195050d 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -498,6 +498,7 @@ private function sortTotals($totals): array ScopeInterface::SCOPE_STORES ); + $sorted = []; foreach ($sorts as $code => $sortOrder) { $sorted[$sortOrder] = $code; } From 5929940c2e47cd30fd55c7f4a7ed30c294beb726 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Wed, 7 Oct 2020 16:16:52 +0300 Subject: [PATCH 0733/1013] MC-24010: Broken integration test RelationTest.php on mainline 2.3 --- .../Product/Flat/Action/RelationTest.php | 102 +++++++++++------- 1 file changed, 61 insertions(+), 41 deletions(-) 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 e3b5bc8d5fd0d..745f71801352b 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 @@ -11,7 +11,12 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Store\Model\StoreManagerInterface; -use Magento\TestFramework\Catalog\Model\Indexer\Product\Flat\Action\Full as FlatIndexerFull; +use Magento\Catalog\Model\Indexer\Product\Flat\Action\Full as FlatIndexerFull; +use Magento\Catalog\Helper\Product\Flat\Indexer; +use Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder; +use Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Helper\Bootstrap; /** * Test relation customization @@ -42,33 +47,79 @@ class RelationTest extends \Magento\TestFramework\Indexer\TestCase */ private $flatUpdated = []; + /** + * @var Indexer + */ + private $productIndexerHelper; + /** * @inheritdoc */ protected function setUp(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); - $tableBuilderMock = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder::class); - $flatTableBuilderMock = $this->createMock(\Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder::class); + $tableBuilderMock = $objectManager->get(TableBuilder::class); + $flatTableBuilderMock = + $objectManager->get(FlatTableBuilder::class); - $productIndexerHelper = $objectManager->create( - \Magento\Catalog\Helper\Product\Flat\Indexer::class, - ['addChildData' => 1] + $this->productIndexerHelper = $objectManager->create( + Indexer::class, + ['addChildData' => true] ); $this->indexer = $objectManager->create( FlatIndexerFull::class, [ - 'productHelper' => $productIndexerHelper, + 'productHelper' => $this->productIndexerHelper, 'tableBuilder' => $tableBuilderMock, 'flatTableBuilder' => $flatTableBuilderMock ] ); - $this->storeManager = $objectManager->create(StoreManagerInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); $this->connection = $objectManager->get(ResourceConnection::class)->getConnection(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + foreach ($this->flatUpdated as $flatTable) { + $this->connection->dropColumn($flatTable, 'child_id'); + $this->connection->dropColumn($flatTable, 'is_child'); + } + } + + /** + * Test that SQL generated for relation customization is valid + * + * @return void + * @throws LocalizedException + * @throws \Exception + */ + public function testExecute() : void + { + $this->addChildColumns(); + try { + $result = $this->indexer->execute(); + } catch (LocalizedException $e) { + if ($e->getPrevious() instanceof \Zend_Db_Statement_Exception) { + $this->fail($e->getMessage()); + } + throw $e; + } + $this->assertInstanceOf(FlatIndexerFull::class, $result); + } + /** + * Add child columns to tables if needed + * + * @return void + */ + private function addChildColumns(): void + { foreach ($this->storeManager->getStores() as $store) { - $flatTable = $productIndexerHelper->getFlatTableName($store->getId()); + $flatTable = $this->productIndexerHelper->getFlatTableName($store->getId()); if ($this->connection->isTableExists($flatTable) && !$this->connection->tableColumnExists($flatTable, 'child_id') && !$this->connection->tableColumnExists($flatTable, 'is_child') @@ -103,35 +154,4 @@ protected function setUp(): void } } } - - /** - * @inheritdoc - */ - protected function tearDown(): void - { - foreach ($this->flatUpdated as $flatTable) { - $this->connection->dropColumn($flatTable, 'child_id'); - $this->connection->dropColumn($flatTable, 'is_child'); - } - } - - /** - * Test that SQL generated for relation customization is valid - * - * @return void - * @throws \Magento\Framework\Exception\LocalizedException - * @throws \Exception - */ - public function testExecute() : void - { - $this->markTestSkipped('MC-19675'); - try { - $this->indexer->execute(); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - if ($e->getPrevious() instanceof \Zend_Db_Statement_Exception) { - $this->fail($e->getMessage()); - } - throw $e; - } - } } From 88f63a6cc4d40e89bcbaabfe27f883981222d66b Mon Sep 17 00:00:00 2001 From: Vadim Malesh <51680850+engcom-Charlie@users.noreply.github.com> Date: Wed, 7 Oct 2020 16:29:02 +0300 Subject: [PATCH 0734/1013] add testCaseId --- .../SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml index 52d456bc6225d..d225e5fa28f97 100644 --- a/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml +++ b/app/code/Magento/OfflineShipping/Test/Mftf/Test/SalesRuleDiscountIsAppliedOnPackageValueForTableRateTest.xml @@ -13,6 +13,7 @@ <title value="SalesRule Discount Is Applied On PackageValue For TableRate"/> <description value="SalesRule Discount Is Applied On PackageValue For TableRate"/> <severity value="AVERAGE"/> + <testCaseId value="MC-38271"/> <group value="shipping"/> </annotations> <before> From 17a1c3ae7492759baf60d3258117da874343f632 Mon Sep 17 00:00:00 2001 From: Viktor Sevch <viktor.sevch@transoftgroup.com> Date: Wed, 7 Oct 2020 17:03:35 +0300 Subject: [PATCH 0735/1013] MC-23536: CatalogProductListWidgetOrderTest is flaky and fails randomly --- .../Test/CatalogProductListCheckWidgetOrderTest.xml | 10 ++-------- .../Mftf/Section/TinyMCESection/TinyMCESection.xml | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml index db09cc96cb791..1d5e369d50e1d 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListCheckWidgetOrderTest.xml @@ -15,7 +15,7 @@ <title value="Checking order of products in the 'catalog Products List' widget"/> <description value="Check that products are ordered with recently added products first"/> <severity value="MAJOR"/> - <testCaseId value="MC-13794"/> + <testCaseId value="MC-27616"/> <useCaseId value="MC-5905"/> <group value="catalogWidget"/> <group value="catalog"/> @@ -46,7 +46,7 @@ <deleteData createDataKey="createFirstProduct" stepKey="deleteFirstProduct"/> <deleteData createDataKey="createSecondProduct" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createThirdProduct" stepKey="deleteThirdProduct"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <!--Open created cms page--> <actionGroup ref="AdminOpenCmsPageActionGroup" stepKey="openEditPage"> @@ -57,10 +57,8 @@ <!--Add widget to cms page--> <waitForElementVisible selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="waitInsertWidgetIconVisible"/> <click selector="{{TinyMCESection.InsertWidgetIcon}}" stepKey="clickInsertWidgetIcon" /> - <waitForPageLoad stepKey="waitForPageLoad1" /> <waitForElementVisible selector="{{WidgetSection.WidgetType}}" stepKey="waitForWidgetTypeSelectorVisible"/> <selectOption selector="{{WidgetSection.WidgetType}}" userInput="Catalog Products List" stepKey="selectCatalogProductsList" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear1" /> <waitForElementVisible selector="{{WidgetSection.AddParam}}" stepKey="waitForAddParamBtnVisible"/> <click selector="{{WidgetSection.AddParam}}" stepKey="clickAddParamBtn" /> <waitForElementVisible selector="{{WidgetSection.ConditionsDropdown}}" stepKey="waitForDropdownVisible"/> @@ -70,11 +68,9 @@ <click selector="{{WidgetSection.RuleParam}}" stepKey="clickRuleParam" /> <waitForElementVisible selector="{{WidgetSection.Chooser}}" stepKey="waitForElement" /> <click selector="{{WidgetSection.Chooser}}" stepKey="clickChooser" /> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear3" /> <waitForElementVisible selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="waitForCategoryVisible" /> <click selector="{{WidgetSection.PreCreateCategory('$simplecategory.name$')}}" stepKey="selectCategory" /> <click selector="{{WidgetSection.InsertWidget}}" stepKey="clickInsertWidget" /> - <waitForPageLoad stepKey="waitForPageLoad2" /> <!--Save cms page and go to Storefront--> <actionGroup ref="SaveCmsPageActionGroup" stepKey="saveCmsPage"/> <actionGroup ref="NavigateToStorefrontForCreatedPageActionGroup" stepKey="navigateToTheStoreFront1"> @@ -83,9 +79,7 @@ <!--Check order of products: recently added first--> <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="waitForThirdProductVisible"/> <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('1','$createThirdProduct.name$')}}" stepKey="seeElementByName1"/> - <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="waitForSecondProductVisible"/> <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('2','$createSecondProduct.name$')}}" stepKey="seeElementByName2"/> - <waitForElementVisible selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="waitForFirstProductVisible"/> <seeElement selector="{{InsertWidgetSection.checkElementStorefrontByName('3','$createFirstProduct.name$')}}" stepKey="seeElementByName3"/> </test> </tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml index e3e6ae9cffc02..b7a6618d76596 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/TinyMCESection.xml @@ -12,7 +12,7 @@ <element name="CheckIfTabExpand" type="button" selector="//div[@data-state-collapsible='closed']//span[text()='Content']"/> <element name="TinyMCE4" type="text" selector=".mce-branding"/> <element name="InsertWidgetBtn" type="button" selector=".action-add-widget"/> - <element name="InsertWidgetIcon" type="button" selector="div[aria-label='Insert Widget']"/> + <element name="InsertWidgetIcon" type="button" selector="div[aria-label='Insert Widget']" timeout="30"/> <element name="InsertVariableBtn" type="button" selector=".scalable.add-variable.plugin"/> <element name="InsertVariableIcon" type="button" selector="div[aria-label='Insert Variable']"/> <element name="InsertImageBtn" type="button" selector=".scalable.action-add-image.plugin"/> From 90a8983674e14ee78294606f6d721e845d75a2b1 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Wed, 7 Oct 2020 17:57:18 +0300 Subject: [PATCH 0736/1013] MC-37493: Create automated test for "Create Invoice for Offline Payment Methods with Async Notification" --- .../Model/InvoiceEmailSenderHandlerTest.php | 11 +++++++---- .../Order/Email/Sender/InvoiceSenderTest.php | 18 ++++++++---------- ...ue.php => invoice_with_send_email_flag.php} | 2 -- ... invoice_with_send_email_flag_rollback.php} | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) rename dev/tests/integration/testsuite/Magento/Sales/_files/{invoice_in_email_send_queue.php => invoice_with_send_email_flag.php} (90%) rename dev/tests/integration/testsuite/Magento/Sales/_files/{invoice_in_email_send_queue_rollback.php => invoice_with_send_email_flag_rollback.php} (90%) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php index 0e3148e9ce4ac..6924e2db016dd 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/InvoiceEmailSenderHandlerTest.php @@ -12,7 +12,6 @@ use Magento\Sales\Api\Data\InvoiceSearchResultInterface; use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; use Magento\Sales\Model\Order\Email\Sender\InvoiceSender; -use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\Spi\InvoiceResourceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Mail\Template\TransportBuilderMock; @@ -87,7 +86,7 @@ public function testInvoiceEmailSenderExecute(): void /** * @magentoDbIsolation disabled - * @magentoDataFixture Magento/Sales/_files/invoice_in_email_send_queue.php + * @magentoDataFixture Magento/Sales/_files/invoice_with_send_email_flag.php * @magentoConfigFixture default/sales_email/general/async_sending 1 * @return void */ @@ -95,7 +94,7 @@ public function testSendEmailsCheckEmailReceived(): void { $invoiceCollection = clone $this->entityCollection; $this->emailSenderHandler->sendEmails(); - /** @var Invoice $invoice */ + /** @var InvoiceInterface $invoice */ $invoice = $invoiceCollection->getFirstItem(); $this->assertNotNull($invoice->getId()); $message = $this->transportBuilderMock->getSentMessage(); @@ -103,7 +102,11 @@ public function testSendEmailsCheckEmailReceived(): void $subject = __('Invoice for your %1 order', $invoice->getStore()->getFrontendName())->render(); $this->assertEquals($message->getSubject(), $subject); $this->assertStringContainsString( - "Your Invoice #{$invoice->getIncrementId()} for Order #{$invoice->getOrder()->getIncrementId()}", + sprintf( + "Your Invoice #%s for Order #%s", + $invoice->getIncrementId(), + $invoice->getOrder()->getIncrementId() + ), $message->getBody()->getParts()[0]->getRawContent() ); } 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 a22ab000cb697..672709cbcd44b 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 @@ -8,13 +8,11 @@ namespace Magento\Sales\Model\Order\Email\Sender; use Magento\Customer\Api\CustomerRepositoryInterface; -use Magento\Customer\Model\ResourceModel\CustomerRepository; -use Magento\Framework\App\Area; use Magento\Framework\ObjectManagerInterface; use Magento\Sales\Api\Data\InvoiceInterface; use Magento\Sales\Api\Data\InvoiceInterfaceFactory; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderInterfaceFactory; -use Magento\Sales\Model\Order; use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; use Magento\Sales\Model\Order\Invoice; use Magento\TestFramework\Helper\Bootstrap; @@ -36,7 +34,7 @@ class InvoiceSenderTest extends TestCase /** @var ObjectManagerInterface */ private $objectManager; - /** @var CustomerRepository */ + /** @var CustomerRepositoryInterface */ private $customerRepository; /** @var InvoiceSender */ @@ -71,11 +69,11 @@ protected function setUp(): void /** * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend * @return void */ public function testSend(): void { - Bootstrap::getInstance()->loadArea(Area::AREA_FRONTEND); $order = $this->getOrder('100000001'); $order->setCustomerEmail('customer@example.com'); $invoice = $this->createInvoice($order); @@ -183,10 +181,10 @@ public function testSendWithAsyncSendingEnabled(): void /** * Create invoice and set order * - * @param Order $order - * @return Invoice + * @param OrderInterface $order + * @return InvoiceInterface */ - private function createInvoice(Order $order): Invoice + private function createInvoice(OrderInterface $order): InvoiceInterface { /** @var Invoice $invoice */ $invoice = $this->invoiceFactory->create(); @@ -199,9 +197,9 @@ private function createInvoice(Order $order): Invoice * Get order by increment_id * * @param string $incrementId - * @return Order + * @return OrderInterface */ - private function getOrder(string $incrementId): Order + private function getOrder(string $incrementId): OrderInterface { return $this->orderFactory->create()->loadByIncrementId($incrementId); } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue.php b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag.php similarity index 90% rename from dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue.php rename to dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag.php index c90e39e086659..c23f7b8cfd423 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag.php @@ -13,7 +13,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order.php'); $objectManager = Bootstrap::getObjectManager(); @@ -29,6 +28,5 @@ $invoice = $invoiceService->prepareInvoice($order); $invoice->register(); $invoice->setSendEmail(true); -//$order = $invoice->getOrder(); $order->setIsInProcess(true); $transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag_rollback.php similarity index 90% rename from dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue_rollback.php rename to dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag_rollback.php index dc455c3cb2c49..07d468289f5b4 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_in_email_send_queue_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/invoice_with_send_email_flag_rollback.php @@ -7,4 +7,4 @@ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_rollback.php'); From 7db164672218f0abca653a088dc9123e45e3c87e Mon Sep 17 00:00:00 2001 From: Dmytro Horytskyi <dhorytskyi@magento.com> Date: Wed, 7 Oct 2020 11:09:46 -0500 Subject: [PATCH 0737/1013] MC-37347: [OnPrem] Catalog Products Filter in 2.3.3 not working correctly --- ...lterByNameByStoreViewOnProductGridTest.xml | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml index c6f3c69a2aa48..7cf2e132c016d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml @@ -19,29 +19,35 @@ <useCaseId value="MC-37347"/> <group value="catalog"/> </annotations> + <before> - <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct1"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct2"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> + <after> - <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToEditPage"> - <argument name="productId" value="$$createSimpleProduct.id$$"/> + <argument name="productId" value="$$createSimpleProduct1.id$$"/> </actionGroup> <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToDefaultStoreView"> <argument name="storeView" value="_defaultStore.name"/> </actionGroup> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> - <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> + <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createSimpleProduct2.name$$" stepKey="fillNewName"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> - <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterGridByName"> - <argument name="product" value="SimpleProduct"/> + <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterGridByName"> + <argument name="name" value="$$createSimpleProduct2.name$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.firstProductRow}}" userInput="{{SimpleProduct2.name}}" stepKey="seeProductNameInGrid"/> + <seeElement selector="{{AdminProductGridSection.productRowBySku('$$createSimpleProduct2.sku$$')}}" stepKey="seeProduct2InGrid"/> + <dontSeeElement selector="{{AdminProductGridSection.productRowBySku('$$createSimpleProduct1.sku$$')}}" stepKey="dontSeeProduct1InGrid"/> </test> </tests> From ae19b0ddc8d05dc788ccb65c60b7518d48991978 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Wed, 7 Oct 2020 20:41:01 +0300 Subject: [PATCH 0738/1013] MC-38031: Checkout with Multiple Addresses - Review Page does not follow the configured total sort order --- app/code/Magento/Multishipping/Block/Checkout/Overview.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index 3d6d4b195050d..942741e9f7975 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -431,8 +431,11 @@ public function getBillingAddressTotals() */ public function renderTotals($totals, $colspan = null) { - //check if the shipment is multi shipment - $totals = $this->sortTotals($this->getMultishippingTotals($totals)); + // check if the shipment is multi shipment + $totals = $this->getMultishippingTotals($totals); + + // sort totals by configuration settings + $totals = $this->sortTotals($totals); if ($colspan === null) { $colspan = 3; From d0b9f8ee04c2f7f9d0d8562285f505834292ecf6 Mon Sep 17 00:00:00 2001 From: Sagar Dahiwala <sagar.dahiwala@briteskies.com> Date: Wed, 7 Oct 2020 16:19:31 -0400 Subject: [PATCH 0739/1013] magento/partners-magento2b2b#325: Automate Currency availability test for Company Credit - Added data fixture for base currecy of second website --- ...cond_website_with_base_second_currency.php | 47 +++++++++++++++++++ ...ite_with_base_second_currency_rollback.php | 27 +++++++++++ 2 files changed, 74 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php create mode 100644 dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php new file mode 100644 index 0000000000000..abe19edfbf148 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +/** @var \Magento\Config\Model\ResourceModel\Config $configResource */ +$configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); +$configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + 'EUR', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $websiteId +); +$configResource->saveConfig( + \Magento\Catalog\Helper\Data::XML_PATH_PRICE_SCOPE, + \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE, + 'default', + 0 +); + +/** + * Configuration cache clean is required to reload currency setting + */ +/** @var Magento\Config\App\Config\Type\System $config */ +$config = $objectManager->get(\Magento\Config\App\Config\Type\System::class); +$config->clean(); + +$observer = $objectManager->get(\Magento\Framework\Event\Observer::class); +$objectManager->get(\Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange::class) + ->execute($observer); + +/** @var \Magento\Directory\Model\ResourceModel\Currency $rate */ +$rate = $objectManager->create(\Magento\Directory\Model\ResourceModel\Currency::class); +$rate->saveRates([ + 'USD' => ['EUR' => 2], + 'EUR' => ['USD' => 0.5] +]); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php new file mode 100644 index 0000000000000..b1927dabb1189 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Config\Model\ResourceModel\Config $configResource */ +$configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); +$configResource->deleteConfig( + \Magento\Catalog\Helper\Data::XML_PATH_PRICE_SCOPE, + 'default', + 0 +); +$website = $objectManager->create(\Magento\Store\Model\Website::class); +/** @var $website \Magento\Store\Model\Website */ +$websiteId = $website->load('test', 'code')->getId(); +if ($websiteId) { + $configResource->deleteConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $websiteId + ); +} + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); From bc53649c4aaa7cf51ab32dc7fa1000c79263d4ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szubert?= <bartlomiejszubert@gmail.com> Date: Wed, 7 Oct 2020 23:05:07 +0200 Subject: [PATCH 0740/1013] Fix #30296 - fix retrieving send count by ip, add verification of ip conversion into int to integration test --- .../Model/ResourceModel/SendFriend.php | 2 +- .../Magento/SendFriend/Model/SendFriendTest.php | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php index 468fb2ed1af66..edb572dfdd4d1 100644 --- a/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php +++ b/app/code/Magento/SendFriend/Model/ResourceModel/SendFriend.php @@ -43,7 +43,7 @@ public function getSendCount($object, $ip, $startTime, $websiteId = null) AND time>=:time AND website_id=:website_id' ); - $bind = ['ip' => ip2long($ip), 'time' => $startTime, 'website_id' => (int)$websiteId]; + $bind = ['ip' => $ip, 'time' => $startTime, 'website_id' => (int)$websiteId]; $row = $connection->fetchRow($select, $bind); return $row['count']; diff --git a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php index 9117f088e6b8d..6098883959dd3 100644 --- a/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php +++ b/dev/tests/integration/testsuite/Magento/SendFriend/Model/SendFriendTest.php @@ -28,6 +28,9 @@ class SendFriendTest extends TestCase /** @var SendFriend */ private $sendFriend; + /** @var ResourceModel\SendFriend */ + private $sendFriendResource; + /** @var CookieManagerInterface */ private $cookieManager; @@ -43,6 +46,7 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->sendFriend = $this->objectManager->get(SendFriendFactory::class)->create(); + $this->sendFriendResource = $this->objectManager->get(ResourceModel\SendFriend::class); $this->cookieManager = $this->objectManager->get(CookieManagerInterface::class); $this->request = $this->objectManager->get(RequestInterface::class); } @@ -191,10 +195,21 @@ public function testisExceedLimitByCookies(): void */ public function testisExceedLimitByIp(): void { + $remoteAddr = '127.0.0.1'; $parameters = $this->objectManager->create(Parameters::class); - $parameters->set('REMOTE_ADDR', '127.0.0.1'); + $parameters->set('REMOTE_ADDR', $remoteAddr); $this->request->setServer($parameters); $this->assertTrue($this->sendFriend->isExceedLimit()); + // Verify that ip is saved correctly as integer value + $this->assertEquals( + 1, + (int)$this->sendFriendResource->getSendCount( + null, + ip2long($remoteAddr), + time() - (60 * 60 * 24 * 365), + 1 + ) + ); } /** From 549d029752893e5a849b2f359f8203f50947c847 Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Tue, 6 Oct 2020 21:39:14 -0500 Subject: [PATCH 0741/1013] MC-37672: Import Error : CSV : Errors - Adding bengali language for translation --- .../Model/Import/ProductTest.php | 6 ++ ...valid_url_keys_with_different_language.csv | 4 + .../Filter/Test/Unit/TranslitTest.php | 3 +- .../Magento/Framework/Filter/Translit.php | 99 +++++++++++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_check_valid_url_keys_with_different_language.csv 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 a9699ea4a8050..5beba9d52f45c 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -1646,6 +1646,12 @@ public function validateUrlKeysDataProvider() RowValidatorInterface::ERROR_DUPLICATE_URL_KEY => 0 ] ], + [ + 'products_to_check_valid_url_keys_with_different_language.csv', + [ + RowValidatorInterface::ERROR_DUPLICATE_URL_KEY => 0 + ] + ], [ 'products_to_check_duplicated_url_keys.csv', [ diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_check_valid_url_keys_with_different_language.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_check_valid_url_keys_with_different_language.csv new file mode 100644 index 0000000000000..0995eba0b3e90 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_check_valid_url_keys_with_different_language.csv @@ -0,0 +1,4 @@ +sku,product_type,store_view_code,name,price,attribute_set_code,url_key +24-MG04,simple,,"লক্ষ্য এনালগ ওয়াচ টি ২০",25,Default,লক্ষ্য এনালগ ওয়াচ টি ২০ +24-MG01,simple,,"ধৈর্যশীলতা ওয়াচ টি ২০",34,Default,ধৈর্যশীলতা ওয়াচ টি ২০ +24-MG03,simple,,"সামিট ওয়াচ টি ২০",58,Default,সামিট ওয়াচ টি ২০ diff --git a/lib/internal/Magento/Framework/Filter/Test/Unit/TranslitTest.php b/lib/internal/Magento/Framework/Filter/Test/Unit/TranslitTest.php index c5bd097a280b8..58238dbc7de57 100644 --- a/lib/internal/Magento/Framework/Filter/Test/Unit/TranslitTest.php +++ b/lib/internal/Magento/Framework/Filter/Test/Unit/TranslitTest.php @@ -62,7 +62,8 @@ public function filterDataProvider() ' EUR -> ', $isIconv ], - ['™', 'tm', 'tm', $isIconv] + ['™', 'tm', 'tm', $isIconv], + ['লক্ষ্য এনালগ ওয়াচ টি ২০', 'laksoa enaalaga oyaoaca tai 20', 'laksoa enaalaga oyaoaca tai 20', $isIconv] ]; } diff --git a/lib/internal/Magento/Framework/Filter/Translit.php b/lib/internal/Magento/Framework/Filter/Translit.php index a6162aa7a7fff..38ec05d1ee9ba 100644 --- a/lib/internal/Magento/Framework/Filter/Translit.php +++ b/lib/internal/Magento/Framework/Filter/Translit.php @@ -399,6 +399,105 @@ class Translit implements \Zend_Filter_Interface 'ώ' => 'o', 'Ω' => 'o', 'Ώ' => 'o', + 'অ' => 'a', + 'আ' => 'aa', + 'ই' => 'i', + 'ঈ' => 'ii', + 'উ' => 'u', + 'ঊ' => 'uu', + 'ঋ' => 'r', + 'ৠ' => 'ri', + 'এ' => 'e', + 'ঐ' => 'ai', + 'ও' => 'o', + 'ঔ' => 'ou', + 'ক' => 'ka', + 'খ' => 'kha', + 'গ' => 'ga', + 'ঘ' => 'gha', + 'ঙ' => 'na', + 'চ' => 'ca', + 'ছ' => 'cha', + 'জ' => 'ja', + 'ঝ' => 'jha', + 'ঞ' => 'na', + 'ট' => 'ta', + 'ঠ' => 'tha', + 'ড' => 'da', + 'ড়' => 'ra', + 'ঢ' => 'dha', + 'ঢ়' => 'rha', + 'ণ' => 'na', + 'ত' => 'ta', + 'ৎ' => 't', + 'থ' => 'tha', + 'দ' => 'da', + 'ধ' => 'dha', + 'ন' => 'na', + 'প' => 'pa', + 'ফ' => 'pha', + 'ব' => 'ba', + 'ভ' => 'bha', + 'ম' => 'ma', + 'য' => 'ya', + 'য়' => 'ya', + 'র' => 'ra', + 'ল' => 'la', + 'শ' => 'sa', + 'ষ' => 'sha', + 'স' => 'sa', + 'হ' => 'ha', + '০' => '0', + '১' => '1', + '২' => '2', + '৩' => '3', + '৪' => '4', + '৫' => '5', + '৬' => '6', + '৭' => '7', + '৮' => '8', + '৯' => '9', + 'ক্ষ' => 'kso', + 'ষ্ণ' => 'sno', + 'জ্ঞ' => 'jno', + 'ঞ্জ' => 'nchho', + 'হ্ম' => 'hmo', + 'ঞ্চ' => 'ncho', + 'ঙ্ক' => 'ngko', + 'ট্ট' => 'tto', + 'ক্ষ্ম' => 'ksmo', + 'হ্ন' => 'hno', + 'হ্ণ' => 'hno', + 'ক্র' => 'kro', + 'গ্ধ' => 'gdho', + 'ত্র' => 'tro', + 'ক্ত' => 'kto', + 'ক্স' => 'kso', + 'ত্ত' => 'tto', + 'ত্ম' => 'tmo', + 'ক্ক' => 'kko', + 'ক্ম' => 'kmo', + 'ক্ল' => 'klo', + 'া' => 'a', + 'ি' => 'i', + 'ী' => 'ee', + 'ু' => 'o', + 'ূ' => 'u', + 'ৃ' => 'ri', + 'ৄ' => 'rii', + 'ে' => 'a', + 'ৈ' => 'ai', + 'ো' => 'o', + 'ৌ' => 'ow', + '্য' => 'a', + '্র' => 'r', + 'ঁ' => 'n', + 'ঃ' => 'oh', + '়' => 'o', + '্' => 'h', + 'ং' => 'ng', + 'ৢ' => 'n', + 'ৣ' => 'nn' ]; /** From 7394a5af99f7f44348d364dd5d08504b3b01995c Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 8 Oct 2020 10:16:05 +0300 Subject: [PATCH 0742/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- .../Catalog/Model/Product/Option/Value.php | 6 +- .../Unit/Model/Product/Option/ValueTest.php | 5 +- ...SimpleProductWithSelectFixedMethodTest.xml | 38 ++++++------- ...ForSimpleProductsWithCustomOptionsTest.xml | 55 ++++++++++--------- 4 files changed, 55 insertions(+), 49 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 638eaca328ff5..12b418c33deec 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -84,7 +84,7 @@ 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 + * @param CalculateCustomOptionCatalogRule|null $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\Model\Context $context, @@ -94,12 +94,12 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], CustomOptionPriceCalculator $customOptionPriceCalculator = null, - CalculateCustomOptionCatalogRule $CalculateCustomOptionCatalogRule = null + CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null ) { $this->_valueCollectionFactory = $valueCollectionFactory; $this->customOptionPriceCalculator = $customOptionPriceCalculator ?? ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); - $this->calculateCustomOptionCatalogRule = $CalculateCustomOptionCatalogRule + $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); parent::__construct( 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 e084a8cbbde27..d4c1db4ec1b28 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 @@ -34,6 +34,9 @@ class ValueTest extends TestCase */ private $calculateCustomOptionCatalogRule; + /** + * @inheritDoc + */ protected function setUp(): void { $mockedResource = $this->getMockedResource(); @@ -49,7 +52,7 @@ protected function setUp(): void [ 'resource' => $mockedResource, 'valueCollectionFactory' => $mockedCollectionFactory, - 'CalculateCustomOptionCatalogRule' => $this->calculateCustomOptionCatalogRule + 'calculateCustomOptionCatalogRule' => $this->calculateCustomOptionCatalogRule ] ); $this->model->setOption($this->getMockedOption()); diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index cb1e20aa2b227..e96526c5cd256 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -14,7 +14,7 @@ <title value="Admin should be able to apply the catalog price rule for simple product with custom options"/> <description value="Admin should be able to apply the catalog price rule for simple product with custom options"/> <severity value="CRITICAL"/> - <testCaseId value="MC-14771"/> + <testCaseId value="MC-28347"/> <group value="catalogRule"/> <group value="mtf_migrated"/> <group value="catalog"/> @@ -24,13 +24,13 @@ <createData entity="_defaultCategory" stepKey="createCategory"/> <!-- Create Simple Product --> - <createData entity="_defaultProduct" stepKey="createProduct1"> + <createData entity="_defaultProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> <field key="price">56.78</field> </createData> <!-- Update all products to have custom options --> - <updateData createDataKey="createProduct1" entity="productWithFixedOptions" stepKey="updateFirstProductWithOptions"/> + <updateData createDataKey="createProduct" entity="productWithFixedOptions" stepKey="updateProductWithOptions"/> <!-- Login as Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -43,7 +43,7 @@ </before> <after> <!-- Delete products and category --> - <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete the catalog price rule --> @@ -52,11 +52,11 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <!-- 1. Begin creating a new catalog price rule --> - <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="startCreatingFirstPriceRule"/> - <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForFirstPriceRule"> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRulePage"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForCatalogPriceRule"> <argument name="groups" value="'NOT LOGGED IN'"/> </actionGroup> - <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="createCatalogPriceRule"> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="fillConditionsForCatalogPriceRule"> <argument name="conditionValue" value="$createCategory.id$"/> </actionGroup> <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> @@ -70,31 +70,31 @@ <argument name="category" value="$createCategory$"/> </actionGroup> - <!-- Check product 1 name on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Name"> - <argument name="productInfo" value="$createProduct1.name$"/> + <!-- Check product name on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="assertStorefrontProductName"> + <argument name="productInfo" value="$createProduct.name$"/> <argument name="productNumber" value="1"/> </actionGroup> - <!-- Check product 1 price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> + <!-- Check product price on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="assertStorefrontProductPrice"> <argument name="productInfo" value="$44.48"/> <argument name="productNumber" value="1"/> </actionGroup> - <!-- Check product 1 regular price on store front category page --> - <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> + <!-- Check product regular price on store front category page --> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="assertStorefrontProductRegularPrice"> <argument name="productInfo" value="$56.78"/> <argument name="productNumber" value="1"/> </actionGroup> <!-- Navigate to product on store front --> - <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage1"> - <argument name="productUrlKey" value="$createProduct1.custom_attributes[url_key]$"/> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage"> + <argument name="productUrlKey" value="$createProduct.custom_attributes[url_key]$"/> </actionGroup> <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> - <actionGroup ref="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> + <actionGroup ref="StorefrontSelectCustomOptionRadioAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices"> <argument name="customOption" value="ProductOptionRadioButton2"/> <argument name="customOptionValue" value="ProductOptionValueRadioButtons1"/> <argument name="productPrice" value="$156.77"/> @@ -102,8 +102,8 @@ </actionGroup> <!-- Add product 1 to cart --> - <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> - <argument name="productName" value="$createProduct1.name$"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> </actionGroup> <!-- Assert sub total on mini shopping cart --> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml index 4cfbae45b4dcc..92567c38ca152 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -14,13 +14,12 @@ <title value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> <description value="Admin should be able to apply the catalog price rule for simple product with 3 custom options"/> <severity value="CRITICAL"/> - <testCaseId value="MC-14769"/> + <testCaseId value="MC-28345"/> <group value="catalogRule"/> <group value="mtf_migrated"/> <group value="catalog"/> </annotations> <before> - <!-- Login as Admin --> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="_defaultProduct" stepKey="createProduct1"> <requiredEntity createDataKey="createCategory"/> @@ -36,10 +35,14 @@ </createData> <!-- Update all products to have custom options --> - <updateData createDataKey="createProduct1" entity="productWithCustomOptions" stepKey="updateProductWithOptions1"/> - <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProductWithOptions2"/> - <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProductWithOptions3"/> + <updateData createDataKey="createProduct1" entity="productWithCustomOptions" stepKey="updateProduc1tWithOptions"/> + <updateData createDataKey="createProduct2" entity="productWithCustomOptions" stepKey="updateProduct2WithOptions"/> + <updateData createDataKey="createProduct3" entity="productWithCustomOptions" stepKey="updateProduct3WithOptions"/> + + <!-- Login as Admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Clear all catalog price rules before test --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> </before> <after> @@ -54,14 +57,14 @@ <magentoCron groups="index" stepKey="fixInvalidatedIndices"/> <!-- Logout --> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <!-- 1. Begin creating a new catalog price rule --> - <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="startCreatingFirstPriceRule"/> - <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForFirstPriceRule"> + <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="openNewCatalogPriceRulePage"/> + <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForCatalogPriceRule"> <argument name="groups" value="'NOT LOGGED IN'"/> </actionGroup> - <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="createCatalogPriceRule"> + <actionGroup ref="AdminFillCatalogRuleConditionActionGroup" stepKey="fillConditionsForCatalogPriceRule"> <argument name="conditionValue" value="$createCategory.id$"/> </actionGroup> <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> @@ -76,86 +79,86 @@ </actionGroup> <!-- Check product 1 price on store front category page --> - <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct1Price"> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct1Price"> <argument name="productName" value="$createProduct1.name$"/> <argument name="productPrice" value="$51.10"/> </actionGroup> <!-- Check product 1 regular price on store front category page --> - <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct1RegularPrice"> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct1RegularPrice"> <argument name="productName" value="$createProduct1.name$"/> <argument name="productPrice" value="$56.78"/> </actionGroup> <!-- Check product 2 price on store front category page --> - <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct2Price"> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct2Price"> <argument name="productName" value="$createProduct2.name$"/> <argument name="productPrice" value="$51.10"/> </actionGroup> <!-- Check product 2 regular price on store front category page --> - <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct2RegularPrice"> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct2RegularPrice"> <argument name="productName" value="$createProduct2.name$"/> <argument name="productPrice" value="$56.78"/> </actionGroup> <!-- Check product 3 price on store front category page --> - <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct3Price"> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct3Price"> <argument name="productName" value="$createProduct3.name$"/> <argument name="productPrice" value="$51.10"/> </actionGroup> <!-- Check product 3 regular price on store front category page --> - <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="storefrontProduct3RegularPrice"> + <actionGroup ref="StorefrontAssertProductPriceOnCategoryPageActionGroup" stepKey="assertStorefrontProduct3RegularPrice"> <argument name="productName" value="$createProduct3.name$"/> <argument name="productPrice" value="$56.78"/> </actionGroup> <!-- Navigate to product 1 on store front --> - <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage1"> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProduct1Page"> <argument name="productUrlKey" value="$createProduct1.custom_attributes[url_key]$"/> </actionGroup> - <!-- Assert regular and special price after selecting ProductOptionValueDropdown1 --> - <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices1"> + <!-- Assert regular and special price for product 1 after selecting ProductOptionValueDropdown1 --> + <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertProduct1Prices"> <argument name="customOption" value="{{ProductOptionValueDropdown1.title}} +$0.01"/> <argument name="productPrice" value="$56.79"/> <argument name="productFinalPrice" value="$51.11"/> </actionGroup> <!-- Add product 1 to cart --> - <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage1"> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProduct1Page"> <argument name="productName" value="$createProduct1.name$"/> </actionGroup> <!-- Navigate to product 2 on store front --> - <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage2"> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProduct2Page"> <argument name="productUrlKey" value="$createProduct2.custom_attributes[url_key]$"/> </actionGroup> - <!-- Assert regular and special price after selecting ProductOptionValueDropdown3 --> - <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertPrices2"> + <!-- Assert regular and special price for product 2 after selecting ProductOptionValueDropdown3 --> + <actionGroup ref="StorefrontSelectCustomOptionDropDownAndAssertPricesActionGroup" stepKey="storefrontSelectCustomOptionAndAssertProduct2Prices"> <argument name="customOption" value="{{ProductOptionValueDropdown3.title}} +$5.11"/> <argument name="productPrice" value="$62.46"/> <argument name="productFinalPrice" value="$56.21"/> </actionGroup> <!-- Add product 2 to cart --> - <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProduct2Page"> <argument name="productName" value="$createProduct2.name$"/> </actionGroup> <!-- Navigate to product 3 on store front --> - <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProductPage3"> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="goToProduct3Page"> <argument name="productUrlKey" value="$createProduct3.custom_attributes[url_key]$"/> </actionGroup> <!-- Add product 3 to cart with no custom option --> - <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage3"> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProduct3Page"> <argument name="productName" value="$createProduct3.name$"/> </actionGroup> - <!-- Assert sub total on mini shopping cart --> + <!-- Assert subtotal on mini shopping cart --> <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> <argument name="subTotal" value="$158.42"/> </actionGroup> From 8ce920244fe83464bf8fe34158bd6b766b8dfc21 Mon Sep 17 00:00:00 2001 From: DmytroPaidych <dimonovp@gmail.com> Date: Thu, 8 Oct 2020 11:16:06 +0200 Subject: [PATCH 0743/1013] MC-23568: Flaky MFTF test: AdminCreateCustomProductAttributeWithDropdownFieldTest --- ...dOptionForDropdownAttributeActionGroup.xml | 26 ++++++ ...uctAttributeInAttributeGridActionGroup.xml | 26 ++++++ ...tAttributeOnProductEditPageActionGroup.xml | 24 +++++ ...oductAttributeOnProductPageActionGroup.xml | 30 ++++++ .../AdminCreateNewProductAttributeSection.xml | 2 +- .../AdminProductAttributeGridSection.xml | 3 +- .../AdminProductFormSection.xml | 5 +- ...mProductAttributeWithDropdownFieldTest.xml | 6 +- ...VerifyCreateCustomProductAttributeTest.xml | 92 +++++++++++++++++++ 9 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddOptionForDropdownAttributeActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeInAttributeGridActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeOnProductEditPageActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminStartCreateProductAttributeOnProductPageActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddOptionForDropdownAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddOptionForDropdownAttributeActionGroup.xml new file mode 100644 index 0000000000000..189370b03fcee --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAddOptionForDropdownAttributeActionGroup.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="AdminAddOptionForDropdownAttributeActionGroup"> + <annotations> + <description>Click on new value of selector attribute and fill the values for storefront view, and admin product edit page</description> + </annotations> + <arguments> + <argument name="storefrontViewAttributeValue" defaultValue="{{ProductAttributeOption8.label}}" type="string" /> + <argument name="adminAttributeValue" defaultValue="{{ProductAttributeOption8.label}}" type="string" /> + <argument name="rowNumber" defaultValue="0" type="string"/> + </arguments> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="scrollToOption"/> + <click selector="{{AdminCreateNewProductAttributeSection.addValue}}" stepKey="clickOnAddValueButton"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultStoreView(rowNumber)}}" stepKey="waitForDefaultStoreViewToVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultStoreView(rowNumber)}}" userInput="{{storefrontViewAttributeValue}}" stepKey="fillDefaultStoreView"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.adminOption(rowNumber)}}" userInput="{{adminAttributeValue}}" stepKey="fillAdminField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeInAttributeGridActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeInAttributeGridActionGroup.xml new file mode 100644 index 0000000000000..d00a0a01c78b9 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeInAttributeGridActionGroup.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="AdminAssertProductAttributeInAttributeGridActionGroup"> + <annotations> + <description>Assert columns label, visible, searchable and comparable for attribute on the product attribute grid</description> + </annotations> + <arguments> + <argument name="productAttributeLabel" type="string"/> + <argument name="productAttributeVisible" defaultValue="Yes" type="string"/> + <argument name="productAttributeSearch" defaultValue="Yes" type="string"/> + <argument name="productAttributeCompare" defaultValue="No" type="string"/> + </arguments> + <see selector="{{AdminProductAttributeGridSection.defaultLabelColumn}}" userInput="{{productAttributeLabel}}" stepKey="seeDefaultLabel"/> + <see selector="{{AdminProductAttributeGridSection.isVisibleColumn}}" userInput="{{productAttributeVisible}}" stepKey="seeIsVisibleColumn"/> + <see selector="{{AdminProductAttributeGridSection.isSearchableColumn}}" userInput="{{productAttributeSearch}}" stepKey="seeSearchableColumn"/> + <see selector="{{AdminProductAttributeGridSection.isComparableColumn}}" userInput="{{productAttributeCompare}}" stepKey="seeComparableColumn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeOnProductEditPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeOnProductEditPageActionGroup.xml new file mode 100644 index 0000000000000..faf9d4f40648d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminAssertProductAttributeOnProductEditPageActionGroup.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="AdminAssertProductAttributeOnProductEditPageActionGroup"> + <annotations> + <description>Assert if product attribute present on the product Create/Edit page</description> + </annotations> + <arguments> + <argument name="attributeCode" defaultValue="{{newProductAttribute.attribute_code}}" type="string"/> + <argument name="attributeLabel" defaultValue="{{ProductAttributeFrontendLabel.label}}" type="string"/> + </arguments> + <conditionalClick selector="{{AdminProductFormSection.attributeTab}}" dependentSelector="{{AdminProductFormSection.attributeTabOpened}}" visible="false" stepKey="clickToOpen"/> + <scrollTo selector="{{AdminProductFormSection.attributeTab}}" stepKey="scrollToAttributeTab"/> + <seeElement selector="{{AdminProductFormSection.attributeLabelByText(attributeLabel)}}" stepKey="seeAttributeLabelInProductForm"/> + <seeElement selector="{{AdminProductFormSection.newAddedAttribute(attributeCode)}}" stepKey="seeProductAttributeIsAdded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminStartCreateProductAttributeOnProductPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminStartCreateProductAttributeOnProductPageActionGroup.xml new file mode 100644 index 0000000000000..214a062704282 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminStartCreateProductAttributeOnProductPageActionGroup.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="AdminStartCreateProductAttributeOnProductPageActionGroup"> + <annotations> + <description>On the Create/Edit product page create new Attribute</description> + </annotations> + <arguments> + <argument name="attributeCode" defaultValue="{{newProductAttribute.attribute_code}}" type="string" /> + <argument name="attributeLabel" defaultValue="{{ProductAttributeFrontendLabel.label}}" type="string" /> + <argument name="inputType" defaultValue="Dropdown" type="string" /> + </arguments> + <click selector="{{AdminProductFormSection.addAttributeBtn}}" stepKey="clickOnAddAttribute"/> + <waitForElementVisible selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="waitForCreateBtn"/> + <click selector="{{AdminProductFormSection.createNewAttributeBtn}}" stepKey="clickCreateNewAttributeButton"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" stepKey="waitForLabelInput"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.defaultLabel}}" userInput="{{attributeLabel}}" stepKey="fillAttributeLabel"/> + <selectOption selector="{{AdminCreateNewProductAttributeSection.inputType}}" userInput="{{inputType}}" stepKey="setInputType"/> + <click selector="{{AdminCreateNewProductAttributeSection.advancedAttributeProperties}}" stepKey="clickOnAdvancedAttributeProperties"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" stepKey="waitForAttributeCodeToVisible"/> + <fillField selector="{{AdminCreateNewProductAttributeSection.attributeCode}}" userInput="{{attributeCode}}" stepKey="fillAttributeCode"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml index 2de7bf19fd378..58c64b6273f79 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCreateNewProductAttributeSection.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="AdminCreateNewProductAttributeSection"> - <element name="saveAttribute" type="button" selector="#save"/> + <element name="saveAttribute" type="button" selector="#save" timeout="30"/> <element name="defaultLabel" type="input" selector="input[name='frontend_label[0]']"/> <element name="inputType" type="select" selector="select[name='frontend_input']" timeout="30"/> <element name="addValue" type="button" selector="//button[contains(@data-action,'add_new_row')]" timeout="30"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml index 5efd04eacb719..e4b33ac795559 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductAttributeGridSection.xml @@ -18,10 +18,11 @@ <element name="FilterByAttributeCode" type="input" selector="#attributeGrid_filter_attribute_code"/> <element name="attributeLabelFilter" type="input" selector="//input[@name='frontend_label']"/> <element name="attributeCodeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-attr-code col-attribute_code')]"/> - <element name="defaultLabelColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'col-label col-frontend_label')]"/> + <element name="defaultLabelColumn" type="text" selector="//div[@id='attributeGrid']//table[@id='attributeGrid_table']//tbody//td[contains(@class,'col-label col-frontend_label')]"/> <element name="isVisibleColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_visible')]"/> <element name="scopeColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_global')]"/> <element name="isSearchableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_searchable')]"/> <element name="isComparableColumn" type="text" selector="//div[@id='attributeGrid']//td[contains(@class,'a-center col-is_comparable')]"/> </section> </sections> + diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml index 5bdd3bd5abcc6..1ca051e2f6669 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/AdminProductFormSection.xml @@ -48,12 +48,13 @@ <element name="contentTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Content']"/> <element name="fieldError" type="text" selector="//input[@name='product[{{fieldName}}]']/following-sibling::label[@class='admin__field-error']" parameterized="true"/> <element name="priceFieldError" type="text" selector="//input[@name='product[price]']/parent::div/parent::div/label[@class='admin__field-error']"/> - <element name="addAttributeBtn" type="button" selector="#addAttribute"/> - <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']"/> + <element name="addAttributeBtn" type="button" selector="#addAttribute" timeout="30"/> + <element name="createNewAttributeBtn" type="button" selector="button[data-index='add_new_attribute_button']" timeout="30"/> <element name="save" type="button" selector="#save-button" timeout="30"/> <element name="saveNewAttribute" type="button" selector="//aside[contains(@class, 'create_new_attribute_modal')]//button[@id='save']"/> <element name="successMessage" type="text" selector="#messages"/> <element name="attributeTab" type="button" selector="//strong[contains(@class, 'admin__collapsible-title')]/span[text()='Attributes']"/> + <element name="attributeTabOpened" type="button" selector="//div[contains(@class, 'admin__collapsible-block-wrapper') and contains(@class, '_show') ]//span[text()='Attributes']"/> <element name="attributeLabel" type="input" selector="//input[@name='frontend_label[0]']"/> <element name="frontendInput" type="select" selector="select[name = 'frontend_input']"/> <element name="productFormTab" type="button" selector="//strong[@class='admin__collapsible-title']/span[contains(text(), '{{tabName}}')]" parameterized="true"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml index 758dcee69525e..10cba3ab209ef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -8,16 +8,16 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminCreateCustomProductAttributeWithDropdownFieldTest"> + <test name="AdminCreateCustomProductAttributeWithDropdownFieldTest" deprecated="Use AdminVerifyCreateCustomProductAttributeTest"> <annotations> <stories value="Create product Attribute"/> - <title value="Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> + <title value="DEPRECATED: Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> <description value="login as admin and create configurable product attribute with Dropdown field"/> <severity value="BLOCKER"/> <testCaseId value="MC-10905"/> <group value="mtf_migrated"/> <skip> - <issueId value="MC-15474"/> + <issueId value="DEPRECATED">Use AdminVerifyCreateCustomProductAttributeTest</issueId> </skip> </annotations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.xml new file mode 100644 index 0000000000000..5cf3d9e38ddd4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminVerifyCreateCustomProductAttributeTest.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="AdminVerifyCreateCustomProductAttributeTest"> + <annotations> + <features value="Catalog"/> + <stories value="Create product Attribute"/> + <title value="Create Custom Product Attribute Dropdown Field (Not Required) from Product Page"/> + <description value="login as admin and create simple product with attribute Dropdown field"/> + <severity value="MAJOR"/> + <testCaseId value="MC-26027"/> + <group value="mtf_migrated"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteCreatedAttribute"> + <argument name="productAttributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdminPanel"/> + </after> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="navigateToProductPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <actionGroup ref="AdminStartCreateProductAttributeOnProductPageActionGroup" stepKey="createDropdownAttribute"> + <argument name="attributeCode" value="{{newProductAttribute.attribute_code}}" /> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}" /> + <argument name="inputType" value="Dropdown" /> + </actionGroup> + <scrollTo selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="scrollToStorefrontProperties"/> + <click selector="{{AdminCreateNewProductAttributeSection.storefrontProperties}}" stepKey="clickOnStorefrontProperties"/> + <waitForElementVisible selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="waitForStoreFrontProperties"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.inSearch}}" stepKey="enableInSearchOption"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.advancedSearch}}" stepKey="enableAdvancedSearch"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.visibleOnStorefront}}" stepKey="enableVisibleOnStorefront"/> + <checkOption selector="{{AdminCreateNewProductAttributeSection.sortProductListing}}" stepKey="enableSortProductListing"/> + <actionGroup ref="AdminAddOptionForDropdownAttributeActionGroup" stepKey="createDropdownOption"> + <argument name="storefrontViewAttributeValue" value="{{ProductAttributeOption8.label}}"/> + <argument name="adminAttributeLabel" value="{{ProductAttributeOption8.label}}"/> + </actionGroup> + <checkOption selector="{{AdminCreateNewProductAttributeSection.defaultRadioButton('1')}}" stepKey="selectRadioButton"/> + <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveTheProduct"/> + <actionGroup ref="AdminAssertProductAttributeOnProductEditPageActionGroup" stepKey="adminProductAssertAttribute"> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="attributeCode" value="{{newProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SearchAttributeByCodeOnProductAttributeGridActionGroup" stepKey="searchAttributeByCodeOnProductAttributeGrid"> + <argument name="productAttributeCode" value="{{newProductAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="AdminAssertProductAttributeInAttributeGridActionGroup" stepKey="assertAttributeOnProductAttributesGrid"> + <argument name="productAttributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + </actionGroup> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProductPageOnStorefront"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertUpdatedProductPriceInStorefrontProductPageActionGroup" stepKey="checkProductPriceAndNameInStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + <argument name="expectedPrice" value="$createProduct.price$"/> + </actionGroup> + <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToAttribute"/> + <actionGroup ref="CheckAttributeInMoreInformationTabActionGroup" stepKey="checkAttributeInMoreInformationTab"> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="attributeValue" value="{{ProductAttributeOption8.value}}"/> + </actionGroup> + <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchByProductAttribute"> + <argument name="phrase" value="{{ProductAttributeOption8.value}}"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeWithOptionInLayeredNavigation"> + <argument name="attributeLabel" value="{{ProductAttributeFrontendLabel.label}}"/> + <argument name="attributeOptionLabel" value="{{ProductAttributeOption8.value}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="assertProductPresentOnSearchPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </test> +</tests> From 61cb299119dacdb5f5553cb1b46f038ca32a39fa Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 8 Oct 2020 12:59:33 +0300 Subject: [PATCH 0744/1013] MC-36830: [Issue] Fix for empty category field values in REST calls --- .../Category/Product/Plugin/TableResolver.php | 5 +- .../Product/Plugin/TableResolverTest.php | 77 +++++++++++++++++++ .../Unit/Model/ResourceModel/IndexTest.php | 42 ++++++++++ .../ScopeResolver/IndexScopeResolver.php | 3 + 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php index 936e6163cbcc5..0c0c72b0322dc 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Plugin/TableResolver.php @@ -55,7 +55,10 @@ public function afterGetTableName( string $result, $modelEntity ) { - if (!is_array($modelEntity) && $modelEntity === AbstractAction::MAIN_INDEX_TABLE) { + if (!is_array($modelEntity) && + $modelEntity === AbstractAction::MAIN_INDEX_TABLE && + $this->storeManager->getStore()->getId() + ) { $catalogCategoryProductDimension = new Dimension( \Magento\Store\Model\Store::ENTITY, $this->storeManager->getStore()->getId() diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php new file mode 100644 index 0000000000000..c5018f1aa6313 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Plugin/TableResolverTest.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Category\Product\Plugin; + +use Magento\Catalog\Model\Indexer\Category\Product\Plugin\TableResolver; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\TestCase; + +class TableResolverTest extends TestCase +{ + /** + * Tests replacing catalog_category_product_index table name + * + * @param int $storeId + * @param string $tableName + * @param string $expected + * @dataProvider afterGetTableNameDataProvider + */ + public function testAfterGetTableName(int $storeId, string $tableName, string $expected): void + { + $storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + + $storeMock = $this->getMockBuilder(Store::class) + ->onlyMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $storeMock->method('getId') + ->willReturn($storeId); + + $storeManagerMock->method('getStore')->willReturn($storeMock); + + $tableResolverMock = $this->getMockBuilder(IndexScopeResolver::class) + ->disableOriginalConstructor() + ->getMock(); + $tableResolverMock->method('resolve')->willReturn('catalog_category_product_index_store1'); + + $subjectMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + + $model = new TableResolver($storeManagerMock, $tableResolverMock); + + $this->assertEquals( + $expected, + $model->afterGetTableName($subjectMock, $tableName, 'catalog_category_product_index') + ); + } + + /** + * Data provider for testAfterGetTableName + * + * @return array + */ + public function afterGetTableNameDataProvider(): array + { + return [ + [ + 'storeId' => 1, + 'tableName' => 'catalog_category_product_index', + 'expected' => 'catalog_category_product_index_store1' + ], + [ + 'storeId' => 0, + 'tableName' => 'catalog_category_product_index', + 'expected' => 'catalog_category_product_index' + ], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php index d6f5fb9368378..5aca7e6c2555b 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php @@ -417,6 +417,48 @@ public function testGetCategoryProductIndexData() ); } + /** + * Test getCategoryProductIndexData method for all stores + */ + public function testGetCategoryProductIndexDataForAllStores() + { + $connection = $this->connection; + $select = $this->select; + + $connection->expects($this->any()) + ->method('select') + ->willReturn($select); + + $select->expects($this->any()) + ->method('from') + ->with( + ['catalog_category_product_index'], + ['category_id', 'product_id', 'position', 'store_id'] + )->willReturnSelf(); + + $select->expects($this->any()) + ->method('where') + ->willReturnSelf(); + + $connection->expects($this->once()) + ->method('fetchAll') + ->with($select) + ->willReturn([[ + 'product_id' => 1, + 'category_id' => 1, + 'position' => 1, + ]]); + + $this->assertEquals( + [ + 1 => [ + 1 => 1, + ], + ], + $this->model->getCategoryProductIndexData(0, [1]) + ); + } + /** * Test getMovedCategoryProductIds method */ diff --git a/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php b/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php index a68de6ad36f9a..164d2da3d9123 100644 --- a/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php +++ b/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php @@ -44,6 +44,9 @@ public function resolve($index, array $dimensions) { $tableNameParts = []; foreach ($dimensions as $dimension) { + if (!$dimension->getValue()) { + continue; + } switch ($dimension->getName()) { case 'scope': $tableNameParts[$dimension->getName()] = $dimension->getName() . $this->getScopeId($dimension); From e8a3261dc87e748f49eef9ac774bc7b960e22ac5 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 8 Oct 2020 14:14:40 +0300 Subject: [PATCH 0745/1013] MC-35065: Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page --- ...yCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml | 5 ++--- ...pplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml index e96526c5cd256..c127f19db3749 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductWithSelectFixedMethodTest.xml @@ -37,9 +37,7 @@ <!-- Clear all catalog price rules and reindex before test --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> - <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> - <argument name="indices" value=""/> - </actionGroup> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> </before> <after> <!-- Delete products and category --> @@ -48,6 +46,7 @@ <!-- Delete the catalog price rule --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfter"/> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml index 92567c38ca152..a616a7ab172f1 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontApplyCatalogRuleForSimpleProductsWithCustomOptionsTest.xml @@ -44,6 +44,7 @@ <!-- Clear all catalog price rules before test --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesBeforeTest"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesBeforeTest"/> </before> <after> <!-- Delete products and category --> @@ -54,7 +55,7 @@ <!-- Delete the catalog price rule --> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogRulesAfterTest"/> - <magentoCron groups="index" stepKey="fixInvalidatedIndices"/> + <magentoCron groups="index" stepKey="fixInvalidatedIndicesAfterTest"/> <!-- Logout --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> From 1c1e70e1f96e9e027b667a9c9fc4cb7aca78dc19 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Thu, 8 Oct 2020 16:50:34 +0530 Subject: [PATCH 0746/1013] 30349: Product filter with category_id does not work as expected - fixed total_count and items issues --- .../Model/Resolver/Products/DataProvider/ProductSearch.php | 2 +- .../CollectionProcessor/FilterProcessor/CategoryFilter.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 4807cad54bd50..13bd29e83d87f 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -113,7 +113,7 @@ public function getList( $searchResults = $this->searchResultsFactory->create(); $searchResults->setSearchCriteria($searchCriteriaForCollection); $searchResults->setItems($collection->getItems()); - $searchResults->setTotalCount($searchResult->getTotalCount()); + $searchResults->setTotalCount($collection->getSize()); return $searchResults; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php index f709f8cd6eb72..92888a2775e17 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php @@ -61,7 +61,6 @@ public function apply(Filter $filter, AbstractDb $collection) $category = $this->categoryFactory->create(); $this->categoryResourceModel->load($category, $categoryId); $categoryProducts[$categoryId] = $category->getProductCollection()->getAllIds(); - $collection->addCategoryFilter($category); } $categoryProductIds = array_unique(array_merge(...$categoryProducts)); From 223388d5712478a24f42cdb07b7fc011b67dfdc2 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Thu, 8 Oct 2020 14:58:54 +0300 Subject: [PATCH 0747/1013] MC-24010: Broken integration test RelationTest.php on mainline 2.3 --- .../Indexer/Product/Flat/Action/Full.php | 22 ------------------- .../Product/Flat/Action/RelationTest.php | 14 +++--------- 2 files changed, 3 insertions(+), 33 deletions(-) delete mode 100644 dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php deleted file mode 100644 index 17ffb5cf2748a..0000000000000 --- a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Indexer/Product/Flat/Action/Full.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -declare(strict_types=1); - -namespace Magento\TestFramework\Catalog\Model\Indexer\Product\Flat\Action; - -/** - * Class Full reindex action - */ -class Full extends \Magento\Catalog\Model\Indexer\Product\Flat\Action\Full -{ - /** - * List of product types available in installation - * - * @var array - */ - protected $_productTypes; -} 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 745f71801352b..c9ad7ad720daa 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 @@ -13,8 +13,6 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Catalog\Model\Indexer\Product\Flat\Action\Full as FlatIndexerFull; use Magento\Catalog\Helper\Product\Flat\Indexer; -use Magento\Catalog\Model\Indexer\Product\Flat\TableBuilder; -use Magento\Catalog\Model\Indexer\Product\Flat\FlatTableBuilder; use Magento\Framework\Exception\LocalizedException; use Magento\TestFramework\Helper\Bootstrap; @@ -59,10 +57,6 @@ protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); - $tableBuilderMock = $objectManager->get(TableBuilder::class); - $flatTableBuilderMock = - $objectManager->get(FlatTableBuilder::class); - $this->productIndexerHelper = $objectManager->create( Indexer::class, ['addChildData' => true] @@ -71,8 +65,6 @@ protected function setUp(): void FlatIndexerFull::class, [ 'productHelper' => $this->productIndexerHelper, - 'tableBuilder' => $tableBuilderMock, - 'flatTableBuilder' => $flatTableBuilderMock ] ); $this->storeManager = $objectManager->get(StoreManagerInterface::class); @@ -120,9 +112,9 @@ private function addChildColumns(): void { foreach ($this->storeManager->getStores() as $store) { $flatTable = $this->productIndexerHelper->getFlatTableName($store->getId()); - if ($this->connection->isTableExists($flatTable) && - !$this->connection->tableColumnExists($flatTable, 'child_id') && - !$this->connection->tableColumnExists($flatTable, 'is_child') + if ($this->connection->isTableExists($flatTable) + && !$this->connection->tableColumnExists($flatTable, 'child_id') + && !$this->connection->tableColumnExists($flatTable, 'is_child') ) { $this->connection->addColumn( $flatTable, From 4a59b3e205907feab9957450f5dc3fd972d396c0 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Thu, 8 Oct 2020 15:54:23 +0300 Subject: [PATCH 0748/1013] MC-36830: [Issue] Fix for empty category field values in REST calls --- .../Unit/Model/ResourceModel/IndexTest.php | 42 ------------------- .../ScopeResolver/IndexScopeResolver.php | 3 -- 2 files changed, 45 deletions(-) diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php index 5aca7e6c2555b..d6f5fb9368378 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/ResourceModel/IndexTest.php @@ -417,48 +417,6 @@ public function testGetCategoryProductIndexData() ); } - /** - * Test getCategoryProductIndexData method for all stores - */ - public function testGetCategoryProductIndexDataForAllStores() - { - $connection = $this->connection; - $select = $this->select; - - $connection->expects($this->any()) - ->method('select') - ->willReturn($select); - - $select->expects($this->any()) - ->method('from') - ->with( - ['catalog_category_product_index'], - ['category_id', 'product_id', 'position', 'store_id'] - )->willReturnSelf(); - - $select->expects($this->any()) - ->method('where') - ->willReturnSelf(); - - $connection->expects($this->once()) - ->method('fetchAll') - ->with($select) - ->willReturn([[ - 'product_id' => 1, - 'category_id' => 1, - 'position' => 1, - ]]); - - $this->assertEquals( - [ - 1 => [ - 1 => 1, - ], - ], - $this->model->getCategoryProductIndexData(0, [1]) - ); - } - /** * Test getMovedCategoryProductIds method */ diff --git a/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php b/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php index 164d2da3d9123..a68de6ad36f9a 100644 --- a/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php +++ b/lib/internal/Magento/Framework/Indexer/ScopeResolver/IndexScopeResolver.php @@ -44,9 +44,6 @@ public function resolve($index, array $dimensions) { $tableNameParts = []; foreach ($dimensions as $dimension) { - if (!$dimension->getValue()) { - continue; - } switch ($dimension->getName()) { case 'scope': $tableNameParts[$dimension->getName()] = $dimension->getName() . $this->getScopeId($dimension); From 646fcd474157906c682c9feb6312d84a5f1d9c4b Mon Sep 17 00:00:00 2001 From: Sagar Dahiwala <sagar.dahiwala@briteskies.com> Date: Thu, 8 Oct 2020 12:24:09 -0400 Subject: [PATCH 0749/1013] magento/partners-magento2b2b#325: Automate Currency availability test for Company Credit - Removed additional rates from the data fixture. --- .../_files/second_website_with_base_second_currency.php | 7 ------- .../second_website_with_base_second_currency_rollback.php | 3 ++- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php index abe19edfbf148..3dc610c5fb943 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php @@ -38,10 +38,3 @@ $observer = $objectManager->get(\Magento\Framework\Event\Observer::class); $objectManager->get(\Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange::class) ->execute($observer); - -/** @var \Magento\Directory\Model\ResourceModel\Currency $rate */ -$rate = $objectManager->create(\Magento\Directory\Model\ResourceModel\Currency::class); -$rate->saveRates([ - 'USD' => ['EUR' => 2], - 'EUR' => ['USD' => 0.5] -]); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php index b1927dabb1189..4fac07ae4f51f 100644 --- a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php @@ -3,9 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Config\Model\ResourceModel\Config $configResource */ $configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); $configResource->deleteConfig( From 374593d408c53dc0272310a756a317e90715971e Mon Sep 17 00:00:00 2001 From: tuna <ladiesman9x@gmail.com> Date: Mon, 5 Oct 2020 18:31:28 +0700 Subject: [PATCH 0750/1013] Clean code and remove whitespace in markup up --- .../frontend/templates/layer/filter.phtml | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml b/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml index 6b65d184b462a..83f40ab4911e7 100644 --- a/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml +++ b/app/code/Magento/LayeredNavigation/view/frontend/templates/layer/filter.phtml @@ -4,10 +4,10 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis ?> <?php -/** @var $block \Magento\LayeredNavigation\Block\Navigation\FilterRenderer */ +/** @var \Magento\LayeredNavigation\Block\Navigation\FilterRenderer $block */ +/** @var \Magento\Framework\Escaper $escaper */ /** @var \Magento\LayeredNavigation\ViewModel\Layer\Filter $viewModel */ $viewModel = $block->getData('product_layer_view_model'); ?> @@ -16,28 +16,29 @@ $viewModel = $block->getData('product_layer_view_model'); <?php foreach ($filterItems as $filterItem): ?> <li class="item"> <?php if ($filterItem->getCount() > 0): ?> - <a href="<?= $block->escapeUrl($filterItem->getUrl()) ?>" rel="nofollow"> - <?= /* @noEscape */ $filterItem->getLabel() ?> - <?php if ($viewModel->shouldDisplayProductCountOnLayer()): ?> - <span class="count"><?= /* @noEscape */ (int)$filterItem->getCount() ?> - <span class="filter-count-label"> - <?php if ($filterItem->getCount() == 1): - ?> <?= $block->escapeHtml(__('item')) ?><?php + <a + href="<?= $escaper->escapeUrl($filterItem->getUrl()) ?>" + rel="nofollow" + ><?= /* @noEscape */ $filterItem->getLabel() ?><?php + if ($viewModel->shouldDisplayProductCountOnLayer()): ?><span + class="count"><?= /* @noEscape */ (int) $filterItem->getCount() ?><span + class="filter-count-label"><?php + if ($filterItem->getCount() == 1): ?> + <?= $escaper->escapeHtml(__('item')) ?><?php else: - ?> <?= $block->escapeHtml(__('item')) ?><?php + ?><?= $escaper->escapeHtml(__('item')) ?><?php endif;?></span></span> - <?php endif; ?> - </a> + <?php endif; ?></a> <?php else: ?> - <?= /* @noEscape */ $filterItem->getLabel() ?> - <?php if ($viewModel->shouldDisplayProductCountOnLayer()): ?> - <span class="count"><?= /* @noEscape */ (int)$filterItem->getCount() ?> - <span class="filter-count-label"> - <?php if ($filterItem->getCount() == 1): - ?><?= $block->escapeHtml(__('items')) ?><?php - else: - ?><?= $block->escapeHtml(__('items')) ?><?php - endif;?></span></span> + <?= /* @noEscape */ $filterItem->getLabel() ?><?php + if ($viewModel->shouldDisplayProductCountOnLayer()): ?><span + class="count"><?= /* @noEscape */ (int) $filterItem->getCount() ?><span + class="filter-count-label"><?php + if ($filterItem->getCount() == 1): ?> + <?= $escaper->escapeHtml(__('items')) ?><?php + else: + ?><?= $escaper->escapeHtml(__('items')) ?><?php + endif;?></span></span> <?php endif; ?> <?php endif; ?> </li> From aa034d06859b1bff72121d98d094ec0944084140 Mon Sep 17 00:00:00 2001 From: Viktor Petryk <victor.petryk@transoftgroup.com> Date: Fri, 9 Oct 2020 12:01:48 +0300 Subject: [PATCH 0751/1013] MC-30631: Database sessions pile up --- .../Magento/Framework/Session/Config.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/internal/Magento/Framework/Session/Config.php b/lib/internal/Magento/Framework/Session/Config.php index 296b7944ea4f6..3bb30d0f6ec3e 100644 --- a/lib/internal/Magento/Framework/Session/Config.php +++ b/lib/internal/Magento/Framework/Session/Config.php @@ -28,6 +28,18 @@ class Config implements ConfigInterface /** Configuration path for session cache limiter */ const PARAM_SESSION_CACHE_LIMITER = 'session/cache_limiter'; + /** Configuration path for session garbage collection probability */ + const PARAM_SESSION_GC_PROBABILITY = 'session/gc_probability'; + + /** Configuration path for session garbage collection divisor */ + const PARAM_SESSION_GC_DIVISOR = 'session/gc_divisor'; + + /** + * Configuration path for session garbage collection max lifetime. + * The number of seconds after which data will be seen as 'garbage'. + */ + const PARAM_SESSION_GC_MAXLIFETIME = 'session/gc_maxlifetime'; + /** Configuration path for cookie domain */ const XML_PATH_COOKIE_DOMAIN = 'web/cookie/cookie_domain'; @@ -102,6 +114,7 @@ class Config implements ConfigInterface * @param string $scopeType * @param string $lifetimePath * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function __construct( \Magento\Framework\ValidatorFactory $validatorFactory, @@ -149,6 +162,30 @@ public function __construct( $this->setOption('session.cache_limiter', $cacheLimiter); } + /** + * Session garbage collection probability + */ + $gcProbability = $deploymentConfig->get(self::PARAM_SESSION_GC_PROBABILITY); + if ($gcProbability) { + $this->setOption('session.gc_probability', $gcProbability); + } + + /** + * Session garbage collection divisor + */ + $gcDivisor = $deploymentConfig->get(self::PARAM_SESSION_GC_DIVISOR); + if ($gcDivisor) { + $this->setOption('session.gc_divisor', $gcDivisor); + } + + /** + * Session garbage collection max lifetime + */ + $gcMaxlifetime = $deploymentConfig->get(self::PARAM_SESSION_GC_MAXLIFETIME); + if ($gcMaxlifetime) { + $this->setOption('session.gc_maxlifetime', $gcMaxlifetime); + } + /** * Cookie settings: lifetime, path, domain, httpOnly. These govern settings for the session cookie. */ From c2758f41ba82574273ce9a47786a1ba57eb1eeef Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 9 Oct 2020 12:15:45 +0300 Subject: [PATCH 0752/1013] MC-35740: Using API to capture payment --- .../AddTransactionCommentAfterCapture.php | 63 +++++++++++++++ .../AddTransactionCommentAfterCaptureTest.php | 81 +++++++++++++++++++ app/code/Magento/Sales/etc/webapi_rest/di.xml | 3 + app/code/Magento/Sales/etc/webapi_soap/di.xml | 3 + 4 files changed, 150 insertions(+) create mode 100644 app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php create mode 100644 app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php diff --git a/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php b/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php new file mode 100644 index 0000000000000..256d097b9eef0 --- /dev/null +++ b/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Plugin\Model\Service\Invoice; + +use Magento\Framework\DB\TransactionFactory; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Model\Service\InvoiceService; + +/** + * Plugin to add transaction comment after capture invoice + */ +class AddTransactionCommentAfterCapture +{ + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var TransactionFactory + */ + private $transactionFactory; + + /** + * @param InvoiceRepositoryInterface $invoiceRepository + * @param TransactionFactory $transactionFactory + */ + public function __construct( + InvoiceRepositoryInterface $invoiceRepository, + TransactionFactory $transactionFactory + ) { + $this->transactionFactory = $transactionFactory; + $this->invoiceRepository = $invoiceRepository; + } + + /** + * Add transaction comment to the order after capture invoice + * + * @param InvoiceService $subject + * @param bool $result + * @param int $invoiceId + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSetCapture(InvoiceService $subject, bool $result, $invoiceId): bool + { + if ($result) { + $invoice = $this->invoiceRepository->get($invoiceId); + $invoice->getOrder()->setIsInProcess(true); + $this->transactionFactory->create() + ->addObject($invoice) + ->addObject($invoice->getOrder()) + ->save(); + } + + return $result; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php b/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php new file mode 100644 index 0000000000000..8d1a2f5256370 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Plugin\Model\Service\Invoice; + +use Magento\Framework\DB\Transaction; +use Magento\Framework\DB\TransactionFactory; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Service\InvoiceService; +use Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test to add transaction comment to the order after capture invoice + */ +class AddTransactionCommentAfterCaptureTest extends TestCase +{ + /** + * @var InvoiceRepositoryInterface|MockObject + */ + private $invoiceRepository; + + /** + * @var TransactionFactory|MockObject + */ + private $transactionFactory; + + /** + * @var AddTransactionCommentAfterCapture + */ + private $plugin; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->invoiceRepository = $this->createMock(InvoiceRepositoryInterface::class); + $this->transactionFactory = $this->createMock(TransactionFactory::class); + + $this->plugin = new AddTransactionCommentAfterCapture( + $this->invoiceRepository, + $this->transactionFactory + ); + } + + /** + * Test to add transaction comment after capture invoice + */ + public function testPlugin(): void + { + $result = true; + $invoiceId = 3; + + $orderMock = $this->createMock(Order::class); + $invoiceMock = $this->createMock(Invoice::class); + $invoiceMock->method('getOrder')->willReturn($orderMock); + $this->invoiceRepository->method('get')->with($invoiceId)->willReturn($invoiceMock); + + $transactionMock = $this->createMock(Transaction::class); + $transactionMock->expects($this->at(0))->method('addObject')->with($invoiceMock)->willReturnSelf(); + $transactionMock->expects($this->at(1))->method('addObject')->with($orderMock)->willReturnSelf(); + $transactionMock->expects($this->once())->method('save'); + $this->transactionFactory->method('create')->willReturn($transactionMock); + + /** @var InvoiceService $invoiceService */ + $invoiceService = $this->createMock(InvoiceService::class); + + $this->assertEquals( + $result, + $this->plugin->afterSetCapture($invoiceService, $result, $invoiceId) + ); + } +} diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 1a8478438b04a..71fc2cab22f13 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -22,4 +22,7 @@ <type name="Magento\Sales\Model\Order\ShipmentRepository"> <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> </type> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> + </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 1a8478438b04a..71fc2cab22f13 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -22,4 +22,7 @@ <type name="Magento\Sales\Model\Order\ShipmentRepository"> <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> </type> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> + </type> </config> From d00db30ea32c35aa6b5d76ca080a2f5b77c84ee7 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Fri, 9 Oct 2020 12:23:57 +0300 Subject: [PATCH 0753/1013] MC-37543: Create automated test for "Add static block on a category page" --- .../Catalog/Block/Category/ViewTest.php | 91 +++++++++++++++++++ .../Category/Save/SaveCategoryTest.php | 83 +++++++++++++++++ .../_files/category_with_cms_block.php | 46 ++++++++++ .../category_with_cms_block_rollback.php | 32 +++++++ 4 files changed, 252 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php new file mode 100644 index 0000000000000..d08af2b85a67b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.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\Block\Category; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for view category block. + * + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class ViewTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Registry */ + private $registry; + + /** @var GetCategoryByName */ + private $getCategoryByName; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var LayoutInterface */ + private $layout; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->registry = $this->objectManager->get(Registry::class); + $this->getCategoryByName = $this->objectManager->get(GetCategoryByName::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_category'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_with_cms_block.php + * + * @return void + */ + public function testCmsBlockDisplayedOnCategory(): void + { + $categoryId = $this->getCategoryByName->execute('Category with cms block')->getId(); + $category = $this->categoryRepository->get($categoryId, 1); + $this->registerCategory($category); + $block = $this->layout->createBlock(View::class)->setTemplate('Magento_Catalog::category/cms.phtml'); + $this->assertStringContainsString('<h1>Fixture Block Title</h1>', $block->toHtml()); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('current_category'); + $this->registry->register('current_category', $category); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php new file mode 100644 index 0000000000000..36641e010dfc6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Category\Save; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Test cases related to save category with enabled category flat. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class SaveCategoryTest extends AbstractSaveCategoryTest +{ + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var GetBlockByIdentifierInterface */ + private $getBlockByIdentifier; + + /** @var string */ + private $createdCategoryId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->getBlockByIdentifier = $this->_objectManager->get(GetBlockByIdentifierInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + try { + $this->categoryRepository->deleteByIdentifier($this->createdCategoryId); + } catch (NoSuchEntityException $e) { + //Category already deleted. + } + $this->createdCategoryId = null; + + parent::tearDown(); + + } + + /** + * @magentoDataFixture Magento/Cms/_files/block.php + * + * @return void + */ + public function testCreateCategoryWithCmsBlock(): void + { + $blockId = $this->getBlockByIdentifier->execute('fixture_block', 1)->getId(); + $postData = [ + CategoryInterface::KEY_NAME => 'Category with cms block', + CategoryInterface::KEY_IS_ACTIVE => 1, + CategoryInterface::KEY_INCLUDE_IN_MENU => 1, + 'display_mode' => Category::DM_MIXED, + 'landing_page' => $blockId, + 'available_sort_by' => 1, + 'default_sort_by' => 1, + ]; + $responseData = $this->performSaveCategoryRequest($postData); + $this->assertRequestIsSuccessfullyPerformed($responseData); + $this->createdCategoryId = $responseData['category']['entity_id']; + $category = $this->categoryRepository->get($this->createdCategoryId); + $this->assertEquals($blockId, $category->getLandingPage()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php new file mode 100644 index 0000000000000..03eb767741579 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Model\Category; +use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Cms/_files/block.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var CategoryInterfaceFactory $categoryFactory */ +$categoryFactory = $objectManager->get(CategoryInterfaceFactory::class); +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var DefaultCategory $categoryHelper */ +$categoryHelper = $objectManager->get(DefaultCategory::class); +$currentStoreId = (int)$storeManager->getStore()->getId(); +/** @var GetBlockByIdentifierInterface $getBlockByIdentifierInterface */ +$getBlockByIdentifier = $objectManager->get(GetBlockByIdentifierInterface::class); +$block = $getBlockByIdentifier->execute('fixture_block', $currentStoreId); + +$storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); +$category = $categoryFactory->create(); +$category->setName('Category with cms block') + ->setParentId($categoryHelper->getId()) + ->setLevel(2) + ->setAvailableSortBy('name') + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setDisplayMode(Category::DM_MIXED) + ->setLandingPage($block->getId()); +$categoryRepository->save($category); +$storeManager->setCurrentStore($currentStoreId); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block_rollback.php new file mode 100644 index 0000000000000..4725fde47818c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block_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\CategoryRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var GetCategoryByName $getCategoryByName */ +$getCategoryByName = $objectManager->get(GetCategoryByName::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$category = $getCategoryByName->execute('Category with cms block'); +if ($category->getId()) { + $categoryRepository->delete($category); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Cms/_files/block_rollback.php'); From fc47ae2420d64f3f070007a43335781d466340c5 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Fri, 9 Oct 2020 15:10:01 +0300 Subject: [PATCH 0754/1013] MC-37665: Updating a category through the REST API will uncheck "Use Default Value" on a bunch of attributes --- .../Catalog/Model/CategoryRepository.php | 57 +++++--- .../CategoryRepository/PopulateWithValues.php | 123 ++++++++++++++++++ .../Catalog/Api/CategoryRepositoryTest.php | 97 +++++++++++++- 3 files changed, 252 insertions(+), 25 deletions(-) create mode 100644 app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index 0ce52b966c32c..fe3ae4cc468a1 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -7,10 +7,16 @@ namespace Magento\Catalog\Model; +use Magento\Catalog\Model\CategoryRepository\PopulateWithValues; +use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\Framework\Api\ExtensibleDataObjectConverter; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Repository for categories. @@ -25,27 +31,27 @@ class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInter protected $instances = []; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Catalog\Model\CategoryFactory + * @var CategoryFactory */ protected $categoryFactory; /** - * @var \Magento\Catalog\Model\ResourceModel\Category + * @var CategoryResource */ protected $categoryResource; /** - * @var \Magento\Framework\EntityManager\MetadataPool + * @var MetadataPool */ protected $metadataPool; /** - * @var \Magento\Framework\Api\ExtensibleDataObjectConverter + * @var ExtensibleDataObjectConverter */ private $extensibleDataObjectConverter; @@ -57,28 +63,37 @@ class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInter protected $useConfigFields = ['available_sort_by', 'default_sort_by', 'filter_price_range']; /** - * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory - * @param \Magento\Catalog\Model\ResourceModel\Category $categoryResource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager + * @var PopulateWithValues + */ + private $populateWithValues; + + /** + * @param CategoryFactory $categoryFactory + * @param CategoryResource $categoryResource + * @param StoreManagerInterface $storeManager + * @param PopulateWithValues $populateWithValues */ public function __construct( - \Magento\Catalog\Model\CategoryFactory $categoryFactory, - \Magento\Catalog\Model\ResourceModel\Category $categoryResource, - \Magento\Store\Model\StoreManagerInterface $storeManager + CategoryFactory $categoryFactory, + CategoryResource $categoryResource, + StoreManagerInterface $storeManager, + PopulateWithValues $populateWithValues ) { $this->categoryFactory = $categoryFactory; $this->categoryResource = $categoryResource; $this->storeManager = $storeManager; + $objectManager = ObjectManager::getInstance(); + $this->populateWithValues = $populateWithValues ?? $objectManager->get(PopulateWithValues::class); } /** * @inheritdoc */ - public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) + public function save(CategoryInterface $category) { $storeId = (int)$this->storeManager->getStore()->getId(); $existingData = $this->getExtensibleDataObjectConverter() - ->toNestedArray($category, [], \Magento\Catalog\Api\Data\CategoryInterface::class); + ->toNestedArray($category, [], CategoryInterface::class); $existingData = array_diff_key($existingData, array_flip(['path', 'level', 'parent_id'])); $existingData['store_id'] = $storeId; @@ -110,7 +125,7 @@ public function save(\Magento\Catalog\Api\Data\CategoryInterface $category) $existingData['parent_id'] = $parentId; $existingData['level'] = null; } - $category->addData($existingData); + $this->populateWithValues->execute($category, $existingData); try { $this->validateCategory($category); $this->categoryResource->save($category); @@ -151,7 +166,7 @@ public function get($categoryId, $storeId = null) /** * @inheritdoc */ - public function delete(\Magento\Catalog\Api\Data\CategoryInterface $category) + public function delete(CategoryInterface $category) { try { $categoryId = $category->getId(); @@ -213,15 +228,15 @@ protected function validateCategory(Category $category) /** * Lazy loader for the converter. * - * @return \Magento\Framework\Api\ExtensibleDataObjectConverter + * @return ExtensibleDataObjectConverter * * @deprecated 101.0.0 */ private function getExtensibleDataObjectConverter() { if ($this->extensibleDataObjectConverter === null) { - $this->extensibleDataObjectConverter = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Api\ExtensibleDataObjectConverter::class); + $this->extensibleDataObjectConverter = ObjectManager::getInstance() + ->get(ExtensibleDataObjectConverter::class); } return $this->extensibleDataObjectConverter; } @@ -229,13 +244,13 @@ private function getExtensibleDataObjectConverter() /** * Lazy loader for the metadata pool. * - * @return \Magento\Framework\EntityManager\MetadataPool + * @return MetadataPool */ private function getMetadataPool() { if (null === $this->metadataPool) { - $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\EntityManager\MetadataPool::class); + $this->metadataPool = ObjectManager::getInstance() + ->get(MetadataPool::class); } return $this->metadataPool; } diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php new file mode 100644 index 0000000000000..6fdde51bd60de --- /dev/null +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -0,0 +1,123 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\CategoryRepository; + +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; +use Magento\Catalog\Model\Category; +use Magento\Eav\Api\AttributeRepositoryInterface as AttributeRepository; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; + +/** + * Add data to category entity and populate with default values + */ +class PopulateWithValues +{ + /** + * @var ScopeOverriddenValue + */ + private $scopeOverriddenValue; + + /** + * @var AttributeRepository + */ + private $attributeRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @param ScopeOverriddenValue $scopeOverriddenValue + * @param AttributeRepository $attributeRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterBuilder $filterBuilder + */ + public function __construct( + ScopeOverriddenValue $scopeOverriddenValue, + AttributeRepository $attributeRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + FilterBuilder $filterBuilder + ) { + $this->scopeOverriddenValue = $scopeOverriddenValue; + $this->attributeRepository = $attributeRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->filterBuilder = $filterBuilder; + } + + /** + * Set null to entity default values + * + * @param CategoryInterface $category + * @param array $existingData + * @return void + */ + public function execute(CategoryInterface $category, array $existingData): void + { + $storeId = $existingData['store_id']; + $overriddenValues = array_filter($category->getData(), function ($key) use ($category, $storeId) { + /** @var Category $category */ + return $this->scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + $key, + $storeId + ); + }, ARRAY_FILTER_USE_KEY); + $defaultValues = array_diff_key($category->getData(), $overriddenValues); + array_walk($defaultValues, function (&$value, $key) { + $attributes = $this->getAttributes(); + if (isset($attributes[$key]) && !$attributes[$key]->isStatic()) { + $value = null; + } + }); + $category->addData($defaultValues); + $category->addData($existingData); + $useDefaultAttributes = array_filter($category->getData(), function ($attributeValue) { + return null === $attributeValue; + }); + $category->setData('use_default', array_map(function () { + return true; + }, $useDefaultAttributes)); + } + + /** + * Returns entity attributes. + * + * @return AttributeInterface[] + */ + private function getAttributes() + { + $searchResult = $this->attributeRepository->getList( + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('is_global') + ->setConditionType('in') + ->setValue([ScopedAttributeInterface::SCOPE_STORE, ScopedAttributeInterface::SCOPE_WEBSITE]) + ->create() + ] + )->create() + ); + $result = []; + foreach ($searchResult->getItems() as $attribute) { + $result[$attribute->getAttributeCode()] = $attribute; + } + + return $result; + } +} 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 fd0519ab2b34e..aaf03e82551fd 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -10,10 +10,14 @@ use Magento\Authorization\Model\RoleFactory; use Magento\Authorization\Model\Rules; use Magento\Authorization\Model\RulesFactory; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Integration\Api\AdminTokenServiceInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\UrlRewrite\Model\Storage\DbStorage; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; /** @@ -145,8 +149,8 @@ public function testCreate() */ public function testDelete() { - /** @var \Magento\UrlRewrite\Model\Storage\DbStorage $storage */ - $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + /** @var DbStorage $storage */ + $storage = Bootstrap::getObjectManager()->get(DbStorage::class); $categoryId = $this->modelId; $data = [ UrlRewrite::ENTITY_ID => $categoryId, @@ -290,7 +294,7 @@ public function testUpdateUrlKey() $this->assertEquals("Update Category Test New Name", $category->getName()); // check for the url rewrite for the new name - $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $storage = Bootstrap::getObjectManager()->get(DbStorage::class); $data = [ UrlRewrite::ENTITY_ID => $categoryId, UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, @@ -307,7 +311,7 @@ public function testUpdateUrlKey() $this->assertEquals('update-category-test-new-name.html', $urlRewrite->getRequestPath()); // check for the forward from the old name to the new name - $storage = Bootstrap::getObjectManager()->get(\Magento\UrlRewrite\Model\Storage\DbStorage::class); + $storage = Bootstrap::getObjectManager()->get(DbStorage::class); $data = [ UrlRewrite::ENTITY_ID => $categoryId, UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE, @@ -557,6 +561,91 @@ public function testSaveDesign(): void $this->createdCategories = [$result['id']]; } + /** + * Check if repository does not override default values for attributes out of request + * + * @throws \Exception + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testUpdateScopeAttribute() + { + $categoryId = 333; + $categoryData = [ + 'name' => 'Scope Specific Value', + ]; + $result = $this->updateCategoryForSpecificStore($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); + + /** @var ScopeOverriddenValue $scopeOverriddenValue */ + $scopeOverriddenValue = Bootstrap::getObjectManager()->get(ScopeOverriddenValue::class); + self::assertTrue($scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + 'name', + Store::DISTRO_STORE_ID + ), 'Name is not saved for specific store'); + self::assertFalse($scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + 'is_active', + Store::DISTRO_STORE_ID + ), 'is_active is overriden for default store'); + self::assertFalse($scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + 'url_key', + Store::DISTRO_STORE_ID + ), 'url_key is overriden for default store'); + + $this->deleteCategory($categoryId); + } + + /** + * Update given category via web API for specific store code. + * + * @param int $id + * @param array $data + * @param string|null $token + * @param string $storeCode + * @return array + */ + protected function updateCategoryForSpecificStore( + int $id, + array $data, + ?string $token = null, + string $storeCode = 'default' + ) { + $serviceInfo = + [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $id, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + if ($token) { + $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; + } + + if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { + $data['id'] = $id; + + return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data], null, $storeCode); + } else { + $data['id'] = $id; + + return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data], null, $storeCode); + } + } + /** * @inheritDoc * From 1fd701539fef71e05bdcfd7bcf5a09671482d199 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Fri, 9 Oct 2020 16:18:07 +0300 Subject: [PATCH 0755/1013] magento/magento2#26967: Fix for queue numeric argument conversion - integration test update. --- .../Magento/Framework/MessageQueue/TopologyTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php index c8600e1e38faa..5c85d6ddb7c70 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php @@ -48,7 +48,7 @@ public function testTopologyInstallation(array $expectedConfig, array $bindingCo $this->assertArrayHasKey($name, $this->declaredExchanges); unset($this->declaredExchanges[$name]['message_stats']); unset($this->declaredExchanges[$name]['user_who_performed_action']); - $this->assertEquals( + $this->assertSame( $expectedConfig, $this->declaredExchanges[$name], 'Invalid exchange configuration: ' . $name @@ -59,7 +59,7 @@ public function testTopologyInstallation(array $expectedConfig, array $bindingCo unset($value['properties_key']); return $value; }, $bindings); - $this->assertEquals( + $this->assertSame( $bindingConfig, $bindings, 'Invalid exchange bindings configuration: ' . $name @@ -121,7 +121,7 @@ public function exchangeDataProvider() 'arguments' => [ 'argument1' => 'value', 'argument2' => true, - 'argument3' => '150', + 'argument3' => 150, ], ], ] From 78dd01ad38fdd662f954c933c9b8ea77d63e1a10 Mon Sep 17 00:00:00 2001 From: Jeroen <jeroen@reachdigital.nl> Date: Fri, 4 Sep 2020 16:34:59 +0200 Subject: [PATCH 0756/1013] Check if copy method is copy before sending comment email --- app/code/Magento/Sales/Model/Order/Email/NotifySender.php | 2 +- .../Unit/Model/Order/Email/Sender/AbstractSenderTest.php | 2 +- .../Order/Email/Sender/CreditmemoCommentSenderTest.php | 7 +++++-- .../Model/Order/Email/Sender/InvoiceCommentSenderTest.php | 7 +++++-- .../Model/Order/Email/Sender/ShipmentCommentSenderTest.php | 7 +++++-- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/Email/NotifySender.php b/app/code/Magento/Sales/Model/Order/Email/NotifySender.php index 9e40ab769b0a6..1bb3053bf751f 100644 --- a/app/code/Magento/Sales/Model/Order/Email/NotifySender.php +++ b/app/code/Magento/Sales/Model/Order/Email/NotifySender.php @@ -35,7 +35,7 @@ protected function checkAndSend(Order $order, $notify = true) if ($notify) { $sender->send(); - } else { + } elseif ($this->identityContainer->getCopyMethod() === 'copy') { // Email copies are sent as separated emails if their copy method // is 'copy' or a customer should not be notified $sender->sendCopyTo(); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php index b826e058b679e..0494616b8cefd 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php @@ -192,7 +192,7 @@ public function stepIdentityContainerInit($identityMockClassName) { $this->identityContainerMock = $this->getMockBuilder($identityMockClassName) ->disableOriginalConstructor() - ->onlyMethods(['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId']) + ->onlyMethods(['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId', 'getCopyMethod']) ->getMock(); $this->identityContainerMock->expects($this->any()) ->method('getStore') diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php index 0ed2a379f5b73..d191ae9356236 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/CreditmemoCommentSenderTest.php @@ -97,7 +97,7 @@ public function testSendVirtualOrder() $this->assertFalse($result); } - public function testSendTrueWithCustomerCopy() + public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; @@ -140,7 +140,7 @@ public function testSendTrueWithCustomerCopy() $this->assertTrue($result); } - public function testSendTrueWithoutCustomerCopy() + public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; @@ -161,6 +161,9 @@ public function testSendTrueWithoutCustomerCopy() $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->willReturn(true); + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php index 68f2bd7b1a628..9e4e89766dfe1 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php @@ -65,7 +65,7 @@ public function testSendFalse() $this->assertFalse($result); } - public function testSendTrueWithCustomerCopy() + public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $this->stepAddressFormat($billingAddress); @@ -110,7 +110,7 @@ public function testSendTrueWithCustomerCopy() $this->assertTrue($result); } - public function testSendTrueWithoutCustomerCopy() + public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $customerName = 'Test Customer'; @@ -132,6 +132,9 @@ public function testSendTrueWithoutCustomerCopy() $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->willReturn(true); + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); $this->templateContainerMock->expects($this->once()) ->method('setTemplateVars') ->with( diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php index 5421991fae848..91e7ae18d3550 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/ShipmentCommentSenderTest.php @@ -58,7 +58,7 @@ public function testSendFalse() $this->assertFalse($result); } - public function testSendTrueWithCustomerCopy() + public function testSendTrueWithoutCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; @@ -101,7 +101,7 @@ public function testSendTrueWithCustomerCopy() $this->assertTrue($result); } - public function testSendTrueWithoutCustomerCopy() + public function testSendTrueWithCustomerCopy() { $billingAddress = $this->addressMock; $comment = 'comment_test'; @@ -116,6 +116,9 @@ public function testSendTrueWithoutCustomerCopy() $this->identityContainerMock->expects($this->once()) ->method('isEnabled') ->willReturn(true); + $this->identityContainerMock->expects($this->once()) + ->method('getCopyMethod') + ->willReturn('copy'); $this->orderMock->expects($this->any()) ->method('getCustomerName') ->willReturn($customerName); From a535faa775d861570058fd80f9ff0f34bbd7515b Mon Sep 17 00:00:00 2001 From: Your Name <mmdhudasia@gmail.com> Date: Sat, 10 Oct 2020 12:26:16 +0530 Subject: [PATCH 0757/1013] 26133: Fixed - Coupon code text field not display in proper width in Internet Explorer/EDGE browser --- .../luma/Magento_Checkout/web/css/source/module/_cart.less | 7 +++++++ 1 file changed, 7 insertions(+) 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 5d9746317af55..e8f2d1c5eb1ed 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 @@ -727,6 +727,13 @@ position: static; } } + .content { + .fieldset { + .actions-toolbar { + width: auto; + } + } + } &.discount { width: auto; } From af2d29a16e8b7bc30a872d9e20914bdae5b24111 Mon Sep 17 00:00:00 2001 From: Sunil Patel <patelsunil42@gmail.com> Date: Sat, 10 Oct 2020 07:29:20 +0000 Subject: [PATCH 0758/1013] magento/magento2#30388 : fix js error on edit review page --- .../Magento/Review/Block/Adminhtml/Edit.php | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index c85374edb8d98..15110f4f89cad 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Review\Block\Adminhtml; /** @@ -44,7 +45,8 @@ public function __construct( \Magento\Review\Helper\Action\Pager $reviewActionPager, \Magento\Framework\Registry $registry, array $data = [] - ) { + ) + { $this->_coreRegistry = $registry; $this->_reviewActionPager = $reviewActionPager; $this->_reviewFactory = $reviewFactory; @@ -78,12 +80,12 @@ protected function _construct() [ 'label' => __('Previous'), 'onclick' => 'setLocation(\'' . $this->getUrl( - 'review/*/*', - [ - 'id' => $prevId, - 'ret' => $this->getRequest()->getParam('ret'), - ] - ) . '\')' + 'review/*/*', + [ + 'id' => $prevId, + 'ret' => $this->getRequest()->getParam('ret'), + ] + ) . '\')' ], 3, 10 @@ -139,12 +141,12 @@ protected function _construct() [ 'label' => __('Next'), 'onclick' => 'setLocation(\'' . $this->getUrl( - 'review/*/*', - [ - 'id' => $nextId, - 'ret' => $this->getRequest()->getParam('ret'), - ] - ) . '\')' + 'review/*/*', + [ + 'id' => $nextId, + 'ret' => $this->getRequest()->getParam('ret'), + ] + ) . '\')' ], 3, 105 @@ -220,10 +222,13 @@ protected function _construct() ); } } - Event.observe(window, \'load\', function(){ - Event.observe($("select_stores"), \'change\', review.updateRating); - }); '; + if (!$this->_storeManager->hasSingleStore()) { + $this->_formInitScripts[] = 'Event.observe(window, \'load\', function(){ + Event.observe($("select_stores"), \'change\', review.updateRating); + }); + '; + } } /** From e08499bd5083956ac807c4bd93354714bc9005f7 Mon Sep 17 00:00:00 2001 From: Sunil Patel <patelsunil42@gmail.com> Date: Sat, 10 Oct 2020 07:40:39 +0000 Subject: [PATCH 0759/1013] magento/magento2#30388 : fix js error on edit review page --- .../Magento/Review/Block/Adminhtml/Edit.php | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index 15110f4f89cad..c5667af20c82c 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Review\Block\Adminhtml; /** @@ -45,8 +44,7 @@ public function __construct( \Magento\Review\Helper\Action\Pager $reviewActionPager, \Magento\Framework\Registry $registry, array $data = [] - ) - { + ) { $this->_coreRegistry = $registry; $this->_reviewActionPager = $reviewActionPager; $this->_reviewFactory = $reviewFactory; @@ -80,12 +78,12 @@ protected function _construct() [ 'label' => __('Previous'), 'onclick' => 'setLocation(\'' . $this->getUrl( - 'review/*/*', - [ - 'id' => $prevId, - 'ret' => $this->getRequest()->getParam('ret'), - ] - ) . '\')' + 'review/*/*', + [ + 'id' => $prevId, + 'ret' => $this->getRequest()->getParam('ret'), + ] + ) . '\')' ], 3, 10 @@ -141,12 +139,12 @@ protected function _construct() [ 'label' => __('Next'), 'onclick' => 'setLocation(\'' . $this->getUrl( - 'review/*/*', - [ - 'id' => $nextId, - 'ret' => $this->getRequest()->getParam('ret'), - ] - ) . '\')' + 'review/*/*', + [ + 'id' => $nextId, + 'ret' => $this->getRequest()->getParam('ret'), + ] + ) . '\')' ], 3, 105 From a4be3e0e51eae4f8d9ebc4c8b9c833ef656aa779 Mon Sep 17 00:00:00 2001 From: Sunil Patel <patelsunil42@gmail.com> Date: Sat, 10 Oct 2020 09:48:23 +0000 Subject: [PATCH 0760/1013] magento/magento2#30347 : fix js error on delete bundle options --- .../view/adminhtml/web/js/components/bundle-dynamic-rows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js index e9b05182b855d..7f07750812497 100644 --- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js @@ -72,7 +72,7 @@ define([ */ removeBundleItemsFromOption: function (index) { var bundleSelections = registry.get(this.name + '.' + index + '.' + this.bundleSelectionsName), - bundleSelectionsLength = (bundleSelections.elems() || []).length, + bundleSelectionsLength = bundleSelections ? (bundleSelections.elems() || []).length:[], i; if (bundleSelectionsLength) { From a88f5808e69a3823645988cb0a6e3f91fc682272 Mon Sep 17 00:00:00 2001 From: Sunil Patel <patelsunil42@gmail.com> Date: Sat, 10 Oct 2020 09:52:49 +0000 Subject: [PATCH 0761/1013] Revert "magento/magento2#30347 : fix js error on delete bundle options" This reverts commit a4be3e0e51eae4f8d9ebc4c8b9c833ef656aa779. --- .../view/adminhtml/web/js/components/bundle-dynamic-rows.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js index 7f07750812497..e9b05182b855d 100644 --- a/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js +++ b/app/code/Magento/Bundle/view/adminhtml/web/js/components/bundle-dynamic-rows.js @@ -72,7 +72,7 @@ define([ */ removeBundleItemsFromOption: function (index) { var bundleSelections = registry.get(this.name + '.' + index + '.' + this.bundleSelectionsName), - bundleSelectionsLength = bundleSelections ? (bundleSelections.elems() || []).length:[], + bundleSelectionsLength = (bundleSelections.elems() || []).length, i; if (bundleSelectionsLength) { From 7ea472187a74f57588ea5945646cadd356e9da95 Mon Sep 17 00:00:00 2001 From: Sunil Patel <patelsunil42@gmail.com> Date: Sat, 10 Oct 2020 10:58:30 +0000 Subject: [PATCH 0762/1013] magento/magento2#30388 : fix js error on edit review page --- app/code/Magento/Review/Block/Adminhtml/Edit.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit.php b/app/code/Magento/Review/Block/Adminhtml/Edit.php index c5667af20c82c..9162d293f9332 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit.php @@ -222,9 +222,12 @@ protected function _construct() } '; if (!$this->_storeManager->hasSingleStore()) { - $this->_formInitScripts[] = 'Event.observe(window, \'load\', function(){ - Event.observe($("select_stores"), \'change\', review.updateRating); - }); + $this->_formInitScripts[] = ' + require(["jquery","prototype"], function(jQuery){ + Event.observe(window, \'load\', function(){ + Event.observe($("select_stores"), \'change\', review.updateRating); + }); + }) '; } } From 6640b7039bf4eb10163a65510d2ae1edf07f4a46 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Sat, 10 Oct 2020 11:53:58 -0500 Subject: [PATCH 0763/1013] 30179: resetPassword mutation returns generic error - fix static error --- .../testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php index be6513c75b081..b5649cbf3bd64 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/ResetPasswordTest.php @@ -225,7 +225,10 @@ public function testNewPasswordCheckMinLength() public function testNewPasswordCheckCharactersStrength() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('Minimum of different classes of characters in password is 3. Classes of characters: Lower Case, Upper Case, Digits, Special Characters.'); + $this->expectExceptionMessage( + 'Minimum of different classes of characters in password is 3. ' . + 'Classes of characters: Lower Case, Upper Case, Digits, Special Characters.' + ); $query = <<<QUERY mutation { resetPassword ( From 5be9b846a007e0b08e4e3349db4e7903c88fe8ae Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Sun, 11 Oct 2020 09:48:30 +0300 Subject: [PATCH 0764/1013] MC-38113: Same shipping address is repeating multiple times in storefront checkout when Reordered --- ...ckoutFillingShippingSectionActionGroup.xml | 1 + ...CustomerHasNoOtherAddressesActionGroup.xml | 17 ++++++ .../Magento/Sales/Model/AdminOrder/Create.php | 6 ++ .../ActionGroup/AdminReorderActionGroup.xml | 22 +++++++ ...eorderAddressNotSavedInAddressBookTest.xml | 58 +++++++++++++++++++ .../templates/order/create/form/address.phtml | 4 +- 6 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerHasNoOtherAddressesActionGroup.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminReorderActionGroup.xml create mode 100644 app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml index 60188224871eb..e1092a87e4a01 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml @@ -27,6 +27,7 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForShippingLoadingMask"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerHasNoOtherAddressesActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerHasNoOtherAddressesActionGroup.xml new file mode 100644 index 0000000000000..58a5069403b7f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerHasNoOtherAddressesActionGroup.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="AssertCustomerHasNoOtherAddressesActionGroup"> + <annotations> + <description>Verifies customer no additional address in address book</description> + </annotations> + <amOnPage url="customer/address/" stepKey="goToAddressPage"/> + <waitForText userInput="You have no other address entries in your address book." selector=".block-addresses-list" stepKey="assertOtherAddresses"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 393d61b69bf22..80e0ce168d7f5 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -550,6 +550,9 @@ public function initFromOrder(\Magento\Sales\Model\Order $order) $quote = $this->getQuote(); if (!$quote->isVirtual() && $this->getShippingAddress()->getSameAsBilling()) { + $quote->getBillingAddress()->setCustomerAddressId( + $quote->getShippingAddress()->getCustomerAddressId() + ); $this->setShippingAsBilling(1); } @@ -2120,6 +2123,9 @@ private function isAddressesAreEqual(Order $order) $billingData['address_type'], $billingData['entity_id'] ); + if (isset($shippingData['customer_address_id']) && !isset($billingData['customer_address_id'])) { + unset($shippingData['customer_address_id']); + } return $shippingData == $billingData; } diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminReorderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminReorderActionGroup.xml new file mode 100644 index 0000000000000..f4f076f25af8b --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminReorderActionGroup.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="AdminReorderActionGroup"> + <annotations> + <description>Reorder existing order. Requires admin order page to be opened.</description> + </annotations> + + <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> + <waitForPageLoad stepKey="waitPageLoad"/> + + <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmit"/> + <waitForPageLoad stepKey="waitOrderCreated"/> + <waitForText selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeOrderCreatedMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml new file mode 100644 index 0000000000000..aca0c4e6a8f8a --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.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="AdminReorderAddressNotSavedInAddressBookTest"> + <annotations> + <title value="Same shipping address is repeating multiple times in storefront checkout when Reordered"/> + <stories value="MC-38113: Same shipping address is repeating multiple times in storefront checkout when Reordered"/> + <description value="Same shipping address is repeating multiple times in storefront checkout when Reordered"/> + <features value="Sales"/> + <testCaseId value="MC-38113"/> + <severity value="MAJOR"/> + <group value="Sales"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="Category"/> + <createData entity="ApiSimpleProduct" stepKey="Product"> + <requiredEntity createDataKey="Category"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountForm"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + </before> + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <actionGroup ref="DeleteCustomerFromAdminActionGroup" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <deleteData createDataKey="Product" stepKey="deleteProduct"/> + <deleteData createDataKey="Category" stepKey="deleteCategory"/> + </after> + + <!-- Create order for registered customer --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToOrder"> + <argument name="product" value="$Product$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="openCheckoutPage"/> + <actionGroup ref="LoggedInUserCheckoutFillingShippingSectionActionGroup" stepKey="fillAddressForm"/> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="clickPlaceOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Reorder created order --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrderById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminReorderActionGroup" stepKey="reorder"/> + + <!-- Assert no additional addresses saved --> + <actionGroup ref="AssertCustomerHasNoOtherAddressesActionGroup" stepKey="assertAddresses"/> + </test> +</tests> 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 dc007e4801b41..12927dcf526a3 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 @@ -114,7 +114,9 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if (!$block->getDontSaveInAddressBook()): ?> checked="checked"<?php endif; ?> + <?php if (!$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> + 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> From 9d093c9d0e91512caa64b4a3476cbf4476d41b04 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Sun, 11 Oct 2020 10:23:43 +0300 Subject: [PATCH 0765/1013] MC-37665: Updating a category through the REST API will uncheck "Use Default Value" on a bunch of attributes --- .../Catalog/Model/CategoryRepository/PopulateWithValues.php | 2 +- .../testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php index 6fdde51bd60de..410aa3db1f255 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -10,7 +10,7 @@ use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Category; -use Magento\Eav\Api\AttributeRepositoryInterface as AttributeRepository; +use Magento\Catalog\Api\CategoryAttributeRepositoryInterface as AttributeRepository; use Magento\Eav\Api\Data\AttributeInterface; use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Framework\Api\FilterBuilder; 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 aaf03e82551fd..e7d47ff64a109 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -593,13 +593,13 @@ public function testUpdateScopeAttribute() $category, 'is_active', Store::DISTRO_STORE_ID - ), 'is_active is overriden for default store'); + ), 'is_active is overridden for default store'); self::assertFalse($scopeOverriddenValue->containsValue( CategoryInterface::class, $category, 'url_key', Store::DISTRO_STORE_ID - ), 'url_key is overriden for default store'); + ), 'url_key is overridden for default store'); $this->deleteCategory($categoryId); } From 28bcc626117050e1306ac2e00ac190acd0e82f69 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Mon, 12 Oct 2020 09:05:43 +0300 Subject: [PATCH 0766/1013] MC-37543: Create automated test for "Add static block on a category page" --- .../Controller/Adminhtml/Category/Save/SaveCategoryTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php index 36641e010dfc6..155a5f255c15a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php @@ -14,7 +14,7 @@ use Magento\Framework\Exception\NoSuchEntityException; /** - * Test cases related to save category with enabled category flat. + * Test cases for save category controller. * * @magentoAppArea adminhtml * @magentoDbIsolation disabled @@ -54,7 +54,6 @@ protected function tearDown(): void $this->createdCategoryId = null; parent::tearDown(); - } /** From d8a15b946939ac1bc48997bea6b5a99e39057622 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 12 Oct 2020 10:22:09 +0300 Subject: [PATCH 0767/1013] MC-25172: [MFTF] AppConfigDumpSuite Does Not Clean Up in After Steps Which Breaks CLI Configuration Steps In Tests That Run Later in Execution --- app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml | 1 + .../AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml index 762d17bdf87f1..127677ce05e0d 100644 --- a/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml +++ b/app/code/Magento/Config/Test/Mftf/Suite/AppConfigDumpSuite.xml @@ -8,6 +8,7 @@ <suites xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Suite/etc/suiteSchema.xsd"> <suite name="AppConfigDumpSuite"> <before> + <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> <magentoCLI command="app:config:dump" stepKey="configDump"/> </before> <after> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml index 188b12c6a91c3..0c0372850a3c4 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCheckInputFieldsDisabledAfterAppConfigDumpTest.xml @@ -13,13 +13,14 @@ <features value="Configuration"/> <stories value="Disable configuration inputs"/> <title value="Check that all input fields disabled after executing CLI app:config:dump"/> - <description value="Check that all input fields disabled after executing CLI app:config:dump"/> + <description value="Check that all input fields disabled after executing CLI app:config:dump. Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again."/> <severity value="MAJOR"/> <testCaseId value="MC-11158"/> <useCaseId value="MAGETWO-96428"/> <group value="configuration"/> </annotations> <before> + <!-- Command app:config:dump is not reversible and magento instance stays configuration read only after this test. You need to restore etc/env.php manually to make magento configuration writable again.--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> From 79c7f31fc0423fce8e9c1581112467c715af6b8b Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Mon, 12 Oct 2020 10:37:28 +0300 Subject: [PATCH 0768/1013] MC-37665: Updating a category through the REST API will uncheck "Use Default Value" on a bunch of attributes --- .../Test/Unit/Model/CategoryRepositoryTest.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php index 900f630a7434d..61e8133da5759 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php @@ -11,6 +11,7 @@ use Magento\Catalog\Model\Category as CategoryModel; use Magento\Catalog\Model\CategoryFactory; use Magento\Catalog\Model\CategoryRepository; +use Magento\Catalog\Model\CategoryRepository\PopulateWithValues; use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\DataObject; use Magento\Framework\EntityManager\EntityMetadata; @@ -63,6 +64,11 @@ class CategoryRepositoryTest extends TestCase */ protected $metadataPoolMock; + /** + * @var MockObject + */ + protected $populateWithValuesMock; + protected function setUp(): void { $this->categoryFactoryMock = $this->createPartialMock( @@ -94,6 +100,12 @@ protected function setUp(): void ->with(CategoryInterface::class) ->willReturn($metadataMock); + $this->populateWithValuesMock = $this + ->getMockBuilder(PopulateWithValues::class) + ->setMethods(['execute']) + ->disableOriginalConstructor() + ->getMock(); + $this->model = (new ObjectManager($this))->getObject( CategoryRepository::class, [ @@ -102,6 +114,7 @@ protected function setUp(): void 'storeManager' => $this->storeManagerMock, 'metadataPool' => $this->metadataPoolMock, 'extensibleDataObjectConverter' => $this->extensibleDataObjectConverterMock, + 'populateWithValues' => $this->populateWithValuesMock, ] ); } @@ -202,7 +215,7 @@ public function testFilterExtraFieldsOnUpdateCategory($categoryId, $categoryData ->method('toNestedArray') ->willReturn($categoryData); $categoryMock->expects($this->once())->method('validate')->willReturn(true); - $categoryMock->expects($this->once())->method('addData')->with($dataForSave); + $this->populateWithValuesMock->expects($this->once())->method('execute')->with($categoryMock, $dataForSave); $this->categoryResourceMock->expects($this->once()) ->method('save') ->willReturn(DataObject::class); @@ -230,11 +243,11 @@ public function testCreateNewCategory() $categoryMock->expects($this->once())->method('getParentId')->willReturn($parentCategoryId); $parentCategoryMock->expects($this->once())->method('getPath')->willReturn('path'); - $categoryMock->expects($this->once())->method('addData')->with($dataForSave); $categoryMock->expects($this->once())->method('validate')->willReturn(true); $this->categoryResourceMock->expects($this->once()) ->method('save') ->willReturn(DataObject::class); + $this->populateWithValuesMock->expects($this->once())->method('execute')->with($categoryMock, $dataForSave); $this->assertEquals($categoryMock, $this->model->save($categoryMock)); } From c7c4ad60ecf20c04104891d7c0bdaa84bacb8903 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Mon, 12 Oct 2020 11:36:47 +0300 Subject: [PATCH 0769/1013] fix static --- app/code/Magento/Sales/Model/Order/Email/NotifySender.php | 1 + .../Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php | 4 +++- .../Model/Order/Email/Sender/InvoiceCommentSenderTest.php | 2 -- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Sales/Model/Order/Email/NotifySender.php b/app/code/Magento/Sales/Model/Order/Email/NotifySender.php index 1bb3053bf751f..468842d7b2ce4 100644 --- a/app/code/Magento/Sales/Model/Order/Email/NotifySender.php +++ b/app/code/Magento/Sales/Model/Order/Email/NotifySender.php @@ -10,6 +10,7 @@ /** * Class NotifySender + * phpcs:disable Magento2.Classes.AbstractApi * @api * @since 100.0.2 */ diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php index 0494616b8cefd..be7788783adc7 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/AbstractSenderTest.php @@ -192,7 +192,9 @@ public function stepIdentityContainerInit($identityMockClassName) { $this->identityContainerMock = $this->getMockBuilder($identityMockClassName) ->disableOriginalConstructor() - ->onlyMethods(['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId', 'getCopyMethod']) + ->onlyMethods( + ['getStore', 'isEnabled', 'getConfigValue', 'getTemplateId', 'getGuestTemplateId', 'getCopyMethod'] + ) ->getMock(); $this->identityContainerMock->expects($this->any()) ->method('getStore') diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php index 9e4e89766dfe1..56d78789d7dda 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Email/Sender/InvoiceCommentSenderTest.php @@ -31,8 +31,6 @@ protected function setUp(): void $this->stepMockSetup(); $this->paymentHelper = $this->createPartialMock(Data::class, ['getInfoBlockHtml']); - $this->invoiceResource = $this->createMock(Invoice::class); - $this->stepIdentityContainerInit(InvoiceCommentIdentity::class); $this->addressRenderer->expects($this->any())->method('format')->willReturn(1); From d46371a645fa0e62ac37a8c456eff963406119bc Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Mon, 12 Oct 2020 12:10:29 +0300 Subject: [PATCH 0770/1013] MC-37543: Create automated test for "Add static block on a category page" --- .../Catalog/Block/Category/ViewTest.php | 8 +++++- .../Category/Save/SaveCategoryTest.php | 26 ++++++++++++------- .../_files/category_with_cms_block.php | 9 ++++--- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php index d08af2b85a67b..8ff4e29b46dde 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/ViewTest.php @@ -12,6 +12,7 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\View\LayoutInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Catalog\Model\GetCategoryByName; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -39,6 +40,9 @@ class ViewTest extends TestCase /** @var LayoutInterface */ private $layout; + /** @var StoreManagerInterface */ + private $storeManager; + /** * @inheritdoc */ @@ -51,6 +55,7 @@ protected function setUp(): void $this->getCategoryByName = $this->objectManager->get(GetCategoryByName::class); $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); } /** @@ -70,8 +75,9 @@ protected function tearDown(): void */ public function testCmsBlockDisplayedOnCategory(): void { + $storeId = (int)$this->storeManager->getStore('default')->getId(); $categoryId = $this->getCategoryByName->execute('Category with cms block')->getId(); - $category = $this->categoryRepository->get($categoryId, 1); + $category = $this->categoryRepository->get($categoryId, $storeId); $this->registerCategory($category); $block = $this->layout->createBlock(View::class)->setTemplate('Magento_Catalog::category/cms.phtml'); $this->assertStringContainsString('<h1>Fixture Block Title</h1>', $block->toHtml()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php index 155a5f255c15a..adef25f88395c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php @@ -12,12 +12,13 @@ use Magento\Catalog\Model\Category; use Magento\Cms\Api\GetBlockByIdentifierInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; /** * Test cases for save category controller. * * @magentoAppArea adminhtml - * @magentoDbIsolation disabled + * @magentoDbIsolation enabled */ class SaveCategoryTest extends AbstractSaveCategoryTest { @@ -30,6 +31,9 @@ class SaveCategoryTest extends AbstractSaveCategoryTest /** @var string */ private $createdCategoryId; + /** @var StoreManagerInterface */ + private $storeManager; + /** * @inheritdoc */ @@ -39,6 +43,7 @@ protected function setUp(): void $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); $this->getBlockByIdentifier = $this->_objectManager->get(GetBlockByIdentifierInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); } /** @@ -46,12 +51,14 @@ protected function setUp(): void */ protected function tearDown(): void { - try { - $this->categoryRepository->deleteByIdentifier($this->createdCategoryId); - } catch (NoSuchEntityException $e) { - //Category already deleted. + if(!empty($this->createdCategoryId)) { + try { + $this->categoryRepository->deleteByIdentifier($this->createdCategoryId); + } catch (NoSuchEntityException $e) { + //Category already deleted. + } + $this->createdCategoryId = null; } - $this->createdCategoryId = null; parent::tearDown(); } @@ -63,15 +70,16 @@ protected function tearDown(): void */ public function testCreateCategoryWithCmsBlock(): void { - $blockId = $this->getBlockByIdentifier->execute('fixture_block', 1)->getId(); + $storeId = (int)$this->storeManager->getStore('default')->getId(); + $blockId = $this->getBlockByIdentifier->execute('fixture_block', $storeId)->getId(); $postData = [ CategoryInterface::KEY_NAME => 'Category with cms block', CategoryInterface::KEY_IS_ACTIVE => 1, CategoryInterface::KEY_INCLUDE_IN_MENU => 1, 'display_mode' => Category::DM_MIXED, 'landing_page' => $blockId, - 'available_sort_by' => 1, - 'default_sort_by' => 1, + CategoryInterface::KEY_AVAILABLE_SORT_BY => ['position'], + 'default_sort_by' => 'position', ]; $responseData = $this->performSaveCategoryRequest($postData); $this->assertRequestIsSuccessfullyPerformed($responseData); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php index 03eb767741579..417b791eb376a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_with_cms_block.php @@ -31,7 +31,6 @@ $getBlockByIdentifier = $objectManager->get(GetBlockByIdentifierInterface::class); $block = $getBlockByIdentifier->execute('fixture_block', $currentStoreId); -$storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); $category = $categoryFactory->create(); $category->setName('Category with cms block') ->setParentId($categoryHelper->getId()) @@ -42,5 +41,9 @@ ->setPosition(1) ->setDisplayMode(Category::DM_MIXED) ->setLandingPage($block->getId()); -$categoryRepository->save($category); -$storeManager->setCurrentStore($currentStoreId); +try { + $storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); + $categoryRepository->save($category); +} finally { + $storeManager->setCurrentStore($currentStoreId); +} From 4ffbd717b999d3976839cef63f927beefd65976e Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Mon, 12 Oct 2020 14:47:33 +0300 Subject: [PATCH 0771/1013] MC-37558: Create automated test for "Override Category settings on Store View level" --- .../Category/Save/UpdateCategoryTest.php | 114 +++++++++++ .../Catalog/Model/CategoryRepositoryTest.php | 180 +++++++++++------- 2 files changed, 221 insertions(+), 73 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php new file mode 100644 index 0000000000000..c3d5ed080bcf2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php @@ -0,0 +1,114 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Controller\Adminhtml\Category\Save; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Test related to update category. + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class UpdateCategoryTest extends AbstractSaveCategoryTest +{ + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + } + + /** + * @dataProvider categoryDataProvider + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/category.php + * + * @param array $postData + * @return void + */ + public function testUpdateCategoryForDefaultStoreView($postData): void + { + $storeId = (int)$this->storeManager->getStore('default')->getId(); + $postData = array_merge($postData, ['store_id' => $storeId]); + $responseData = $this->performSaveCategoryRequest($postData); + $this->assertRequestIsSuccessfullyPerformed($responseData); + $category = $this->categoryRepository->get($postData['entity_id'], $postData['store_id']); + unset($postData['use_default']); + unset($postData['use_config']); + foreach ($postData as $key => $value) { + $this->assertEquals($value, $category->getData($key)); + } + } + + /** + * @return array + */ + public function categoryDataProvider(): array + { + return [ + [ + 'post_data' => [ + 'entity_id' => 333, + CategoryInterface::KEY_IS_ACTIVE => '0', + CategoryInterface::KEY_INCLUDE_IN_MENU => '0', + CategoryInterface::KEY_NAME => 'Category default store', + 'description' => 'Description for default store', + 'landing_page' => '', + 'display_mode' => Category::DM_MIXED, + CategoryInterface::KEY_AVAILABLE_SORT_BY => ['name', 'price'], + 'default_sort_by' => 'price', + 'filter_price_range' => 5, + 'url_key' => 'default-store-category', + 'meta_title' => 'meta_title default store', + 'meta_keywords' => 'meta_keywords default store', + 'meta_description' => 'meta_description default store', + 'custom_use_parent_settings' => '0', + 'custom_design' => '2', + 'page_layout' => '2columns-right', + 'custom_apply_to_products' => '1', + 'use_default' => [ + CategoryInterface::KEY_NAME => '0', + CategoryInterface::KEY_IS_ACTIVE => '0', + CategoryInterface::KEY_INCLUDE_IN_MENU => '0', + 'url_key' => '0', + 'meta_title' => '0', + 'custom_use_parent_settings' => '0', + 'custom_apply_to_products' => '0', + 'description' => '0', + 'landing_page' => '0', + 'display_mode' => '0', + 'custom_design' => '0', + 'page_layout' => '0', + 'meta_keywords' => '0', + 'meta_description' => '0', + 'custom_layout_update' => '0', + ], + 'use_config' => [ + CategoryInterface::KEY_AVAILABLE_SORT_BY => false, + 'default_sort_by' => false, + 'filter_price_range' => false, + ], + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php index 7fd7627c738d6..6469f80ff49b8 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php @@ -8,93 +8,82 @@ namespace Magento\Catalog\Model; use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Catalog\Api\CategoryRepositoryInterfaceFactory; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\Cms\Api\GetBlockByIdentifierInterface; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\StoreManagementInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** * Provide tests for CategoryRepository model. + * + * @magentoDbIsolation enabled */ class CategoryRepositoryTest extends TestCase { - private const FIXTURE_CATEGORY_ID = 333; - private const FIXTURE_TWO_STORES_CATEGORY_ID = 555; - private const FIXTURE_SECOND_STORE_CODE = 'fixturestore'; - private const FIXTURE_FIRST_STORE_CODE = 'default'; + /** @var ObjectManagerInterface */ + private $objectManager; - /** - * @var CategoryLayoutUpdateManager - */ + /** @var CategoryLayoutUpdateManager */ private $layoutManager; - /** - * @var CategoryRepositoryInterfaceFactory - */ - private $repositoryFactory; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; - /** - * @var CollectionFactory - */ + /** @var CollectionFactory */ private $productCollectionFactory; - /** - * @var CategoryCollectionFactory - */ + /** @var CategoryCollectionFactory */ private $categoryCollectionFactory; + /** @var StoreManagementInterface */ + private $storeManager; + + /** @var GetBlockByIdentifierInterface */ + private $getBlockByIdentifier; + /** - * Sets up common objects. - * - * @inheritDoc + * @inheritdoc */ protected function setUp(): void { - Bootstrap::getObjectManager()->configure([ + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->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); - $this->categoryCollectionFactory = Bootstrap::getObjectManager()->create(CategoryCollectionFactory::class); - } - - /** - * Create subject object. - * - * @return CategoryRepositoryInterface - */ - private function createRepo(): CategoryRepositoryInterface - { - return $this->repositoryFactory->create(); + $this->layoutManager = $this->objectManager->get(CategoryLayoutUpdateManager::class); + $this->productCollectionFactory = $this->objectManager->get(CollectionFactory::class); + $this->categoryCollectionFactory = $this->objectManager->get(CategoryCollectionFactory::class); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->getBlockByIdentifier = $this->objectManager->get(GetBlockByIdentifierInterface::class); } /** * Test that custom layout file attribute is saved. * - * @return void - * @throws \Throwable * @magentoDataFixture Magento/Catalog/_files/category.php - * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * + * @return void */ public function testCustomLayout(): void { - //New valid value - $repo = $this->createRepo(); - $category = $repo->get(self::FIXTURE_CATEGORY_ID); + $category = $this->categoryRepository->get(333); $newFile = 'test'; - $this->layoutManager->setCategoryFakeFiles(self::FIXTURE_CATEGORY_ID, [$newFile]); + $this->layoutManager->setCategoryFakeFiles(333, [$newFile]); $category->setCustomAttribute('custom_layout_update_file', $newFile); - $repo->save($category); - $repo = $this->createRepo(); - $category = $repo->get(self::FIXTURE_CATEGORY_ID); + $this->categoryRepository->save($category); + $category = $this->categoryRepository->get(333); $this->assertEquals($newFile, $category->getCustomAttribute('custom_layout_update_file')->getValue()); //Setting non-existent value @@ -102,7 +91,7 @@ public function testCustomLayout(): void $category->setCustomAttribute('custom_layout_update_file', $newFile); $caughtException = false; try { - $repo->save($category); + $this->categoryRepository->save($category); } catch (LocalizedException $exception) { $caughtException = true; } @@ -112,9 +101,9 @@ public function testCustomLayout(): void /** * Test removal of categories. * - * @magentoDbIsolation enabled * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoAppArea adminhtml + * * @return void */ public function testCategoryBehaviourAfterDelete(): void @@ -122,7 +111,7 @@ public function testCategoryBehaviourAfterDelete(): void $productCollection = $this->productCollectionFactory->create(); $deletedCategories = ['3', '4', '5', '13']; $categoryCollectionIds = $this->categoryCollectionFactory->create()->getAllIds(); - $this->createRepo()->deleteByIdentifier(3); + $this->categoryRepository->deleteByIdentifier(3); $this->assertEquals( 0, $productCollection->addCategoriesFilter(['in' => $deletedCategories])->getSize(), @@ -131,42 +120,87 @@ public function testCategoryBehaviourAfterDelete(): void $newCategoryCollectionIds = $this->categoryCollectionFactory->create()->getAllIds(); $difference = array_diff($categoryCollectionIds, $newCategoryCollectionIds); sort($difference); - $this->assertEquals( - $deletedCategories, - $difference, - 'Wrong categories was deleted' - ); + $this->assertEquals($deletedCategories, $difference, 'Wrong categories was deleted'); } /** * Verifies whether `get()` method `$storeId` attribute works as expected. * - * @magentoDbIsolation enabled * @magentoDataFixture Magento/Store/_files/core_fixturestore.php * @magentoDataFixture Magento/Catalog/_files/category_with_two_stores.php + * + * @return void */ - public function testGetCategoryForProvidedStore() + public function testGetCategoryForProvidedStore(): void { - $categoryRepository = $this->repositoryFactory->create(); - - $categoryDefault = $categoryRepository->get( - self::FIXTURE_TWO_STORES_CATEGORY_ID - ); - + $categoryId = 555; + $categoryDefault = $this->categoryRepository->get($categoryId); $this->assertSame('category-defaultstore', $categoryDefault->getUrlKey()); - - $categoryFirstStore = $categoryRepository->get( - self::FIXTURE_TWO_STORES_CATEGORY_ID, - self::FIXTURE_FIRST_STORE_CODE - ); - + $defaultStoreId = $this->storeManager->getStore('default')->getId(); + $categoryFirstStore = $this->categoryRepository->get($categoryId, $defaultStoreId); $this->assertSame('category-defaultstore', $categoryFirstStore->getUrlKey()); + $fixtureStoreId = $this->storeManager->getStore('fixturestore')->getId(); + $categorySecondStore = $this->categoryRepository->get($categoryId, $fixtureStoreId); + $this->assertSame('category-fixturestore', $categorySecondStore->getUrlKey()); + } - $categorySecondStore = $categoryRepository->get( - self::FIXTURE_TWO_STORES_CATEGORY_ID, - self::FIXTURE_SECOND_STORE_CODE - ); + /** + * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Cms/_files/block.php + * + * @return void + */ + public function testUpdateCategoryDefaultStoreView(): void + { + $categoryId = 333; + $defaultStoreId = (int)$this->storeManager->getStore('default')->getId(); + $secondStoreId = (int)$this->storeManager->getStore('fixture_second_store')->getId(); + $blockId = $this->getBlockByIdentifier->execute('fixture_block', $defaultStoreId)->getId(); + $origData = $this->categoryRepository->get($categoryId)->getData(); + unset($origData[CategoryInterface::KEY_UPDATED_AT]); + $category = $this->categoryRepository->get($categoryId, $defaultStoreId); + $dataForDefaultStore = [ + CategoryInterface::KEY_IS_ACTIVE => 0, + CategoryInterface::KEY_INCLUDE_IN_MENU => 0, + CategoryInterface::KEY_NAME => 'Category default store', + 'image' => 'test.png', + 'description' => 'Description for default store', + 'landing_page' => $blockId, + 'display_mode' => Category::DM_MIXED, + CategoryInterface::KEY_AVAILABLE_SORT_BY => ['name', 'price'], + 'default_sort_by' => 'price', + 'filter_price_range' => 5, + 'url_key' => 'default-store-category', + 'meta_title' => 'meta_title default store', + 'meta_keywords' => 'meta_keywords default store', + 'meta_description' => 'meta_description default store', + 'custom_use_parent_settings' => '0', + 'custom_design' => '2', + 'page_layout' => '2columns-right', + 'custom_apply_to_products' => '1', + ]; + $category->addData($dataForDefaultStore); + $updatedCategory = $this->categoryRepository->save($category); + $this->assertCategoryData($dataForDefaultStore, $updatedCategory); + $categorySecondStore = $this->categoryRepository->get($categoryId, $secondStoreId); + $this->assertCategoryData($origData, $categorySecondStore); + foreach ($dataForDefaultStore as $key => $value) { + $this->assertNotEquals($value, $categorySecondStore->getData($key)); + } + } - $this->assertSame('category-fixturestore', $categorySecondStore->getUrlKey()); + /** + * Assert category data. + * + * @param array $expectedData + * @param CategoryInterface $category + * @return void + */ + private function assertCategoryData(array $expectedData, CategoryInterface $category): void + { + foreach ($expectedData as $key => $value) { + $this->assertEquals($value, $category->getData($key)); + } } } From afe1fc243f2322515abadb0d1d9618324a74b626 Mon Sep 17 00:00:00 2001 From: Viktor Sevch <viktor.sevch@transoftgroup.com> Date: Mon, 12 Oct 2020 14:49:05 +0300 Subject: [PATCH 0772/1013] MC-34292: AdminCreateUserRoleWithReportsActionGroup needs to be refactored to be based in CE --- ...AdminChooseUserRoleResourceActionGroup.xml | 23 ++++ .../AdminSaveUserRoleActionGroup.xml | 18 +++ .../AdminStartCreateUserRoleActionGroup.xml | 26 +++++ .../AdminUserSaveRoleActionGroup.xml | 2 +- .../Mftf/Section/AdminCreateRoleSection.xml | 6 +- .../Mftf/Section/AdminEditRoleInfoSection.xml | 2 +- .../Section/AdminEditRoleResourcesSection.xml | 2 + ...inReviewOrderWithReportsPermissionTest.xml | 109 ++++++++++++++++++ 8 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 app/code/Magento/User/Test/Mftf/ActionGroup/AdminChooseUserRoleResourceActionGroup.xml create mode 100644 app/code/Magento/User/Test/Mftf/ActionGroup/AdminSaveUserRoleActionGroup.xml create mode 100644 app/code/Magento/User/Test/Mftf/ActionGroup/AdminStartCreateUserRoleActionGroup.xml create mode 100644 app/code/Magento/User/Test/Mftf/Test/AdminReviewOrderWithReportsPermissionTest.xml diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminChooseUserRoleResourceActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminChooseUserRoleResourceActionGroup.xml new file mode 100644 index 0000000000000..7072830e2036b --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminChooseUserRoleResourceActionGroup.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="AdminChooseUserRoleResourceActionGroup"> + <annotations> + <description>Check the resource access checkbox. Leave the form open.</description> + </annotations> + <arguments> + <argument name="resourceId" type="string" defaultValue="Magento_Backend::dashboard"/> + <argument name="resourceName" type="string" defaultValue="Dashboard"/> + </arguments> + + <waitForElementVisible selector="{{AdminEditRoleResourcesSection.resourceCheckboxLink(resourceId, resourceName)}}" stepKey="waitForResourceCheckboxVisible"/> + <click selector="{{AdminEditRoleResourcesSection.resourceCheckboxLink(resourceId, resourceName)}}" stepKey="checkResource"/> + <seeCheckboxIsChecked selector="{{AdminEditRoleResourcesSection.resourceCheckbox(resourceId)}}" stepKey="seeCheckedResource"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSaveUserRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSaveUserRoleActionGroup.xml new file mode 100644 index 0000000000000..4a90630161e99 --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminSaveUserRoleActionGroup.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="AdminSaveUserRoleActionGroup"> + <annotations> + <description>Click to Save Role</description> + </annotations> + <click selector="{{AdminEditRoleInfoSection.saveButton}}" stepKey="clickSaveRoleButton" /> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the role." stepKey="seeSuccessMessageForSavedRole"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminStartCreateUserRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminStartCreateUserRoleActionGroup.xml new file mode 100644 index 0000000000000..2a1dec5b8574e --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminStartCreateUserRoleActionGroup.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="AdminStartCreateUserRoleActionGroup"> + <annotations> + <description>Open Admin Edit Role page. Fills role, user password, resource access. Leave the form open.</description> + </annotations> + <arguments> + <argument name="roleName" type="string" defaultValue="{{limitedRole.name}}"/> + <argument name="userPassword" type="string" defaultValue="123123q"/> + <argument name="resourceAccess" type="string" defaultValue="Custom"/> + </arguments> + <amOnPage url="{{AdminEditRolePage.url}}" stepKey="openNewAdminRolePage"/> + <waitForElementVisible selector="{{AdminCreateRoleSection.name}}" stepKey="waitForName"/> + <fillField selector="{{AdminCreateRoleSection.name}}" userInput="{{roleName}}" stepKey="setTheRoleName"/> + <fillField selector="{{AdminCreateRoleSection.password}}" userInput="{{userPassword}}" stepKey="setPassword"/> + <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> + <selectOption selector="{{AdminEditRoleResourcesSection.resourceAccess}}" userInput="{{resourceAccess}}" stepKey="chooseResourceAccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserSaveRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserSaveRoleActionGroup.xml index 824e9407125f5..e247db64deeab 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserSaveRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserSaveRoleActionGroup.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminUserSaveRoleActionGroup"> <annotations> - <description>Click to Save Role</description> + <description>Deprecated. Please use AdminSaveUserRoleActionGroup</description> </annotations> <click selector="{{AdminEditRoleInfoSection.saveButton}}" stepKey="clickSaveRoleButton" /> <see userInput="You saved the role." stepKey="seeUserRoleSavedMessage"/> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml index 93acfc2753b61..96aaf879e2054 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminCreateRoleSection.xml @@ -9,13 +9,13 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCreateRoleSection"> - <element name="create" type="button" selector="#add"/> + <element name="create" type="button" selector="#add" timeout="30"/> <element name="name" type="button" selector="#role_name"/> <element name="password" type="input" selector="#current_password"/> <element name="resourceAccess" type="select" selector="[data-ui-id='adminhtml-user-editroles-tab-content-account'] [name='all']"/> <element name="resourceTree" type="block" selector="[data-ui-id='adminhtml-user-editroles-tab-content-account'] [data-role='resource-tree']"/> - <element name="roleResources" type="button" selector="#role_info_tabs_account"/> + <element name="roleResources" type="button" selector="#role_info_tabs_account" timeout="30"/> <element name="roleResource" type="button" selector="#gws_is_all"/> <element name="roleResourceNew" type="button" selector="#all"/> <element name="resourceValue" type="button" selector="//*[text()='{{arg1}}']" parameterized="true"/> @@ -24,7 +24,7 @@ <element name="scopeValue" type="button" selector="//select[@id='all']/*[text()='{{arg2}}']" parameterized="true"/> <element name="website" type="checkbox" selector="//*[contains(text(), '{{arg3}}')]" parameterized="true"/> <element name="selectWebsite" type="checkbox" selector="//label[contains(text(), '{{websiteName}}')]/preceding-sibling::input" parameterized="true"/> - <element name="save" type="button" selector="//button[@title='Save Role']"/> + <element name="save" type="button" selector="//button[@title='Save Role']" timeout="30"/> <element name="roleNameFilterTextField" type="input" selector="#permissionsUserRolesGrid_filter_role_name"/> <element name="searchButton" type="button" selector=".admin__data-grid-header button[title=Search]"/> <element name="searchResultFirstRow" type="text" selector=".data-grid>tbody>tr"/> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml index 57659e1aff075..b8430eb3b7313 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleInfoSection.xml @@ -13,7 +13,7 @@ <element name="backButton" type="button" selector="button[title='Back']"/> <element name="resetButton" type="button" selector="button[title='Reset']"/> <element name="deleteButton" type="button" selector="button[title='Delete Role']"/> - <element name="saveButton" type="button" selector="button[title='Save Role']"/> + <element name="saveButton" type="button" selector="button[title='Save Role']" timeout="30"/> <element name="message" type="text" selector=".modal-popup.confirm div.modal-content"/> <element name="cancel" type="button" selector=".modal-popup.confirm button.action-dismiss"/> <element name="ok" type="button" selector=".modal-popup.confirm button.action-accept" timeout="60"/> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml index 48873bd9d152e..2352575257afb 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminEditRoleResourcesSection.xml @@ -11,6 +11,8 @@ <element name="resources" type="checkbox" selector="#role_info_tabs_account"/> <element name="storeName" type="checkbox" selector="//label[contains(text(),'{{var1}}')]" parameterized="true"/> <element name="reportsCheckbox" type="text" selector="//li[@data-id='Magento_Reports::report']//a[text()='Reports']"/> + <element name="resourceCheckboxLink" type="checkbox" selector="//li[@data-id='{{resourceId}}']//a[text()='{{resourceName}}']" timeout="30" parameterized="true"/> + <element name="resourceCheckbox" type="checkbox" selector="//li[@data-id='{{resourceId}}']/input" timeout="30" parameterized="true"/> <element name="userRoles" type="text" selector="//span[contains(text(), 'User Roles')]"/> </section> </sections> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminReviewOrderWithReportsPermissionTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminReviewOrderWithReportsPermissionTest.xml new file mode 100644 index 0000000000000..8629187fe3ffb --- /dev/null +++ b/app/code/Magento/User/Test/Mftf/Test/AdminReviewOrderWithReportsPermissionTest.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminReviewOrderWithReportsPermissionTest"> + <annotations> + <features value="User"/> + <stories value="Admin with restricted permissions"/> + <title value="User should be able to review ordered products with only 'Reports' permission"/> + <description value="User should be able to review ordered products with only 'Reports' permission"/> + <severity value="MAJOR"/> + <testCaseId value="MAGETWO-25812"/> + <group value="user"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> + <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> + <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeGroupName" value="{{NewStoreData.name}}"/> + <argument name="storeGroupCode" value="{{NewStoreData.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="NewStoreData"/> + <argument name="customStore" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="AdminCreateCustomerWithWebsiteAndStoreViewActionGroup" stepKey="createCustomerWithWebsiteAndStoreView"> + <argument name="customerData" value="Simple_US_Customer"/> + <argument name="address" value="US_Address_NY"/> + <argument name="website" value="{{NewWebSiteData.name}}"/> + <argument name="storeView" value="{{NewStoreViewData.name}}"/> + </actionGroup> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProductOnAdmin"> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="CreatedProductConnectToWebsiteActionGroup" stepKey="productConnectToWebsite"> + <argument name="website" value="NewWebSiteData"/> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="CreateOrderInStoreChoosingPaymentMethodActionGroup" stepKey="createOrder"> + <argument name="product" value="$createProduct$"/> + <argument name="customer" value="Simple_US_Customer"/> + <argument name="storeView" value="NewStoreViewData"/> + </actionGroup> + <actionGroup ref="AdminStartCreateUserRoleActionGroup" stepKey="startCreateUserRole"> + <argument name="roleName" value="{{limitedRole.name}}"/> + <argument name="userPassword" value="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <argument name="resourceAccess" value="Custom"/> + </actionGroup> + <actionGroup ref="AdminChooseUserRoleResourceActionGroup" stepKey="setResourceAccess"> + <argument name="resourceId" value="Magento_Reports::report"/> + <argument name="resourceName" value="Reports"/> + </actionGroup> + <actionGroup ref="AdminSaveUserRoleActionGroup" stepKey="saveRole"/> + <actionGroup ref="AdminCreateUserWithRoleActionGroup" stepKey="createUser"> + <argument name="role" value="limitedRole"/> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdminUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminDeleteCreatedUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearAdminUserGridFilters"/> + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRoleGrid"/> + <actionGroup ref="AdminDeleteRoleActionGroup" stepKey="deleteRole"> + <argument name="role" value="limitedRole"/> + </actionGroup> + + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer.email"/> + </actionGroup> + <actionGroup ref="AdminClearCustomersFiltersActionGroup" stepKey="clearCustomerFilters"/> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{NewWebSiteData.name}}"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="logAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + <actionGroup ref="AdminReviewOrderActionGroup" stepKey="reviewOrder"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + </test> +</tests> From 732dfebe0a1ac65d264480c186984bc4c5259456 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Mon, 12 Oct 2020 14:54:13 +0300 Subject: [PATCH 0773/1013] MC-37543: Create automated test for "Add static block on a category page" --- .../Controller/Adminhtml/Category/Save/SaveCategoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php index adef25f88395c..dc74a2c2cba7b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/SaveCategoryTest.php @@ -51,7 +51,7 @@ protected function setUp(): void */ protected function tearDown(): void { - if(!empty($this->createdCategoryId)) { + if (!empty($this->createdCategoryId)) { try { $this->categoryRepository->deleteByIdentifier($this->createdCategoryId); } catch (NoSuchEntityException $e) { From 20d7d416e252b8b27141ebdb5e2b768c85d3f84b Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Mon, 12 Oct 2020 15:05:42 +0300 Subject: [PATCH 0774/1013] MC-37546: Create automated test for "Create new Category Update" --- ...rontCheckPresentSubCategoryActionGroup.xml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml new file mode 100644 index 0000000000000..7d8113f05518b --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.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="StorefrontCheckPresentSubCategoryActionGroup"> + <annotations> + <description>Checks for a subcategory in topmenu</description> + </annotations> + <arguments> + <argument name="parenCategoryName" type="string"/> + <argument name="childCategoryName" type="string"/> + </arguments> + + <waitForElement selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(parenCategoryName)}}" stepKey="waitForTopMenuLoaded"/> + <moveMouseOver selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(parenCategoryName)}}" stepKey="moveMouseToParentCategory"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(childCategoryName)}}" stepKey="seeCategoryUpdated"/> + </actionGroup> +</actionGroups> From a37cf33bc2a02671302f66ededb5085f6e911ea4 Mon Sep 17 00:00:00 2001 From: Your Name <mmdhudasia@gmail.com> Date: Mon, 12 Oct 2020 17:51:47 +0530 Subject: [PATCH 0775/1013] fixed spacing issue --- .../web/css/source/module/_cart.less | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 e8f2d1c5eb1ed..405bc1d2af373 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 @@ -267,7 +267,7 @@ .lib-icon-font-symbol( @_icon-font-content: @icon-trash ); - + &:hover { .lib-css(text-decoration, @link__text-decoration); } @@ -574,7 +574,7 @@ .widget { float: left; - + &.block { margin-bottom: @indent__base; } @@ -728,11 +728,11 @@ } } .content { - .fieldset { - .actions-toolbar { - width: auto; + .fieldset { + .actions-toolbar { + width: auto; + } } - } } &.discount { width: auto; From 372049ac123cfc1055850358adb5e88db67cc6ce Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Mon, 12 Oct 2020 16:55:59 +0300 Subject: [PATCH 0776/1013] MC-38113: Same shipping address is repeating multiple times in storefront checkout when Reordered --- ...reFrontCustomerHasNoOtherAddressesActionGroup.xml} | 2 +- .../AdminReorderAddressNotSavedInAddressBookTest.xml | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) rename app/code/Magento/Customer/Test/Mftf/ActionGroup/{AssertCustomerHasNoOtherAddressesActionGroup.xml => AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml} (89%) diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerHasNoOtherAddressesActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml similarity index 89% rename from app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerHasNoOtherAddressesActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml index 58a5069403b7f..2fde4d915c99f 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertCustomerHasNoOtherAddressesActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml @@ -7,7 +7,7 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AssertCustomerHasNoOtherAddressesActionGroup"> + <actionGroup name="AssertStoreFrontCustomerHasNoOtherAddressesActionGroup"> <annotations> <description>Verifies customer no additional address in address book</description> </annotations> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml index aca0c4e6a8f8a..2b4bb43ec36cd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml @@ -22,19 +22,18 @@ <createData entity="ApiSimpleProduct" stepKey="Product"> <requiredEntity createDataKey="Category"/> </createData> + <createData entity="Simple_Customer_Without_Address" stepKey="Customer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> - <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountForm"> - <argument name="customer" value="CustomerEntityOne"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$Customer$"/> </actionGroup> - <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> </before> <after> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> - <actionGroup ref="DeleteCustomerFromAdminActionGroup" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <deleteData createDataKey="Product" stepKey="deleteProduct"/> <deleteData createDataKey="Category" stepKey="deleteCategory"/> + <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> </after> <!-- Create order for registered customer --> @@ -53,6 +52,6 @@ <actionGroup ref="AdminReorderActionGroup" stepKey="reorder"/> <!-- Assert no additional addresses saved --> - <actionGroup ref="AssertCustomerHasNoOtherAddressesActionGroup" stepKey="assertAddresses"/> + <actionGroup ref="AssertStoreFrontCustomerHasNoOtherAddressesActionGroup" stepKey="assertAddresses"/> </test> </tests> From 763b5d303a701e440e7e0f7f700340f42d251865 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Mon, 12 Oct 2020 16:57:23 +0300 Subject: [PATCH 0777/1013] MC-38026: [SAQ] - Schedule update remove body class in category --- .../Controller/Adminhtml/CategoryTest.php | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) 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 6245e4e9f8de7..cd58cd2ac3819 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -10,6 +10,8 @@ use Magento\Framework\Acl\Builder; use Magento\Backend\App\Area\FrontNameResolver; use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\ProductMetadata; +use Magento\Framework\App\ProductMetadataInterface; use Magento\Framework\App\Request\Http as HttpRequest; use Magento\Framework\Message\MessageInterface; use Magento\Framework\Registry; @@ -270,7 +272,7 @@ public function testSuggestCategoriesActionNoSuggestions(): void */ public function saveActionDataProvider(): array { - return [ + $result = [ 'default values' => [ [ 'id' => '2', @@ -390,6 +392,20 @@ public function saveActionDataProvider(): array ], ], ]; + + $productMetadataInterface = Bootstrap::getObjectManager()->get(ProductMetadataInterface::class); + if ($productMetadataInterface->getEdition() !== ProductMetadata::EDITION_NAME) { + /** + * Skip save custom_design_from and custom_design_to attributes, + * because this logic is rewritten on EE by Catalog Schedule + */ + foreach (array_keys($result['custom values']) as $index) { + unset($result['custom values'][$index]['custom_design_from']); + unset($result['custom values'][$index]['custom_design_to']); + } + } + + return $result; } /** @@ -398,6 +414,11 @@ public function saveActionDataProvider(): array */ public function testIncorrectDateFrom(): void { + $productMetadataInterface = Bootstrap::getObjectManager()->get(ProductMetadataInterface::class); + if ($productMetadataInterface->getEdition() !== ProductMetadata::EDITION_NAME) { + $this->markTestSkipped('Skipped, because this logic is rewritten on EE by Catalog Schedule'); + } + $data = [ 'name' => 'Test Category', 'attribute_set_id' => '3', From a8743bbf0121d2e5cceb2f1df37524031501c5ed Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Mon, 12 Oct 2020 17:27:59 +0300 Subject: [PATCH 0778/1013] MC-37070: Create automated test for "Import products with shared images" --- .../Product/Gallery/UpdateHandlerTest.php | 102 +++++++- .../Import/ImportWithSharedImagesTest.php | 236 ++++++++++++++++++ ...talog_import_products_with_same_images.csv | 3 + .../_files/magento_image.jpg | Bin 0 -> 13353 bytes 4 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/catalog_import_products_with_same_images.csv create mode 100644 dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/magento_image.jpg diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 7ee2c62453df5..ce36b27d51e7d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Model\Product\Gallery; +use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; @@ -351,6 +352,23 @@ public function testExecuteWithTwoImagesOnStoreView(): void } } + /** + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * + * @return void + */ + public function testDeleteSharedImage(): void + { + $product = $this->getProduct(null, 'simple'); + $this->duplicateMediaGalleryForProduct('/m/a/magento_image.jpg', 'simple2'); + $secondProduct = $this->getProduct(null, 'simple2'); + $this->updateHandler->execute($this->prepareRemoveImage($product), []); + $product = $this->getProduct(null, 'simple'); + $this->assertEmpty($product->getMediaGalleryImages()->getItems()); + $this->checkProductImageExist($secondProduct, '/m/a/magento_image.jpg'); + } + /** * @inheritdoc */ @@ -371,11 +389,13 @@ protected function tearDown(): void * Returns current product. * * @param int|null $storeId + * @param string|null $sku * @return ProductInterface|Product */ - private function getProduct(?int $storeId = null): ProductInterface + private function getProduct(?int $storeId = null, ?string $sku = null): ProductInterface { - return $this->productRepository->get('simple', false, $storeId, true); + $sku = $sku ?: 'simple'; + return $this->productRepository->get($sku, false, $storeId, true); } /** @@ -464,6 +484,84 @@ public function testDeleteWithMultiWebsites(): void $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); } + /** + * Check product image link and product image exist + * + * @param ProductInterface $product + * @param string $imagePath + * @return void + */ + private function checkProductImageExist(ProductInterface $product, string $imagePath): void + { + $productImageItem = $product->getMediaGalleryImages()->getFirstItem(); + $this->assertEquals($imagePath, $productImageItem->getFile()); + $productImageFile = $productImageItem->getPath(); + $this->assertNotEmpty($productImageFile); + $this->assertTrue($this->mediaDirectory->getDriver()->isExists($productImageFile)); + $this->fileName = $productImageFile; + } + + /** + * Prepare the product to remove image + * + * @param ProductInterface $product + * @return ProductInterface + */ + private function prepareRemoveImage(ProductInterface $product): ProductInterface + { + $item = $product->getMediaGalleryImages()->getFirstItem(); + $item->setRemoved('1'); + $galleryData = [ + 'images' => [ + (int)$item->getValueId() => $item->getData(), + ] + ]; + $product->setData(ProductInterface::MEDIA_GALLERY, $galleryData); + $product->setStoreId(0); + + return $product; + } + + /** + * Duplicate media gallery entries for a product + * + * @param string $imagePath + * @param string $productSku + * @return void + */ + private function duplicateMediaGalleryForProduct(string $imagePath, string $productSku): void + { + $product = $this->getProduct(null, $productSku); + $connect = $this->galleryResource->getConnection(); + $select = $connect->select()->from($this->galleryResource->getMainTable())->where('value = ?', $imagePath); + $res = $connect->fetchRow($select); + $value_id = $res['value_id']; + unset($res['value_id']); + $rows = [ + 'attribute_id' => $res['attribute_id'], + 'value' => $res['value'], + ProductAttributeMediaGalleryEntryInterface::MEDIA_TYPE => $res['media_type'], + ProductAttributeMediaGalleryEntryInterface::DISABLED => $res['disabled'], + ]; + $connect->insert($this->galleryResource->getMainTable(), $rows); + $select = $connect->select() + ->from($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE)) + ->where('value_id = ?', $value_id); + $res = $connect->fetchRow($select); + $newValueId = (int)$value_id + 1; + $rows = [ + 'value_id' => $newValueId, + 'store_id' => $res['store_id'], + ProductAttributeMediaGalleryEntryInterface::LABEL => $res['label'], + ProductAttributeMediaGalleryEntryInterface::POSITION => $res['position'], + ProductAttributeMediaGalleryEntryInterface::DISABLED => $res['disabled'], + 'row_id' => $product->getRowId(), + ]; + $connect->insert($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE), $rows); + $rows = ['value_id' => $newValueId, 'row_id' => $product->getRowId()]; + $connect->insert($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE), $rows); + } + /** * @param Product $product * @param array $roles diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php new file mode 100644 index 0000000000000..4c04e5a8814e5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php @@ -0,0 +1,236 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogImportExport\Model\Import; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product as ProductEntity; +use Magento\Catalog\Model\Product\Media\ConfigInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\ObjectManagerInterface; +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Model\Import\Source\CsvFactory; +use Magento\ImportExport\Model\ResourceModel\Import\Data; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks that product import with same images can be successfully done + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ImportWithSharedImagesTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Filesystem */ + private $fileSystem; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var File */ + private $fileDriver; + + /** @var Import */ + private $import; + + /** @var ConfigInterface */ + private $mediaConfig; + + /** @var array */ + private $appParams; + + /** @var array */ + private $createdProductsSkus = []; + + /** @var array */ + private $filesToRemove = []; + + /** @var CsvFactory */ + private $csvFactory; + + /** @var Data */ + private $importDataResource; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->fileSystem = $this->objectManager->get(Filesystem::class); + $this->fileDriver = $this->objectManager->get(File::class); + $this->mediaConfig = $this->objectManager->get(ConfigInterface::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->import = $this->objectManager->get(ProductFactory::class)->create(); + $this->csvFactory = $this->objectManager->get(CsvFactory::class); + $this->importDataResource = $this->objectManager->get(Data::class); + $this->appParams = Bootstrap::getInstance()->getBootstrap()->getApplication() + ->getInitParams()[\Magento\Framework\App\Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS]; + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->removeFiles(); + $this->removeProducts(); + $this->importDataResource->cleanBunches(); + + parent::tearDown(); + } + + /** + * @return void + */ + public function testImportProductsWithSameImages(): void + { + $this->moveImages('magento_image.jpg'); + $source = $this->prepareFile('catalog_import_products_with_same_images.csv'); + $this->updateUploader(); + $errors = $this->import->setParameters([ + 'behavior' => Import::BEHAVIOR_ADD_UPDATE, + 'entity' => ProductEntity::ENTITY, + ]) + ->setSource($source)->validateData(); + $this->assertEmpty($errors->getAllErrors()); + $this->import->importData(); + $this->createdProductsSkus = ['SimpleProductForTest1', 'SimpleProductForTest2']; + $this->checkProductsImages('/m/a/magento_image.jpg', $this->createdProductsSkus); + } + + /** + * Check product images + * + * @param string $expectedImagePath + * @param array $productSkus + * @return void + */ + private function checkProductsImages(string $expectedImagePath, array $productSkus): void + { + foreach ($productSkus as $productSku) { + $product = $this->productRepository->get($productSku); + $productImageItem = $product->getMediaGalleryImages()->getFirstItem(); + $productImageFile = $productImageItem->getFile(); + $productImagePath = $productImageItem->getPath(); + $this->filesToRemove[] = $productImagePath; + $this->assertEquals($expectedImagePath, $productImageFile); + $this->assertNotEmpty($productImagePath); + $this->assertTrue($this->fileDriver->isExists($productImagePath)); + } + } + + /** + * Remove created files + * + * @return void + */ + private function removeFiles(): void + { + foreach ($this->filesToRemove as $file) { + if ($this->fileDriver->isExists($file)) { + $this->fileDriver->deleteFile($file); + } + } + } + + /** + * Remove created products + * + * @return void + */ + private function removeProducts(): void + { + foreach ($this->createdProductsSkus as $sku) { + try { + $this->productRepository->deleteById($sku); + } catch (NoSuchEntityException $e) { + //already removed + } + } + } + + /** + * Prepare file + * + * @param string $fileName + * @return Csv + */ + private function prepareFile(string $fileName): Csv + { + $tmpDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $fixtureDir = realpath(__DIR__ . '/../../_files'); + $filePath = $tmpDirectory->getAbsolutePath($fileName); + $this->filesToRemove[] = $filePath; + $tmpDirectory->getDriver()->copy($fixtureDir . DIRECTORY_SEPARATOR . $fileName, $filePath); + $source = $this->csvFactory->create( + [ + 'file' => $fileName, + 'directory' => $tmpDirectory + ] + ); + + return $source; + } + + /** + * Update upload to use sandbox folders + * + * @return void + */ + private function updateUploader(): void + { + $uploader = $this->import->getUploader(); + $rootDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::ROOT); + $destDir = $rootDirectory->getRelativePath( + $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] + . DS . $this->mediaConfig->getBaseMediaPath() + ); + $tmpDir = $rootDirectory->getRelativePath( + $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] + ); + $rootDirectory->create($destDir); + $rootDirectory->create($tmpDir); + $uploader->setDestDir($destDir); + $uploader->setTmpDir($tmpDir); + } + + /** + * Move images to appropriate folder + * + * @param string $fileName + * @return void + */ + private function moveImages(string $fileName): void + { + $rootDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::ROOT); + $tmpDir = $rootDirectory->getRelativePath( + $this->appParams[DirectoryList::MEDIA][DirectoryList::PATH] + ); + $fixtureDir = realpath(__DIR__ . '/../../_files'); + $tmpFilePath = $rootDirectory->getAbsolutePath($tmpDir . DS . $fileName); + $this->fileDriver->createDirectory($tmpDir); + $rootDirectory->getDriver()->copy( + $fixtureDir . DIRECTORY_SEPARATOR . $fileName, + $tmpFilePath + ); + $this->filesToRemove[] = $tmpFilePath; + } +} diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/catalog_import_products_with_same_images.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/catalog_import_products_with_same_images.csv new file mode 100644 index 0000000000000..7761ed7ac2360 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/catalog_import_products_with_same_images.csv @@ -0,0 +1,3 @@ +sku,store_view_code,attribute_set_code,product_type,categories,product_websites,name,description,short_description,weight,product_online,tax_class_name,visibility,price,special_price,special_price_from_date,special_price_to_date,url_key,meta_title,meta_keywords,meta_description,base_image,base_image_label,small_image,small_image_label,thumbnail_image,thumbnail_image_label,swatch_image,swatch_image_label,created_at,updated_at,new_from_date,new_to_date,display_product_options_in,map_price,msrp_price,map_enabled,gift_message_available,custom_design,custom_design_from,custom_design_to,custom_layout_update,page_layout,product_options_container,msrp_display_actual_price_type,country_of_manufacture,additional_attributes,qty,out_of_stock_qty,use_config_min_qty,is_qty_decimal,allow_backorders,use_config_backorders,min_cart_qty,use_config_min_sale_qty,max_cart_qty,use_config_max_sale_qty,is_in_stock,notify_on_stock_below,use_config_notify_stock_qty,manage_stock,use_config_manage_stock,use_config_qty_increments,qty_increments,use_config_enable_qty_inc,enable_qty_increments,is_decimal_divided,website_id,related_skus,related_position,crosssell_skus,crosssell_position,upsell_skus,upsell_position,additional_images,additional_image_labels,hide_from_product_page,custom_options,bundle_price_type,bundle_sku_type,bundle_price_view,bundle_weight_type,bundle_values,bundle_shipment_type,configurable_variations,configurable_variation_labels,associated_skus +SimpleProductForTest1,,Default,simple,Default,base,SimpleProductAfterImport1,,,1,1,Taxable Goods,"Catalog, Search",250,,,,simple-product-for-test-1,,,,magento_image.jpg,BASE magento_image.jpg,magento_image.jpg,SMALL blueshirt,magento_image.jpg,Thumb Image,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,Block after Info Column,,,,,,,,,,,Use config,,,100,0,1,0,0,1,1,1,0,1,1,,1,0,1,1,0,1,0,0,0,,,,,,,,,,,,,,,,,,, +SimpleProductForTest2,,Default,simple,Default,base,SimpleProductAfterImport2,,,1,1,Taxable Goods,"Catalog, Search",300,,,,simple-product-for-test-2,,,,magento_image.jpg,BASE magento_image.jpg,magento_image.jpg,SMALL blueshirt,magento_image.jpg,Thumb Image,,,"3/4/19, 5:53 AM","3/4/19, 4:47 PM",,,Block after Info Column,,,,,,,,,,,Use config,,,100,0,1,0,0,1,1,1,0,1,1,,1,0,1,1,0,1,0,0,0,,,,,,,,,,,,,,,,,,, diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/magento_image.jpg b/dev/tests/integration/testsuite/Magento/CatalogImportExport/_files/magento_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3b825a41b2101a758ee9c45b9304a6d08a90c729 GIT binary patch literal 13353 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 A%K!iX literal 0 HcmV?d00001 From 2070e582f54d43e09f2b1471666d0fd746995bbd Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Mon, 12 Oct 2020 09:53:20 -0500 Subject: [PATCH 0779/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Model/Indexer/Product/Price/AbstractAction.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 f3a4b322e29df..b8896dd128f6b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -387,12 +387,9 @@ protected function _reindexRows($changedIds = []) $parentProductsTypes = $this->getParentProductsTypes($changedIds); $changedIds = array_unique(array_merge($changedIds, ...array_values($parentProductsTypes))); + $pendingDeleteIds = $changedIds; $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes); - if ($changedIds) { - $this->deleteIndexData($changedIds); - } - $typeIndexers = $this->getTypeIndexers(); foreach ($typeIndexers as $productType => $indexer) { $entityIds = $productsTypes[$productType] ?? []; @@ -419,6 +416,11 @@ protected function _reindexRows($changedIds = []) $indexer->reindexEntity($entityIds); $this->_syncData($entityIds); } + $pendingDeleteIds = array_diff($pendingDeleteIds, $entityIds); + } + + if (!empty($pendingDeleteIds)) { + $this->deleteIndexData($pendingDeleteIds); } return $changedIds; From df5ae12ecc26edee6bdb2c01cf5e0c6c3198f9f4 Mon Sep 17 00:00:00 2001 From: TuNa <ladiesman9x@gmail.com> Date: Mon, 12 Oct 2020 22:13:19 +0700 Subject: [PATCH 0780/1013] Remove incorrect use important in swatches option text up --- .../Magento_Swatches/web/css/source/_module.less | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less index 07317e1670a0b..ce1b009c24d42 100644 --- a/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Swatches/web/css/source/_module.less @@ -65,7 +65,6 @@ // _____________________________________________ & when (@media-common = true) { - .swatch { &-attribute { &-label { @@ -155,7 +154,7 @@ padding: 4px 8px; &.selected { - .lib-css(background-color, @swatch-option-text__selected__background-color) !important; + .lib-css(background-color, @swatch-option-text__selected__background-color); } } @@ -201,6 +200,7 @@ top: 0; } } + &-disabled { border: 0; cursor: default; @@ -208,6 +208,7 @@ &:after { .lib-rotate(-30deg); + .lib-css(background, @swatch-option__disabled__background); content: ''; height: 2px; left: -4px; @@ -215,7 +216,6 @@ top: 10px; width: 42px; z-index: 995; - .lib-css(background, @swatch-option__disabled__background); } } @@ -226,6 +226,7 @@ &-tooltip { .lib-css(border, @swatch-option-tooltip__border); .lib-css(color, @swatch-option-tooltip__color); + .lib-css(background, @swatch-option-tooltip__background); display: none; max-height: 100%; min-height: 20px; @@ -234,7 +235,6 @@ position: absolute; text-align: center; z-index: 999; - .lib-css(background, @swatch-option-tooltip__background); &, &-layered { @@ -278,9 +278,9 @@ } &-layered { + .lib-css(background, @swatch-option-tooltip-layered__background); .lib-css(border, @swatch-option-tooltip-layered__border); .lib-css(color, @swatch-option-tooltip-layered__color); - .lib-css(background, @swatch-option-tooltip-layered__background); display: none; left: -47px; position: absolute; @@ -326,7 +326,6 @@ margin: 2px 0; padding: 2px; position: static; - z-index: 1; } &-visual-tooltip-layered { From daf0dc67926bde5de6e05f956ff0b09756ed0123 Mon Sep 17 00:00:00 2001 From: niravkrish <niravpatel5393@gmail.com> Date: Mon, 12 Oct 2020 22:08:35 +0530 Subject: [PATCH 0781/1013] removed unneccesary css to resolve the issue --- lib/web/css/source/components/_modals.less | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/web/css/source/components/_modals.less b/lib/web/css/source/components/_modals.less index 58c9c0674b6ad..8513db545f1ec 100644 --- a/lib/web/css/source/components/_modals.less +++ b/lib/web/css/source/components/_modals.less @@ -103,10 +103,6 @@ &.confirm { .modal-inner-wrap { .lib-css(max-width, @modal-popup-confirm__width); - - .modal-content { - padding-right: 7rem; - } } } From 8c8b9cf5220db0a8ffdc2ec16f1a092efcbe4fbd Mon Sep 17 00:00:00 2001 From: Bartosz Kubicki <bartosz.kubicki@lizardmedia.pl> Date: Mon, 12 Oct 2020 12:59:50 +0200 Subject: [PATCH 0782/1013] Possible fix for integration test --- .../Framework/MessageQueue/TopologyTest.php | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php index 5c85d6ddb7c70..cbe48c99da020 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/TopologyTest.php @@ -3,16 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\MessageQueue; +use Magento\TestFramework\Helper\Amqp; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\MessageQueue\PreconditionFailedException; +use PHPUnit\Framework\TestCase; /** * @see dev/tests/integration/_files/Magento/TestModuleMessageQueueConfiguration * @see dev/tests/integration/_files/Magento/TestModuleMessageQueueConfigOverride */ -class TopologyTest extends \PHPUnit\Framework\TestCase +class TopologyTest extends TestCase { /** * List of declared exchanges. @@ -22,13 +26,16 @@ class TopologyTest extends \PHPUnit\Framework\TestCase private $declaredExchanges; /** - * @var \Magento\TestFramework\Helper\Amqp + * @var Amqp */ private $helper; + /** + * @return void + */ protected function setUp(): void { - $this->helper = Bootstrap::getObjectManager()->create(\Magento\TestFramework\Helper\Amqp::class); + $this->helper = Bootstrap::getObjectManager()->create(Amqp::class); if (!$this->helper->isAvailable()) { $this->fail('This test relies on RabbitMQ Management Plugin.'); @@ -42,24 +49,28 @@ protected function setUp(): void * @param array $expectedConfig * @param array $bindingConfig */ - public function testTopologyInstallation(array $expectedConfig, array $bindingConfig) + public function testTopologyInstallation(array $expectedConfig, array $bindingConfig): void { $name = $expectedConfig['name']; $this->assertArrayHasKey($name, $this->declaredExchanges); - unset($this->declaredExchanges[$name]['message_stats']); - unset($this->declaredExchanges[$name]['user_who_performed_action']); - $this->assertSame( + unset( + $this->declaredExchanges[$name]['message_stats'], + $this->declaredExchanges[$name]['user_who_performed_action'] + ); + + $this->assertEquals( $expectedConfig, $this->declaredExchanges[$name], 'Invalid exchange configuration: ' . $name ); $bindings = $this->helper->getExchangeBindings($name); - $bindings = array_map(function ($value) { + $bindings = array_map(static function ($value) { unset($value['properties_key']); return $value; }, $bindings); - $this->assertSame( + + $this->assertEquals( $bindingConfig, $bindings, 'Invalid exchange bindings configuration: ' . $name @@ -70,7 +81,7 @@ public function testTopologyInstallation(array $expectedConfig, array $bindingCo * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function exchangeDataProvider() + public function exchangeDataProvider(): array { return [ 'magento-topic-based-exchange1' => [ From f19ea1f4b11cec03a19a4c4c4bff379aaa2c4a01 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Mon, 12 Oct 2020 13:59:31 -0500 Subject: [PATCH 0783/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 578e3099a2fde..6d7e7293f9e9d 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 @@ -778,7 +778,7 @@ protected function _movePriceDataToIndexTable($entityIds = null) $select->where('entity_id in (?)', count($entityIds) > 0 ? $entityIds : 0, \Zend_Db::INT_TYPE); } - $query = $select->insertFromSelect($this->getIdxTable(), [], false); + $query = $select->insertFromSelect($this->getIdxTable(), [], true); $connection->query($query); $connection->delete($table); From d2d6c607bece5c137b1a5a8119974fc24571e6e3 Mon Sep 17 00:00:00 2001 From: Viktor Petryk <victor.petryk@transoftgroup.com> Date: Mon, 12 Oct 2020 22:50:39 +0300 Subject: [PATCH 0784/1013] MC-30631: Database sessions pile up --- lib/internal/Magento/Framework/Session/Config.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/Magento/Framework/Session/Config.php b/lib/internal/Magento/Framework/Session/Config.php index 3bb30d0f6ec3e..1791ab09156fd 100644 --- a/lib/internal/Magento/Framework/Session/Config.php +++ b/lib/internal/Magento/Framework/Session/Config.php @@ -29,16 +29,16 @@ class Config implements ConfigInterface const PARAM_SESSION_CACHE_LIMITER = 'session/cache_limiter'; /** Configuration path for session garbage collection probability */ - const PARAM_SESSION_GC_PROBABILITY = 'session/gc_probability'; + private const PARAM_SESSION_GC_PROBABILITY = 'session/gc_probability'; /** Configuration path for session garbage collection divisor */ - const PARAM_SESSION_GC_DIVISOR = 'session/gc_divisor'; + private const PARAM_SESSION_GC_DIVISOR = 'session/gc_divisor'; /** * Configuration path for session garbage collection max lifetime. * The number of seconds after which data will be seen as 'garbage'. */ - const PARAM_SESSION_GC_MAXLIFETIME = 'session/gc_maxlifetime'; + private const PARAM_SESSION_GC_MAXLIFETIME = 'session/gc_maxlifetime'; /** Configuration path for cookie domain */ const XML_PATH_COOKIE_DOMAIN = 'web/cookie/cookie_domain'; From 7037dec973f500bb9b5e3468413a105ec313131b Mon Sep 17 00:00:00 2001 From: magento packaging service <magento-comops@adobe.com> Date: Mon, 12 Oct 2020 20:30:22 +0000 Subject: [PATCH 0785/1013] Updating composer versions for version-setter for 2.4.1 --- app/code/Magento/AdminAnalytics/composer.json | 22 +++--- .../Magento/AdminNotification/composer.json | 24 ++++--- .../AdvancedPricingImportExport/composer.json | 28 ++++---- app/code/Magento/AdvancedSearch/composer.json | 28 ++++---- app/code/Magento/Amqp/composer.json | 18 ++--- app/code/Magento/AmqpStore/composer.json | 22 +++--- app/code/Magento/Analytics/composer.json | 18 ++--- .../AsynchronousOperations/composer.json | 26 +++---- app/code/Magento/Authorization/composer.json | 16 +++-- app/code/Magento/Backend/composer.json | 48 +++++++------ app/code/Magento/Backup/composer.json | 20 +++--- app/code/Magento/Bundle/composer.json | 50 ++++++------- app/code/Magento/BundleGraphQl/composer.json | 26 +++---- .../Magento/BundleImportExport/composer.json | 26 +++---- .../Magento/CacheInvalidate/composer.json | 16 +++-- app/code/Magento/Captcha/composer.json | 24 ++++--- .../Magento/CardinalCommerce/composer.json | 20 +++--- app/code/Magento/Catalog/composer.json | 70 ++++++++++--------- .../Magento/CatalogAnalytics/composer.json | 14 ++-- .../Magento/CatalogCmsGraphQl/composer.json | 22 +++--- .../CatalogCustomerGraphQl/composer.json | 18 ++--- app/code/Magento/CatalogGraphQl/composer.json | 32 +++++---- .../Magento/CatalogImportExport/composer.json | 34 ++++----- .../Magento/CatalogInventory/composer.json | 28 ++++---- .../CatalogInventoryGraphQl/composer.json | 16 +++-- app/code/Magento/CatalogRule/composer.json | 32 +++++---- .../CatalogRuleConfigurable/composer.json | 22 +++--- .../Magento/CatalogRuleGraphQl/composer.json | 14 ++-- app/code/Magento/CatalogSearch/composer.json | 38 +++++----- .../Magento/CatalogUrlRewrite/composer.json | 32 +++++---- .../CatalogUrlRewriteGraphQl/composer.json | 22 +++--- app/code/Magento/CatalogWidget/composer.json | 32 +++++---- app/code/Magento/Checkout/composer.json | 54 +++++++------- .../Magento/CheckoutAgreements/composer.json | 22 +++--- .../CheckoutAgreementsGraphQl/composer.json | 18 ++--- app/code/Magento/Cms/composer.json | 34 ++++----- app/code/Magento/CmsGraphQl/composer.json | 24 ++++--- app/code/Magento/CmsUrlRewrite/composer.json | 20 +++--- .../CmsUrlRewriteGraphQl/composer.json | 22 +++--- app/code/Magento/Config/composer.json | 28 ++++---- .../ConfigurableImportExport/composer.json | 26 +++---- .../Magento/ConfigurableProduct/composer.json | 50 ++++++------- .../ConfigurableProductGraphQl/composer.json | 22 +++--- .../ConfigurableProductSales/composer.json | 22 +++--- app/code/Magento/Contact/composer.json | 22 +++--- app/code/Magento/Cookie/composer.json | 18 ++--- app/code/Magento/Cron/composer.json | 18 ++--- app/code/Magento/Csp/composer.json | 16 +++-- app/code/Magento/CurrencySymbol/composer.json | 24 ++++--- app/code/Magento/Customer/composer.json | 54 +++++++------- .../Magento/CustomerAnalytics/composer.json | 14 ++-- .../CustomerDownloadableGraphQl/composer.json | 18 ++--- .../Magento/CustomerGraphQl/composer.json | 26 +++---- .../CustomerImportExport/composer.json | 26 +++---- app/code/Magento/Deploy/composer.json | 22 +++--- app/code/Magento/Developer/composer.json | 18 ++--- app/code/Magento/Dhl/composer.json | 34 ++++----- app/code/Magento/Directory/composer.json | 20 +++--- .../Magento/DirectoryGraphQl/composer.json | 16 +++-- app/code/Magento/Downloadable/composer.json | 48 +++++++------ .../Magento/DownloadableGraphQl/composer.json | 28 ++++---- .../DownloadableImportExport/composer.json | 26 +++---- app/code/Magento/Eav/composer.json | 24 ++++--- app/code/Magento/EavGraphQl/composer.json | 16 +++-- app/code/Magento/Elasticsearch/composer.json | 32 +++++---- app/code/Magento/Elasticsearch6/composer.json | 24 ++++--- app/code/Magento/Elasticsearch7/composer.json | 24 ++++--- app/code/Magento/Email/composer.json | 34 ++++----- app/code/Magento/EncryptionKey/composer.json | 18 ++--- app/code/Magento/Fedex/composer.json | 30 ++++---- app/code/Magento/GiftMessage/composer.json | 34 ++++----- .../Magento/GiftMessageGraphQl/composer.json | 16 +++-- app/code/Magento/GoogleAdwords/composer.json | 18 ++--- .../Magento/GoogleAnalytics/composer.json | 22 +++--- .../Magento/GoogleOptimizer/composer.json | 26 +++---- app/code/Magento/GraphQl/composer.json | 18 ++--- app/code/Magento/GraphQlCache/composer.json | 14 ++-- .../GroupedCatalogInventory/composer.json | 20 +++--- .../Magento/GroupedImportExport/composer.json | 24 ++++--- app/code/Magento/GroupedProduct/composer.json | 42 +++++------ .../GroupedProductGraphQl/composer.json | 16 +++-- app/code/Magento/ImportExport/composer.json | 26 +++---- app/code/Magento/Indexer/composer.json | 16 +++-- .../Magento/InstantPurchase/composer.json | 18 ++--- app/code/Magento/Integration/composer.json | 28 ++++---- .../Magento/LayeredNavigation/composer.json | 18 ++--- .../Magento/LoginAsCustomer/composer.json | 26 ++++--- .../LoginAsCustomerAdminUi/composer.json | 33 +++++---- .../Magento/LoginAsCustomerApi/composer.json | 14 ++-- .../LoginAsCustomerAssistance/composer.json | 31 ++++---- .../LoginAsCustomerFrontendUi/composer.json | 21 +++--- .../Magento/LoginAsCustomerLog/composer.json | 31 ++++---- .../LoginAsCustomerPageCache/composer.json | 25 ++++--- .../LoginAsCustomerQuote/composer.json | 23 +++--- .../LoginAsCustomerSales/composer.json | 23 +++--- app/code/Magento/Marketplace/composer.json | 16 +++-- app/code/Magento/MediaContent/composer.json | 14 ++-- .../Magento/MediaContentApi/composer.json | 12 ++-- .../Magento/MediaContentCatalog/composer.json | 18 ++--- .../Magento/MediaContentCms/composer.json | 14 ++-- .../MediaContentSynchronization/composer.json | 22 +++--- .../composer.json | 10 +-- .../composer.json | 16 +++-- .../composer.json | 16 +++-- app/code/Magento/MediaGallery/composer.json | 14 ++-- .../Magento/MediaGalleryApi/composer.json | 10 +-- .../Magento/MediaGalleryCatalog/composer.json | 14 ++-- .../composer.json | 24 ++++--- .../MediaGalleryCatalogUi/composer.json | 20 +++--- .../Magento/MediaGalleryCmsUi/composer.json | 14 ++-- .../MediaGalleryIntegration/composer.json | 32 +++++---- .../MediaGalleryMetadata/composer.json | 12 ++-- .../MediaGalleryMetadataApi/composer.json | 10 +-- .../MediaGallerySynchronization/composer.json | 16 +++-- .../composer.json | 12 ++-- .../composer.json | 16 +++-- app/code/Magento/MediaGalleryUi/composer.json | 28 ++++---- .../Magento/MediaGalleryUiApi/composer.json | 10 +-- app/code/Magento/MediaStorage/composer.json | 30 ++++---- app/code/Magento/MessageQueue/composer.json | 16 +++-- app/code/Magento/Msrp/composer.json | 28 ++++---- .../MsrpConfigurableProduct/composer.json | 20 +++--- .../Magento/MsrpGroupedProduct/composer.json | 20 +++--- app/code/Magento/Multishipping/composer.json | 34 ++++----- app/code/Magento/MysqlMq/composer.json | 18 ++--- .../Magento/NewRelicReporting/composer.json | 26 +++---- app/code/Magento/Newsletter/composer.json | 32 +++++---- .../Magento/NewsletterGraphQl/composer.json | 22 +++--- .../Magento/OfflinePayments/composer.json | 20 +++--- .../Magento/OfflineShipping/composer.json | 36 +++++----- app/code/Magento/PageCache/composer.json | 20 +++--- app/code/Magento/Payment/composer.json | 28 ++++---- app/code/Magento/Paypal/composer.json | 48 +++++++------ app/code/Magento/PaypalCaptcha/composer.json | 22 +++--- app/code/Magento/PaypalGraphQl/composer.json | 34 ++++----- app/code/Magento/Persistent/composer.json | 26 +++---- app/code/Magento/ProductAlert/composer.json | 26 +++---- app/code/Magento/ProductVideo/composer.json | 28 ++++---- app/code/Magento/Quote/composer.json | 44 ++++++------ app/code/Magento/QuoteAnalytics/composer.json | 14 ++-- .../Magento/QuoteBundleOptions/composer.json | 12 ++-- .../QuoteConfigurableOptions/composer.json | 12 ++-- .../QuoteDownloadableLinks/composer.json | 12 ++-- app/code/Magento/QuoteGraphQl/composer.json | 34 ++++----- .../RelatedProductGraphQl/composer.json | 18 ++--- .../Magento/ReleaseNotification/composer.json | 22 +++--- app/code/Magento/Reports/composer.json | 46 ++++++------ app/code/Magento/RequireJs/composer.json | 14 ++-- app/code/Magento/Review/composer.json | 34 ++++----- .../Magento/ReviewAnalytics/composer.json | 14 ++-- app/code/Magento/ReviewGraphQl/composer.json | 22 +++--- app/code/Magento/Robots/composer.json | 18 ++--- app/code/Magento/Rss/composer.json | 20 +++--- app/code/Magento/Rule/composer.json | 22 +++--- app/code/Magento/Sales/composer.json | 64 +++++++++-------- app/code/Magento/SalesAnalytics/composer.json | 14 ++-- app/code/Magento/SalesGraphQl/composer.json | 24 ++++--- app/code/Magento/SalesInventory/composer.json | 22 +++--- app/code/Magento/SalesRule/composer.json | 54 +++++++------- app/code/Magento/SalesSequence/composer.json | 14 ++-- app/code/Magento/SampleData/composer.json | 16 +++-- app/code/Magento/Search/composer.json | 24 ++++--- app/code/Magento/Security/composer.json | 22 +++--- app/code/Magento/SendFriend/composer.json | 26 +++---- .../Magento/SendFriendGraphQl/composer.json | 16 +++-- app/code/Magento/Shipping/composer.json | 46 ++++++------ app/code/Magento/Sitemap/composer.json | 34 ++++----- app/code/Magento/Store/composer.json | 32 +++++---- app/code/Magento/StoreGraphQl/composer.json | 14 ++-- app/code/Magento/Swagger/composer.json | 14 ++-- app/code/Magento/SwaggerWebapi/composer.json | 16 +++-- .../Magento/SwaggerWebapiAsync/composer.json | 18 ++--- app/code/Magento/Swatches/composer.json | 38 +++++----- .../Magento/SwatchesGraphQl/composer.json | 16 +++-- .../SwatchesLayeredNavigation/composer.json | 14 ++-- app/code/Magento/Tax/composer.json | 44 ++++++------ app/code/Magento/TaxGraphQl/composer.json | 16 +++-- .../Magento/TaxImportExport/composer.json | 24 ++++--- app/code/Magento/Theme/composer.json | 40 ++++++----- app/code/Magento/ThemeGraphQl/composer.json | 14 ++-- app/code/Magento/Tinymce3/composer.json | 24 ++++--- app/code/Magento/Translation/composer.json | 24 ++++--- app/code/Magento/Ui/composer.json | 26 +++---- app/code/Magento/Ups/composer.json | 30 ++++---- app/code/Magento/UrlRewrite/composer.json | 28 ++++---- .../Magento/UrlRewriteGraphQl/composer.json | 16 +++-- app/code/Magento/User/composer.json | 28 ++++---- app/code/Magento/Usps/composer.json | 30 ++++---- app/code/Magento/Variable/composer.json | 22 +++--- app/code/Magento/Vault/composer.json | 29 ++++---- app/code/Magento/VaultGraphQl/composer.json | 14 ++-- app/code/Magento/Version/composer.json | 14 ++-- app/code/Magento/Webapi/composer.json | 26 +++---- app/code/Magento/WebapiAsync/composer.json | 24 ++++--- app/code/Magento/WebapiSecurity/composer.json | 16 +++-- app/code/Magento/Weee/composer.json | 40 ++++++----- app/code/Magento/WeeeGraphQl/composer.json | 20 +++--- app/code/Magento/Widget/composer.json | 30 ++++---- app/code/Magento/Wishlist/composer.json | 48 +++++++------ .../Magento/WishlistAnalytics/composer.json | 14 ++-- .../Magento/WishlistGraphQl/composer.json | 16 +++-- .../adminhtml/Magento/backend/composer.json | 14 ++-- .../frontend/Magento/blank/composer.json | 14 ++-- .../frontend/Magento/luma/composer.json | 16 +++-- app/i18n/Magento/de_DE/composer.json | 6 +- app/i18n/Magento/en_US/composer.json | 6 +- app/i18n/Magento/es_ES/composer.json | 6 +- app/i18n/Magento/fr_FR/composer.json | 6 +- app/i18n/Magento/nl_NL/composer.json | 6 +- app/i18n/Magento/pt_BR/composer.json | 6 +- app/i18n/Magento/zh_Hans_CN/composer.json | 6 +- .../Magento/Framework/Amqp/composer.json | 18 ++--- .../Magento/Framework/Bulk/composer.json | 18 ++--- .../Framework/MessageQueue/composer.json | 18 ++--- lib/internal/Magento/Framework/composer.json | 10 +-- 215 files changed, 2738 insertions(+), 2302 deletions(-) diff --git a/app/code/Magento/AdminAnalytics/composer.json b/app/code/Magento/AdminAnalytics/composer.json index cf60b1d88ae55..69a1a44212a2c 100644 --- a/app/code/Magento/AdminAnalytics/composer.json +++ b/app/code/Magento/AdminAnalytics/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-admin-analytics", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-config": "*", - "magento/module-ui": "*", - "magento/module-release-notification": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-ui": "101.2.*", + "magento/module-release-notification": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json index d421fc869621b..3b3251458449e 100644 --- a/app/code/Magento/AdminNotification/composer.json +++ b/app/code/Magento/AdminNotification/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-admin-notification", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*", - "magento/module-ui": "*", - "magento/module-config": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json index ea6a39fba2c3d..d5124bdd0953d 100644 --- a/app/code/Magento/AdvancedPricingImportExport/composer.json +++ b/app/code/Magento/AdvancedPricingImportExport/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-advanced-pricing-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-import-export": "*", - "magento/module-catalog-inventory": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-import-export": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-import-export": "101.1.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/AdvancedSearch/composer.json b/app/code/Magento/AdvancedSearch/composer.json index 720309b619e43..9da312cc176de 100644 --- a/app/code/Magento/AdvancedSearch/composer.json +++ b/app/code/Magento/AdvancedSearch/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-advanced-search", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-search": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-search": "*", - "magento/module-store": "*", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-search": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-search": "101.1.*", + "magento/module-store": "101.1.*", "php": "~7.3.0||~7.4.0" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/Amqp/composer.json b/app/code/Magento/Amqp/composer.json index 9e7a035112b04..8faeaa24be85c 100644 --- a/app/code/Magento/Amqp/composer.json +++ b/app/code/Magento/Amqp/composer.json @@ -1,20 +1,21 @@ { "name": "magento/module-amqp", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*", - "magento/framework-amqp": "*", - "magento/framework-message-queue": "*", + "magento/framework": "103.0.*", + "magento/framework-amqp": "100.4.*", + "magento/framework-message-queue": "100.4.*", "php": "~7.3.0||~7.4.0" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/AmqpStore/composer.json b/app/code/Magento/AmqpStore/composer.json index 70a10810ece21..f886ad8cd9217 100644 --- a/app/code/Magento/AmqpStore/composer.json +++ b/app/code/Magento/AmqpStore/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-amqp-store", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*", - "magento/framework-amqp": "*", - "magento/module-store": "*", + "magento/framework": "103.0.*", + "magento/framework-amqp": "100.4.*", + "magento/module-store": "101.1.*", "php": "~7.3.0||~7.4.0" }, "suggest": { - "magento/module-asynchronous-operations": "*", - "magento/framework-message-queue": "*" + "magento/module-asynchronous-operations": "100.4.*", + "magento/framework-message-queue": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json index 84f8af066bf11..5f635c21979ed 100644 --- a/app/code/Magento/Analytics/composer.json +++ b/app/code/Magento/Analytics/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-analytics", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-backend": "*", - "magento/module-config": "*", - "magento/module-integration": "*", - "magento/module-store": "*", - "magento/framework": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-integration": "100.4.*", + "magento/module-store": "101.1.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/AsynchronousOperations/composer.json b/app/code/Magento/AsynchronousOperations/composer.json index b5de631418e72..a70ead26df02c 100644 --- a/app/code/Magento/AsynchronousOperations/composer.json +++ b/app/code/Magento/AsynchronousOperations/composer.json @@ -1,27 +1,28 @@ { "name": "magento/module-asynchronous-operations", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { - "magento/framework": "*", - "magento/framework-message-queue": "*", - "magento/framework-bulk": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-ui": "*", + "magento/framework": "103.0.*", + "magento/framework-message-queue": "100.4.*", + "magento/framework-bulk": "101.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-ui": "101.2.*", "php": "~7.3.0||~7.4.0" }, "suggest": { - "magento/module-admin-notification": "*", + "magento/module-admin-notification": "100.4.*", "magento/module-logging": "*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json index 401444404ca3e..3da4582ee5071 100644 --- a/app/code/Magento/Authorization/composer.json +++ b/app/code/Magento/Authorization/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-authorization", "description": "Authorization module provides access to Magento ACL functionality.", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index ee5491057d861..fc3b62d645bfc 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -1,37 +1,38 @@ { "name": "magento/module-backend", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "102.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backup": "*", - "magento/module-catalog": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-developer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-quote": "*", - "magento/module-reports": "*", - "magento/module-require-js": "*", - "magento/module-sales": "*", - "magento/module-security": "*", - "magento/module-store": "*", - "magento/module-translation": "*", - "magento/module-ui": "*", - "magento/module-user": "*" + "magento/framework": "103.0.*", + "magento/module-backup": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-developer": "100.4.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-require-js": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-security": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-translation": "100.4.*", + "magento/module-ui": "101.2.*", + "magento/module-user": "101.2.*" }, "suggest": { - "magento/module-theme": "*" + "magento/module-theme": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -41,3 +42,4 @@ } } } + diff --git a/app/code/Magento/Backup/composer.json b/app/code/Magento/Backup/composer.json index 9a5904beda550..18949cdb04456 100644 --- a/app/code/Magento/Backup/composer.json +++ b/app/code/Magento/Backup/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-backup", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-cron": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cron": "100.4.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json index 1b5ca24ee098c..1d584dc896dec 100644 --- a/app/code/Magento/Bundle/composer.json +++ b/app/code/Magento/Bundle/composer.json @@ -1,38 +1,39 @@ { "name": "magento/module-bundle", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-catalog-rule": "*", - "magento/module-checkout": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-gift-message": "*", - "magento/module-media-storage": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-catalog-rule": "101.2.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-gift-message": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-webapi": "*", - "magento/module-bundle-sample-data": "*", - "magento/module-sales-rule": "*" + "magento/module-webapi": "100.4.*", + "magento/module-bundle-sample-data": "Sample Data version: 100.4.*", + "magento/module-sales-rule": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -42,3 +43,4 @@ } } } + diff --git a/app/code/Magento/BundleGraphQl/composer.json b/app/code/Magento/BundleGraphQl/composer.json index e3c54719f4d0e..1d0cca1b02786 100644 --- a/app/code/Magento/BundleGraphQl/composer.json +++ b/app/code/Magento/BundleGraphQl/composer.json @@ -2,22 +2,23 @@ "name": "magento/module-bundle-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-catalog": "*", - "magento/module-bundle": "*", - "magento/module-catalog-graph-ql": "*", - "magento/module-quote": "*", - "magento/module-quote-graph-ql": "*", - "magento/module-store": "*", - "magento/module-sales": "*", - "magento/module-sales-graph-ql": "*", - "magento/framework": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-catalog": "104.0.*", + "magento/module-bundle": "101.0.*", + "magento/module-catalog-graph-ql": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-quote-graph-ql": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-graph-ql": "100.4.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/BundleImportExport/composer.json b/app/code/Magento/BundleImportExport/composer.json index faca3eac9a721..a1134382212ab 100644 --- a/app/code/Magento/BundleImportExport/composer.json +++ b/app/code/Magento/BundleImportExport/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-bundle-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-bundle": "*", - "magento/module-store": "*", - "magento/module-catalog": "*", - "magento/module-catalog-import-export": "*", - "magento/module-eav": "*", - "magento/module-import-export": "*" + "magento/framework": "103.0.*", + "magento/module-bundle": "101.0.*", + "magento/module-store": "101.1.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-import-export": "101.1.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/CacheInvalidate/composer.json b/app/code/Magento/CacheInvalidate/composer.json index 7801554c890e1..385683d208b10 100644 --- a/app/code/Magento/CacheInvalidate/composer.json +++ b/app/code/Magento/CacheInvalidate/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-cache-invalidate", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-page-cache": "*" + "magento/framework": "103.0.*", + "magento/module-page-cache": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/Captcha/composer.json b/app/code/Magento/Captcha/composer.json index 3c3aa58c3fe2f..5f9c0d31d7fbf 100644 --- a/app/code/Magento/Captcha/composer.json +++ b/app/code/Magento/Captcha/composer.json @@ -1,26 +1,27 @@ { "name": "magento/module-captcha", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-store": "*", - "magento/module-authorization": "*", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-authorization": "100.4.*", "laminas/laminas-captcha": "^2.7.1", "laminas/laminas-db": "^2.8.2", "laminas/laminas-session": "^2.7.3" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -30,3 +31,4 @@ } } } + diff --git a/app/code/Magento/CardinalCommerce/composer.json b/app/code/Magento/CardinalCommerce/composer.json index 8b2989ef915e1..0c560942989fa 100644 --- a/app/code/Magento/CardinalCommerce/composer.json +++ b/app/code/Magento/CardinalCommerce/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-cardinal-commerce", "description": "Provides a possibility to enable 3-D Secure 2.0 support for payment methods.", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-checkout": "*", - "magento/module-payment": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 6dde1d76e5e81..42e7ad2c1d5e8 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -1,48 +1,49 @@ { "name": "magento/module-catalog", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "104.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-asynchronous-operations": "*", - "magento/module-backend": "*", - "magento/module-catalog-inventory": "*", - "magento/module-catalog-rule": "*", - "magento/module-catalog-url-rewrite": "*", - "magento/module-checkout": "*", - "magento/module-cms": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-indexer": "*", - "magento/module-media-storage": "*", - "magento/module-msrp": "*", - "magento/module-page-cache": "*", - "magento/module-product-alert": "*", - "magento/module-quote": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-theme": "*", - "magento/module-ui": "*", - "magento/module-url-rewrite": "*", - "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-asynchronous-operations": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-catalog-rule": "101.2.*", + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-indexer": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-msrp": "100.4.*", + "magento/module-page-cache": "100.4.*", + "magento/module-product-alert": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-url-rewrite": "102.0.*", + "magento/module-widget": "101.2.*", + "magento/module-wishlist": "101.2.*" }, "suggest": { - "magento/module-cookie": "*", - "magento/module-sales": "*", - "magento/module-catalog-sample-data": "*" + "magento/module-cookie": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-catalog-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -52,3 +53,4 @@ } } } + diff --git a/app/code/Magento/CatalogAnalytics/composer.json b/app/code/Magento/CatalogAnalytics/composer.json index 43fb4c8a6f433..0efaa58bede19 100644 --- a/app/code/Magento/CatalogAnalytics/composer.json +++ b/app/code/Magento/CatalogAnalytics/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-catalog-analytics", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-analytics": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-analytics": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/CatalogCmsGraphQl/composer.json b/app/code/Magento/CatalogCmsGraphQl/composer.json index aa7a742f2f315..38c79b614b63e 100644 --- a/app/code/Magento/CatalogCmsGraphQl/composer.json +++ b/app/code/Magento/CatalogCmsGraphQl/composer.json @@ -2,21 +2,22 @@ "name": "magento/module-catalog-cms-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-cms-graph-ql": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-cms-graph-ql": "100.4.*" }, "suggest": { - "magento/module-graph-ql": "*", - "magento/module-cms": "*", - "magento/module-catalog-graph-ql": "*" + "magento/module-graph-ql": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-catalog-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/CatalogCustomerGraphQl/composer.json b/app/code/Magento/CatalogCustomerGraphQl/composer.json index a7c887af0379b..ebb5040875c2a 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/composer.json +++ b/app/code/Magento/CatalogCustomerGraphQl/composer.json @@ -2,18 +2,19 @@ "name": "magento/module-catalog-customer-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-customer": "*", - "magento/module-catalog-graph-ql": "*", - "magento/module-store": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-catalog-graph-ql": "100.4.*", + "magento/module-store": "101.1.*" + }, "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index 46d7454a6d7e2..50757cd357a86 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -2,26 +2,27 @@ "name": "magento/module-catalog-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-eav": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-search": "*", - "magento/module-store": "*", - "magento/module-eav-graph-ql": "*", - "magento/module-catalog-search": "*", - "magento/framework": "*", - "magento/module-graph-ql": "*" + "magento/module-eav": "102.1.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-search": "101.1.*", + "magento/module-store": "101.1.*", + "magento/module-eav-graph-ql": "100.4.*", + "magento/module-catalog-search": "102.0.*", + "magento/framework": "103.0.*", + "magento/module-graph-ql": "100.4.*" }, "suggest": { - "magento/module-graph-ql-cache": "*", - "magento/module-store-graph-ql": "*" + "magento/module-graph-ql-cache": "100.4.*", + "magento/module-store-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json index 92a6620827990..9a8ab5adde9fd 100644 --- a/app/code/Magento/CatalogImportExport/composer.json +++ b/app/code/Magento/CatalogImportExport/composer.json @@ -1,29 +1,30 @@ { "name": "magento/module-catalog-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", "ext-ctype": "*", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-catalog-url-rewrite": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-import-export": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-authorization": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-authorization": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -33,3 +34,4 @@ } } } + diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index b810e6613aebb..e55114501b28a 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-catalog-inventory", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-quote": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -30,3 +31,4 @@ }, "abandoned": "magento/inventory-composer-metapackage" } + diff --git a/app/code/Magento/CatalogInventoryGraphQl/composer.json b/app/code/Magento/CatalogInventoryGraphQl/composer.json index d6d5b01091341..9138144e82c8f 100644 --- a/app/code/Magento/CatalogInventoryGraphQl/composer.json +++ b/app/code/Magento/CatalogInventoryGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-catalog-inventory-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/CatalogRule/composer.json b/app/code/Magento/CatalogRule/composer.json index 7c40ca8a9a33a..e1d4f361ef731 100644 --- a/app/code/Magento/CatalogRule/composer.json +++ b/app/code/Magento/CatalogRule/composer.json @@ -1,29 +1,30 @@ { "name": "magento/module-catalog-rule", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-rule": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-rule": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-import-export": "*", - "magento/module-catalog-rule-sample-data": "*" + "magento/module-import-export": "101.0.*", + "magento/module-catalog-rule-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -33,3 +34,4 @@ } } } + diff --git a/app/code/Magento/CatalogRuleConfigurable/composer.json b/app/code/Magento/CatalogRuleConfigurable/composer.json index 19274fbae146f..95bd798a6f507 100644 --- a/app/code/Magento/CatalogRuleConfigurable/composer.json +++ b/app/code/Magento/CatalogRuleConfigurable/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-catalog-rule-configurable", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", + "magento/framework": "103.0.*", "magento/magento-composer-installer": "*", - "magento/module-catalog": "*", - "magento/module-catalog-rule": "*", - "magento/module-configurable-product": "*" + "magento/module-catalog": "104.0.*", + "magento/module-catalog-rule": "101.2.*", + "magento/module-configurable-product": "100.4.*" }, "suggest": { - "magento/module-catalog-rule": "*" + "magento/module-catalog-rule": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/CatalogRuleGraphQl/composer.json b/app/code/Magento/CatalogRuleGraphQl/composer.json index c82d9bb20ddab..aed9eef07394e 100644 --- a/app/code/Magento/CatalogRuleGraphQl/composer.json +++ b/app/code/Magento/CatalogRuleGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-catalog-rule-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-catalog-rule": "*" + "magento/module-catalog-rule": "101.2.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json index 1efece402fd84..201447c73a031 100644 --- a/app/code/Magento/CatalogSearch/composer.json +++ b/app/code/Magento/CatalogSearch/composer.json @@ -1,32 +1,33 @@ { "name": "magento/module-catalog-search", "description": "Catalog search", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "102.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-indexer": "*", - "magento/module-catalog-inventory": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-search": "*", - "magento/module-store": "*", - "magento/module-theme": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-indexer": "100.4.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-search": "101.1.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -36,3 +37,4 @@ } } } + diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json index fe489bcf0a3a0..16f804631b29b 100644 --- a/app/code/Magento/CatalogUrlRewrite/composer.json +++ b/app/code/Magento/CatalogUrlRewrite/composer.json @@ -1,29 +1,30 @@ { "name": "magento/module-catalog-url-rewrite", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-import-export": "*", - "magento/module-eav": "*", - "magento/module-import-export": "*", - "magento/module-store": "*", - "magento/module-ui": "*", - "magento/module-url-rewrite": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-import-export": "101.1.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-url-rewrite": "102.0.*" }, "suggest": { - "magento/module-webapi": "*" + "magento/module-webapi": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -33,3 +34,4 @@ } } } + diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json index 3b64d51b85568..20012a583e2ef 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json @@ -2,21 +2,22 @@ "name": "magento/module-catalog-url-rewrite-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-store": "*", - "magento/module-catalog": "*", - "magento/framework": "*" + "magento/module-store": "101.1.*", + "magento/module-catalog": "104.0.*", + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-catalog-url-rewrite": "*", - "magento/module-catalog-graph-ql": "*", - "magento/module-url-rewrite-graph-ql": "*" + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-catalog-graph-ql": "100.4.*", + "magento/module-url-rewrite-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 305fb3ec47ad6..0ddbd5d7201dc 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -1,27 +1,28 @@ { "name": "magento/module-catalog-widget", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-rule": "*", - "magento/module-store": "*", - "magento/module-widget": "*", - "magento/module-wishlist": "*", - "magento/module-theme": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-rule": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-widget": "101.2.*", + "magento/module-wishlist": "101.2.*", + "magento/module-theme": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 5f7b5425667e5..9474d0942726a 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -1,40 +1,41 @@ { "name": "magento/module-checkout", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-msrp": "*", - "magento/module-page-cache": "*", - "magento/module-payment": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-sales-rule": "*", - "magento/module-shipping": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-theme": "*", - "magento/module-ui": "*", - "magento/module-captcha": "*", - "magento/module-authorization": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-msrp": "100.4.*", + "magento/module-page-cache": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-captcha": "100.4.*", + "magento/module-authorization": "100.4.*" }, "suggest": { - "magento/module-cookie": "*" + "magento/module-cookie": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -44,3 +45,4 @@ } } } + diff --git a/app/code/Magento/CheckoutAgreements/composer.json b/app/code/Magento/CheckoutAgreements/composer.json index 1741de53e8637..b9e1011bc7172 100644 --- a/app/code/Magento/CheckoutAgreements/composer.json +++ b/app/code/Magento/CheckoutAgreements/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-checkout-agreements", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-checkout": "*", - "magento/module-quote": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/composer.json b/app/code/Magento/CheckoutAgreementsGraphQl/composer.json index 26b80a4457b4a..ab59af7c0746f 100644 --- a/app/code/Magento/CheckoutAgreementsGraphQl/composer.json +++ b/app/code/Magento/CheckoutAgreementsGraphQl/composer.json @@ -2,19 +2,20 @@ "name": "magento/module-checkout-agreements-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-checkout-agreements": "*" + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-checkout-agreements": "100.4.*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/Cms/composer.json b/app/code/Magento/Cms/composer.json index 8d69320102b5e..600ea98cceb16 100644 --- a/app/code/Magento/Cms/composer.json +++ b/app/code/Magento/Cms/composer.json @@ -1,30 +1,31 @@ { "name": "magento/module-cms", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "104.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-email": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*", - "magento/module-theme": "*", - "magento/module-ui": "*", - "magento/module-variable": "*", - "magento/module-widget": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-email": "101.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-variable": "100.4.*", + "magento/module-widget": "101.2.*" }, "suggest": { - "magento/module-cms-sample-data": "*" + "magento/module-cms-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -34,3 +35,4 @@ } } } + diff --git a/app/code/Magento/CmsGraphQl/composer.json b/app/code/Magento/CmsGraphQl/composer.json index 0e4c849fe8344..e98d8501711c6 100644 --- a/app/code/Magento/CmsGraphQl/composer.json +++ b/app/code/Magento/CmsGraphQl/composer.json @@ -2,22 +2,23 @@ "name": "magento/module-cms-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-cms": "*", - "magento/module-widget": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-widget": "101.2.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-graph-ql": "*", - "magento/module-graph-ql-cache": "*", - "magento/module-store-graph-ql": "*" + "magento/module-graph-ql": "100.4.*", + "magento/module-graph-ql-cache": "100.4.*", + "magento/module-store-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/CmsUrlRewrite/composer.json b/app/code/Magento/CmsUrlRewrite/composer.json index 80e150771975f..e4c1baef35c36 100644 --- a/app/code/Magento/CmsUrlRewrite/composer.json +++ b/app/code/Magento/CmsUrlRewrite/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-cms-url-rewrite", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-cms": "*", - "magento/module-store": "*", - "magento/module-url-rewrite": "*" + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-store": "101.1.*", + "magento/module-url-rewrite": "102.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json index d8fbbb4c2e6fd..5a0f1c6d393ab 100644 --- a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json @@ -2,21 +2,22 @@ "name": "magento/module-cms-url-rewrite-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-cms": "*", - "magento/module-store": "*", - "magento/module-url-rewrite-graph-ql": "*" + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-store": "101.1.*", + "magento/module-url-rewrite-graph-ql": "100.4.*" }, "suggest": { - "magento/module-cms-url-rewrite": "*", - "magento/module-catalog-graph-ql": "*" + "magento/module-cms-url-rewrite": "100.4.*", + "magento/module-catalog-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json index 63eca42a6ac48..95611a0acecfb 100644 --- a/app/code/Magento/Config/composer.json +++ b/app/code/Magento/Config/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-config", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-cron": "*", - "magento/module-deploy": "*", - "magento/module-directory": "*", - "magento/module-email": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cron": "100.4.*", + "magento/module-deploy": "100.4.*", + "magento/module-directory": "100.4.*", + "magento/module-email": "101.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json index e27510166a421..7da2c2c3b886c 100644 --- a/app/code/Magento/ConfigurableImportExport/composer.json +++ b/app/code/Magento/ConfigurableImportExport/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-configurable-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-import-export": "*", - "magento/module-configurable-product": "*", - "magento/module-eav": "*", - "magento/module-import-export": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-import-export": "101.1.*", + "magento/module-configurable-product": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index 7b1b1a18416f5..0e8afa3bbb2f8 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -1,38 +1,39 @@ { "name": "magento/module-configurable-product", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-media-storage": "*", - "magento/module-quote": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-msrp": "*", - "magento/module-webapi": "*", - "magento/module-sales": "*", - "magento/module-sales-rule": "*", - "magento/module-product-video": "*", - "magento/module-configurable-sample-data": "*", - "magento/module-product-links-sample-data": "*", - "magento/module-tax": "*" + "magento/module-msrp": "100.4.*", + "magento/module-webapi": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-product-video": "100.4.*", + "magento/module-configurable-sample-data": "Sample Data version: 100.4.*", + "magento/module-product-links-sample-data": "Sample Data version: 100.4.*", + "magento/module-tax": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -42,3 +43,4 @@ } } } + diff --git a/app/code/Magento/ConfigurableProductGraphQl/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index 295efb65b1978..c2b06b3a8f9e0 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -2,20 +2,21 @@ "name": "magento/module-configurable-product-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "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": "*", - "magento/framework": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-catalog": "104.0.*", + "magento/module-configurable-product": "100.4.*", + "magento/module-graph-ql": "100.4.*", + "magento/module-catalog-graph-ql": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-quote-graph-ql": "100.4.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/ConfigurableProductSales/composer.json b/app/code/Magento/ConfigurableProductSales/composer.json index edac2b7782dcc..8b8d2467c58cf 100644 --- a/app/code/Magento/ConfigurableProductSales/composer.json +++ b/app/code/Magento/ConfigurableProductSales/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-configurable-product-sales", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-sales": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-configurable-product": "*" + "magento/module-configurable-product": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/Contact/composer.json b/app/code/Magento/Contact/composer.json index 1600c1e0c2543..1eb955166e5e8 100644 --- a/app/code/Magento/Contact/composer.json +++ b/app/code/Magento/Contact/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-contact", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-cms": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Cookie/composer.json b/app/code/Magento/Cookie/composer.json index 5a47a5c7993bf..2a47b375d35d9 100644 --- a/app/code/Magento/Cookie/composer.json +++ b/app/code/Magento/Cookie/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-cookie", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-backend": "*" + "magento/module-backend": "102.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Cron/composer.json b/app/code/Magento/Cron/composer.json index 00da35140744b..a40fd986bee27 100644 --- a/app/code/Magento/Cron/composer.json +++ b/app/code/Magento/Cron/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-cron", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Csp/composer.json b/app/code/Magento/Csp/composer.json index 352735712b1b0..af4759029168b 100644 --- a/app/code/Magento/Csp/composer.json +++ b/app/code/Magento/Csp/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-csp", "description": "CSP module enables Content Security Policies for Magento", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/CurrencySymbol/composer.json b/app/code/Magento/CurrencySymbol/composer.json index 746cfa0ed033d..8a5536844630f 100644 --- a/app/code/Magento/CurrencySymbol/composer.json +++ b/app/code/Magento/CurrencySymbol/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-currency-symbol", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-page-cache": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-page-cache": "100.4.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json index db3108a78e9aa..51a533c1eda19 100644 --- a/app/code/Magento/Customer/composer.json +++ b/app/code/Magento/Customer/composer.json @@ -1,40 +1,41 @@ { "name": "magento/module-customer", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "103.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-checkout": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-integration": "*", - "magento/module-media-storage": "*", - "magento/module-newsletter": "*", - "magento/module-page-cache": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-theme": "*", - "magento/module-ui": "*", - "magento/module-wishlist": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-integration": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-newsletter": "100.4.*", + "magento/module-page-cache": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-wishlist": "101.2.*" }, "suggest": { - "magento/module-cookie": "*", - "magento/module-customer-sample-data": "*" + "magento/module-cookie": "100.4.*", + "magento/module-customer-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -44,3 +45,4 @@ } } } + diff --git a/app/code/Magento/CustomerAnalytics/composer.json b/app/code/Magento/CustomerAnalytics/composer.json index abd9e93d89583..56bca0fb02456 100644 --- a/app/code/Magento/CustomerAnalytics/composer.json +++ b/app/code/Magento/CustomerAnalytics/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-customer-analytics", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-customer": "*", - "magento/module-analytics": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-customer": "103.0.*", + "magento/module-analytics": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/CustomerDownloadableGraphQl/composer.json b/app/code/Magento/CustomerDownloadableGraphQl/composer.json index f7cdbb0dc86d6..5518f0cb68f6b 100644 --- a/app/code/Magento/CustomerDownloadableGraphQl/composer.json +++ b/app/code/Magento/CustomerDownloadableGraphQl/composer.json @@ -2,19 +2,20 @@ "name": "magento/module-customer-downloadable-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-downloadable-graph-ql": "*", - "magento/module-graph-ql": "*", - "magento/framework": "*" + "magento/module-downloadable-graph-ql": "100.4.*", + "magento/module-graph-ql": "100.4.*", + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-catalog-graph-ql": "*" + "magento/module-catalog-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json index 2ec396ca8ee92..82c85db4ce6fa 100644 --- a/app/code/Magento/CustomerGraphQl/composer.json +++ b/app/code/Magento/CustomerGraphQl/composer.json @@ -2,22 +2,23 @@ "name": "magento/module-customer-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-authorization": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-graph-ql": "*", - "magento/module-newsletter": "*", - "magento/module-integration": "*", - "magento/module-store": "*", - "magento/framework": "*", - "magento/module-directory": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-authorization": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-graph-ql": "100.4.*", + "magento/module-newsletter": "100.4.*", + "magento/module-integration": "100.4.*", + "magento/module-store": "101.1.*", + "magento/framework": "103.0.*", + "magento/module-directory": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/CustomerImportExport/composer.json b/app/code/Magento/CustomerImportExport/composer.json index 8104ea01875a6..2773a9295f239 100644 --- a/app/code/Magento/CustomerImportExport/composer.json +++ b/app/code/Magento/CustomerImportExport/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-customer-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-import-export": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/Deploy/composer.json b/app/code/Magento/Deploy/composer.json index d8668dbb84874..752b7551777f2 100644 --- a/app/code/Magento/Deploy/composer.json +++ b/app/code/Magento/Deploy/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-deploy", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-config": "*", - "magento/module-require-js": "*", - "magento/module-store": "*", - "magento/module-user": "*" + "magento/framework": "103.0.*", + "magento/module-config": "101.2.*", + "magento/module-require-js": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-user": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "cli_commands.php", @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/Developer/composer.json b/app/code/Magento/Developer/composer.json index c5c949ec45f62..14c1fb91706a9 100644 --- a/app/code/Magento/Developer/composer.json +++ b/app/code/Magento/Developer/composer.json @@ -1,20 +1,21 @@ { "name": "magento/module-developer", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-config": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/Dhl/composer.json b/app/code/Magento/Dhl/composer.json index d81ae0d7b4969..4f359ba46f25d 100644 --- a/app/code/Magento/Dhl/composer.json +++ b/app/code/Magento/Dhl/composer.json @@ -1,31 +1,32 @@ { "name": "magento/module-dhl", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-checkout": "*" + "magento/module-checkout": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -35,3 +36,4 @@ } } } + diff --git a/app/code/Magento/Directory/composer.json b/app/code/Magento/Directory/composer.json index e3646d38fe64d..d9c54914a790a 100644 --- a/app/code/Magento/Directory/composer.json +++ b/app/code/Magento/Directory/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-directory", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-config": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/DirectoryGraphQl/composer.json b/app/code/Magento/DirectoryGraphQl/composer.json index ef473e1c43b94..3c74e1d497969 100644 --- a/app/code/Magento/DirectoryGraphQl/composer.json +++ b/app/code/Magento/DirectoryGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-directory-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-directory": "*", - "magento/module-store": "*", - "magento/module-graph-ql": "*", - "magento/framework": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-directory": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-graph-ql": "100.4.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/Downloadable/composer.json b/app/code/Magento/Downloadable/composer.json index 992bdbd1e263c..045bfdd234ffa 100644 --- a/app/code/Magento/Downloadable/composer.json +++ b/app/code/Magento/Downloadable/composer.json @@ -1,37 +1,38 @@ { "name": "magento/module-downloadable", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-checkout": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-gift-message": "*", - "magento/module-media-storage": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-theme": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-gift-message": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-downloadable-sample-data": "*" + "magento/module-downloadable-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -41,3 +42,4 @@ } } } + diff --git a/app/code/Magento/DownloadableGraphQl/composer.json b/app/code/Magento/DownloadableGraphQl/composer.json index d03a5953506e5..8eba317d8b413 100644 --- a/app/code/Magento/DownloadableGraphQl/composer.json +++ b/app/code/Magento/DownloadableGraphQl/composer.json @@ -2,24 +2,25 @@ "name": "magento/module-downloadable-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-store": "*", - "magento/module-catalog": "*", - "magento/module-downloadable": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-quote-graph-ql": "*", - "magento/framework": "*" + "magento/module-store": "101.1.*", + "magento/module-catalog": "104.0.*", + "magento/module-downloadable": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-quote-graph-ql": "100.4.*", + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-catalog-graph-ql": "*", - "magento/module-sales-graph-ql": "*" + "magento/module-catalog-graph-ql": "100.4.*", + "magento/module-sales-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/DownloadableImportExport/composer.json b/app/code/Magento/DownloadableImportExport/composer.json index 6dd7043fc02a9..230c4a04b17ad 100644 --- a/app/code/Magento/DownloadableImportExport/composer.json +++ b/app/code/Magento/DownloadableImportExport/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-downloadable-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-import-export": "*", - "magento/module-downloadable": "*", - "magento/module-eav": "*", - "magento/module-import-export": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-import-export": "101.1.*", + "magento/module-downloadable": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-import-export": "101.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/Eav/composer.json b/app/code/Magento/Eav/composer.json index 5636b0d05841c..b94035e5b518f 100644 --- a/app/code/Magento/Eav/composer.json +++ b/app/code/Magento/Eav/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-eav", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "102.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-config": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/EavGraphQl/composer.json b/app/code/Magento/EavGraphQl/composer.json index ba4138f67cf62..962768a55d178 100644 --- a/app/code/Magento/EavGraphQl/composer.json +++ b/app/code/Magento/EavGraphQl/composer.json @@ -2,18 +2,19 @@ "name": "magento/module-eav-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-eav": "*" + "magento/framework": "103.0.*", + "magento/module-eav": "102.1.*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index b79ae7bc5cc47..ade8dbeb3429f 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -1,27 +1,28 @@ { "name": "magento/module-elasticsearch", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-advanced-search": "*", - "magento/module-catalog": "*", - "magento/module-catalog-search": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-search": "*", - "magento/module-store": "*", - "magento/module-catalog-inventory": "*", - "magento/framework": "*", + "magento/module-advanced-search": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-search": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-search": "101.1.*", + "magento/module-store": "101.1.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/framework": "103.0.*", "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/Elasticsearch6/composer.json b/app/code/Magento/Elasticsearch6/composer.json index 1ee92c0b0a3b3..f68744be48139 100644 --- a/app/code/Magento/Elasticsearch6/composer.json +++ b/app/code/Magento/Elasticsearch6/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-elasticsearch-6", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-advanced-search": "*", - "magento/module-catalog-search": "*", - "magento/module-search": "*", - "magento/module-elasticsearch": "*", + "magento/framework": "103.0.*", + "magento/module-advanced-search": "100.4.*", + "magento/module-catalog-search": "102.0.*", + "magento/module-search": "101.1.*", + "magento/module-elasticsearch": "101.0.*", "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/Elasticsearch7/composer.json b/app/code/Magento/Elasticsearch7/composer.json index 1e59ceaebaf84..ab2c52a2b9b32 100644 --- a/app/code/Magento/Elasticsearch7/composer.json +++ b/app/code/Magento/Elasticsearch7/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-elasticsearch-7", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-elasticsearch": "*", + "magento/framework": "103.0.*", + "magento/module-elasticsearch": "101.0.*", "elasticsearch/elasticsearch": "~7.7.0", - "magento/module-advanced-search": "*", - "magento/module-catalog-search": "*" + "magento/module-advanced-search": "100.4.*", + "magento/module-catalog-search": "102.0.*" }, "suggest": { - "magento/module-config": "*", - "magento/module-search": "*" + "magento/module-config": "101.2.*", + "magento/module-search": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/Email/composer.json b/app/code/Magento/Email/composer.json index 334bbcf9d4617..c5cee1c6610fd 100644 --- a/app/code/Magento/Email/composer.json +++ b/app/code/Magento/Email/composer.json @@ -1,30 +1,31 @@ { "name": "magento/module-email", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-cms": "*", - "magento/module-config": "*", - "magento/module-store": "*", - "magento/module-theme": "*", - "magento/module-require-js": "*", - "magento/module-media-storage": "*", - "magento/module-variable": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-require-js": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-variable": "100.4.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-theme": "*" + "magento/module-theme": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -34,3 +35,4 @@ } } } + diff --git a/app/code/Magento/EncryptionKey/composer.json b/app/code/Magento/EncryptionKey/composer.json index 6677a5b181f83..9efe0d770e7c3 100644 --- a/app/code/Magento/EncryptionKey/composer.json +++ b/app/code/Magento/EncryptionKey/composer.json @@ -1,20 +1,21 @@ { "name": "magento/module-encryption-key", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-config": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/Fedex/composer.json b/app/code/Magento/Fedex/composer.json index 575311e148457..46835867dde94 100644 --- a/app/code/Magento/Fedex/composer.json +++ b/app/code/Magento/Fedex/composer.json @@ -1,27 +1,28 @@ { "name": "magento/module-fedex", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/GiftMessage/composer.json b/app/code/Magento/GiftMessage/composer.json index cdf0533c3270d..389964813fa57 100644 --- a/app/code/Magento/GiftMessage/composer.json +++ b/app/code/Magento/GiftMessage/composer.json @@ -1,30 +1,31 @@ { "name": "magento/module-gift-message", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-eav": "*", - "magento/module-multishipping": "*" + "magento/module-eav": "102.1.*", + "magento/module-multishipping": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -34,3 +35,4 @@ } } } + diff --git a/app/code/Magento/GiftMessageGraphQl/composer.json b/app/code/Magento/GiftMessageGraphQl/composer.json index 48088f2a48a32..ba8f9ebd37db1 100644 --- a/app/code/Magento/GiftMessageGraphQl/composer.json +++ b/app/code/Magento/GiftMessageGraphQl/composer.json @@ -2,18 +2,19 @@ "name": "magento/module-gift-message-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-gift-message": "*" + "magento/framework": "103.0.*", + "magento/module-gift-message": "100.4.*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/GoogleAdwords/composer.json b/app/code/Magento/GoogleAdwords/composer.json index a37470115584f..8043d7eba9488 100644 --- a/app/code/Magento/GoogleAdwords/composer.json +++ b/app/code/Magento/GoogleAdwords/composer.json @@ -1,20 +1,21 @@ { "name": "magento/module-google-adwords", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-sales": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/GoogleAnalytics/composer.json b/app/code/Magento/GoogleAnalytics/composer.json index 64d210c4f4811..3f068a0f8361d 100644 --- a/app/code/Magento/GoogleAnalytics/composer.json +++ b/app/code/Magento/GoogleAnalytics/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-google-analytics", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-cookie": "*", - "magento/module-sales": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-cookie": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/GoogleOptimizer/composer.json b/app/code/Magento/GoogleOptimizer/composer.json index 426526a922ec8..2f496d75f9042 100644 --- a/app/code/Magento/GoogleOptimizer/composer.json +++ b/app/code/Magento/GoogleOptimizer/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-google-optimizer", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-cms": "*", - "magento/module-google-analytics": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-cms": "104.0.*", + "magento/module-google-analytics": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index 401e77a787acf..ffb6c0fbc7d94 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -2,19 +2,20 @@ "name": "magento/module-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-eav": "*", - "magento/framework": "*", - "magento/module-webapi": "*" + "magento/module-eav": "102.1.*", + "magento/framework": "103.0.*", + "magento/module-webapi": "100.4.*" }, "suggest": { - "magento/module-graph-ql-cache": "*" + "magento/module-graph-ql-cache": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/GraphQlCache/composer.json b/app/code/Magento/GraphQlCache/composer.json index 4cfdd0c4f660a..57b470cb512b9 100644 --- a/app/code/Magento/GraphQlCache/composer.json +++ b/app/code/Magento/GraphQlCache/composer.json @@ -2,16 +2,17 @@ "name": "magento/module-graph-ql-cache", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-page-cache": "*", - "magento/module-graph-ql": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-page-cache": "100.4.*", + "magento/module-graph-ql": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/GroupedCatalogInventory/composer.json b/app/code/Magento/GroupedCatalogInventory/composer.json index 0d91d939494a8..668bf4428e4ff 100644 --- a/app/code/Magento/GroupedCatalogInventory/composer.json +++ b/app/code/Magento/GroupedCatalogInventory/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-grouped-catalog-inventory", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-grouped-product": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-grouped-product": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/GroupedImportExport/composer.json b/app/code/Magento/GroupedImportExport/composer.json index 8806058c2bfc8..90c595a9e531e 100644 --- a/app/code/Magento/GroupedImportExport/composer.json +++ b/app/code/Magento/GroupedImportExport/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-grouped-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-import-export": "*", - "magento/module-eav": "*", - "magento/module-grouped-product": "*", - "magento/module-import-export": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-import-export": "101.1.*", + "magento/module-eav": "102.1.*", + "magento/module-grouped-product": "100.4.*", + "magento/module-import-export": "101.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/GroupedProduct/composer.json b/app/code/Magento/GroupedProduct/composer.json index 554b0c239c8fb..2d0958c694875 100644 --- a/app/code/Magento/GroupedProduct/composer.json +++ b/app/code/Magento/GroupedProduct/composer.json @@ -1,34 +1,35 @@ { "name": "magento/module-grouped-product", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-media-storage": "*", - "magento/module-msrp": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-ui": "*", - "magento/module-wishlist": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-msrp": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-wishlist": "101.2.*" }, "suggest": { - "magento/module-grouped-product-sample-data": "*" + "magento/module-grouped-product-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -38,3 +39,4 @@ } } } + diff --git a/app/code/Magento/GroupedProductGraphQl/composer.json b/app/code/Magento/GroupedProductGraphQl/composer.json index 5784acb5f5d04..e3dd433f606e5 100644 --- a/app/code/Magento/GroupedProductGraphQl/composer.json +++ b/app/code/Magento/GroupedProductGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-grouped-product-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-grouped-product": "*", - "magento/module-catalog": "*", - "magento/module-catalog-graph-ql": "*", - "magento/framework": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-grouped-product": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-graph-ql": "100.4.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/ImportExport/composer.json b/app/code/Magento/ImportExport/composer.json index 3be5c03dc2828..251a477cab76f 100644 --- a/app/code/Magento/ImportExport/composer.json +++ b/app/code/Magento/ImportExport/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", "ext-ctype": "*", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-eav": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/Indexer/composer.json b/app/code/Magento/Indexer/composer.json index 07d652e9fa2b5..9451b257e8a01 100644 --- a/app/code/Magento/Indexer/composer.json +++ b/app/code/Magento/Indexer/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-indexer", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/InstantPurchase/composer.json b/app/code/Magento/InstantPurchase/composer.json index 0807926b755a0..89623cc92d2fe 100644 --- a/app/code/Magento/InstantPurchase/composer.json +++ b/app/code/Magento/InstantPurchase/composer.json @@ -6,16 +6,17 @@ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-store": "*", - "magento/module-catalog": "*", - "magento/module-customer": "*", - "magento/module-sales": "*", - "magento/module-shipping": "*", - "magento/module-quote": "*", - "magento/module-vault": "*", - "magento/framework": "*" + "magento/module-store": "101.1.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-vault": "101.2.*", + "magento/framework": "103.0.*" }, "autoload": { "files": [ @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Integration/composer.json b/app/code/Magento/Integration/composer.json index c85e84284b43f..34a2e17f4f087 100644 --- a/app/code/Magento/Integration/composer.json +++ b/app/code/Magento/Integration/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-integration", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-security": "*", - "magento/module-store": "*", - "magento/module-user": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-security": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-user": "101.2.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/LayeredNavigation/composer.json b/app/code/Magento/LayeredNavigation/composer.json index fa3c90dbbd774..1c8cd342d071e 100644 --- a/app/code/Magento/LayeredNavigation/composer.json +++ b/app/code/Magento/LayeredNavigation/composer.json @@ -1,20 +1,21 @@ { "name": "magento/module-layered-navigation", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-config": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/LoginAsCustomer/composer.json b/app/code/Magento/LoginAsCustomer/composer.json index e58ec90e8f8bb..09f2957e8ab29 100755 --- a/app/code/Magento/LoginAsCustomer/composer.json +++ b/app/code/Magento/LoginAsCustomer/composer.json @@ -1,25 +1,29 @@ { "name": "magento/module-login-as-customer", "description": "Allow for admin to enter a customer account", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-login-as-customer-api": "*" - }, - "suggest": { - "magento/module-backend": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-login-as-customer-api": "100.4.*" + }, + "suggest": { + "magento/module-backend": "102.0.*" + }, "autoload": { - "files": [ "registration.php" ], + "files": [ + "registration.php" + ], "psr-4": { "Magento\\LoginAsCustomer\\": "" } } } + diff --git a/app/code/Magento/LoginAsCustomerAdminUi/composer.json b/app/code/Magento/LoginAsCustomerAdminUi/composer.json index b6291226827a8..90694eeb1d1d0 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/composer.json +++ b/app/code/Magento/LoginAsCustomerAdminUi/composer.json @@ -1,28 +1,31 @@ { "name": "magento/module-login-as-customer-admin-ui", - "description": "", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-login-as-customer-api": "*", - "magento/module-login-as-customer-frontend-ui": "*", - "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-sales": "*", - "magento/module-store": "*" - }, - "suggest": { - "magento/module-login-as-customer": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-login-as-customer-api": "100.4.*", + "magento/module-login-as-customer-frontend-ui": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*" + }, + "suggest": { + "magento/module-login-as-customer": "100.4.*" + }, "autoload": { - "files": [ "registration.php" ], + "files": [ + "registration.php" + ], "psr-4": { "Magento\\LoginAsCustomerAdminUi\\": "" } } } + diff --git a/app/code/Magento/LoginAsCustomerApi/composer.json b/app/code/Magento/LoginAsCustomerApi/composer.json index b48319b61398f..ba88a33748821 100644 --- a/app/code/Magento/LoginAsCustomerApi/composer.json +++ b/app/code/Magento/LoginAsCustomerApi/composer.json @@ -1,19 +1,23 @@ { "name": "magento/module-login-as-customer-api", "description": "Allow for admin to enter a customer account", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*" + }, "autoload": { - "files": [ "registration.php" ], + "files": [ + "registration.php" + ], "psr-4": { "Magento\\LoginAsCustomerApi\\": "" } } } + diff --git a/app/code/Magento/LoginAsCustomerAssistance/composer.json b/app/code/Magento/LoginAsCustomerAssistance/composer.json index a02852533b950..9c6ac755ed809 100644 --- a/app/code/Magento/LoginAsCustomerAssistance/composer.json +++ b/app/code/Magento/LoginAsCustomerAssistance/composer.json @@ -1,27 +1,30 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-login-as-customer": "100.4.*", + "magento/module-login-as-customer-api": "100.4.*" + }, + "suggest": { + "magento/module-login-as-customer-admin-ui": "100.4.*" + }, "autoload": { - "files": [ "registration.php" ], + "files": [ + "registration.php" + ], "psr-4": { "Magento\\LoginAsCustomerAssistance\\": "" } } } + diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/composer.json b/app/code/Magento/LoginAsCustomerFrontendUi/composer.json index 279d8ae3ec79e..a4d772dd2f0e2 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/composer.json +++ b/app/code/Magento/LoginAsCustomerFrontendUi/composer.json @@ -1,22 +1,25 @@ { "name": "magento/module-login-as-customer-frontend-ui", - "description": "", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-login-as-customer-api": "*", - "magento/module-customer": "*", - "magento/module-store": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-login-as-customer-api": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*" + }, "autoload": { - "files": [ "registration.php" ], + "files": [ + "registration.php" + ], "psr-4": { "Magento\\LoginAsCustomerFrontendUi\\": "" } } } + diff --git a/app/code/Magento/LoginAsCustomerLog/composer.json b/app/code/Magento/LoginAsCustomerLog/composer.json index cf888f8cb1a59..85cd7de469078 100644 --- a/app/code/Magento/LoginAsCustomerLog/composer.json +++ b/app/code/Magento/LoginAsCustomerLog/composer.json @@ -1,27 +1,30 @@ { "name": "magento/module-login-as-customer-log", - "description": "", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-login-as-customer-api": "*", - "magento/module-ui": "*", - "magento/module-user": "*" - }, - "suggest": { - "magento/module-login-as-customer": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-login-as-customer-api": "100.4.*", + "magento/module-ui": "101.2.*", + "magento/module-user": "101.2.*" + }, + "suggest": { + "magento/module-login-as-customer": "100.4.*" + }, "autoload": { - "files": [ "registration.php" ], + "files": [ + "registration.php" + ], "psr-4": { "Magento\\LoginAsCustomerLog\\": "" } } } + diff --git a/app/code/Magento/LoginAsCustomerPageCache/composer.json b/app/code/Magento/LoginAsCustomerPageCache/composer.json index 84d7f2e2a6730..d16bb45538b09 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/composer.json +++ b/app/code/Magento/LoginAsCustomerPageCache/composer.json @@ -1,24 +1,27 @@ { "name": "magento/module-login-as-customer-page-cache", - "description": "", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-login-as-customer-api": "*" - }, - "suggest": { - "magento/module-page-cache": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-login-as-customer-api": "100.4.*" + }, + "suggest": { + "magento/module-page-cache": "100.4.*" + }, "autoload": { - "files": [ "registration.php" ], + "files": [ + "registration.php" + ], "psr-4": { "Magento\\LoginAsCustomerPageCache\\": "" } } } + diff --git a/app/code/Magento/LoginAsCustomerQuote/composer.json b/app/code/Magento/LoginAsCustomerQuote/composer.json index 556ffc0d3be43..089cc8d2f2eb0 100644 --- a/app/code/Magento/LoginAsCustomerQuote/composer.json +++ b/app/code/Magento/LoginAsCustomerQuote/composer.json @@ -1,21 +1,21 @@ { "name": "magento/module-login-as-customer-quote", - "description": "", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-quote": "*" - }, - "suggest": { - "magento/module-login-as-customer-api": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-quote": "101.2.*" + }, + "suggest": { + "magento/module-login-as-customer-api": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -25,3 +25,4 @@ } } } + diff --git a/app/code/Magento/LoginAsCustomerSales/composer.json b/app/code/Magento/LoginAsCustomerSales/composer.json index 3891504e54092..63295a7cebb8f 100644 --- a/app/code/Magento/LoginAsCustomerSales/composer.json +++ b/app/code/Magento/LoginAsCustomerSales/composer.json @@ -1,21 +1,21 @@ { "name": "magento/module-login-as-customer-sales", - "description": "", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-user": "*", - "magento/module-login-as-customer-api": "*" - }, - "suggest": { - "magento/module-sales": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-user": "101.2.*", + "magento/module-login-as-customer-api": "100.4.*" + }, + "suggest": { + "magento/module-sales": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -25,3 +25,4 @@ } } } + diff --git a/app/code/Magento/Marketplace/composer.json b/app/code/Magento/Marketplace/composer.json index 42bbcf151a17b..442825c378948 100644 --- a/app/code/Magento/Marketplace/composer.json +++ b/app/code/Magento/Marketplace/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-marketplace", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/MediaContent/composer.json b/app/code/Magento/MediaContent/composer.json index 4dc2b3eba0f68..22655d67ab269 100644 --- a/app/code/Magento/MediaContent/composer.json +++ b/app/code/Magento/MediaContent/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-media-content", "description": "Magento module provides the implementation for managing relations between content and media files used in that content", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-media-content-api": "*", - "magento/module-media-gallery-api": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-content-api": "100.4.*", + "magento/module-media-gallery-api": "101.0.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/MediaContentApi/composer.json b/app/code/Magento/MediaContentApi/composer.json index fd1f2f9a0f265..a89c7e1a56c58 100644 --- a/app/code/Magento/MediaContentApi/composer.json +++ b/app/code/Magento/MediaContentApi/composer.json @@ -1,16 +1,17 @@ { "name": "magento/module-media-content-api", "description": "Magento module provides the API interfaces for managing relations between content and media files used in that content", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-media-gallery-api": "*", - "magento/framework": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-media-gallery-api": "101.0.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -20,3 +21,4 @@ } } } + diff --git a/app/code/Magento/MediaContentCatalog/composer.json b/app/code/Magento/MediaContentCatalog/composer.json index 2b19bc95f6ed3..c8c82ce54e609 100644 --- a/app/code/Magento/MediaContentCatalog/composer.json +++ b/app/code/Magento/MediaContentCatalog/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-media-content-catalog", "description": "Magento module provides the implementation of MediaContent functionality for Magento_Catalog module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-media-content-api": "*", - "magento/module-catalog": "*", - "magento/module-eav": "*", - "magento/module-store": "*", - "magento/framework": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-media-content-api": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-eav": "102.1.*", + "magento/module-store": "101.1.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/MediaContentCms/composer.json b/app/code/Magento/MediaContentCms/composer.json index ea32fdd7a49fa..5ab23bff4aa6e 100644 --- a/app/code/Magento/MediaContentCms/composer.json +++ b/app/code/Magento/MediaContentCms/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-media-content-cms", "description": "Magento module provides the implementation of MediaContent functionality for Magento_Cms module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-media-content-api": "*", - "magento/module-cms": "*", - "magento/framework": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-media-content-api": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index 3be5f535487ec..224bd97ecb763 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -1,21 +1,22 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-content-synchronization-api": "100.4.*", + "magento/framework-message-queue": "100.4.*", + "magento/module-media-content-api": "100.4.*" + }, + "suggest": { + "magento/module-media-gallery-synchronization": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json index 1f1e5e4b51c5b..37bb04c7ce195 100644 --- a/app/code/Magento/MediaContentSynchronizationApi/composer.json +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -1,15 +1,16 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -19,3 +20,4 @@ } } } + diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/composer.json b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json index 733f29d3a42c2..b4746f335a654 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/composer.json +++ b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json @@ -1,18 +1,19 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-content-synchronization-api": "100.4.*", + "magento/module-media-gallery-synchronization-api": "100.4.*", + "magento/module-media-content-api": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/MediaContentSynchronizationCms/composer.json b/app/code/Magento/MediaContentSynchronizationCms/composer.json index 9028b9dacd0a2..1e83d61ae1b8c 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/composer.json +++ b/app/code/Magento/MediaContentSynchronizationCms/composer.json @@ -1,18 +1,19 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-content-synchronization-api": "100.4.*", + "magento/module-media-gallery-synchronization-api": "100.4.*", + "magento/module-media-content-api": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/MediaGallery/composer.json b/app/code/Magento/MediaGallery/composer.json index d430a174a9738..ff098785c27c6 100644 --- a/app/code/Magento/MediaGallery/composer.json +++ b/app/code/Magento/MediaGallery/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-media-gallery", "description": "Magento module responsible for media handling", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-media-gallery-api": "*", - "magento/module-cms": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-gallery-api": "101.0.*", + "magento/module-cms": "104.0.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryApi/composer.json b/app/code/Magento/MediaGalleryApi/composer.json index 8bea8ee95b55a..d2b6985699c72 100644 --- a/app/code/Magento/MediaGalleryApi/composer.json +++ b/app/code/Magento/MediaGalleryApi/composer.json @@ -1,15 +1,16 @@ { "name": "magento/module-media-gallery-api", "description": "Magento module responsible for media gallery asset attributes storage and management", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "101.0.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -19,3 +20,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryCatalog/composer.json b/app/code/Magento/MediaGalleryCatalog/composer.json index 192d86684aa76..c77e76985cc89 100644 --- a/app/code/Magento/MediaGalleryCatalog/composer.json +++ b/app/code/Magento/MediaGalleryCatalog/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-media-gallery-catalog", "description": "Magento module responsible for catalog gallery processor delete operation handling", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-media-gallery-api": "*", - "magento/module-catalog": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-gallery-api": "101.0.*", + "magento/module-catalog": "104.0.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/composer.json b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json index efabb70da9f39..80e8d398a0086 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/composer.json +++ b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json @@ -1,22 +1,23 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-media-gallery-api": "101.0.*", + "magento/module-media-gallery-synchronization-api": "100.4.*", + "magento/module-media-gallery-ui-api": "100.4.*" + }, + "suggest": { + "magento/module-catalog": "104.0.*" + }, "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json index 985d581beff25..2299ec3079b35 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/composer.json +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -1,20 +1,21 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" + }, "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json index 1ecfb9a3c8855..f45a802a86e00 100644 --- a/app/code/Magento/MediaGalleryCmsUi/composer.json +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -1,17 +1,18 @@ { "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": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-cms": "104.0.*", + "magento/module-backend": "102.0.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryIntegration/composer.json b/app/code/Magento/MediaGalleryIntegration/composer.json index a9709da81222e..61db48751bd94 100644 --- a/app/code/Magento/MediaGalleryIntegration/composer.json +++ b/app/code/Magento/MediaGalleryIntegration/composer.json @@ -1,26 +1,24 @@ { "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": "*", - "magento/module-ui": "*" - }, - "require-dev": { - "magento/module-cms": "*" - }, - "suggest": { - "magento/module-catalog": "*", - "magento/module-cms": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-gallery-ui-api": "100.4.*", + "magento/module-media-gallery-api": "101.0.*", + "magento/module-media-gallery-synchronization-api": "100.4.*", + "magento/module-ui": "101.2.*" + }, + "suggest": { + "magento/module-catalog": "104.0.*", + "magento/module-cms": "104.0.*" + }, "autoload": { "files": [ "registration.php" @@ -28,5 +26,9 @@ "psr-4": { "Magento\\MediaGalleryIntegration\\": "" } + }, + "require-dev": { + "magento/module-cms": "*" } } + diff --git a/app/code/Magento/MediaGalleryMetadata/composer.json b/app/code/Magento/MediaGalleryMetadata/composer.json index c2ce66ce64c36..0c085040ed450 100644 --- a/app/code/Magento/MediaGalleryMetadata/composer.json +++ b/app/code/Magento/MediaGalleryMetadata/composer.json @@ -1,16 +1,17 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-gallery-metadata-api": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -20,3 +21,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryMetadataApi/composer.json b/app/code/Magento/MediaGalleryMetadataApi/composer.json index f8673884b050c..9ad2057bf30d9 100644 --- a/app/code/Magento/MediaGalleryMetadataApi/composer.json +++ b/app/code/Magento/MediaGalleryMetadataApi/composer.json @@ -1,15 +1,16 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -19,3 +20,4 @@ } } } + diff --git a/app/code/Magento/MediaGallerySynchronization/composer.json b/app/code/Magento/MediaGallerySynchronization/composer.json index f9d642dd02568..cd07f2b07e396 100644 --- a/app/code/Magento/MediaGallerySynchronization/composer.json +++ b/app/code/Magento/MediaGallerySynchronization/composer.json @@ -1,18 +1,19 @@ { "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": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-gallery-api": "101.0.*", + "magento/module-media-gallery-synchronization-api": "100.4.*", + "magento/framework-message-queue": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/MediaGallerySynchronizationApi/composer.json b/app/code/Magento/MediaGallerySynchronizationApi/composer.json index 19bab75dd5f42..7c07b904301df 100644 --- a/app/code/Magento/MediaGallerySynchronizationApi/composer.json +++ b/app/code/Magento/MediaGallerySynchronizationApi/composer.json @@ -1,16 +1,17 @@ { "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": "*", - "magento/module-media-gallery-api": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-gallery-api": "101.0.*" + }, "autoload": { "files": [ "registration.php" @@ -20,3 +21,4 @@ } } } + diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json index 0674014026b24..c037034b9f216 100644 --- a/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json @@ -1,18 +1,19 @@ { "name": "magento/module-media-gallery-synchronization-metadata", "description": "Magento module responsible for images metadata synchronization", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-media-gallery-api": "*", - "magento/module-media-gallery-metadata-api": "*", - "magento/module-media-gallery-synchronization-api": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-media-gallery-api": "101.0.*", + "magento/module-media-gallery-metadata-api": "100.4.*", + "magento/module-media-gallery-synchronization-api": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index f4701306eb369..a2257294ac44c 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -1,24 +1,25 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-ui": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-media-gallery-ui-api": "100.4.*", + "magento/module-media-gallery-api": "101.0.*", + "magento/module-media-gallery-metadata-api": "100.4.*", + "magento/module-media-gallery-synchronization-api": "100.4.*", + "magento/module-media-content-api": "100.4.*", + "magento/module-cms": "104.0.*" + }, "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json index f8d5ef11058c1..3d35e4e8adf2c 100644 --- a/app/code/Magento/MediaGalleryUiApi/composer.json +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -1,15 +1,16 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*" + }, "autoload": { "files": [ "registration.php" @@ -19,3 +20,4 @@ } } } + diff --git a/app/code/Magento/MediaStorage/composer.json b/app/code/Magento/MediaStorage/composer.json index cb1057febb23e..f82969d6a90d3 100644 --- a/app/code/Magento/MediaStorage/composer.json +++ b/app/code/Magento/MediaStorage/composer.json @@ -1,26 +1,27 @@ { "name": "magento/module-media-storage", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/framework-bulk": "*", - "magento/module-backend": "*", - "magento/module-config": "*", - "magento/module-store": "*", - "magento/module-catalog": "*", - "magento/module-theme": "*", - "magento/module-asynchronous-operations": "*", - "magento/module-authorization": "*" + "magento/framework": "103.0.*", + "magento/framework-bulk": "101.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-catalog": "104.0.*", + "magento/module-theme": "101.1.*", + "magento/module-asynchronous-operations": "100.4.*", + "magento/module-authorization": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -30,3 +31,4 @@ } } } + diff --git a/app/code/Magento/MessageQueue/composer.json b/app/code/Magento/MessageQueue/composer.json index 57603f0a73acc..994288c14f5c9 100644 --- a/app/code/Magento/MessageQueue/composer.json +++ b/app/code/Magento/MessageQueue/composer.json @@ -1,20 +1,21 @@ { "name": "magento/module-message-queue", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { - "magento/framework": "*", - "magento/framework-message-queue": "*", + "magento/framework": "103.0.*", + "magento/framework-message-queue": "100.4.*", "magento/magento-composer-installer": "*", "php": "~7.3.0||~7.4.0" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/Msrp/composer.json b/app/code/Magento/Msrp/composer.json index 5c9d2e4cf58fa..29f328a219ac1 100644 --- a/app/code/Magento/Msrp/composer.json +++ b/app/code/Magento/Msrp/composer.json @@ -1,27 +1,28 @@ { "name": "magento/module-msrp", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-downloadable": "*", - "magento/module-eav": "*", - "magento/module-store": "*", - "magento/module-tax": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-downloadable": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*" }, "suggest": { - "magento/module-bundle": "*", - "magento/module-msrp-sample-data": "*" + "magento/module-bundle": "101.0.*", + "magento/module-msrp-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/MsrpConfigurableProduct/composer.json b/app/code/Magento/MsrpConfigurableProduct/composer.json index 53d274a3c4006..352971f7772dc 100644 --- a/app/code/Magento/MsrpConfigurableProduct/composer.json +++ b/app/code/Magento/MsrpConfigurableProduct/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-msrp-configurable-product", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-msrp": "*", - "magento/module-configurable-product": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-msrp": "100.4.*", + "magento/module-configurable-product": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/MsrpGroupedProduct/composer.json b/app/code/Magento/MsrpGroupedProduct/composer.json index 5c426b5910ad7..b77c21b583595 100644 --- a/app/code/Magento/MsrpGroupedProduct/composer.json +++ b/app/code/Magento/MsrpGroupedProduct/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-msrp-grouped-product", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-msrp": "*", - "magento/module-grouped-product": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-msrp": "100.4.*", + "magento/module-grouped-product": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/Multishipping/composer.json b/app/code/Magento/Multishipping/composer.json index 8834603562332..77ff7f8fdec33 100644 --- a/app/code/Magento/Multishipping/composer.json +++ b/app/code/Magento/Multishipping/composer.json @@ -1,28 +1,29 @@ { "name": "magento/module-multishipping", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-payment": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-theme": "*", - "magento/module-captcha": "*" + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-captcha": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -32,3 +33,4 @@ } } } + diff --git a/app/code/Magento/MysqlMq/composer.json b/app/code/Magento/MysqlMq/composer.json index 225b3a091a462..508adbcf69acc 100644 --- a/app/code/Magento/MysqlMq/composer.json +++ b/app/code/Magento/MysqlMq/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-mysql-mq", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*", - "magento/framework-message-queue": "*", + "magento/framework": "103.0.*", + "magento/framework-message-queue": "100.4.*", "magento/magento-composer-installer": "*", - "magento/module-store": "*", + "magento/module-store": "101.1.*", "php": "~7.3.0||~7.4.0" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/NewRelicReporting/composer.json b/app/code/Magento/NewRelicReporting/composer.json index ca4c72d5a3aad..c098476a0af20 100644 --- a/app/code/Magento/NewRelicReporting/composer.json +++ b/app/code/Magento/NewRelicReporting/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-new-relic-reporting", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", + "magento/framework": "103.0.*", "magento/magento-composer-installer": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-config": "*", - "magento/module-configurable-product": "*", - "magento/module-customer": "*", - "magento/module-store": "*" + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-configurable-product": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/Newsletter/composer.json b/app/code/Magento/Newsletter/composer.json index 790370c328644..3ef7c63be0459 100644 --- a/app/code/Magento/Newsletter/composer.json +++ b/app/code/Magento/Newsletter/composer.json @@ -1,27 +1,28 @@ { "name": "magento/module-newsletter", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-cms": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-email": "*", - "magento/module-require-js": "*", - "magento/module-store": "*", - "magento/module-widget": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cms": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-email": "101.1.*", + "magento/module-require-js": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-widget": "101.2.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/NewsletterGraphQl/composer.json b/app/code/Magento/NewsletterGraphQl/composer.json index 92352a8a9adfe..e55f7915448bb 100644 --- a/app/code/Magento/NewsletterGraphQl/composer.json +++ b/app/code/Magento/NewsletterGraphQl/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-newsletter-graph-ql", "description": "Provides GraphQl functionality for the newsletter subscriptions.", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, - "type": "magento2-module", + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-customer": "*", - "magento/module-newsletter": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-customer": "103.0.*", + "magento/module-newsletter": "100.4.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/OfflinePayments/composer.json b/app/code/Magento/OfflinePayments/composer.json index 56c7eb2778c48..3b88c1161e932 100644 --- a/app/code/Magento/OfflinePayments/composer.json +++ b/app/code/Magento/OfflinePayments/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-offline-payments", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-checkout": "*", - "magento/module-payment": "*" + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-payment": "100.4.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/OfflineShipping/composer.json b/app/code/Magento/OfflineShipping/composer.json index 7cd6f05f8ad1c..00e5e2deedc31 100644 --- a/app/code/Magento/OfflineShipping/composer.json +++ b/app/code/Magento/OfflineShipping/composer.json @@ -1,31 +1,32 @@ { "name": "magento/module-offline-shipping", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-sales-rule": "*", - "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-checkout": "*", - "magento/module-offline-shipping-sample-data": "*" + "magento/module-checkout": "100.4.*", + "magento/module-offline-shipping-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -35,3 +36,4 @@ } } } + diff --git a/app/code/Magento/PageCache/composer.json b/app/code/Magento/PageCache/composer.json index 506fd54886d92..991f2967e0fc9 100644 --- a/app/code/Magento/PageCache/composer.json +++ b/app/code/Magento/PageCache/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-page-cache", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-config": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-config": "101.2.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json index 72246c5698f80..37df1e5c84606 100644 --- a/app/code/Magento/Payment/composer.json +++ b/app/code/Magento/Payment/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-payment", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-checkout": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/Paypal/composer.json b/app/code/Magento/Paypal/composer.json index 1b35fae2de1bc..e71f4eee3450c 100644 --- a/app/code/Magento/Paypal/composer.json +++ b/app/code/Magento/Paypal/composer.json @@ -1,38 +1,39 @@ { "name": "magento/module-paypal", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-checkout": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-instant-purchase": "*", - "magento/module-payment": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-theme": "*", - "magento/module-ui": "*", - "magento/module-vault": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-instant-purchase": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-vault": "101.2.*" }, "suggest": { - "magento/module-checkout-agreements": "*" + "magento/module-checkout-agreements": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -42,3 +43,4 @@ } } } + diff --git a/app/code/Magento/PaypalCaptcha/composer.json b/app/code/Magento/PaypalCaptcha/composer.json index b88eb2f1a552e..bedfad57a4090 100644 --- a/app/code/Magento/PaypalCaptcha/composer.json +++ b/app/code/Magento/PaypalCaptcha/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-paypal-captcha", "description": "Provides CAPTCHA validation for PayPal Payflow Pro", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-captcha": "*", - "magento/module-checkout": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-captcha": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-paypal": "*" + "magento/module-paypal": "101.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/PaypalGraphQl/composer.json b/app/code/Magento/PaypalGraphQl/composer.json index 285217da64d72..00d549a1f86ee 100644 --- a/app/code/Magento/PaypalGraphQl/composer.json +++ b/app/code/Magento/PaypalGraphQl/composer.json @@ -1,30 +1,31 @@ { "name": "magento/module-paypal-graph-ql", "description": "GraphQl support for Paypal", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-quote": "*", - "magento/module-checkout": "*", - "magento/module-paypal": "*", - "magento/module-quote-graph-ql": "*", - "magento/module-sales": "*", - "magento/module-payment": "*", - "magento/module-store": "*", - "magento/module-vault": "*" + "magento/framework": "103.0.*", + "magento/module-quote": "101.2.*", + "magento/module-checkout": "100.4.*", + "magento/module-paypal": "101.0.*", + "magento/module-quote-graph-ql": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-payment": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-vault": "101.2.*" }, "suggest": { - "magento/module-graph-ql": "*", - "magento/module-store-graph-ql": "*" + "magento/module-graph-ql": "100.4.*", + "magento/module-store-graph-ql": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -34,3 +35,4 @@ } } } + diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json index 68fe5cb47c00e..5a260afc1947e 100644 --- a/app/code/Magento/Persistent/composer.json +++ b/app/code/Magento/Persistent/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-persistent", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-checkout": "*", - "magento/module-cron": "*", - "magento/module-customer": "*", - "magento/module-page-cache": "*", - "magento/module-quote": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-cron": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-page-cache": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/ProductAlert/composer.json b/app/code/Magento/ProductAlert/composer.json index bfe2a43b373ce..69753d3edaf70 100644 --- a/app/code/Magento/ProductAlert/composer.json +++ b/app/code/Magento/ProductAlert/composer.json @@ -1,26 +1,27 @@ { "name": "magento/module-product-alert", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-customer": "*", - "magento/module-store": "*", - "magento/module-theme": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -30,3 +31,4 @@ } } } + diff --git a/app/code/Magento/ProductVideo/composer.json b/app/code/Magento/ProductVideo/composer.json index b7268338398a7..38c1e2aa09fc0 100644 --- a/app/code/Magento/ProductVideo/composer.json +++ b/app/code/Magento/ProductVideo/composer.json @@ -1,28 +1,29 @@ { "name": "magento/module-product-video", "description": "Add Video to Products", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", + "magento/framework": "103.0.*", "magento/magento-composer-installer": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-eav": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*" + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-customer": "*", - "magento/module-config": "*" + "magento/module-customer": "103.0.*", + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -32,3 +33,4 @@ } } } + diff --git a/app/code/Magento/Quote/composer.json b/app/code/Magento/Quote/composer.json index 31312fae26e78..fa7f2430f1c2e 100644 --- a/app/code/Magento/Quote/composer.json +++ b/app/code/Magento/Quote/composer.json @@ -1,35 +1,36 @@ { "name": "magento/module-quote", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-payment": "*", - "magento/module-sales": "*", - "magento/module-sales-sequence": "*", - "magento/module-shipping": "*", - "magento/module-store": "*", - "magento/module-tax": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-payment": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-sequence": "100.4.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*" }, "suggest": { - "magento/module-webapi": "*" + "magento/module-webapi": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -39,3 +40,4 @@ } } } + diff --git a/app/code/Magento/QuoteAnalytics/composer.json b/app/code/Magento/QuoteAnalytics/composer.json index 4bfb7172c4c83..19e70e0326d57 100644 --- a/app/code/Magento/QuoteAnalytics/composer.json +++ b/app/code/Magento/QuoteAnalytics/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-quote-analytics", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-quote": "*", - "magento/module-analytics": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-quote": "101.2.*", + "magento/module-analytics": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/QuoteBundleOptions/composer.json b/app/code/Magento/QuoteBundleOptions/composer.json index a2651272018a8..2d6cd95f82761 100644 --- a/app/code/Magento/QuoteBundleOptions/composer.json +++ b/app/code/Magento/QuoteBundleOptions/composer.json @@ -1,16 +1,17 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-quote": "101.2.*" + }, "autoload": { "files": [ "registration.php" @@ -20,3 +21,4 @@ } } } + diff --git a/app/code/Magento/QuoteConfigurableOptions/composer.json b/app/code/Magento/QuoteConfigurableOptions/composer.json index 51d6933d5c6d6..0411ce38850c3 100644 --- a/app/code/Magento/QuoteConfigurableOptions/composer.json +++ b/app/code/Magento/QuoteConfigurableOptions/composer.json @@ -1,16 +1,17 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-quote": "101.2.*" + }, "autoload": { "files": [ "registration.php" @@ -20,3 +21,4 @@ } } } + diff --git a/app/code/Magento/QuoteDownloadableLinks/composer.json b/app/code/Magento/QuoteDownloadableLinks/composer.json index ad120dea96263..a098f1c196fd1 100644 --- a/app/code/Magento/QuoteDownloadableLinks/composer.json +++ b/app/code/Magento/QuoteDownloadableLinks/composer.json @@ -1,16 +1,17 @@ { "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" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-quote": "101.2.*" + }, "autoload": { "files": [ "registration.php" @@ -20,3 +21,4 @@ } } } + diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 25f089cf75a62..9327504d78941 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -2,27 +2,28 @@ "name": "magento/module-quote-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-quote": "*", - "magento/module-checkout": "*", - "magento/module-catalog": "*", - "magento/module-store": "*", - "magento/module-customer": "*", - "magento/module-customer-graph-ql": "*", - "magento/module-sales": "*", - "magento/module-directory": "*", - "magento/module-graph-ql": "*", - "magento/module-gift-message": "*" + "magento/framework": "103.0.*", + "magento/module-quote": "101.2.*", + "magento/module-checkout": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-store": "101.1.*", + "magento/module-customer": "103.0.*", + "magento/module-customer-graph-ql": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-graph-ql": "100.4.*", + "magento/module-gift-message": "100.4.*" }, "suggest": { - "magento/module-graph-ql-cache": "*" + "magento/module-graph-ql-cache": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -32,3 +33,4 @@ } } } + diff --git a/app/code/Magento/RelatedProductGraphQl/composer.json b/app/code/Magento/RelatedProductGraphQl/composer.json index 2cb851d56e58e..5b2fa809ef789 100644 --- a/app/code/Magento/RelatedProductGraphQl/composer.json +++ b/app/code/Magento/RelatedProductGraphQl/composer.json @@ -2,19 +2,20 @@ "name": "magento/module-related-product-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-catalog": "*", - "magento/module-catalog-graph-ql": "*", - "magento/framework": "*" + "magento/module-catalog": "104.0.*", + "magento/module-catalog-graph-ql": "100.4.*", + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -24,3 +25,4 @@ } } } + diff --git a/app/code/Magento/ReleaseNotification/composer.json b/app/code/Magento/ReleaseNotification/composer.json index c2e347bc66ef0..d59f25a07b456 100644 --- a/app/code/Magento/ReleaseNotification/composer.json +++ b/app/code/Magento/ReleaseNotification/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-release-notification", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-user": "*", - "magento/module-backend": "*", - "magento/module-ui": "*", - "magento/framework": "*" - }, - "suggest": { - "magento/module-config": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-user": "101.2.*", + "magento/module-backend": "102.0.*", + "magento/module-ui": "101.2.*", + "magento/framework": "103.0.*" + }, + "suggest": { + "magento/module-config": "101.2.*" + }, "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/Reports/composer.json b/app/code/Magento/Reports/composer.json index f1fe6c1e2c83a..1560ed99b6971 100644 --- a/app/code/Magento/Reports/composer.json +++ b/app/code/Magento/Reports/composer.json @@ -1,34 +1,35 @@ { "name": "magento/module-reports", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-cms": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-downloadable": "*", - "magento/module-eav": "*", - "magento/module-quote": "*", - "magento/module-review": "*", - "magento/module-sales": "*", - "magento/module-sales-rule": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-downloadable": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-quote": "101.2.*", + "magento/module-review": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-widget": "101.2.*", + "magento/module-wishlist": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -38,3 +39,4 @@ } } } + diff --git a/app/code/Magento/RequireJs/composer.json b/app/code/Magento/RequireJs/composer.json index 9c3b84e88df53..9835ed7a6d9d9 100644 --- a/app/code/Magento/RequireJs/composer.json +++ b/app/code/Magento/RequireJs/composer.json @@ -1,18 +1,19 @@ { "name": "magento/module-require-js", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/Review/composer.json b/app/code/Magento/Review/composer.json index 5a428ae15fd67..530828348fe3d 100644 --- a/app/code/Magento/Review/composer.json +++ b/app/code/Magento/Review/composer.json @@ -1,30 +1,31 @@ { "name": "magento/module-review", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-newsletter": "*", - "magento/module-store": "*", - "magento/module-theme": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-newsletter": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-cookie": "*", - "magento/module-review-sample-data": "*" + "magento/module-cookie": "100.4.*", + "magento/module-review-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -34,3 +35,4 @@ } } } + diff --git a/app/code/Magento/ReviewAnalytics/composer.json b/app/code/Magento/ReviewAnalytics/composer.json index d18ec43a93ac1..04f8f7b96deb4 100644 --- a/app/code/Magento/ReviewAnalytics/composer.json +++ b/app/code/Magento/ReviewAnalytics/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-review-analytics", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-review": "*", - "magento/module-analytics": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-review": "100.4.*", + "magento/module-analytics": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/ReviewGraphQl/composer.json b/app/code/Magento/ReviewGraphQl/composer.json index 819ddefd76213..9bb3d32aa62b5 100644 --- a/app/code/Magento/ReviewGraphQl/composer.json +++ b/app/code/Magento/ReviewGraphQl/composer.json @@ -2,21 +2,22 @@ "name": "magento/module-review-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-catalog": "*", - "magento/module-review": "*", - "magento/module-store": "*", - "magento/framework": "*" + "magento/module-catalog": "104.0.*", + "magento/module-review": "100.4.*", + "magento/module-store": "101.1.*", + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-graph-ql": "*", - "magento/module-graph-ql-cache": "*" + "magento/module-graph-ql": "100.4.*", + "magento/module-graph-ql-cache": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Robots/composer.json b/app/code/Magento/Robots/composer.json index 2035010b0ce8b..486a817d02118 100644 --- a/app/code/Magento/Robots/composer.json +++ b/app/code/Magento/Robots/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-robots", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.1.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-theme": "*" + "magento/module-theme": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Rss/composer.json b/app/code/Magento/Rss/composer.json index bd845acc12f9a..26493fdcd774d 100644 --- a/app/code/Magento/Rss/composer.json +++ b/app/code/Magento/Rss/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-rss", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/Rule/composer.json b/app/code/Magento/Rule/composer.json index 0ab2b6780dcad..882c7a7660bda 100644 --- a/app/code/Magento/Rule/composer.json +++ b/app/code/Magento/Rule/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-rule", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-eav": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-eav": "102.1.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/Sales/composer.json b/app/code/Magento/Sales/composer.json index 411ad3739d560..12f4188747357 100644 --- a/app/code/Magento/Sales/composer.json +++ b/app/code/Magento/Sales/composer.json @@ -1,45 +1,46 @@ { "name": "magento/module-sales", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "103.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-bundle": "*", - "magento/module-catalog-inventory": "*", - "magento/module-checkout": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-gift-message": "*", - "magento/module-media-storage": "*", - "magento/module-payment": "*", - "magento/module-quote": "*", - "magento/module-reports": "*", - "magento/module-sales-rule": "*", - "magento/module-sales-sequence": "*", - "magento/module-shipping": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-theme": "*", - "magento/module-ui": "*", - "magento/module-widget": "*", - "magento/module-wishlist": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-bundle": "101.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-gift-message": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-sales-rule": "101.2.*", + "magento/module-sales-sequence": "100.4.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-widget": "101.2.*", + "magento/module-wishlist": "101.2.*" }, "suggest": { - "magento/module-sales-sample-data": "*" + "magento/module-sales-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -49,3 +50,4 @@ } } } + diff --git a/app/code/Magento/SalesAnalytics/composer.json b/app/code/Magento/SalesAnalytics/composer.json index ca7926f2d8b5a..5e57e330c1c5f 100644 --- a/app/code/Magento/SalesAnalytics/composer.json +++ b/app/code/Magento/SalesAnalytics/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-sales-analytics", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-sales": "*", - "magento/module-analytics": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-sales": "103.0.*", + "magento/module-analytics": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/SalesGraphQl/composer.json b/app/code/Magento/SalesGraphQl/composer.json index b85d8c0f852da..1d7cf3da05685 100644 --- a/app/code/Magento/SalesGraphQl/composer.json +++ b/app/code/Magento/SalesGraphQl/composer.json @@ -2,21 +2,22 @@ "name": "magento/module-sales-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-catalog": "*", - "magento/module-tax": "*", - "magento/module-quote": "*", - "magento/module-graph-ql": "*", - "magento/module-shipping": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-catalog": "104.0.*", + "magento/module-tax": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-graph-ql": "100.4.*", + "magento/module-shipping": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/SalesInventory/composer.json b/app/code/Magento/SalesInventory/composer.json index 6a91b04a7c0d9..fbf4820f4d110 100644 --- a/app/code/Magento/SalesInventory/composer.json +++ b/app/code/Magento/SalesInventory/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-sales-inventory", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-sales": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/SalesRule/composer.json b/app/code/Magento/SalesRule/composer.json index 572e191093275..65fd55a2b3806 100644 --- a/app/code/Magento/SalesRule/composer.json +++ b/app/code/Magento/SalesRule/composer.json @@ -1,40 +1,41 @@ { "name": "magento/module-sales-rule", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-rule": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-payment": "*", - "magento/module-quote": "*", - "magento/module-reports": "*", - "magento/module-rule": "*", - "magento/module-sales": "*", - "magento/module-shipping": "*", - "magento/module-store": "*", - "magento/module-ui": "*", - "magento/module-widget": "*", - "magento/module-captcha": "*", - "magento/module-checkout": "*", - "magento/module-authorization": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-rule": "101.2.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-rule": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-widget": "101.2.*", + "magento/module-captcha": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-authorization": "100.4.*" }, "suggest": { - "magento/module-sales-rule-sample-data": "*" + "magento/module-sales-rule-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -44,3 +45,4 @@ } } } + diff --git a/app/code/Magento/SalesSequence/composer.json b/app/code/Magento/SalesSequence/composer.json index a0f9cb45cafc8..a39c350628c8a 100644 --- a/app/code/Magento/SalesSequence/composer.json +++ b/app/code/Magento/SalesSequence/composer.json @@ -1,18 +1,19 @@ { "name": "magento/module-sales-sequence", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/SampleData/composer.json b/app/code/Magento/SampleData/composer.json index 30efc94bc9274..baa802aa8753c 100644 --- a/app/code/Magento/SampleData/composer.json +++ b/app/code/Magento/SampleData/composer.json @@ -1,21 +1,22 @@ { "name": "magento/module-sample-data", "description": "Sample Data fixtures", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, "suggest": { - "magento/sample-data-media": "*" + "magento/sample-data-media": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "cli_commands.php", @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Search/composer.json b/app/code/Magento/Search/composer.json index 3df1dc5935ad8..945a016262d2b 100644 --- a/app/code/Magento/Search/composer.json +++ b/app/code/Magento/Search/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-search", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog-search": "*", - "magento/module-reports": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog-search": "102.0.*", + "magento/module-reports": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/Security/composer.json b/app/code/Magento/Security/composer.json index 4978f0c628f96..6b3ac2c37ef29 100644 --- a/app/code/Magento/Security/composer.json +++ b/app/code/Magento/Security/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-security", "description": "Security management module", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-store": "*", - "magento/module-user": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-store": "101.1.*", + "magento/module-user": "101.2.*" }, "suggest": { - "magento/module-customer": "*" + "magento/module-customer": "103.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index 17c908ab33e3e..cf8ef2f923eb3 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -1,24 +1,25 @@ { "name": "magento/module-send-friend", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-customer": "*", - "magento/module-store": "*", - "magento/module-captcha": "*", - "magento/module-authorization": "*", - "magento/module-theme": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-customer": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-captcha": "100.4.*", + "magento/module-authorization": "100.4.*", + "magento/module-theme": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -28,3 +29,4 @@ } } } + diff --git a/app/code/Magento/SendFriendGraphQl/composer.json b/app/code/Magento/SendFriendGraphQl/composer.json index 456780c1c1841..b0ce37458b1c5 100644 --- a/app/code/Magento/SendFriendGraphQl/composer.json +++ b/app/code/Magento/SendFriendGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-send-friend-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-send-friend": "*", - "magento/module-graph-ql": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-send-friend": "100.4.*", + "magento/module-graph-ql": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/Shipping/composer.json b/app/code/Magento/Shipping/composer.json index 5ea8430226ad8..ca9c0486b25df 100644 --- a/app/code/Magento/Shipping/composer.json +++ b/app/code/Magento/Shipping/composer.json @@ -1,37 +1,38 @@ { "name": "magento/module-shipping", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", "ext-gd": "*", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-contact": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-payment": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-ui": "*", - "magento/module-user": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-contact": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-ui": "101.2.*", + "magento/module-user": "101.2.*" }, "suggest": { - "magento/module-fedex": "*", - "magento/module-ups": "*", - "magento/module-config": "*" + "magento/module-fedex": "100.4.*", + "magento/module-ups": "100.4.*", + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -41,3 +42,4 @@ } } } + diff --git a/app/code/Magento/Sitemap/composer.json b/app/code/Magento/Sitemap/composer.json index 6a9f20ac8bddf..64ab7b60e17ab 100644 --- a/app/code/Magento/Sitemap/composer.json +++ b/app/code/Magento/Sitemap/composer.json @@ -1,30 +1,31 @@ { "name": "magento/module-sitemap", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-url-rewrite": "*", - "magento/module-cms": "*", - "magento/module-config": "*", - "magento/module-eav": "*", - "magento/module-media-storage": "*", - "magento/module-robots": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-robots": "101.1.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -34,3 +35,4 @@ } } } + diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json index e6f7f0d5ac274..870a77578672b 100644 --- a/app/code/Magento/Store/composer.json +++ b/app/code/Magento/Store/composer.json @@ -1,29 +1,30 @@ { "name": "magento/module-store", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-media-storage": "*", - "magento/module-ui": "*", - "magento/module-customer": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-ui": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*" }, "suggest": { - "magento/module-deploy": "*" + "magento/module-deploy": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -33,3 +34,4 @@ } } } + diff --git a/app/code/Magento/StoreGraphQl/composer.json b/app/code/Magento/StoreGraphQl/composer.json index a7cab5851a9ee..42023b2403e91 100644 --- a/app/code/Magento/StoreGraphQl/composer.json +++ b/app/code/Magento/StoreGraphQl/composer.json @@ -2,16 +2,17 @@ "name": "magento/module-store-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-graph-ql": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-graph-ql": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/Swagger/composer.json b/app/code/Magento/Swagger/composer.json index 759e72350b0a6..2cffb5d716d54 100644 --- a/app/code/Magento/Swagger/composer.json +++ b/app/code/Magento/Swagger/composer.json @@ -1,18 +1,19 @@ { "name": "magento/module-swagger", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/SwaggerWebapi/composer.json b/app/code/Magento/SwaggerWebapi/composer.json index 78021f7cb4ec5..09c078448232d 100644 --- a/app/code/Magento/SwaggerWebapi/composer.json +++ b/app/code/Magento/SwaggerWebapi/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-swagger-webapi", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-swagger": "*" + "magento/framework": "103.0.*", + "magento/module-swagger": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/SwaggerWebapiAsync/composer.json b/app/code/Magento/SwaggerWebapiAsync/composer.json index 283b2fe1f1758..d42e387499e8a 100644 --- a/app/code/Magento/SwaggerWebapiAsync/composer.json +++ b/app/code/Magento/SwaggerWebapiAsync/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-swagger-webapi-async", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-swagger": "*" + "magento/framework": "103.0.*", + "magento/module-swagger": "100.4.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Swatches/composer.json b/app/code/Magento/Swatches/composer.json index 2c9b7a03ba011..4198e66b8f06a 100644 --- a/app/code/Magento/Swatches/composer.json +++ b/app/code/Magento/Swatches/composer.json @@ -1,32 +1,33 @@ { "name": "magento/module-swatches", "description": "Add Swatches to Products", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-config": "*", - "magento/module-configurable-product": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-page-cache": "*", - "magento/module-media-storage": "*", - "magento/module-store": "*", - "magento/module-theme": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-configurable-product": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-page-cache": "100.4.*", + "magento/module-media-storage": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*" }, "suggest": { - "magento/module-layered-navigation": "*", - "magento/module-swatches-sample-data": "*" + "magento/module-layered-navigation": "100.4.*", + "magento/module-swatches-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -36,3 +37,4 @@ } } } + diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 1b98b4044a2ff..87d6ff61e5fe2 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-swatches-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-swatches": "*", - "magento/module-catalog": "*", - "magento/module-catalog-graph-ql": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-swatches": "100.4.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-graph-ql": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/SwatchesLayeredNavigation/composer.json b/app/code/Magento/SwatchesLayeredNavigation/composer.json index 3b987f8096f18..f0f794db094f2 100644 --- a/app/code/Magento/SwatchesLayeredNavigation/composer.json +++ b/app/code/Magento/SwatchesLayeredNavigation/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-swatches-layered-navigation", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", + "magento/framework": "103.0.*", "magento/magento-composer-installer": "*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/Tax/composer.json b/app/code/Magento/Tax/composer.json index 2fe0597c85a63..1b43e4c5ac6fd 100644 --- a/app/code/Magento/Tax/composer.json +++ b/app/code/Magento/Tax/composer.json @@ -1,35 +1,36 @@ { "name": "magento/module-tax", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-checkout": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-page-cache": "*", - "magento/module-quote": "*", - "magento/module-reports": "*", - "magento/module-sales": "*", - "magento/module-shipping": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-page-cache": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-reports": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-tax-sample-data": "*" + "magento/module-tax-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -39,3 +40,4 @@ } } } + diff --git a/app/code/Magento/TaxGraphQl/composer.json b/app/code/Magento/TaxGraphQl/composer.json index b97e414cacb67..0b4805fe24493 100644 --- a/app/code/Magento/TaxGraphQl/composer.json +++ b/app/code/Magento/TaxGraphQl/composer.json @@ -2,18 +2,19 @@ "name": "magento/module-tax-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-tax": "*", - "magento/module-catalog-graph-ql": "*" + "magento/module-tax": "100.4.*", + "magento/module-catalog-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/TaxImportExport/composer.json b/app/code/Magento/TaxImportExport/composer.json index 01c069b4299c1..0d4ea14db838f 100644 --- a/app/code/Magento/TaxImportExport/composer.json +++ b/app/code/Magento/TaxImportExport/composer.json @@ -1,23 +1,24 @@ { "name": "magento/module-tax-import-export", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-directory": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-directory": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -27,3 +28,4 @@ } } } + diff --git a/app/code/Magento/Theme/composer.json b/app/code/Magento/Theme/composer.json index 63779c6f9bf5d..97e1664ed563e 100644 --- a/app/code/Magento/Theme/composer.json +++ b/app/code/Magento/Theme/composer.json @@ -1,33 +1,34 @@ { "name": "magento/module-theme", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-cms": "*", - "magento/module-config": "*", - "magento/module-customer": "*", - "magento/module-eav": "*", - "magento/module-media-storage": "*", - "magento/module-require-js": "*", - "magento/module-store": "*", - "magento/module-ui": "*", - "magento/module-widget": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-cms": "104.0.*", + "magento/module-config": "101.2.*", + "magento/module-customer": "103.0.*", + "magento/module-eav": "102.1.*", + "magento/module-media-storage": "100.4.*", + "magento/module-require-js": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-widget": "101.2.*" }, "suggest": { - "magento/module-theme-sample-data": "*", - "magento/module-deploy": "*", - "magento/module-directory": "*" + "magento/module-theme-sample-data": "Sample Data version: 100.4.*", + "magento/module-deploy": "100.4.*", + "magento/module-directory": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -37,3 +38,4 @@ } } } + diff --git a/app/code/Magento/ThemeGraphQl/composer.json b/app/code/Magento/ThemeGraphQl/composer.json index cee740d449b37..d2308d43f1670 100644 --- a/app/code/Magento/ThemeGraphQl/composer.json +++ b/app/code/Magento/ThemeGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-theme-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, "suggest": { - "magento/module-store-graph-ql": "*" + "magento/module-store-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/Tinymce3/composer.json b/app/code/Magento/Tinymce3/composer.json index 0b8cf6824295e..b5ab9f5d6b680 100644 --- a/app/code/Magento/Tinymce3/composer.json +++ b/app/code/Magento/Tinymce3/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-tinymce-3", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-ui": "*", - "magento/module-variable": "*", - "magento/module-widget": "*" - }, - "suggest": { - "magento/module-cms": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-ui": "101.2.*", + "magento/module-variable": "100.4.*", + "magento/module-widget": "101.2.*" + }, + "suggest": { + "magento/module-cms": "104.0.*" + }, "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Translation/composer.json b/app/code/Magento/Translation/composer.json index 7f67749fa88f4..7813065f14141 100644 --- a/app/code/Magento/Translation/composer.json +++ b/app/code/Magento/Translation/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-translation", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-developer": "*", - "magento/module-store": "*", - "magento/module-theme": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-developer": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*" }, "suggest": { - "magento/module-deploy": "*" + "magento/module-deploy": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/Ui/composer.json b/app/code/Magento/Ui/composer.json index b4aeda0fc1e6a..b856a805b1010 100644 --- a/app/code/Magento/Ui/composer.json +++ b/app/code/Magento/Ui/composer.json @@ -1,26 +1,27 @@ { "name": "magento/module-ui", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-eav": "*", - "magento/module-store": "*", - "magento/module-user": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-eav": "102.1.*", + "magento/module-store": "101.1.*", + "magento/module-user": "101.2.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -30,3 +31,4 @@ } } } + diff --git a/app/code/Magento/Ups/composer.json b/app/code/Magento/Ups/composer.json index fa8962f0af592..1115802f21fa6 100644 --- a/app/code/Magento/Ups/composer.json +++ b/app/code/Magento/Ups/composer.json @@ -1,28 +1,29 @@ { "name": "magento/module-ups", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog-inventory": "*", - "magento/module-directory": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-directory": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-config": "*" + "magento/module-config": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -32,3 +33,4 @@ } } } + diff --git a/app/code/Magento/UrlRewrite/composer.json b/app/code/Magento/UrlRewrite/composer.json index 44ca51e8bcbe2..9e1146fa7fe44 100644 --- a/app/code/Magento/UrlRewrite/composer.json +++ b/app/code/Magento/UrlRewrite/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-url-rewrite", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "102.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-url-rewrite": "*", - "magento/module-cms": "*", - "magento/module-cms-url-rewrite": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-url-rewrite": "100.4.*", + "magento/module-cms": "104.0.*", + "magento/module-cms-url-rewrite": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/UrlRewriteGraphQl/composer.json b/app/code/Magento/UrlRewriteGraphQl/composer.json index 766ad3ab46ebd..15b97a44941cb 100644 --- a/app/code/Magento/UrlRewriteGraphQl/composer.json +++ b/app/code/Magento/UrlRewriteGraphQl/composer.json @@ -2,18 +2,19 @@ "name": "magento/module-url-rewrite-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-url-rewrite": "*" + "magento/framework": "103.0.*", + "magento/module-url-rewrite": "102.0.*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/User/composer.json b/app/code/Magento/User/composer.json index 6ba4be749cc7c..9242b5461d5df 100644 --- a/app/code/Magento/User/composer.json +++ b/app/code/Magento/User/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-user", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-email": "*", - "magento/module-integration": "*", - "magento/module-security": "*", - "magento/module-store": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-email": "101.1.*", + "magento/module-integration": "100.4.*", + "magento/module-security": "100.4.*", + "magento/module-store": "101.1.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/Usps/composer.json b/app/code/Magento/Usps/composer.json index 3d5c0669c679d..8648000e4b08e 100644 --- a/app/code/Magento/Usps/composer.json +++ b/app/code/Magento/Usps/composer.json @@ -1,27 +1,28 @@ { "name": "magento/module-usps", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-config": "*", - "magento/module-directory": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-config": "101.2.*", + "magento/module-directory": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-shipping": "100.4.*", + "magento/module-store": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -31,3 +32,4 @@ } } } + diff --git a/app/code/Magento/Variable/composer.json b/app/code/Magento/Variable/composer.json index e6eed40a814db..52231c86a49ca 100644 --- a/app/code/Magento/Variable/composer.json +++ b/app/code/Magento/Variable/composer.json @@ -1,22 +1,23 @@ { "name": "magento/module-variable", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-store": "*", - "magento/module-config": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-store": "101.1.*", + "magento/module-config": "101.2.*", + "magento/module-ui": "101.2.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -26,3 +27,4 @@ } } } + diff --git a/app/code/Magento/Vault/composer.json b/app/code/Magento/Vault/composer.json index 31d5ceb906246..7dacdfe0d6e26 100644 --- a/app/code/Magento/Vault/composer.json +++ b/app/code/Magento/Vault/composer.json @@ -1,25 +1,25 @@ { "name": "magento/module-vault", - "description": "", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-payment": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-theme": "*" + "magento/framework": "103.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-payment": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +29,4 @@ } } } + diff --git a/app/code/Magento/VaultGraphQl/composer.json b/app/code/Magento/VaultGraphQl/composer.json index aff9a700fbcad..9e7832c8ee184 100644 --- a/app/code/Magento/VaultGraphQl/composer.json +++ b/app/code/Magento/VaultGraphQl/composer.json @@ -2,16 +2,17 @@ "name": "magento/module-vault-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-vault": "*", - "magento/module-graph-ql": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-vault": "101.2.*", + "magento/module-graph-ql": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/Version/composer.json b/app/code/Magento/Version/composer.json index d2b2127446c21..d1a9f535b7c2d 100644 --- a/app/code/Magento/Version/composer.json +++ b/app/code/Magento/Version/composer.json @@ -1,18 +1,19 @@ { "name": "magento/module-version", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/code/Magento/Webapi/composer.json b/app/code/Magento/Webapi/composer.json index 11382cc318554..0313c903d367a 100644 --- a/app/code/Magento/Webapi/composer.json +++ b/app/code/Magento/Webapi/composer.json @@ -1,26 +1,27 @@ { "name": "magento/module-webapi", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-authorization": "*", - "magento/module-backend": "*", - "magento/module-integration": "*", - "magento/module-store": "*" + "magento/framework": "103.0.*", + "magento/module-authorization": "100.4.*", + "magento/module-backend": "102.0.*", + "magento/module-integration": "100.4.*", + "magento/module-store": "101.1.*" }, "suggest": { - "magento/module-user": "*", - "magento/module-customer": "*" + "magento/module-user": "101.2.*", + "magento/module-customer": "103.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -30,3 +31,4 @@ } } } + diff --git a/app/code/Magento/WebapiAsync/composer.json b/app/code/Magento/WebapiAsync/composer.json index e0c6a96f1ffe6..715dd1e0be707 100644 --- a/app/code/Magento/WebapiAsync/composer.json +++ b/app/code/Magento/WebapiAsync/composer.json @@ -1,25 +1,26 @@ { "name": "magento/module-webapi-async", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/framework-message-queue": "*", - "magento/module-webapi": "*", - "magento/module-asynchronous-operations": "*" + "magento/framework": "103.0.*", + "magento/framework-message-queue": "100.4.*", + "magento/module-webapi": "100.4.*", + "magento/module-asynchronous-operations": "100.4.*" }, "suggest": { - "magento/module-user": "*", - "magento/module-customer": "*" + "magento/module-user": "101.2.*", + "magento/module-customer": "103.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -29,3 +30,4 @@ } } } + diff --git a/app/code/Magento/WebapiSecurity/composer.json b/app/code/Magento/WebapiSecurity/composer.json index 5b48ed8644709..adcbf01fe212b 100644 --- a/app/code/Magento/WebapiSecurity/composer.json +++ b/app/code/Magento/WebapiSecurity/composer.json @@ -1,19 +1,20 @@ { "name": "magento/module-webapi-security", "description": "WebapiSecurity module provides option to loosen security on some webapi resources.", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-webapi": "*" + "magento/framework": "103.0.*", + "magento/module-webapi": "100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -23,3 +24,4 @@ } } } + diff --git a/app/code/Magento/Weee/composer.json b/app/code/Magento/Weee/composer.json index 7024de0f595c7..f7da07b15fe74 100644 --- a/app/code/Magento/Weee/composer.json +++ b/app/code/Magento/Weee/composer.json @@ -1,33 +1,34 @@ { "name": "magento/module-weee", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-directory": "*", - "magento/module-eav": "*", - "magento/module-page-cache": "*", - "magento/module-quote": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-directory": "100.4.*", + "magento/module-eav": "102.1.*", + "magento/module-page-cache": "100.4.*", + "magento/module-quote": "101.2.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-bundle": "*" + "magento/module-bundle": "101.0.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -37,3 +38,4 @@ } } } + diff --git a/app/code/Magento/WeeeGraphQl/composer.json b/app/code/Magento/WeeeGraphQl/composer.json index be7e50ab2fca1..9355b5a196ae7 100644 --- a/app/code/Magento/WeeeGraphQl/composer.json +++ b/app/code/Magento/WeeeGraphQl/composer.json @@ -2,20 +2,21 @@ "name": "magento/module-weee-graph-ql", "description": "N/A", "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-tax": "*", - "magento/module-weee": "*" + "magento/framework": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-tax": "100.4.*", + "magento/module-weee": "100.4.*" }, "suggest": { - "magento/module-catalog-graph-ql": "*" + "magento/module-catalog-graph-ql": "100.4.*" }, - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -25,3 +26,4 @@ } } } + diff --git a/app/code/Magento/Widget/composer.json b/app/code/Magento/Widget/composer.json index 2cf8429095ce7..9785dcb9099f8 100644 --- a/app/code/Magento/Widget/composer.json +++ b/app/code/Magento/Widget/composer.json @@ -1,28 +1,29 @@ { "name": "magento/module-widget", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-cms": "*", - "magento/module-store": "*", - "magento/module-theme": "*", - "magento/module-variable": "*", - "magento/module-ui": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-cms": "104.0.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-variable": "100.4.*", + "magento/module-ui": "101.2.*" }, "suggest": { - "magento/module-widget-sample-data": "*" + "magento/module-widget-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -32,3 +33,4 @@ } } } + diff --git a/app/code/Magento/Wishlist/composer.json b/app/code/Magento/Wishlist/composer.json index b426ffe01cecc..c08de372b366f 100644 --- a/app/code/Magento/Wishlist/composer.json +++ b/app/code/Magento/Wishlist/composer.json @@ -1,37 +1,38 @@ { "name": "magento/module-wishlist", "description": "N/A", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-inventory": "*", - "magento/module-checkout": "*", - "magento/module-customer": "*", - "magento/module-rss": "*", - "magento/module-sales": "*", - "magento/module-store": "*", - "magento/module-theme": "*", - "magento/module-ui": "*", - "magento/module-captcha": "*" + "magento/framework": "103.0.*", + "magento/module-backend": "102.0.*", + "magento/module-catalog": "104.0.*", + "magento/module-catalog-inventory": "100.4.*", + "magento/module-checkout": "100.4.*", + "magento/module-customer": "103.0.*", + "magento/module-rss": "100.4.*", + "magento/module-sales": "103.0.*", + "magento/module-store": "101.1.*", + "magento/module-theme": "101.1.*", + "magento/module-ui": "101.2.*", + "magento/module-captcha": "100.4.*" }, "suggest": { - "magento/module-configurable-product": "*", - "magento/module-downloadable": "*", - "magento/module-bundle": "*", - "magento/module-cookie": "*", - "magento/module-grouped-product": "*", - "magento/module-wishlist-sample-data": "*" + "magento/module-configurable-product": "100.4.*", + "magento/module-downloadable": "100.4.*", + "magento/module-bundle": "101.0.*", + "magento/module-cookie": "100.4.*", + "magento/module-grouped-product": "100.4.*", + "magento/module-wishlist-sample-data": "Sample Data version: 100.4.*" }, - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" @@ -41,3 +42,4 @@ } } } + diff --git a/app/code/Magento/WishlistAnalytics/composer.json b/app/code/Magento/WishlistAnalytics/composer.json index 309257f857ed2..91ad9baeecb37 100644 --- a/app/code/Magento/WishlistAnalytics/composer.json +++ b/app/code/Magento/WishlistAnalytics/composer.json @@ -1,17 +1,18 @@ { "name": "magento/module-wishlist-analytics", "description": "N/A", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-wishlist": "*", - "magento/module-analytics": "*" - }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.0", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-wishlist": "101.2.*", + "magento/module-analytics": "100.4.*" + }, "autoload": { "files": [ "registration.php" @@ -21,3 +22,4 @@ } } } + diff --git a/app/code/Magento/WishlistGraphQl/composer.json b/app/code/Magento/WishlistGraphQl/composer.json index 7a3fca599a4b3..85cac3bb4b0a1 100644 --- a/app/code/Magento/WishlistGraphQl/composer.json +++ b/app/code/Magento/WishlistGraphQl/composer.json @@ -2,17 +2,18 @@ "name": "magento/module-wishlist-graph-ql", "description": "N/A", "type": "magento2-module", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/module-catalog-graph-ql": "*", - "magento/module-wishlist": "*", - "magento/module-store": "*" - }, "license": [ "OSL-3.0", "AFL-3.0" ], + "version": "100.4.1", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "103.0.*", + "magento/module-catalog-graph-ql": "100.4.*", + "magento/module-wishlist": "101.2.*", + "magento/module-store": "101.1.*" + }, "autoload": { "files": [ "registration.php" @@ -22,3 +23,4 @@ } } } + diff --git a/app/design/adminhtml/Magento/backend/composer.json b/app/design/adminhtml/Magento/backend/composer.json index 249441be1753e..daeffa56c2a62 100644 --- a/app/design/adminhtml/Magento/backend/composer.json +++ b/app/design/adminhtml/Magento/backend/composer.json @@ -1,21 +1,23 @@ { "name": "magento/theme-adminhtml-backend", "description": "N/A", + "type": "magento2-theme", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-theme", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/design/frontend/Magento/blank/composer.json b/app/design/frontend/Magento/blank/composer.json index 066d0cd1cc9f2..2e39bcf46a49c 100644 --- a/app/design/frontend/Magento/blank/composer.json +++ b/app/design/frontend/Magento/blank/composer.json @@ -1,21 +1,23 @@ { "name": "magento/theme-frontend-blank", "description": "N/A", + "type": "magento2-theme", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-theme", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/design/frontend/Magento/luma/composer.json b/app/design/frontend/Magento/luma/composer.json index 16bed43fe8cbf..e3c39dd579604 100644 --- a/app/design/frontend/Magento/luma/composer.json +++ b/app/design/frontend/Magento/luma/composer.json @@ -1,22 +1,24 @@ { "name": "magento/theme-frontend-luma", "description": "N/A", + "type": "magento2-theme", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "config": { "sort-packages": true }, + "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "*", - "magento/theme-frontend-blank": "*" + "magento/framework": "103.0.*", + "magento/theme-frontend-blank": "100.4.*" }, - "type": "magento2-theme", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/i18n/Magento/de_DE/composer.json b/app/i18n/Magento/de_DE/composer.json index 5a488a3e32c2b..fd23d037ba459 100644 --- a/app/i18n/Magento/de_DE/composer.json +++ b/app/i18n/Magento/de_DE/composer.json @@ -1,6 +1,7 @@ { "name": "magento/language-de_de", "description": "German (Germany) language", + "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -8,13 +9,14 @@ "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/i18n/Magento/en_US/composer.json b/app/i18n/Magento/en_US/composer.json index 1108c70de28a6..194854d58bbe2 100644 --- a/app/i18n/Magento/en_US/composer.json +++ b/app/i18n/Magento/en_US/composer.json @@ -1,6 +1,7 @@ { "name": "magento/language-en_us", "description": "English (United States) language", + "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -8,13 +9,14 @@ "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/i18n/Magento/es_ES/composer.json b/app/i18n/Magento/es_ES/composer.json index 5bc3cb5730adf..0b49475587d54 100644 --- a/app/i18n/Magento/es_ES/composer.json +++ b/app/i18n/Magento/es_ES/composer.json @@ -1,6 +1,7 @@ { "name": "magento/language-es_es", "description": "Spanish (Spain) language", + "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -8,13 +9,14 @@ "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/i18n/Magento/fr_FR/composer.json b/app/i18n/Magento/fr_FR/composer.json index 50c541308673b..ada414e6a7a32 100644 --- a/app/i18n/Magento/fr_FR/composer.json +++ b/app/i18n/Magento/fr_FR/composer.json @@ -1,6 +1,7 @@ { "name": "magento/language-fr_fr", "description": "French (France) language", + "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -8,13 +9,14 @@ "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/i18n/Magento/nl_NL/composer.json b/app/i18n/Magento/nl_NL/composer.json index a182e179d4103..a881eed112ea0 100644 --- a/app/i18n/Magento/nl_NL/composer.json +++ b/app/i18n/Magento/nl_NL/composer.json @@ -1,6 +1,7 @@ { "name": "magento/language-nl_nl", "description": "Dutch (Netherlands) language", + "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -8,13 +9,14 @@ "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/i18n/Magento/pt_BR/composer.json b/app/i18n/Magento/pt_BR/composer.json index 46734cc09b363..6e10bc16f6a79 100644 --- a/app/i18n/Magento/pt_BR/composer.json +++ b/app/i18n/Magento/pt_BR/composer.json @@ -1,6 +1,7 @@ { "name": "magento/language-pt_br", "description": "Portuguese (Brazil) language", + "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -8,13 +9,14 @@ "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } + diff --git a/app/i18n/Magento/zh_Hans_CN/composer.json b/app/i18n/Magento/zh_Hans_CN/composer.json index ce214ce649f56..8491eced1389f 100644 --- a/app/i18n/Magento/zh_Hans_CN/composer.json +++ b/app/i18n/Magento/zh_Hans_CN/composer.json @@ -1,6 +1,7 @@ { "name": "magento/language-zh_hans_cn", "description": "Chinese (China) language", + "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -8,13 +9,14 @@ "config": { "sort-packages": true }, + "version": "100.4.0", "require": { - "magento/framework": "*" + "magento/framework": "103.0.*" }, - "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } + diff --git a/lib/internal/Magento/Framework/Amqp/composer.json b/lib/internal/Magento/Framework/Amqp/composer.json index fc65e37d12ecf..c1a34426fe87e 100644 --- a/lib/internal/Magento/Framework/Amqp/composer.json +++ b/lib/internal/Magento/Framework/Amqp/composer.json @@ -1,25 +1,27 @@ { "name": "magento/framework-amqp", "description": "N/A", - "config": { - "sort-packages": true - }, "type": "magento2-library", "license": [ "OSL-3.0", "AFL-3.0" ], + "config": { + "sort-packages": true + }, + "version": "100.4.0", "require": { - "magento/framework": "*", + "magento/framework": "103.0.*", "php": "~7.3.0||~7.4.0", "php-amqplib/php-amqplib": "~2.7.0||~2.10.0" }, "autoload": { - "psr-4": { - "Magento\\Framework\\Amqp\\": "" - }, "files": [ "registration.php" - ] + ], + "psr-4": { + "Magento\\Framework\\Amqp\\": "" + } } } + diff --git a/lib/internal/Magento/Framework/Bulk/composer.json b/lib/internal/Magento/Framework/Bulk/composer.json index b8e0992182169..aa9dad9589657 100644 --- a/lib/internal/Magento/Framework/Bulk/composer.json +++ b/lib/internal/Magento/Framework/Bulk/composer.json @@ -1,24 +1,26 @@ { "name": "magento/framework-bulk", "description": "N/A", - "config": { - "sort-packages": true - }, "type": "magento2-library", "license": [ "OSL-3.0", "AFL-3.0" ], + "config": { + "sort-packages": true + }, + "version": "101.0.0", "require": { - "magento/framework": "*", + "magento/framework": "103.0.*", "php": "~7.3.0||~7.4.0" }, "autoload": { - "psr-4": { - "Magento\\Framework\\Bulk\\": "" - }, "files": [ "registration.php" - ] + ], + "psr-4": { + "Magento\\Framework\\Bulk\\": "" + } } } + diff --git a/lib/internal/Magento/Framework/MessageQueue/composer.json b/lib/internal/Magento/Framework/MessageQueue/composer.json index 056f1d40c39cf..86119b13adb37 100644 --- a/lib/internal/Magento/Framework/MessageQueue/composer.json +++ b/lib/internal/Magento/Framework/MessageQueue/composer.json @@ -1,24 +1,26 @@ { "name": "magento/framework-message-queue", "description": "N/A", - "config": { - "sort-packages": true - }, "type": "magento2-library", "license": [ "OSL-3.0", "AFL-3.0" ], + "config": { + "sort-packages": true + }, + "version": "100.4.1", "require": { - "magento/framework": "*", + "magento/framework": "103.0.*", "php": "~7.3.0||~7.4.0" }, "autoload": { - "psr-4": { - "Magento\\Framework\\MessageQueue\\": "" - }, "files": [ "registration.php" - ] + ], + "psr-4": { + "Magento\\Framework\\MessageQueue\\": "" + } } } + diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index dfc81189bf544..e33ba5fdfd6b1 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -9,6 +9,7 @@ "config": { "sort-packages": true }, + "version": "103.0.1", "require": { "php": "~7.3.0||~7.4.0", "ext-bcmath": "*", @@ -53,11 +54,12 @@ "ext-imagick": "Use Image Magick >=3.0.0 as an optional alternative image processing library" }, "autoload": { - "psr-4": { - "Magento\\Framework\\": "" - }, "files": [ "registration.php" - ] + ], + "psr-4": { + "Magento\\Framework\\": "" + } } } + From 445b0f1a3d6a91b5da32d311077c2112ef0b1503 Mon Sep 17 00:00:00 2001 From: magento packaging service <magento-comops@adobe.com> Date: Mon, 12 Oct 2020 20:38:24 +0000 Subject: [PATCH 0786/1013] Updating root composer files for publication service for 2.4.1 --- composer.json | 514 +++++++++++++++++++++++++------------------------- 1 file changed, 258 insertions(+), 256 deletions(-) diff --git a/composer.json b/composer.json index 1af86e438882c..c90159dd0e380 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "preferred-install": "dist", "sort-packages": true }, + "version": "2.4.1", "require": { "php": "~7.3.0||~7.4.0", "ext-bcmath": "*", @@ -82,6 +83,31 @@ "webonyx/graphql-php": "^0.13.8", "wikimedia/less.php": "~1.8.0" }, + "suggest": { + "ext-pcntl": "Need for run processes in parallel mode" + }, + "autoload": { + "exclude-from-classmap": [ + "**/dev/**", + "**/update/**", + "**/Test/**" + ], + "files": [ + "app/etc/NonComposerComponentRegistration.php" + ], + "psr-0": { + "": [ + "app/code/", + "generated/code/" + ] + }, + "psr-4": { + "Magento\\": "app/code/Magento/", + "Magento\\Framework\\": "lib/internal/Magento/Framework/", + "Magento\\Setup\\": "setup/src/Magento/Setup/", + "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" + } + }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", @@ -97,284 +123,260 @@ "sebastian/phpcpd": "~5.0.0", "squizlabs/php_codesniffer": "~3.5.4" }, - "suggest": { - "ext-pcntl": "Need for run processes in parallel mode" + "conflict": { + "gene/bluefoot": "*" }, "replace": { - "magento/module-marketplace": "*", - "magento/module-admin-analytics": "*", - "magento/module-admin-notification": "*", - "magento/module-advanced-pricing-import-export": "*", - "magento/module-amqp": "*", - "magento/module-amqp-store": "*", - "magento/module-analytics": "*", - "magento/module-asynchronous-operations": "*", - "magento/module-authorization": "*", - "magento/module-advanced-search": "*", - "magento/module-backend": "*", - "magento/module-backup": "*", - "magento/module-bundle": "*", - "magento/module-bundle-graph-ql": "*", - "magento/module-bundle-import-export": "*", - "magento/module-cache-invalidate": "*", - "magento/module-captcha": "*", - "magento/module-cardinal-commerce": "*", - "magento/module-catalog": "*", - "magento/module-catalog-customer-graph-ql": "*", - "magento/module-catalog-analytics": "*", - "magento/module-catalog-import-export": "*", - "magento/module-catalog-inventory": "*", - "magento/module-catalog-inventory-graph-ql": "*", - "magento/module-catalog-rule": "*", - "magento/module-catalog-rule-graph-ql": "*", - "magento/module-catalog-rule-configurable": "*", - "magento/module-catalog-search": "*", - "magento/module-catalog-url-rewrite": "*", - "magento/module-catalog-widget": "*", - "magento/module-checkout": "*", - "magento/module-checkout-agreements": "*", - "magento/module-checkout-agreements-graph-ql": "*", - "magento/module-cms": "*", - "magento/module-cms-url-rewrite": "*", - "magento/module-config": "*", - "magento/module-configurable-import-export": "*", - "magento/module-configurable-product": "*", - "magento/module-configurable-product-sales": "*", - "magento/module-contact": "*", - "magento/module-cookie": "*", - "magento/module-cron": "*", - "magento/module-currency-symbol": "*", - "magento/module-customer": "*", - "magento/module-customer-analytics": "*", - "magento/module-customer-downloadable-graph-ql": "*", - "magento/module-customer-import-export": "*", - "magento/module-deploy": "*", - "magento/module-developer": "*", - "magento/module-dhl": "*", - "magento/module-directory": "*", - "magento/module-directory-graph-ql": "*", - "magento/module-downloadable": "*", - "magento/module-downloadable-graph-ql": "*", - "magento/module-downloadable-import-export": "*", - "magento/module-eav": "*", - "magento/module-elasticsearch": "*", - "magento/module-elasticsearch-6": "*", - "magento/module-elasticsearch-7": "*", - "magento/module-email": "*", - "magento/module-encryption-key": "*", - "magento/module-fedex": "*", - "magento/module-gift-message": "*", - "magento/module-gift-message-graph-ql": "*", - "magento/module-google-adwords": "*", - "magento/module-google-analytics": "*", - "magento/module-google-optimizer": "*", - "magento/module-graph-ql": "*", - "magento/module-graph-ql-cache": "*", - "magento/module-catalog-graph-ql": "*", - "magento/module-catalog-cms-graph-ql": "*", - "magento/module-catalog-url-rewrite-graph-ql": "*", - "magento/module-configurable-product-graph-ql": "*", - "magento/module-customer-graph-ql": "*", - "magento/module-eav-graph-ql": "*", - "magento/module-swatches-graph-ql": "*", - "magento/module-tax-graph-ql": "*", - "magento/module-url-rewrite-graph-ql": "*", - "magento/module-cms-url-rewrite-graph-ql": "*", - "magento/module-weee-graph-ql": "*", - "magento/module-cms-graph-ql": "*", - "magento/module-grouped-import-export": "*", - "magento/module-grouped-product": "*", - "magento/module-grouped-catalog-inventory": "*", - "magento/module-grouped-product-graph-ql": "*", - "magento/module-import-export": "*", - "magento/module-indexer": "*", - "magento/module-instant-purchase": "*", - "magento/module-integration": "*", - "magento/module-layered-navigation": "*", - "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": "*", - "magento/module-login-as-customer-page-cache": "*", - "magento/module-login-as-customer-sales": "*", - "magento/module-media-content": "*", - "magento/module-media-content-api": "*", - "magento/module-media-content-catalog": "*", - "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-synchronization-metadata": "*", - "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": "*", - "magento/module-msrp": "*", - "magento/module-msrp-configurable-product": "*", - "magento/module-msrp-grouped-product": "*", - "magento/module-multishipping": "*", - "magento/module-mysql-mq": "*", - "magento/module-new-relic-reporting": "*", - "magento/module-newsletter": "*", - "magento/module-newsletter-graph-ql": "*", - "magento/module-offline-payments": "*", - "magento/module-offline-shipping": "*", - "magento/module-page-cache": "*", - "magento/module-payment": "*", - "magento/module-paypal": "*", - "magento/module-paypal-captcha": "*", - "magento/module-paypal-graph-ql": "*", - "magento/module-persistent": "*", - "magento/module-product-alert": "*", - "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": "*", - "magento/module-rule": "*", - "magento/module-sales": "*", - "magento/module-sales-analytics": "*", - "magento/module-sales-graph-ql": "*", - "magento/module-sales-inventory": "*", - "magento/module-sales-rule": "*", - "magento/module-sales-sequence": "*", - "magento/module-sample-data": "*", - "magento/module-search": "*", - "magento/module-security": "*", - "magento/module-send-friend": "*", - "magento/module-send-friend-graph-ql": "*", - "magento/module-shipping": "*", - "magento/module-sitemap": "*", - "magento/module-store": "*", - "magento/module-store-graph-ql": "*", - "magento/module-swagger": "*", - "magento/module-swagger-webapi": "*", - "magento/module-swagger-webapi-async": "*", - "magento/module-swatches": "*", - "magento/module-swatches-layered-navigation": "*", - "magento/module-tax": "*", - "magento/module-tax-import-export": "*", - "magento/module-theme": "*", - "magento/module-theme-graph-ql": "*", - "magento/module-translation": "*", - "magento/module-ui": "*", - "magento/module-ups": "*", - "magento/module-url-rewrite": "*", - "magento/module-user": "*", - "magento/module-usps": "*", - "magento/module-variable": "*", - "magento/module-vault": "*", - "magento/module-vault-graph-ql": "*", - "magento/module-version": "*", - "magento/module-webapi": "*", - "magento/module-webapi-async": "*", - "magento/module-webapi-security": "*", - "magento/module-weee": "*", - "magento/module-widget": "*", - "magento/module-wishlist": "*", - "magento/module-wishlist-graph-ql": "*", - "magento/module-wishlist-analytics": "*", - "magento/theme-adminhtml-backend": "*", - "magento/theme-frontend-blank": "*", - "magento/theme-frontend-luma": "*", - "magento/language-de_de": "*", - "magento/language-en_us": "*", - "magento/language-es_es": "*", - "magento/language-fr_fr": "*", - "magento/language-nl_nl": "*", - "magento/language-pt_br": "*", - "magento/language-zh_hans_cn": "*", - "magento/framework": "*", - "magento/framework-amqp": "*", - "magento/framework-bulk": "*", - "magento/framework-message-queue": "*", + "magento/module-marketplace": "100.4.0", + "magento/module-admin-analytics": "100.4.1", + "magento/module-admin-notification": "100.4.0", + "magento/module-advanced-pricing-import-export": "100.4.1", + "magento/module-amqp": "100.4.0", + "magento/module-amqp-store": "100.4.0", + "magento/module-analytics": "100.4.1", + "magento/module-asynchronous-operations": "100.4.1", + "magento/module-authorization": "100.4.1", + "magento/module-advanced-search": "100.4.0", + "magento/module-backend": "102.0.1", + "magento/module-backup": "100.4.1", + "magento/module-bundle": "101.0.1", + "magento/module-bundle-graph-ql": "100.4.1", + "magento/module-bundle-import-export": "100.4.0", + "magento/module-cache-invalidate": "100.4.0", + "magento/module-captcha": "100.4.1", + "magento/module-cardinal-commerce": "100.4.0", + "magento/module-catalog": "104.0.1", + "magento/module-catalog-customer-graph-ql": "100.4.1", + "magento/module-catalog-analytics": "100.4.0", + "magento/module-catalog-import-export": "101.1.1", + "magento/module-catalog-inventory": "100.4.1", + "magento/module-catalog-inventory-graph-ql": "100.4.0", + "magento/module-catalog-rule": "101.2.1", + "magento/module-catalog-rule-graph-ql": "100.4.0", + "magento/module-catalog-rule-configurable": "100.4.0", + "magento/module-catalog-search": "102.0.1", + "magento/module-catalog-url-rewrite": "100.4.1", + "magento/module-catalog-widget": "100.4.1", + "magento/module-checkout": "100.4.1", + "magento/module-checkout-agreements": "100.4.0", + "magento/module-checkout-agreements-graph-ql": "100.4.0", + "magento/module-cms": "104.0.1", + "magento/module-cms-url-rewrite": "100.4.1", + "magento/module-config": "101.2.1", + "magento/module-configurable-import-export": "100.4.0", + "magento/module-configurable-product": "100.4.1", + "magento/module-configurable-product-sales": "100.4.0", + "magento/module-contact": "100.4.1", + "magento/module-cookie": "100.4.1", + "magento/module-cron": "100.4.1", + "magento/module-currency-symbol": "100.4.0", + "magento/module-customer": "103.0.1", + "magento/module-customer-analytics": "100.4.0", + "magento/module-customer-downloadable-graph-ql": "100.4.0", + "magento/module-customer-import-export": "100.4.1", + "magento/module-deploy": "100.4.1", + "magento/module-developer": "100.4.1", + "magento/module-dhl": "100.4.0", + "magento/module-directory": "100.4.1", + "magento/module-directory-graph-ql": "100.4.0", + "magento/module-downloadable": "100.4.1", + "magento/module-downloadable-graph-ql": "100.4.1", + "magento/module-downloadable-import-export": "100.4.0", + "magento/module-eav": "102.1.1", + "magento/module-elasticsearch": "101.0.1", + "magento/module-elasticsearch-6": "100.4.1", + "magento/module-elasticsearch-7": "100.4.1", + "magento/module-email": "101.1.1", + "magento/module-encryption-key": "100.4.0", + "magento/module-fedex": "100.4.1", + "magento/module-gift-message": "100.4.0", + "magento/module-gift-message-graph-ql": "100.4.0", + "magento/module-google-adwords": "100.4.0", + "magento/module-google-analytics": "100.4.0", + "magento/module-google-optimizer": "100.4.1", + "magento/module-graph-ql": "100.4.1", + "magento/module-graph-ql-cache": "100.4.0", + "magento/module-catalog-graph-ql": "100.4.1", + "magento/module-catalog-cms-graph-ql": "100.4.0", + "magento/module-catalog-url-rewrite-graph-ql": "100.4.0", + "magento/module-configurable-product-graph-ql": "100.4.1", + "magento/module-customer-graph-ql": "100.4.1", + "magento/module-eav-graph-ql": "100.4.0", + "magento/module-swatches-graph-ql": "100.4.1", + "magento/module-tax-graph-ql": "100.4.0", + "magento/module-url-rewrite-graph-ql": "100.4.0", + "magento/module-cms-url-rewrite-graph-ql": "100.4.0", + "magento/module-weee-graph-ql": "100.4.0", + "magento/module-cms-graph-ql": "100.4.0", + "magento/module-grouped-import-export": "100.4.0", + "magento/module-grouped-product": "100.4.1", + "magento/module-grouped-catalog-inventory": "100.4.0", + "magento/module-grouped-product-graph-ql": "100.4.1", + "magento/module-import-export": "101.0.1", + "magento/module-indexer": "100.4.1", + "magento/module-instant-purchase": "100.4.0", + "magento/module-integration": "100.4.1", + "magento/module-layered-navigation": "100.4.1", + "magento/module-login-as-customer": "100.4.1", + "magento/module-login-as-customer-admin-ui": "100.4.1", + "magento/module-login-as-customer-api": "100.4.1", + "magento/module-login-as-customer-assistance": "100.4.0", + "magento/module-login-as-customer-frontend-ui": "100.4.1", + "magento/module-login-as-customer-log": "100.4.0", + "magento/module-login-as-customer-quote": "100.4.0", + "magento/module-login-as-customer-page-cache": "100.4.1", + "magento/module-login-as-customer-sales": "100.4.1", + "magento/module-media-content": "100.4.1", + "magento/module-media-content-api": "100.4.1", + "magento/module-media-content-catalog": "100.4.1", + "magento/module-media-content-cms": "100.4.1", + "magento/module-media-gallery": "100.4.1", + "magento/module-media-gallery-api": "101.0.1", + "magento/module-media-gallery-ui": "100.4.0", + "magento/module-media-gallery-ui-api": "100.4.0", + "magento/module-media-gallery-integration": "100.4.0", + "magento/module-media-gallery-synchronization": "100.4.0", + "magento/module-media-gallery-synchronization-api": "100.4.0", + "magento/module-media-content-synchronization": "100.4.0", + "magento/module-media-content-synchronization-api": "100.4.0", + "magento/module-media-content-synchronization-catalog": "100.4.0", + "magento/module-media-content-synchronization-cms": "100.4.0", + "magento/module-media-gallery-synchronization-metadata": "100.4.0", + "magento/module-media-gallery-metadata": "100.4.0", + "magento/module-media-gallery-metadata-api": "100.4.0", + "magento/module-media-gallery-catalog-ui": "100.4.0", + "magento/module-media-gallery-cms-ui": "100.4.0", + "magento/module-media-gallery-catalog-integration": "100.4.0", + "magento/module-media-gallery-catalog": "100.4.0", + "magento/module-media-storage": "100.4.0", + "magento/module-message-queue": "100.4.1", + "magento/module-msrp": "100.4.0", + "magento/module-msrp-configurable-product": "100.4.0", + "magento/module-msrp-grouped-product": "100.4.0", + "magento/module-multishipping": "100.4.1", + "magento/module-mysql-mq": "100.4.0", + "magento/module-new-relic-reporting": "100.4.0", + "magento/module-newsletter": "100.4.1", + "magento/module-newsletter-graph-ql": "100.4.0", + "magento/module-offline-payments": "100.4.0", + "magento/module-offline-shipping": "100.4.0", + "magento/module-page-cache": "100.4.1", + "magento/module-payment": "100.4.1", + "magento/module-paypal": "101.0.1", + "magento/module-paypal-captcha": "100.4.0", + "magento/module-paypal-graph-ql": "100.4.1", + "magento/module-persistent": "100.4.1", + "magento/module-product-alert": "100.4.1", + "magento/module-product-video": "100.4.1", + "magento/module-quote": "101.2.1", + "magento/module-quote-analytics": "100.4.1", + "magento/module-quote-bundle-options": "100.4.0", + "magento/module-quote-configurable-options": "100.4.0", + "magento/module-quote-downloadable-links": "100.4.0", + "magento/module-quote-graph-ql": "100.4.1", + "magento/module-related-product-graph-ql": "100.4.0", + "magento/module-release-notification": "100.4.0", + "magento/module-reports": "100.4.1", + "magento/module-require-js": "100.4.0", + "magento/module-review": "100.4.1", + "magento/module-review-graph-ql": "100.4.0", + "magento/module-review-analytics": "100.4.0", + "magento/module-robots": "101.1.0", + "magento/module-rss": "100.4.0", + "magento/module-rule": "100.4.0", + "magento/module-sales": "103.0.1", + "magento/module-sales-analytics": "100.4.0", + "magento/module-sales-graph-ql": "100.4.1", + "magento/module-sales-inventory": "100.4.0", + "magento/module-sales-rule": "101.2.1", + "magento/module-sales-sequence": "100.4.1", + "magento/module-sample-data": "100.4.1", + "magento/module-search": "101.1.1", + "magento/module-security": "100.4.1", + "magento/module-send-friend": "100.4.0", + "magento/module-send-friend-graph-ql": "100.4.0", + "magento/module-shipping": "100.4.1", + "magento/module-sitemap": "100.4.1", + "magento/module-store": "101.1.1", + "magento/module-store-graph-ql": "100.4.1", + "magento/module-swagger": "100.4.0", + "magento/module-swagger-webapi": "100.4.0", + "magento/module-swagger-webapi-async": "100.4.0", + "magento/module-swatches": "100.4.1", + "magento/module-swatches-layered-navigation": "100.4.0", + "magento/module-tax": "100.4.1", + "magento/module-tax-import-export": "100.4.1", + "magento/module-theme": "101.1.1", + "magento/module-theme-graph-ql": "100.4.0", + "magento/module-translation": "100.4.1", + "magento/module-ui": "101.2.1", + "magento/module-ups": "100.4.1", + "magento/module-url-rewrite": "102.0.1", + "magento/module-user": "101.2.1", + "magento/module-usps": "100.4.0", + "magento/module-variable": "100.4.0", + "magento/module-vault": "101.2.1", + "magento/module-vault-graph-ql": "100.4.0", + "magento/module-version": "100.4.0", + "magento/module-webapi": "100.4.0", + "magento/module-webapi-async": "100.4.0", + "magento/module-webapi-security": "100.4.0", + "magento/module-weee": "100.4.1", + "magento/module-widget": "101.2.1", + "magento/module-wishlist": "101.2.1", + "magento/module-wishlist-graph-ql": "100.4.1", + "magento/module-wishlist-analytics": "100.4.0", + "magento/theme-adminhtml-backend": "100.4.1", + "magento/theme-frontend-blank": "100.4.1", + "magento/theme-frontend-luma": "100.4.1", + "magento/language-de_de": "100.4.0", + "magento/language-en_us": "100.4.0", + "magento/language-es_es": "100.4.0", + "magento/language-fr_fr": "100.4.0", + "magento/language-nl_nl": "100.4.0", + "magento/language-pt_br": "100.4.0", + "magento/language-zh_hans_cn": "100.4.0", + "magento/framework": "103.0.1", + "magento/framework-amqp": "100.4.0", + "magento/framework-bulk": "101.0.0", + "magento/framework-message-queue": "100.4.1", "trentrichardson/jquery-timepicker-addon": "1.4.3", "components/jquery": "1.11.0", "blueimp/jquery-file-upload": "5.6.14", "components/jqueryui": "1.10.4", "twbs/bootstrap": "3.1.0", "tinymce/tinymce": "3.4.7", - "magento/module-tinymce-3": "*", - "magento/module-csp": "*" + "magento/module-tinymce-3": "100.4.1", + "magento/module-csp": "100.4.0" }, - "conflict": { - "gene/bluefoot": "*" + "autoload-dev": { + "psr-4": { + "Magento\\PhpStan\\": "dev/tests/static/framework/Magento/PhpStan/", + "Magento\\Sniffs\\": "dev/tests/static/framework/Magento/Sniffs/", + "Magento\\TestFramework\\Inspection\\": "dev/tests/static/framework/Magento/TestFramework/Inspection/", + "Magento\\TestFramework\\Utility\\": "dev/tests/static/framework/Magento/TestFramework/Utility/", + "Magento\\Tools\\": "dev/tools/Magento/Tools/", + "Magento\\Tools\\Sanity\\": "dev/build/publication/sanity/Magento/Tools/Sanity/" + } }, + "prefer-stable": true, "extra": { "component_paths": { - "trentrichardson/jquery-timepicker-addon": "lib/web/jquery/jquery-ui-timepicker-addon.js", + "blueimp/jquery-file-upload": "lib/web/jquery/fileUploader", "components/jquery": [ "lib/web/jquery.js", "lib/web/jquery/jquery.min.js", "lib/web/jquery/jquery-migrate.js" ], - "blueimp/jquery-file-upload": "lib/web/jquery/fileUploader", "components/jqueryui": [ "lib/web/jquery/jquery-ui.js" ], + "tinymce/tinymce": "lib/web/tiny_mce_4", + "trentrichardson/jquery-timepicker-addon": "lib/web/jquery/jquery-ui-timepicker-addon.js", "twbs/bootstrap": [ "lib/web/jquery/jquery.tabs.js" - ], - "tinymce/tinymce": "lib/web/tiny_mce_4" - } - }, - "autoload": { - "psr-4": { - "Magento\\Framework\\": "lib/internal/Magento/Framework/", - "Magento\\Setup\\": "setup/src/Magento/Setup/", - "Magento\\": "app/code/Magento/", - "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" - }, - "psr-0": { - "": [ - "app/code/", - "generated/code/" ] - }, - "files": [ - "app/etc/NonComposerComponentRegistration.php" - ], - "exclude-from-classmap": [ - "**/dev/**", - "**/update/**", - "**/Test/**" - ] - }, - "autoload-dev": { - "psr-4": { - "Magento\\Sniffs\\": "dev/tests/static/framework/Magento/Sniffs/", - "Magento\\Tools\\": "dev/tools/Magento/Tools/", - "Magento\\Tools\\Sanity\\": "dev/build/publication/sanity/Magento/Tools/Sanity/", - "Magento\\TestFramework\\Inspection\\": "dev/tests/static/framework/Magento/TestFramework/Inspection/", - "Magento\\TestFramework\\Utility\\": "dev/tests/static/framework/Magento/TestFramework/Utility/", - "Magento\\PhpStan\\": "dev/tests/static/framework/Magento/PhpStan/" } - }, - "prefer-stable": true + } } + From 38715a87322c5066b2654ae7ddf69b61defa6450 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Mon, 12 Oct 2020 16:23:49 -0500 Subject: [PATCH 0787/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Indexer/Product/Price/AbstractAction.php | 12 +-- .../Indexer/Product/Price/Action/Rows.php | 96 ++++++++++++++++++- 2 files changed, 96 insertions(+), 12 deletions(-) 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 b8896dd128f6b..fdbfad201954c 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -387,9 +387,12 @@ protected function _reindexRows($changedIds = []) $parentProductsTypes = $this->getParentProductsTypes($changedIds); $changedIds = array_unique(array_merge($changedIds, ...array_values($parentProductsTypes))); - $pendingDeleteIds = $changedIds; $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes); + if (!empty($changedIds)) { + $this->deleteIndexData($changedIds); + } + $typeIndexers = $this->getTypeIndexers(); foreach ($typeIndexers as $productType => $indexer) { $entityIds = $productsTypes[$productType] ?? []; @@ -416,11 +419,6 @@ protected function _reindexRows($changedIds = []) $indexer->reindexEntity($entityIds); $this->_syncData($entityIds); } - $pendingDeleteIds = array_diff($pendingDeleteIds, $entityIds); - } - - if (!empty($pendingDeleteIds)) { - $this->deleteIndexData($pendingDeleteIds); } return $changedIds; @@ -472,9 +470,7 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) if (!empty($excludeIds)) { $select->where('child_id NOT IN(?)', $excludeIds); } - $children = $this->getConnection()->fetchCol($select); - if ($children) { foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $select = $this->getConnection()->select()->from( diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php index 27b50eea883b0..ce2f1ff75adbe 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php @@ -5,12 +5,82 @@ */ namespace Magento\Catalog\Model\Indexer\Product\Price\Action; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Store\Model\StoreManagerInterface; + /** * Class Rows reindex action for mass actions * */ class Rows extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction { + /** + * Default batch size + */ + private const BATCH_SIZE = 100; + + /** + * @var int + */ + private $batchSize; + + /** + * @param ScopeConfigInterface $config + * @param StoreManagerInterface $storeManager + * @param CurrencyFactory $currencyFactory + * @param TimezoneInterface $localeDate + * @param DateTime $dateTime + * @param Type $catalogProductType + * @param Factory $indexerPriceFactory + * @param DefaultPrice $defaultIndexerResource + * @param TierPrice|null $tierPriceIndexResource + * @param DimensionCollectionFactory|null $dimensionCollectionFactory + * @param TableMaintainer|null $tableMaintainer + * @param int|null $batchSize + * @SuppressWarnings(PHPMD.NPathComplexity) Added to backward compatibility with abstract class + * @SuppressWarnings(PHPMD.CyclomaticComplexity) Added to backward compatibility with abstract class + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Added to backward compatibility with abstract class + */ + public function __construct( + ScopeConfigInterface $config, + StoreManagerInterface $storeManager, + CurrencyFactory $currencyFactory, + TimezoneInterface $localeDate, + DateTime $dateTime, + Type $catalogProductType, + Factory $indexerPriceFactory, + DefaultPrice $defaultIndexerResource, + TierPrice $tierPriceIndexResource = null, + DimensionCollectionFactory $dimensionCollectionFactory = null, + TableMaintainer $tableMaintainer = null, + ?int $batchSize = null + ) { + parent::__construct( + $config, + $storeManager, + $currencyFactory, + $localeDate, + $dateTime, + $catalogProductType, + $indexerPriceFactory, + $defaultIndexerResource, + $tierPriceIndexResource, + $dimensionCollectionFactory, + $tableMaintainer + ); + $this->batchSize = $batchSize ?? self::BATCH_SIZE; + } + /** * Execute Rows reindex * @@ -24,10 +94,28 @@ public function execute($ids) if (empty($ids)) { throw new \Magento\Framework\Exception\InputException(__('Bad value was supplied.')); } - try { - $this->_reindexRows($ids); - } catch (\Exception $e) { - throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + $currentBatch = []; + $i = 0; + + foreach ($ids as $id) { + $currentBatch[] = $id; + if (++$i === $this->batchSize) { + try { + $this->_reindexRows($currentBatch); + } catch (\Exception $e) { + throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + } + $i = 0; + $currentBatch = []; + } + } + + if (!empty($currentBatch)) { + try { + $this->_reindexRows($currentBatch); + } catch (\Exception $e) { + throw new \Magento\Framework\Exception\LocalizedException(__($e->getMessage()), $e); + } } } } From 1c829a7233944b7ef7b05e53478f471cccb10c0f Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Mon, 12 Oct 2020 16:29:08 -0500 Subject: [PATCH 0788/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Catalog/Model/Indexer/Product/Price/AbstractAction.php | 4 +++- .../ResourceModel/Product/Indexer/Price/DefaultPrice.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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 fdbfad201954c..f3a4b322e29df 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -389,7 +389,7 @@ protected function _reindexRows($changedIds = []) $changedIds = array_unique(array_merge($changedIds, ...array_values($parentProductsTypes))); $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes); - if (!empty($changedIds)) { + if ($changedIds) { $this->deleteIndexData($changedIds); } @@ -470,7 +470,9 @@ protected function _copyRelationIndexData($parentIds, $excludeIds = null) if (!empty($excludeIds)) { $select->where('child_id NOT IN(?)', $excludeIds); } + $children = $this->getConnection()->fetchCol($select); + if ($children) { foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $select = $this->getConnection()->select()->from( 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 6d7e7293f9e9d..578e3099a2fde 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 @@ -778,7 +778,7 @@ protected function _movePriceDataToIndexTable($entityIds = null) $select->where('entity_id in (?)', count($entityIds) > 0 ? $entityIds : 0, \Zend_Db::INT_TYPE); } - $query = $select->insertFromSelect($this->getIdxTable(), [], true); + $query = $select->insertFromSelect($this->getIdxTable(), [], false); $connection->query($query); $connection->delete($table); From 4e40bc152a530d0bf549adb36817b1d5fced9e8b Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Mon, 12 Oct 2020 16:34:35 -0500 Subject: [PATCH 0789/1013] [AWS S3] MC-37463: Support by Magento Maintenance Mode (#6215) * MC-37479: Support by Magento Content Design * MC-37463: Support by Magento Maintenance Mode --- app/code/Magento/AwsS3/Driver/AwsS3.php | 45 +++++++- .../Magento/AwsS3/Driver/AwsS3Factory.php | 31 ++---- .../AwsS3/Test/Mftf/Data/ConfigData.xml | 17 +++ ...3AdminMarketingCreateSitemapEntityTest.xml | 61 +++++++++++ ...wsS3AdminMarketingSiteMapCreateNewTest.xml | 39 +++++++ .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 47 +++++++- app/code/Magento/AwsS3/etc/di.xml | 4 +- .../Command/AbstractMaintenanceCommand.php | 26 +++-- .../Command/MaintenanceAllowIpsCommand.php | 24 +++-- .../Command/MaintenanceDisableCommand.php | 10 +- .../Command/MaintenanceEnableCommand.php | 10 +- .../Command/MaintenanceStatusCommand.php | 17 +-- .../Magento/Backend/Console/CommandList.php | 64 +++++++++++ .../Backend/Model}/Validator/IpValidator.php | 2 +- .../MaintenanceAllowIpsCommandTest.php | 6 +- .../Command/MaintenanceDisableCommandTest.php | 6 +- .../Command/MaintenanceEnableCommandTest.php | 6 +- .../Command/MaintenanceStatusCommandTest.php | 4 +- .../Unit/Model}/Validator/IpValidatorTest.php | 20 ++-- app/code/Magento/Backend/cli_commands.php | 8 ++ app/code/Magento/Backend/composer.json | 3 +- app/code/Magento/Backend/etc/di.xml | 4 + .../Command/RemoteStorageDisableCommand.php | 74 +++++++++++++ .../Command/RemoteStorageEnableCommand.php | 100 ++++++++++++++---- .../Driver/DriverFactoryInterface.php | 4 +- .../Driver/DriverFactoryPool.php | 57 ++++++++++ .../RemoteStorage/Driver/DriverPool.php | 45 +++++--- .../Magento/RemoteStorage/Model/Config.php | 29 ++++- app/code/Magento/RemoteStorage/etc/di.xml | 6 ++ .../src/Magento/Setup/Console/CommandList.php | 4 - 30 files changed, 638 insertions(+), 135 deletions(-) create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/AbstractMaintenanceCommand.php (85%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceAllowIpsCommand.php (90%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceDisableCommand.php (84%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceEnableCommand.php (80%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Console/Command/MaintenanceStatusCommand.php (85%) create mode 100644 app/code/Magento/Backend/Console/CommandList.php rename {setup/src/Magento/Setup => app/code/Magento/Backend/Model}/Validator/IpValidator.php (97%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php (96%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php (95%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php (93%) rename {setup/src/Magento/Setup => app/code/Magento/Backend}/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php (95%) rename {setup/src/Magento/Setup/Test/Unit => app/code/Magento/Backend/Test/Unit/Model}/Validator/IpValidatorTest.php (79%) create mode 100644 app/code/Magento/Backend/cli_commands.php create mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index a224d9d6ce0ef..8a862a6812107 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -195,9 +195,40 @@ public function readDirectory($path): array */ public function getRealPathSafety($path) { - return $this->normalizeAbsolutePath( - $this->normalizeRelativePath($path) + if (strpos($path, '/.') === false) { + return $path; + } + + $isAbsolute = strpos($path, $this->normalizeAbsolutePath()) === 0; + $path = $this->normalizeRelativePath($path); + + //Removing redundant directory separators. + $path = preg_replace( + '/\\/\\/+/', + '/', + $path ); + $pathParts = explode('/', $path); + if (end($pathParts) === '.') { + $pathParts[count($pathParts) - 1] = ''; + } + $realPath = []; + foreach ($pathParts as $pathPart) { + if ($pathPart === '.') { + continue; + } + if ($pathPart === '..') { + array_pop($realPath); + continue; + } + $realPath[] = $pathPart; + } + + if ($isAbsolute) { + return $this->normalizeAbsolutePath(implode('/', $realPath)); + } + + return implode('/', $realPath); } /** @@ -227,6 +258,14 @@ public function getAbsolutePath($basePath, $path, $scheme = null) private function normalizeAbsolutePath(string $path = '.'): string { $path = ltrim($path, '/'); + $path = str_replace( + $this->adapter->getClient()->getObjectUrl( + $this->adapter->getBucket(), + $this->adapter->applyPathPrefix('.') + ), + '', + $path + ); if (!$path) { $path = '.'; @@ -317,7 +356,7 @@ public function getRelativePath($basePath, $path = null): string public function getParentDirectory($path): string { //phpcs:ignore Magento2.Functions.DiscouragedFunction - return dirname($this->normalizeAbsolutePath($path)); + return rtrim(dirname($this->normalizeAbsolutePath($path)), '/') . '/'; } /** diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index 6ab8f93fa6e2b..d9efe6f7fd10e 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -9,7 +9,6 @@ use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; -use Magento\AwsS3\Model\Config; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\ObjectManagerInterface; use Magento\RemoteStorage\Driver\DriverFactoryInterface; @@ -24,43 +23,27 @@ class AwsS3Factory implements DriverFactoryInterface */ private $objectManager; - /** - * @var Config - */ - private $config; - /** * @param ObjectManagerInterface $objectManager - * @param Config $config */ - public function __construct(ObjectManagerInterface $objectManager, Config $config) + public function __construct(ObjectManagerInterface $objectManager) { $this->objectManager = $objectManager; - $this->config = $config; } /** * Creates an instance of AWS S3 driver. * + * @param array $config + * @param string $prefix * @return DriverInterface */ - public function create(): DriverInterface + public function create(array $config, string $prefix): DriverInterface { - $config = [ - 'region' => $this->config->getRegion(), + $config += [ 'version' => 'latest' ]; - $key = $this->config->getAccessKey(); - $secret = $this->config->getSecretKey(); - - if ($key && $secret) { - $config['credentials'] = [ - 'key' => $key, - 'secret' => $secret, - ]; - } - return $this->objectManager->create( AwsS3::class, [ @@ -68,8 +51,8 @@ public function create(): DriverInterface AwsS3Adapter::class, [ 'client' => $this->objectManager->create(S3Client::class, ['args' => $config]), - 'bucket' => $this->config->getBucket(), - 'prefix' => $this->config->getPrefix() + 'bucket' => $config['bucket'], + 'prefix' => $prefix ] ) ] diff --git a/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..bc43aff37a491 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.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="RemoteStorageAwsS3ConfigData"> + <data key="driver">{{_ENV.REMOTE_STORAGE_AWSS3_DRIVER}}</data> + <data key="region">{{_ENV.REMOTE_STORAGE_AWSS3_REGION}}</data> + <data key="prefix">{{_ENV.REMOTE_STORAGE_AWSS3_PREFIX}}</data> + <data key="bucket">{{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}}</data> + <data key="access_key">{{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}}</data> + <data key="secret_key">{{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}}</data> + </entity> +</entities> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml new file mode 100644 index 0000000000000..4d411fcffc682 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.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="AwsS3AdminMarketingCreateSitemapEntityTest"> + <annotations> + <features value="Sitemap"/> + <stories value="AWS S3 Admin Creates Sitemap Entity"/> + <title value="AWS S3 Sitemap Creation"/> + <description value="Sitemap Entity Creation"/> + <testCaseId value="MC-38319"/> + <severity value="MAJOR"/> + <group value="sitemap"/> + <group value="mtf_migrated"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteCreatedSitemap"> + <argument name="filename" value="sitemap.xml"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + + <!--TEST BODY --> + <!--Navigate to Marketing->Sitemap Page --> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSiteMapPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuSEOAndSearchSiteMap.dataUiId}}"/> + </actionGroup> + <!-- Navigate to New Sitemap Creation Page --> + <actionGroup ref="AdminMarketingNavigateToNewSitemapPageActionGroup" stepKey="navigateToAddNewSitemap"/> + <!-- Create Sitemap Entity --> + <actionGroup ref="AdminMarketingCreateSitemapEntityActionGroup" stepKey="createSitemap"> + <argument name="filename" value="sitemap.xml"/> + <argument name="path" value="/"/> + </actionGroup> + <!-- Assert Success Message --> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeSuccessMessage"> + <argument name="message" value="You saved the sitemap."/> + <argument name="messageType" value="success"/> + </actionGroup> + <!-- Find Created Sitemap On Grid --> + <actionGroup ref="AdminMarketingSearchSitemapActionGroup" stepKey="findCreatedSitemapInGrid"> + <argument name="name" value="sitemap.xml"/> + </actionGroup> + <actionGroup ref="AssertAdminSitemapInGridActionGroup" stepKey="assertSitemapInGrid"> + <argument name="name" value="sitemap.xml"/> + </actionGroup> + <!--END TEST BODY --> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml new file mode 100644 index 0000000000000..43cf305e3fd17 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminMarketingSiteMapCreateNewTest"> + <annotations> + <features value="Sitemap"/> + <stories value="AWS S3 Create Site Map"/> + <title value="AWS S3 Create New Site Map with valid data"/> + <description value="Create New Site Map with valid data"/> + <testCaseId value="MC-38320" /> + <severity value="CRITICAL"/> + <group value="sitemap"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteSiteMap"> + <argument name="filename" value="{{DefaultSiteMap.filename}}" /> + </actionGroup> + <actionGroup ref="AssertSiteMapDeleteSuccessActionGroup" stepKey="assertDeleteSuccessMessage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + <actionGroup ref="AdminMarketingSiteMapNavigateNewActionGroup" stepKey="navigateNewSiteMap"/> + <actionGroup ref="AdminMarketingSiteMapFillFormActionGroup" stepKey="fillSiteMapForm"> + <argument name="sitemap" value="DefaultSiteMap" /> + </actionGroup> + <actionGroup ref="AssertSiteMapCreateSuccessActionGroup" stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index 5ddc4811230ec..b3de684ed67dd 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -59,9 +59,7 @@ protected function setUp(): void return self::URL . $path; }); - $this->driver = new AwsS3( - $this->adapterMock - ); + $this->driver = new AwsS3($this->adapterMock); } /** @@ -116,6 +114,11 @@ public function getAbsolutePathDataProvider(): array self::URL . 'media/', '/catalog/test.png', self::URL . 'media/catalog/test.png' + ], + [ + '', + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' ] ]; } @@ -137,7 +140,7 @@ public function testGetRelativePath(string $basePath, string $path, string $expe */ public function getRelativePathDataProvider(): array { - return [ + return [ [ '', 'test/test.txt', @@ -324,4 +327,40 @@ public function isFileDataProvider(): array ] ]; } + + /** + * @param string $path + * @param string $expected + * + * @dataProvider getRealPathSafetyDataProvider + */ + public function testGetRealPathSafety(string $path, string $expected): void + { + self::assertSame($expected, $this->driver->getRealPathSafety($path)); + } + + /** + * @return array + */ + public function getRealPathSafetyDataProvider(): array + { + return [ + [ + self::URL, + self::URL + ], + [ + 'test.txt', + 'test.txt' + ], + [ + self::URL . 'test/test/../test.txt', + self::URL . 'test/test.txt' + ], + [ + 'test/test/../test.txt', + 'test/test.txt' + ] + ]; + } } diff --git a/app/code/Magento/AwsS3/etc/di.xml b/app/code/Magento/AwsS3/etc/di.xml index 2b66da74299ea..94df51fcd6856 100644 --- a/app/code/Magento/AwsS3/etc/di.xml +++ b/app/code/Magento/AwsS3/etc/di.xml @@ -6,9 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\RemoteStorage\Driver\DriverPool"> + <type name="Magento\RemoteStorage\Driver\DriverFactoryPool"> <arguments> - <argument name="remotePool" xsi:type="array"> + <argument name="pool" xsi:type="array"> <item name="aws-s3" xsi:type="object">Magento\AwsS3\Driver\AwsS3Factory</item> </argument> </arguments> diff --git a/setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php b/app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php similarity index 85% rename from setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php rename to app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php index 85ae008adf366..eb452f62e91ce 100644 --- a/setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php +++ b/app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php @@ -3,14 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Validator\IpValidator; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; +use Magento\Backend\Model\Validator\IpValidator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * General maintenance command. + */ abstract class AbstractMaintenanceCommand extends AbstractSetupCommand { /** @@ -38,6 +43,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal { $this->maintenanceMode = $maintenanceMode; $this->ipValidator = $ipValidator; + parent::__construct(); } @@ -57,6 +63,7 @@ protected function configure() ), ]; $this->setDefinition($options); + parent::configure(); } @@ -75,16 +82,18 @@ abstract protected function isEnable(); abstract protected function getDisplayString(); /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $addresses = $input->getOption(self::INPUT_KEY_IP); $messages = $this->validate($addresses); + if (!empty($messages)) { $output->writeln('<error>' . implode('</error>' . PHP_EOL . '<error>', $messages)); - // we must have an exit code higher than zero to indicate something was wrong - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + + // We must have an exit code higher than zero to indicate something was wrong + return Cli::RETURN_FAILURE; } $this->maintenanceMode->set($this->isEnable()); @@ -92,14 +101,15 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!empty($addresses)) { $addresses = implode(',', $addresses); - $addresses = ('none' == $addresses) ? '' : $addresses; + $addresses = ('none' === $addresses) ? '' : $addresses; $this->maintenanceMode->setAddresses($addresses); $output->writeln( '<info>Set exempt IP-addresses: ' . (implode(', ', $this->maintenanceMode->getAddressInfo()) ?: 'none') . '</info>' ); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php similarity index 90% rename from setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php index 09f33cf85062c..230c6a6814ebc 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Framework\Module\ModuleList; -use Magento\Setup\Validator\IpValidator; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; +use Magento\Backend\Model\Validator\IpValidator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -37,8 +37,6 @@ class MaintenanceAllowIpsCommand extends AbstractSetupCommand private $ipValidator; /** - * Constructor - * * @param MaintenanceMode $maintenanceMode * @param IpValidator $ipValidator */ @@ -46,6 +44,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal { $this->maintenanceMode = $maintenanceMode; $this->ipValidator = $ipValidator; + parent::__construct(); } @@ -54,7 +53,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal * * @return void */ - protected function configure() + protected function configure(): void { $arguments = [ new InputArgument( @@ -80,19 +79,21 @@ protected function configure() $this->setName('maintenance:allow-ips') ->setDescription('Sets maintenance mode exempt IPs') ->setDefinition(array_merge($arguments, $options)); + parent::configure(); } /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { if (!$input->getOption(self::INPUT_KEY_NONE)) { $addresses = $input->getArgument(self::INPUT_KEY_IP); $messages = $this->validate($addresses); if (!empty($messages)) { $output->writeln('<error>' . implode('</error>' . PHP_EOL . '<error>', $messages)); + // we must have an exit code higher than zero to indicate something was wrong return \Magento\Framework\Console\Cli::RETURN_FAILURE; } @@ -111,7 +112,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->maintenanceMode->setAddresses(''); $output->writeln('<info>Set exempt IP-addresses: none</info>'); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** @@ -120,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output) * @param string[] $addresses * @return string[] */ - protected function validate(array $addresses) + protected function validate(array $addresses): array { return $this->ipValidator->validateIps($addresses, false); } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php similarity index 84% rename from setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php index abebbdb76346b..5108866fbe65c 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php @@ -3,8 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; /** * Command for disabling maintenance mode @@ -19,6 +18,7 @@ class MaintenanceDisableCommand extends AbstractMaintenanceCommand protected function configure() { $this->setName('maintenance:disable')->setDescription('Disables maintenance mode'); + parent::configure(); } @@ -27,7 +27,7 @@ protected function configure() * * @return bool */ - protected function isEnable() + protected function isEnable(): bool { return false; } @@ -37,7 +37,7 @@ protected function isEnable() * * @return string */ - protected function getDisplayString() + protected function getDisplayString(): string { return '<info>Disabled maintenance mode</info>'; } @@ -47,7 +47,7 @@ protected function getDisplayString() * * @return bool */ - public function isSetAddressInfo() + public function isSetAddressInfo(): bool { return count($this->maintenanceMode->getAddressInfo()) > 0; } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php similarity index 80% rename from setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php index 94ab312b60811..7e5e034483d20 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php @@ -3,8 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; /** * Command for enabling maintenance mode @@ -16,9 +15,10 @@ class MaintenanceEnableCommand extends AbstractMaintenanceCommand * * @return void */ - protected function configure() + protected function configure(): void { $this->setName('maintenance:enable')->setDescription('Enables maintenance mode'); + parent::configure(); } @@ -27,7 +27,7 @@ protected function configure() * * @return bool */ - protected function isEnable() + protected function isEnable(): bool { return true; } @@ -37,7 +37,7 @@ protected function isEnable() * * @return string */ - protected function getDisplayString() + protected function getDisplayString(): string { return '<info>Enabled maintenance mode</info>'; } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php similarity index 85% rename from setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php index f2d3d2bf30caa..e7feae32cf8b0 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php @@ -3,11 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Framework\Module\ModuleList; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -29,6 +29,7 @@ class MaintenanceStatusCommand extends AbstractSetupCommand public function __construct(MaintenanceMode $maintenanceMode) { $this->maintenanceMode = $maintenanceMode; + parent::__construct(); } @@ -37,17 +38,18 @@ public function __construct(MaintenanceMode $maintenanceMode) * * @return void */ - protected function configure() + protected function configure(): void { $this->setName('maintenance:status') ->setDescription('Displays maintenance mode status'); + parent::configure(); } /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln( '<info>Status: maintenance mode is ' . @@ -56,6 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $addressInfo = $this->maintenanceMode->getAddressInfo(); $addresses = implode(' ', $addressInfo); $output->writeln('<info>List of exempt IP-addresses: ' . ($addresses ? $addresses : 'none') . '</info>'); - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } } diff --git a/app/code/Magento/Backend/Console/CommandList.php b/app/code/Magento/Backend/Console/CommandList.php new file mode 100644 index 0000000000000..563ef964812ab --- /dev/null +++ b/app/code/Magento/Backend/Console/CommandList.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backend\Console; + +use Magento\Backend\Console\Command\MaintenanceAllowIpsCommand; +use Magento\Backend\Console\Command\MaintenanceDisableCommand; +use Magento\Backend\Console\Command\MaintenanceEnableCommand; +use Magento\Backend\Console\Command\MaintenanceStatusCommand; +use Magento\Framework\Console\CommandListInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Provides list of commands to be available for uninstalled application + */ +class CommandList implements CommandListInterface +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * Gets list of command classes + * + * @return string[] + */ + private function getCommandsClasses(): array + { + return [ + MaintenanceAllowIpsCommand::class, + MaintenanceDisableCommand::class, + MaintenanceEnableCommand::class, + MaintenanceStatusCommand::class + ]; + } + + /** + * @inheritdoc + */ + public function getCommands(): array + { + $commands = []; + foreach ($this->getCommandsClasses() as $class) { + if (class_exists($class)) { + $commands[] = $this->objectManager->get($class); + } else { + throw new \RuntimeException('Class ' . $class . ' does not exist'); + } + } + + return $commands; + } +} diff --git a/setup/src/Magento/Setup/Validator/IpValidator.php b/app/code/Magento/Backend/Model/Validator/IpValidator.php similarity index 97% rename from setup/src/Magento/Setup/Validator/IpValidator.php rename to app/code/Magento/Backend/Model/Validator/IpValidator.php index 5d1e83021e34b..f208d02ee140a 100644 --- a/setup/src/Magento/Setup/Validator/IpValidator.php +++ b/app/code/Magento/Backend/Model/Validator/IpValidator.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Setup\Validator; +namespace Magento\Backend\Model\Validator; /** * Class to validate list of IPs for maintenance commands diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php similarity index 96% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php index 2a18a892ed06d..281065c51337d 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceAllowIpsCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceAllowIpsCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php similarity index 95% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php index 73afa22f3ebcd..6663a7f9f6504 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceDisableCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceDisableCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php similarity index 93% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php index 0b1afb7310c08..c4a2e35d37d49 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceEnableCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceEnableCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php similarity index 95% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php index 731eff370b00f..8e3970aa5529e 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php @@ -5,10 +5,10 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceStatusCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceStatusCommand; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php similarity index 79% rename from setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php rename to app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php index b6f9f01c80ee5..ccffc58d79780 100644 --- a/setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php @@ -5,11 +5,14 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Validator; +namespace Magento\Backend\Test\Unit\Model\Validator; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\TestCase; +/** + * @see IpValidator + */ class IpValidatorTest extends TestCase { /** @@ -17,6 +20,9 @@ class IpValidatorTest extends TestCase */ private $ipValidator; + /** + * @inheritDoc + */ protected function setUp(): void { $this->ipValidator = new IpValidator(); @@ -27,15 +33,15 @@ protected function setUp(): void * @param string[] $ips * @param string[] $expectedMessages */ - public function testValidateIpsNoneAllowed($ips, $expectedMessages) + public function testValidateIpsNoneAllowed(array $ips, array $expectedMessages): void { - $this->assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, true)); + self::assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, true)); } /** * @return array */ - public function validateIpsNoneAllowedDataProvider() + public function validateIpsNoneAllowedDataProvider(): array { return [ [['127.0.0.1', '127.0.0.2'], []], @@ -54,9 +60,9 @@ public function validateIpsNoneAllowedDataProvider() * @param string[] $ips * @param string[] $expectedMessages */ - public function testValidateIpsNoneNotAllowed($ips, $expectedMessages) + public function testValidateIpsNoneNotAllowed($ips, $expectedMessages): void { - $this->assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, false)); + self::assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, false)); } /** diff --git a/app/code/Magento/Backend/cli_commands.php b/app/code/Magento/Backend/cli_commands.php new file mode 100644 index 0000000000000..3c4140b40a993 --- /dev/null +++ b/app/code/Magento/Backend/cli_commands.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +if (PHP_SAPI === 'cli') { + \Magento\Framework\Console\CommandLocator::register(\Magento\Backend\Console\CommandList::class); +} diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index ee5491057d861..017f247adbf43 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -34,7 +34,8 @@ ], "autoload": { "files": [ - "registration.php" + "registration.php", + "cli_commands.php" ], "psr-4": { "Magento\\Backend\\": "" diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml index 65f73f028eb20..1297bd9603a1f 100644 --- a/app/code/Magento/Backend/etc/di.xml +++ b/app/code/Magento/Backend/etc/di.xml @@ -150,6 +150,10 @@ <item name="cacheFlushCommand" xsi:type="object">Magento\Backend\Console\Command\CacheFlushCommand</item> <item name="cacheCleanCommand" xsi:type="object">Magento\Backend\Console\Command\CacheCleanCommand</item> <item name="cacheStatusCommand" xsi:type="object">Magento\Backend\Console\Command\CacheStatusCommand</item> + <item name="maintenanceAllowIps" xsi:type="object">Magento\Backend\Console\Command\MaintenanceAllowIpsCommand</item> + <item name="maintenanceDisable" xsi:type="object">Magento\Backend\Console\Command\MaintenanceDisableCommand</item> + <item name="maintenanceEnableCommand" xsi:type="object">Magento\Backend\Console\Command\MaintenanceDisableCommand</item> + <item name="maintenanceStatusCommand" xsi:type="object">Magento\Backend\Console\Command\MaintenanceStatusCommand</item> </argument> </arguments> </type> diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php new file mode 100644 index 0000000000000..e87ca584299e7 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Console\Command; + +use Magento\Framework\App\DeploymentConfig\Writer; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverPool; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Remote storage configuration disablement. + */ +class RemoteStorageDisableCommand extends Command +{ + private const NAME = 'remote-storage:disable'; + + /** + * @var Writer + */ + private $writer; + + /** + * @param Writer $writer + */ + public function __construct(Writer $writer) + { + $this->writer = $writer; + + parent::__construct(); + } + + /** + * @inheritDoc + */ + protected function configure(): void + { + $this->setName(self::NAME) + ->setDescription('Disable remote storage'); + } + + /** + * Executes command. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->writer->saveConfig([ + ConfigFilePool::APP_ENV => [ + 'remote_storage' => [ + 'driver' => DriverPool::FILE, + ] + ] + ], true); + + $output->writeln('<info>Config was saved.</info>'); + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php index cad5ecf314da2..e308f609a3d69 100644 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php @@ -9,7 +9,10 @@ use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Console\Cli; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverFactoryPool; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -23,11 +26,11 @@ class RemoteStorageEnableCommand extends Command { private const NAME = 'remote-storage:enable'; private const ARG_DRIVER = 'driver'; - private const OPTION_BUCKET = 'bucket'; - private const OPTION_REGION = 'region'; - private const OPTION_ACCESS_KEY = 'access-key'; - private const OPTION_SECRET_KEY = 'secret-key'; - private const OPTION_PREFIX = 'prefix'; + private const ARGUMENT_BUCKET = 'bucket'; + private const ARGUMENT_REGION = 'region'; + private const ARGUMENT_ACCESS_KEY = 'access-key'; + private const ARGUMENT_SECRET_KEY = 'secret-key'; + private const ARGUMENT_PREFIX = 'prefix'; private const OPTION_IS_PUBLIC = 'is-public'; /** @@ -35,12 +38,19 @@ class RemoteStorageEnableCommand extends Command */ private $writer; + /** + * @var DriverFactoryPool + */ + private $driverFactoryPool; + /** * @param Writer $writer + * @param DriverFactoryPool $driverFactoryPool */ - public function __construct(Writer $writer) + public function __construct(Writer $writer, DriverFactoryPool $driverFactoryPool) { $this->writer = $writer; + $this->driverFactoryPool = $driverFactoryPool; parent::__construct(); } @@ -52,13 +62,13 @@ protected function configure(): void { $this->setName(self::NAME) ->setDescription('Enable remote storage integration') - ->addArgument(self::ARG_DRIVER, InputArgument::REQUIRED, 'Remote driver') - ->addOption(self::OPTION_BUCKET, null, InputOption::VALUE_REQUIRED, 'Bucket') - ->addOption(self::OPTION_REGION, null, InputOption::VALUE_REQUIRED, 'Region') - ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_REQUIRED, 'Access key') - ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_REQUIRED, 'Secret key') - ->addOption(self::OPTION_PREFIX, null, InputOption::VALUE_REQUIRED, 'Prefix', '') - ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_NONE, 'Is public'); + ->addArgument(self::ARG_DRIVER, InputArgument::OPTIONAL, 'Remote driver', DriverPool::FILE) + ->addArgument(self::ARGUMENT_BUCKET, InputArgument::OPTIONAL, 'Bucket') + ->addArgument(self::ARGUMENT_REGION, InputArgument::OPTIONAL, 'Region') + ->addArgument(self::ARGUMENT_PREFIX, InputArgument::OPTIONAL, 'Prefix', '') + ->addArgument(self::ARGUMENT_ACCESS_KEY, InputArgument::OPTIONAL, 'Access key') + ->addArgument(self::ARGUMENT_SECRET_KEY, InputArgument::OPTIONAL, 'Secret key') + ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_REQUIRED, 'Is public', false); } /** @@ -66,25 +76,69 @@ protected function configure(): void * * @param InputInterface $input * @param OutputInterface $output - * @return void + * @return int * @throws FileSystemException */ - public function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { + $driver = $input->getArgument(self::ARG_DRIVER); + + if ($driver === DriverPool::FILE) { + $output->writeln(sprintf( + 'Driver "%s" was specified. Skipping', + $driver + )); + + return Cli::RETURN_SUCCESS; + } + + if (!$this->driverFactoryPool->has($driver)) { + $output->writeln('Driver %s was not found', $driver); + + return Cli::RETURN_FAILURE; + } + + $prefix = (string)$input->getArgument(self::ARGUMENT_PREFIX); + $config = [ + 'bucket' => (string)$input->getArgument(self::ARGUMENT_BUCKET), + 'region' => (string)$input->getArgument(self::ARGUMENT_REGION), + ]; + $isPublic = (bool)$input->getOption(self::OPTION_IS_PUBLIC); + + if (($key = (string)$input->getArgument(self::ARGUMENT_ACCESS_KEY)) + && ($secret = (string)$input->getArgument(self::ARGUMENT_SECRET_KEY)) + ) { + $config['credentials']['key'] = $key; + $config['credentials']['secret'] = $secret; + } + + try { + $this->driverFactoryPool->get($driver)->create($config, $prefix); + } catch (\Exception $exception) { + $output->writeln(sprintf( + '<error>Config cannot be set: %s</error>', + $exception->getMessage() + )); + + return Cli::RETURN_FAILURE; + } + $this->writer->saveConfig([ ConfigFilePool::APP_ENV => [ 'remote_storage' => [ - 'driver' => (string)$input->getArgument(self::ARG_DRIVER), - 'bucket' => (string)$input->getOption(self::OPTION_BUCKET), - 'region' => (string)$input->getOption(self::OPTION_REGION), - 'access_key' => (string)$input->getOption(self::OPTION_ACCESS_KEY), - 'secret_key' => (string)$input->getOption(self::OPTION_SECRET_KEY), - 'prefix' => (string)$input->getOption(self::OPTION_PREFIX), - 'is_public' => (bool)$input->getOption(self::OPTION_IS_PUBLIC) + 'driver' => $driver, + 'prefix' => $prefix, + 'is_public' => $isPublic, + 'config' => $config ] ] ], true); - $output->writeln('<info>Config was saved.</info>'); + $output->writeln(sprintf( + '<info>Config for driver "%s" was saved.</info>', + $driver + )); + + return Cli::RETURN_SUCCESS; } } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php index a95284fb27391..ab7a1bcaa6cc5 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -17,7 +17,9 @@ interface DriverFactoryInterface /** * Creates pre-configured driver. * + * @param array $config + * @param string $prefix * @return DriverInterface */ - public function create(): DriverInterface; + public function create(array $config, string $prefix): DriverInterface; } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php new file mode 100644 index 0000000000000..aa4e057af5383 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Exception\RuntimeException; + +/** + * Pool of driver factories. + */ +class DriverFactoryPool +{ + /** + * @var DriverFactoryInterface[] + */ + private $pool; + + /** + * @param DriverFactoryInterface[] $pool + */ + public function __construct(array $pool) + { + $this->pool = $pool; + } + + /** + * Check if factory exists. + * + * @param string $name + * @return bool + */ + public function has(string $name): bool + { + return isset($this->pool[$name]); + } + + /** + * Retrieve factory. + * + * @param string $name + * @return DriverFactoryInterface + * + * @throws RuntimeException + */ + public function get(string $name): DriverFactoryInterface + { + if (!$this->has($name)) { + throw new RuntimeException(__('Factory %1 does not exist', $name)); + } + + return $this->pool[$name]; + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index 4c2c834f0d776..a1e758f170e7e 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -21,38 +21,47 @@ class DriverPool implements DriverPoolInterface { public const PATH_DRIVER = 'remote_storage/driver'; public const PATH_IS_PUBLIC = 'remote_storage/is_public'; + public const PATH_PREFIX = 'remote_storage/prefix'; + public const PATH_CONFIG = 'remote_storage/config'; + + /** + * Driver name. + */ public const REMOTE = 'remote'; /** - * @var DriverPool + * @var Config */ - private $driverPool; + private $config; /** - * @var DriverInterface[] + * @var DriverFactoryPool */ - private $pool = []; + private $driverFactoryPool; /** - * @var DriverFactoryInterface[] + * @var DriverPool */ - private $remotePool; + private $driverPool; /** - * @var Config + * @var array */ - private $config; + private $pool = []; /** - * @param BaseDriverPool $driverPool * @param Config $config - * @param array $remotePool + * @param DriverFactoryPool $driverFactoryPool + * @param BaseDriverPool $driverPool */ - public function __construct(BaseDriverPool $driverPool, Config $config, array $remotePool = []) - { - $this->driverPool = $driverPool; + public function __construct( + Config $config, + DriverFactoryPool $driverFactoryPool, + BaseDriverPool $driverPool + ) { $this->config = $config; - $this->remotePool = $remotePool; + $this->driverFactoryPool = $driverFactoryPool; + $this->driverPool = $driverPool; } /** @@ -60,7 +69,6 @@ public function __construct(BaseDriverPool $driverPool, Config $config, array $r * * @param string $code * @return DriverInterface - * * @throws RuntimeException * @throws FileSystemException */ @@ -73,8 +81,11 @@ public function getDriver($code = self::REMOTE): DriverInterface $driver = $this->config->getDriver(); - if ($driver && isset($this->remotePool[$driver])) { - return $this->pool[$code] = $this->remotePool[$driver]->create(); + if ($driver && $this->driverFactoryPool->has($driver)) { + return $this->pool[$code] = $this->driverFactoryPool->get($driver)->create( + $this->config->getConfig(), + $this->config->getPrefix() + ); } throw new RuntimeException(__('Remote driver is not available.')); diff --git a/app/code/Magento/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php index 164d94cefddee..36238b20b38bb 100644 --- a/app/code/Magento/RemoteStorage/Model/Config.php +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -11,6 +11,7 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\RuntimeException; use Magento\RemoteStorage\Driver\DriverPool; +use Magento\Framework\Filesystem\DriverPool as BaseDriverPool; /** * Configuration for remote storage. @@ -51,7 +52,9 @@ public function getDriver(): ?string */ public function isEnabled(): bool { - return $this->config->get(DriverPool::PATH_DRIVER) !== null; + $driver = $this->config->get(DriverPool::PATH_DRIVER); + + return $driver && $driver !== BaseDriverPool::FILE; } /** @@ -65,4 +68,28 @@ public function isPublic(): bool { return (bool)$this->config->get(DriverPool::PATH_IS_PUBLIC, false); } + + /** + * Retrieves config. + * + * @return array + * @throws FileSystemException + * @throws RuntimeException + */ + public function getConfig(): array + { + return (array)$this->config->get(DriverPool::PATH_CONFIG, []); + } + + /** + * Retrieves prefix. + * + * @return string + * @throws FileSystemException + * @throws RuntimeException + */ + public function getPrefix(): string + { + return (string)$this->config->get(DriverPool::PATH_PREFIX, ''); + } } diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index a43a6160c554f..d9124326e65c2 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -65,7 +65,13 @@ <arguments> <argument name="commands" xsi:type="array"> <item name="remoteStorageEnable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageEnableCommand</item> + <item name="remoteStorageDisable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageDisableCommand</item> </argument> </arguments> </type> + <type name="Magento\Framework\App\MaintenanceMode"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> </config> diff --git a/setup/src/Magento/Setup/Console/CommandList.php b/setup/src/Magento/Setup/Console/CommandList.php index ab31a3add07ed..ae65e82bba12b 100644 --- a/setup/src/Magento/Setup/Console/CommandList.php +++ b/setup/src/Magento/Setup/Console/CommandList.php @@ -66,10 +66,6 @@ protected function getCommandsClasses() \Magento\Setup\Console\Command\ModuleStatusCommand::class, \Magento\Setup\Console\Command\ModuleUninstallCommand::class, \Magento\Setup\Console\Command\ModuleConfigStatusCommand::class, - \Magento\Setup\Console\Command\MaintenanceAllowIpsCommand::class, - \Magento\Setup\Console\Command\MaintenanceDisableCommand::class, - \Magento\Setup\Console\Command\MaintenanceEnableCommand::class, - \Magento\Setup\Console\Command\MaintenanceStatusCommand::class, \Magento\Setup\Console\Command\RollbackCommand::class, \Magento\Setup\Console\Command\UpgradeCommand::class, \Magento\Setup\Console\Command\UninstallCommand::class, From 8090eb0eafdabd3462a28f7fe47554fc9ff57a1c Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 13 Oct 2020 10:13:15 +0300 Subject: [PATCH 0790/1013] MC-38074: Report - Products in Carts not following user roles scope --- .../Reports/Model/Product/DataRetriever.php | 106 ++++++++++++++++++ .../ResourceModel/Quote/Item/Collection.php | 18 ++- .../Report/Quote/CollectionTest.php | 44 ++++---- .../Model/Product/DataRetrieverTest.php | 54 +++++++++ 4 files changed, 195 insertions(+), 27 deletions(-) create mode 100644 app/code/Magento/Reports/Model/Product/DataRetriever.php create mode 100644 dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php diff --git a/app/code/Magento/Reports/Model/Product/DataRetriever.php b/app/code/Magento/Reports/Model/Product/DataRetriever.php new file mode 100644 index 0000000000000..c6260a4e7bacc --- /dev/null +++ b/app/code/Magento/Reports/Model/Product/DataRetriever.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Model\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Retrieve products data for reports by entity id's + */ +class DataRetriever +{ + /** + * @var ProductCollectionFactory + */ + private $productCollectionFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * DataRetriever constructor. + * + * @param ProductCollectionFactory $productCollectionFactory + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ProductCollectionFactory $productCollectionFactory, + StoreManagerInterface $storeManager + ) { + $this->productCollectionFactory = $productCollectionFactory; + $this->storeManager = $storeManager; + } + + /** + * Retrieve products data by entity id's + * + * @param array $entityIds + * @return array + */ + public function execute(array $entityIds = []): array + { + $productCollection = $this->getProductCollection($entityIds); + + return $this->prepareDataByCollection($productCollection); + } + + /** + * Get product collection filtered by entity id's + * + * @param array $entityIds + * @return ProductCollection + */ + private function getProductCollection(array $entityIds = []): ProductCollection + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addAttributeToSelect('name'); + $productCollection->addIdFilter($entityIds); + $productCollection->addPriceData(null, $this->getWebsiteIdForFilter()); + + return $productCollection; + } + + /** + * Retrieve website id for filter collection + * + * @return int + */ + private function getWebsiteIdForFilter(): int + { + $defaultStoreView = $this->storeManager->getDefaultStoreView(); + if ($defaultStoreView) { + $websiteId = (int)$defaultStoreView->getWebsiteId(); + } else { + $websites = $this->storeManager->getWebsites(); + $website = reset($websites); + $websiteId = (int)$website->getId(); + } + + return $websiteId; + } + + /** + * Prepare data by collection + * + * @param ProductCollection $productCollection + * @return array + */ + private function prepareDataByCollection(ProductCollection $productCollection): array + { + $productsData = []; + foreach ($productCollection as $product) { + $productsData[$product->getId()] = $product->getData(); + } + + return $productsData; + } +} diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php index 16df2d30db40d..e7dc28eb74a49 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php @@ -7,7 +7,8 @@ namespace Magento\Reports\Model\ResourceModel\Quote\Item; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; +use Magento\Reports\Model\Product\DataRetriever as ProductDataRetriever; /** * Collection of Magento\Quote\Model\Quote\Item @@ -49,6 +50,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab */ protected $orderResource; + /** + * @var ProductDataRetriever + */ + private $productDataRetriever; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger @@ -59,6 +65,9 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @param \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource + * @param ProductDataRetriever|null $productDataRetriever + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, @@ -69,7 +78,8 @@ public function __construct( \Magento\Customer\Model\ResourceModel\Customer $customerResource, \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null + \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null, + ?ProductDataRetriever $productDataRetriever = null ) { parent::__construct( $entityFactory, @@ -82,6 +92,8 @@ public function __construct( $this->productResource = $productResource; $this->customerResource = $customerResource; $this->orderResource = $orderResource; + $this->productDataRetriever = $productDataRetriever + ?? ObjectManager::getInstance()->get(ProductDataRetriever::class); } /** @@ -225,7 +237,7 @@ protected function _afterLoad() foreach ($items as $item) { $productIds[] = $item->getProductId(); } - $productData = $this->getProductData($productIds); + $productData = $this->productDataRetriever->execute($productIds); $orderData = $this->getOrdersData($productIds); foreach ($items as $item) { $item->setId($item->getProductId()); diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php index 6e7d5bdce16f5..90d224ee417db 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php @@ -7,15 +7,17 @@ namespace Magento\Reports\Test\Unit\Model\ResourceModel\Report\Quote; -use Magento\Eav\Model\Entity\AbstractEntity; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Model\ResourceModel\Quote; -use Magento\Reports\Model\ResourceModel\Quote\Collection; +use Magento\Reports\Model\Product\DataRetriever as ProductDataRetriever; +use Magento\Reports\Model\ResourceModel\Quote\Collection as QuoteCollection; +use Magento\Reports\Model\ResourceModel\Quote\Item\Collection as QuoteItemCollection; +use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -34,16 +36,22 @@ class CollectionTest extends TestCase */ protected $selectMock; + /** + * @var ProductDataRetriever|MockObject + */ + private $productDataRetriever; + protected function setUp(): void { $this->objectManager = new ObjectManager($this); $this->selectMock = $this->createMock(Select::class); + $this->productDataRetriever = $this->createMock(ProductDataRetriever::class); } public function testGetSelectCountSql() { /** @var MockObject $collection */ - $collection = $this->getMockBuilder(Collection::class) + $collection = $this->getMockBuilder(QuoteCollection::class) ->setMethods(['getSelect']) ->disableOriginalConstructor() ->getMock(); @@ -61,8 +69,8 @@ public function testPrepareActiveCartItems() { /** @var MockObject $collection */ $constructArgs = $this->objectManager - ->getConstructArguments(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class); - $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class) + ->getConstructArguments(QuoteItemCollection::class); + $collection = $this->getMockBuilder(QuoteItemCollection::class) ->setMethods(['getSelect', 'getTable', 'getFlag', 'setFlag']) ->disableOriginalConstructor() ->setConstructorArgs($constructArgs) @@ -88,18 +96,18 @@ public function testLoadWithFilter() { /** @var MockObject $collection */ $constructArgs = $this->objectManager - ->getConstructArguments(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class); + ->getConstructArguments(QuoteItemCollection::class); $constructArgs['eventManager'] = $this->getMockForAbstractClass(ManagerInterface::class); - $connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); $resourceMock = $this->createMock(Quote::class); $resourceMock->expects($this->any())->method('getConnection') ->willReturn($this->createMock(Mysql::class)); $constructArgs['resource'] = $resourceMock; - $productResourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + $productResourceMock = $this->createMock(ProductCollection::class); $constructArgs['productResource'] = $productResourceMock; - $orderResourceMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); + $orderResourceMock = $this->createMock(OrderCollection::class); $constructArgs['orderResource'] = $orderResourceMock; - $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class) + $constructArgs['productDataRetriever'] = $this->productDataRetriever; + $collection = $this->getMockBuilder(QuoteItemCollection::class) ->setMethods( [ '_beforeLoad', @@ -129,24 +137,12 @@ public function testLoadWithFilter() //productLoad() $productAttributeMock = $this->createMock(AbstractAttribute::class); $priceAttributeMock = $this->createMock(AbstractAttribute::class); - $productResourceMock->expects($this->once())->method('getConnection')->willReturn($connectionMock); $productResourceMock->expects($this->any())->method('getAttribute') ->willReturnMap([['name', $productAttributeMock], ['price', $priceAttributeMock]]); - $productResourceMock->expects($this->once())->method('getSelect')->willReturn($this->selectMock); - $eavEntity = $this->createMock(AbstractEntity::class); - $eavEntity->expects($this->once())->method('getLinkField')->willReturn('entity_id'); - $productResourceMock->expects($this->once())->method('getEntity')->willReturn($eavEntity); - $this->selectMock->expects($this->once())->method('reset')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('useStraightJoin')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('joinInner')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('joinLeft')->willReturnSelf(); $collection->expects($this->once())->method('getOrdersData')->willReturn([]); - $productAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf(); - $priceAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf(); - $connectionMock->expects($this->once())->method('fetchAssoc')->willReturn([1, 2, 3]); //_afterLoad() $collection->expects($this->once())->method('getItems')->willReturn([]); + $this->productDataRetriever->expects($this->once())->method('execute')->willReturn([]); $collection->loadWithFilter(); } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php b/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php new file mode 100644 index 0000000000000..086078685ae94 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Model\Product; + +use Magento\Catalog\Model\Indexer\Product\Price\Processor; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea adminhtml + */ +class DataRetrieverTest extends TestCase +{ + /** + * @var DataRetriever + */ + private $dataRetriever; + + /** + * @var Processor + */ + private $priceIndexerProcessor; + + protected function setUp(): void + { + $this->dataRetriever = Bootstrap::getObjectManager()->create(DataRetriever::class); + $this->priceIndexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); + } + + /** + * Test retrieve products data for reports by entity id's + * Do not use magentoDbIsolation because index statement changing "tears" transaction (triggers creating) + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default/reports/options/enabled 1 + * @magentoDbIsolation disabled + * + * @return void + */ + public function testExecute(): void + { + $productId = 1; + $this->priceIndexerProcessor->reindexAll(); + $actualResult = $this->dataRetriever->execute([$productId]); + $this->assertNotEmpty($actualResult); + $this->assertCount(1, $actualResult); + $this->assertEquals(10, $actualResult[$productId]['price']); + } +} From 8f92a813b9132e75af4f54747959b44b6cfc9872 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 13 Oct 2020 10:55:53 +0300 Subject: [PATCH 0791/1013] MC-38269: [CLARIFICATION] [Magento Cloud] - Persistent Shopping cart Header Weelcome & Not you? --- .../Persistent/view/frontend/templates/additional.phtml | 7 ++----- .../view/frontend/web/js/view/additional-welcome.js | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml index 40c8674bc025a..0c19a1c5c8b05 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -4,13 +4,10 @@ * See COPYING.txt for license details. */ ?> -<?php if ($block->getCustomerId()) :?> - <span> - <a <?= /* @noEscape */ $block->getLinkAttributes()?>><?= $block->escapeHtml(__('Not you?'));?></a> - </span> -<?php endif;?> <script type="application/javascript"> window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; + window.notYou = '<span> <a <?= /* @noEscape */ $block->getLinkAttributes()?>>' + + '<?= $block->escapeHtml(__("Not you?"));?></a></span>'; </script> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js index 7ace6e60d1c39..fb57d311e35c3 100644 --- a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -40,6 +40,7 @@ define([ $(this).attr('data-bind', html); $(this).html(html); + $(this).after(window.notYou); }); } } From 2bee5bc62a08cba4a46100e5dcb2d1e3d6abbb67 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Tue, 13 Oct 2020 12:53:35 +0300 Subject: [PATCH 0792/1013] MC-38113: Same shipping address is repeating multiple times in storefront checkout when Reordered --- ...ckoutFillingShippingSectionActionGroup.xml | 2 +- ...ustomerHasNoOtherAddressesActionGroup.xml} | 4 +-- ...nStartReorderFromOrderPageActionGroup.xml} | 8 ++--- ...eorderAddressNotSavedInAddressBookTest.xml | 36 ++++++++++--------- 4 files changed, 25 insertions(+), 25 deletions(-) rename app/code/Magento/Customer/Test/Mftf/ActionGroup/{AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml => AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml} (79%) rename app/code/Magento/Sales/Test/Mftf/ActionGroup/{AdminReorderActionGroup.xml => AdminStartReorderFromOrderPageActionGroup.xml} (66%) diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml index e1092a87e4a01..d3127362c637e 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml @@ -29,7 +29,7 @@ <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> <waitForLoadingMaskToDisappear stepKey="waitForShippingLoadingMask"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> + <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml similarity index 79% rename from app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml index 2fde4d915c99f..8634ebb626e6d 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStoreFrontCustomerHasNoOtherAddressesActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml @@ -7,9 +7,9 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AssertStoreFrontCustomerHasNoOtherAddressesActionGroup"> + <actionGroup name="AssertStorefrontCustomerHasNoOtherAddressesActionGroup"> <annotations> - <description>Verifies customer no additional address in address book</description> + <description>Verifies customer has no additional address in address book</description> </annotations> <amOnPage url="customer/address/" stepKey="goToAddressPage"/> <waitForText userInput="You have no other address entries in your address book." selector=".block-addresses-list" stepKey="assertOtherAddresses"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminReorderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminStartReorderFromOrderPageActionGroup.xml similarity index 66% rename from app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminReorderActionGroup.xml rename to app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminStartReorderFromOrderPageActionGroup.xml index f4f076f25af8b..28a179faff9ac 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminReorderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminStartReorderFromOrderPageActionGroup.xml @@ -7,16 +7,14 @@ --> <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminReorderActionGroup"> + <actionGroup name="AdminStartReorderFromOrderPageActionGroup"> <annotations> <description>Reorder existing order. Requires admin order page to be opened.</description> </annotations> <click selector="{{AdminOrderDetailsMainActionsSection.reorder}}" stepKey="clickReorder"/> <waitForPageLoad stepKey="waitPageLoad"/> - - <click selector="{{AdminOrderFormActionSection.SubmitOrder}}" stepKey="clickSubmit"/> - <waitForPageLoad stepKey="waitOrderCreated"/> - <waitForText selector="{{AdminMessagesSection.success}}" userInput="You created the order." stepKey="seeOrderCreatedMessage"/> + <waitForElementVisible selector="{{AdminHeaderSection.pageTitle}}" stepKey="waitForPageTitle"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeCreateNewOrderPageTitle"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml index 2b4bb43ec36cd..1c3ab70857151 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderAddressNotSavedInAddressBookTest.xml @@ -9,36 +9,37 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminReorderAddressNotSavedInAddressBookTest"> <annotations> - <title value="Same shipping address is repeating multiple times in storefront checkout when Reordered"/> - <stories value="MC-38113: Same shipping address is repeating multiple times in storefront checkout when Reordered"/> - <description value="Same shipping address is repeating multiple times in storefront checkout when Reordered"/> <features value="Sales"/> - <testCaseId value="MC-38113"/> + <stories value="Reorder"/> + <title value="Same shipping address is not repeating multiple times in storefront checkout when Reordered"/> + <description value="Same shipping address is not repeating multiple times in storefront checkout when Reordered"/> + <testCaseId value="MC-38412"/> + <useCaseId value="MC-38113"/> <severity value="MAJOR"/> - <group value="Sales"/> + <group value="sales"/> </annotations> <before> - <createData entity="ApiCategory" stepKey="Category"/> - <createData entity="ApiSimpleProduct" stepKey="Product"> - <requiredEntity createDataKey="Category"/> + <createData entity="ApiCategory" stepKey="category"/> + <createData entity="ApiSimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> </createData> - <createData entity="Simple_Customer_Without_Address" stepKey="Customer"/> + <createData entity="Simple_Customer_Without_Address" stepKey="customer"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> - <argument name="Customer" value="$Customer$"/> + <argument name="Customer" value="$customer$"/> </actionGroup> </before> <after> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> - <deleteData createDataKey="Product" stepKey="deleteProduct"/> - <deleteData createDataKey="Category" stepKey="deleteCategory"/> - <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> </after> <!-- Create order for registered customer --> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProductToOrder"> - <argument name="product" value="$Product$"/> + <argument name="product" value="$product$"/> </actionGroup> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="openCheckoutPage"/> <actionGroup ref="LoggedInUserCheckoutFillingShippingSectionActionGroup" stepKey="fillAddressForm"/> @@ -47,11 +48,12 @@ <!-- Reorder created order --> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="openOrderById"> - <argument name="orderId" value="$grabOrderNumber"/> + <argument name="orderId" value="{$grabOrderNumber}"/> </actionGroup> - <actionGroup ref="AdminReorderActionGroup" stepKey="reorder"/> + <actionGroup ref="AdminStartReorderFromOrderPageActionGroup" stepKey="startReorder"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> <!-- Assert no additional addresses saved --> - <actionGroup ref="AssertStoreFrontCustomerHasNoOtherAddressesActionGroup" stepKey="assertAddresses"/> + <actionGroup ref="AssertStorefrontCustomerHasNoOtherAddressesActionGroup" stepKey="assertAddresses"/> </test> </tests> From 56d8eae58d9083552c4d48de39aab94a435b3580 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Tue, 13 Oct 2020 13:08:21 +0300 Subject: [PATCH 0793/1013] MC-38243: Create automated test for "Product Categories Indexer in Update on Schedule mode" --- ...inProductCategoryIndexerInUpdateOnScheduleModeTest.xml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index eebd3472cbd95..3008e89fd9dd1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -10,17 +10,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminProductCategoryIndexerInUpdateOnScheduleModeTest"> <annotations> + <features value="Catalog"/> <stories value="Product Categories Indexer"/> <title value="Product Categories Indexer in Update on Schedule mode"/> <description value="The test verifies that in Update on Schedule mode if displaying of category products on Storefront changes due to product properties change, the changes are NOT applied immediately, but applied only after cron runs (twice)."/> - <severity value="BLOCKER"/> - <testCaseId value="MC-11146"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-26119"/> <group value="catalog"/> <group value="indexer"/> - <skip> - <issueId value="MC-20392"/> - </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> From c72589d3132aa145ae98fbac2095d0b9b3db2965 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Tue, 13 Oct 2020 13:12:38 +0300 Subject: [PATCH 0794/1013] MC-37070: Create automated test for "Import products with shared images" --- .../Product/Gallery/UpdateHandlerTest.php | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index ce36b27d51e7d..1101b7291528b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -16,6 +16,7 @@ use Magento\Catalog\Model\ResourceModel\Product as ProductResource; use Magento\Catalog\Model\ResourceModel\Product\Gallery; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\ObjectManagerInterface; @@ -90,6 +91,9 @@ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase */ private $currentStoreId; + /** @var MetadataPool */ + private $metadataPool; + /** * @inheritdoc */ @@ -109,6 +113,7 @@ protected function setUp(): void $this->mediaDirectory = $this->objectManager->get(Filesystem::class) ->getDirectoryWrite(DirectoryList::MEDIA); $this->mediaDirectory->writeFile($this->fileName, 'Test'); + $this->metadataPool = $this->objectManager->get(MetadataPool::class); } /** @@ -534,28 +539,30 @@ private function duplicateMediaGalleryForProduct(string $imagePath, string $prod $product = $this->getProduct(null, $productSku); $connect = $this->galleryResource->getConnection(); $select = $connect->select()->from($this->galleryResource->getMainTable())->where('value = ?', $imagePath); - $res = $connect->fetchRow($select); - $value_id = $res['value_id']; - unset($res['value_id']); + $result = $connect->fetchRow($select); + $value_id = $result['value_id']; + unset($result['value_id']); $rows = [ - 'attribute_id' => $res['attribute_id'], - 'value' => $res['value'], - ProductAttributeMediaGalleryEntryInterface::MEDIA_TYPE => $res['media_type'], - ProductAttributeMediaGalleryEntryInterface::DISABLED => $res['disabled'], + 'attribute_id' => $result['attribute_id'], + 'value' => $result['value'], + ProductAttributeMediaGalleryEntryInterface::MEDIA_TYPE => $result['media_type'], + ProductAttributeMediaGalleryEntryInterface::DISABLED => $result['disabled'], ]; $connect->insert($this->galleryResource->getMainTable(), $rows); $select = $connect->select() ->from($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE)) ->where('value_id = ?', $value_id); - $res = $connect->fetchRow($select); + $result = $connect->fetchRow($select); $newValueId = (int)$value_id + 1; + $metadata = $this->metadataPool->getMetadata(ProductInterface::class); + $linkField = $metadata->getLinkField(); $rows = [ 'value_id' => $newValueId, - 'store_id' => $res['store_id'], - ProductAttributeMediaGalleryEntryInterface::LABEL => $res['label'], - ProductAttributeMediaGalleryEntryInterface::POSITION => $res['position'], - ProductAttributeMediaGalleryEntryInterface::DISABLED => $res['disabled'], - 'row_id' => $product->getRowId(), + 'store_id' => $result['store_id'], + ProductAttributeMediaGalleryEntryInterface::LABEL => $result['label'], + ProductAttributeMediaGalleryEntryInterface::POSITION => $result['position'], + ProductAttributeMediaGalleryEntryInterface::DISABLED => $result['disabled'], + $linkField => $product->getData($linkField), ]; $connect->insert($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE), $rows); $rows = ['value_id' => $newValueId, 'row_id' => $product->getRowId()]; From 12b60201014a997a72823b5cfeaac6ee126b3857 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Tue, 13 Oct 2020 13:38:50 +0300 Subject: [PATCH 0795/1013] MC-37558: Create automated test for "Override Category settings on Store View level" --- .../Controller/Adminhtml/Category/Save/UpdateCategoryTest.php | 2 +- .../testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php index c3d5ed080bcf2..75b96a1af3b09 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Category/Save/UpdateCategoryTest.php @@ -45,7 +45,7 @@ protected function setUp(): void * @param array $postData * @return void */ - public function testUpdateCategoryForDefaultStoreView($postData): void + public function testUpdateCategoryForDefaultStoreView(array $postData): void { $storeId = (int)$this->storeManager->getStore('default')->getId(); $postData = array_merge($postData, ['store_id' => $storeId]); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php index 6469f80ff49b8..e829801d60e1a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php @@ -24,6 +24,7 @@ * Provide tests for CategoryRepository model. * * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CategoryRepositoryTest extends TestCase { From 71b027dedf046771e334fd2a9cbf2b648e62201c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Tue, 13 Oct 2020 14:26:30 +0300 Subject: [PATCH 0796/1013] MC-29411: PHPStan: "Return typehint of method has invalid type " errors --- app/code/Magento/CatalogSearch/Model/Advanced.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index 5143762a07e08..ba58066cae917 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -359,6 +359,8 @@ protected function getPreparedSearchCriteria($attribute, $value) if (is_array($value)) { if (isset($value['from']) && isset($value['to'])) { if (!empty($value['from']) || !empty($value['to'])) { + $from = ''; + $to = ''; if (isset($value['currency'])) { /** @var $currencyModel Currency */ $currencyModel = $this->_currencyFactory->create()->load($value['currency']); From 101b711b00e64e35bdf4f41b2ac4a9ca043701ed Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 13 Oct 2020 15:07:18 +0300 Subject: [PATCH 0797/1013] MC-38315: Reorder is not working with custom options date after enabled JavaScript Calendar --- .../Model/Product/Option/Type/Date.php | 40 +++++++ .../Magento/Sales/Model/AdminOrder/Create.php | 21 +--- .../Adminhtml/Order/Create/ReorderTest.php | 108 ++++++++++++++++++ .../order_with_date_time_option_product.php | 97 ++++++++++++++++ ...with_date_time_option_product_rollback.php | 12 ++ 5 files changed, 262 insertions(+), 16 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 8001d692c011b..725635bf4fc45 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -72,6 +72,9 @@ public function validateUserValue($values) $dateValid = true; if ($this->_dateExists()) { if ($this->useCalendar()) { + if (is_array($value) && $this->checkDateWithoutJSCalendar($value)) { + $value['date'] = sprintf("%s/%s/%s", $value['day'], $value['month'], $value['year']); + } /* Fixed validation if the date was not saved correctly after re-saved the order for example: "09\/24\/2020,2020-09-24 00:00:00" */ if (is_string($value) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4},+(\w|\W)*$/', $value)) { @@ -81,6 +84,9 @@ public function validateUserValue($values) } $dateValid = isset($value['date']) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4}$/', $value['date']); } else { + if (is_array($value)) { + $value = $this->prepareDateByDateInternal($value); + } $dateValid = isset( $value['day'] ) && isset( @@ -411,4 +417,38 @@ protected function _timeExists() ] ); } + + /** + * Check is date without JS Calendar + * + * @param array $value + * + * @return bool + */ + private function checkDateWithoutJSCalendar(array $value): bool + { + return empty($value['date']) + && !empty($value['day']) + && !empty($value['month']) + && !empty($value['year']); + } + + /** + * Prepare date by date internal + * + * @param array $value + * @return array + */ + private function prepareDateByDateInternal(array $value): array + { + if (!empty($value['date']) && !empty($value['date_internal'])) { + $formatDate = explode(' ', $value['date_internal']); + $date = explode('-', $formatDate[0]); + $value['year'] = $date[0]; + $value['month'] = $date[1]; + $value['day'] = $date[2]; + } + + return $value; + } } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 393d61b69bf22..bbbe2bee1b205 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -667,12 +667,14 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q $productOptions = $orderItem->getProductOptions(); if ($productOptions !== null && !empty($productOptions['options'])) { $formattedOptions = []; - $useFrontendCalendar = $this->useFrontendCalendar(); foreach ($productOptions['options'] as $option) { - if (in_array($option['option_type'], ['date', 'date_time']) && $useFrontendCalendar) { + if (in_array($option['option_type'], ['date', 'date_time', 'time', 'file'])) { $product->setSkipCheckRequiredOption(false); - break; + $formattedOptions[$option['option_id']] = + $buyRequest->getDataByKey('options')[$option['option_id']]; + continue; } + $formattedOptions[$option['option_id']] = $option['option_value']; } if (!empty($formattedOptions)) { @@ -2123,17 +2125,4 @@ private function isAddressesAreEqual(Order $order) return $shippingData == $billingData; } - - /** - * Use Calendar on frontend or not - * - * @return bool - */ - private function useFrontendCalendar(): bool - { - return (bool)$this->_scopeConfig->getValue( - 'catalog/custom_options/use_calendar', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php new file mode 100644 index 0000000000000..af2cd504a1728 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -0,0 +1,108 @@ +<?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\Customer\Model\Session; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Escaper; +use Magento\Framework\Registry; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Core\Version\View; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Test for reorder controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder + * @magentoAppArea adminhtml + */ +class ReorderTest extends AbstractBackendController +{ + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CartInterface */ + private $quote; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); + } + + /** + * Reorder with JS calendar options + * + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @magentoDataFixture Magento/Sales/_files/order_with_date_time_option_product.php + * + * @return void + */ + public function testReorderAfterJSCalendarEnabled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('backend/sales/order_create')); + $this->quote = $this->getQuote('customer@example.com'); + $this->assertTrue(!empty($this->quote)); + } + + /** + * Dispatch reorder request. + * + * @param null|int $orderId + * @return void + */ + private function dispatchReorderRequest(?int $orderId = null): void + { + $this->getRequest()->setMethod(Request::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/reorder'); + } + + /** + * Gets quote by reserved order id. + * + * @return \Magento\Quote\Api\Data\CartInterface + */ + private function getQuote(string $customerEmail): \Magento\Quote\Api\Data\CartInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('customer_email', $customerEmail) + ->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/Sales/_files/order_with_date_time_option_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php new file mode 100644 index 0000000000000..23fbeb94d2004 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php @@ -0,0 +1,97 @@ +<?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.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'); + +/** @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' => [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; +$optionsDate = [ + [ + 'label' => 'date', + 'value' => 'Mar 5, 2020', + 'print_value' => 'Mar 5, 2020', + 'option_id' => '1', + 'option_type' => 'date', + 'option_value' => '2020-03-05 00:00:00', + 'custom_view' => '', + ] +]; + +$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->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions([ + 'info_buyRequest' => $requestInfo, + 'options' => $optionsDate, +]); + +/** @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@example.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->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php new file mode 100644 index 0000000000000..0966f21645e3b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php @@ -0,0 +1,12 @@ +<?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'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); From 3ffc2e809d87b45a07dbd3b439523fb30ff1636b Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Tue, 13 Oct 2020 15:17:09 +0300 Subject: [PATCH 0798/1013] MC-37070: Create automated test for "Import products with shared images" --- .../Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 1101b7291528b..c5221f1ae5e76 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -565,7 +565,7 @@ private function duplicateMediaGalleryForProduct(string $imagePath, string $prod $linkField => $product->getData($linkField), ]; $connect->insert($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TABLE), $rows); - $rows = ['value_id' => $newValueId, 'row_id' => $product->getRowId()]; + $rows = ['value_id' => $newValueId, $linkField => $product->getData($linkField)]; $connect->insert($this->galleryResource->getTable(Gallery::GALLERY_VALUE_TO_ENTITY_TABLE), $rows); } From e8ad9aee3fe307fce2e0c4b0cdd0661567170ae7 Mon Sep 17 00:00:00 2001 From: Solwininfotech <stdabhoya@yahoo.com> Date: Tue, 13 Oct 2020 17:51:54 +0530 Subject: [PATCH 0799/1013] removed .content and .fieldset selectors --- .../web/css/source/module/_cart.less | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 405bc1d2af373..2c8c52bdb7af2 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 @@ -727,16 +727,14 @@ position: static; } } - .content { - .fieldset { - .actions-toolbar { - width: auto; - } - } - } + &.discount { width: auto; } + + .actions-toolbar { + width: auto; + } } } From 80e33111c3325423ea2fbced8094313a34d87cbb Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Tue, 13 Oct 2020 15:51:36 +0300 Subject: [PATCH 0800/1013] MC-37546: Create automated test for "Create new Category Update" --- .../StorefrontCheckPresentSubCategoryActionGroup.xml | 2 +- .../AdminCategoryBasicFieldSection.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml index 7d8113f05518b..7cb3287614433 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml @@ -18,6 +18,6 @@ <waitForElement selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(parenCategoryName)}}" stepKey="waitForTopMenuLoaded"/> <moveMouseOver selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(parenCategoryName)}}" stepKey="moveMouseToParentCategory"/> - <seeElement selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(childCategoryName)}}" stepKey="seeCategoryUpdated"/> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(childCategoryName)}}" stepKey="seeSubcategoryInTree"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml index aff7ffe4d5763..1b041c5ca306f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryBasicFieldSection/AdminCategoryBasicFieldSection.xml @@ -22,5 +22,6 @@ <element name="FieldError" type="text" selector=".admin__field-error[data-bind='attr: {for: {{field}}}, text: error']" parameterized="true"/> <element name="panelFieldControl" type="input" selector="//aside//div[@data-index="{{arg1}}"]/descendant::*[@name="{{arg2}}"]" parameterized="true"/> <element name="productsInCategory" type="input" selector="div[data-index='assign_products']" timeout="30"/> + <element name="scheduleDesignUpdateTab" type="block" selector="div[data-index='schedule_design_update']" timeout="15"/> </section> </sections> From b0c27189e8e4930ee8dd2acd778c8a35dd4c9bf9 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 13 Oct 2020 16:07:33 +0300 Subject: [PATCH 0801/1013] MC-38269: [CLARIFICATION] [Magento Cloud] - Persistent Shopping cart Header Weelcome & Not you? --- .../view/frontend/templates/additional.phtml | 5 ++- .../web/js/view/additional-welcome.js | 2 +- .../Block/Header/AdditionalTest.php | 33 ++++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml index 0c19a1c5c8b05..61feeae04369d 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -5,9 +5,8 @@ */ ?> <script type="application/javascript"> - window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; - window.notYou = '<span> <a <?= /* @noEscape */ $block->getLinkAttributes()?>>' - + '<?= $block->escapeHtml(__("Not you?"));?></a></span>'; + window.persistent = <?=/* @noEscape */ $block->getConfig()?>; + window.notYouLink = '<?=/* @noEscape */ $block->getLinkAttributes()?>'; </script> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js index fb57d311e35c3..2f5c42f090d18 100644 --- a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -40,7 +40,7 @@ define([ $(this).attr('data-bind', html); $(this).html(html); - $(this).after(window.notYou); + $(this).after('<span><a ' + window.notYouLink + '>' + $t('Not you?') + '</a></span>'); }); } } diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php index c923a809441ba..42390f5303a94 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php @@ -6,41 +6,47 @@ namespace Magento\Persistent\Block\Header; +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Helper\Session as SessionHelper; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * @magentoDataFixture Magento/Persistent/_files/persistent.php */ -class AdditionalTest extends \PHPUnit\Framework\TestCase +class AdditionalTest extends TestCase { /** - * @var \Magento\Persistent\Block\Header\Additional + * @var Additional */ protected $_block; /** - * @var \Magento\Persistent\Helper\Session + * @var SessionHelper */ protected $_persistentSessionHelper; /** - * @var \Magento\Customer\Model\Session + * @var Session */ protected $_customerSession; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $_objectManager; protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->_objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Persistent\Helper\Session $persistentSessionHelper */ - $this->_persistentSessionHelper = $this->_objectManager->create(\Magento\Persistent\Helper\Session::class); + /** @var Session $persistentSessionHelper */ + $this->_persistentSessionHelper = $this->_objectManager->create(Session::class); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + $this->_customerSession = $this->_objectManager->get(Session::class); - $this->_block = $this->_objectManager->create(\Magento\Persistent\Block\Header\Additional::class); + $this->_block = $this->_objectManager->create(Additional::class); } /** @@ -54,12 +60,7 @@ protected function setUp(): void public function testToHtml() { $this->_customerSession->loginById(1); - $translation = __('Not you?'); - - $this->assertStringContainsString( - '<a href="' . $this->_block->getHref() . '">' . $translation . '</a>', - $this->_block->toHtml() - ); + $this->assertStringContainsString($this->_block->getHref(), $this->_block->toHtml()); $this->_customerSession->logout(); } } From 3b2ece2b3f203c67bc0117e74bf5513f26ed2f35 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Tue, 13 Oct 2020 17:09:57 +0300 Subject: [PATCH 0802/1013] MC-38113: Same shipping address is repeating multiple times in storefront checkout when Reordered --- ...oggedInUserCheckoutFillingShippingSectionActionGroup.xml | 6 +++--- ...sertStorefrontCustomerHasNoOtherAddressesActionGroup.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml index d3127362c637e..4b6680442a470 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/LoggedInUserCheckoutFillingShippingSectionActionGroup.xml @@ -24,10 +24,10 @@ <selectOption selector="{{CheckoutShippingSection.region}}" userInput="{{customerAddressVar.state}}" stepKey="selectRegion"/> <fillField selector="{{CheckoutShippingSection.postcode}}" userInput="{{customerAddressVar.postcode}}" stepKey="enterPostcode"/> <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{customerAddressVar.telephone}}" stepKey="enterTelephone"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> + <waitForPageLoad stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForShippingLoadingMask"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <waitForPageLoad stepKey="waitForShippingLoadingMask"/> <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> <waitForElementVisible selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml index 8634ebb626e6d..1f56ba505128f 100644 --- a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertStorefrontCustomerHasNoOtherAddressesActionGroup.xml @@ -11,7 +11,7 @@ <annotations> <description>Verifies customer has no additional address in address book</description> </annotations> - <amOnPage url="customer/address/" stepKey="goToAddressPage"/> + <amOnPage url="{{StorefrontCustomerAddressesPage.url}}" stepKey="goToAddressPage"/> <waitForText userInput="You have no other address entries in your address book." selector=".block-addresses-list" stepKey="assertOtherAddresses"/> </actionGroup> </actionGroups> From 4e5ac6783aacb9e78b6cdb095ab2297723899e8d Mon Sep 17 00:00:00 2001 From: DmytroPaidych <dimonovp@gmail.com> Date: Tue, 13 Oct 2020 16:15:30 +0200 Subject: [PATCH 0803/1013] MC-37896: Create automated test for "Reset Widget" --- .../AdminSaveAndContinueWidgetActionGroup.xml | 21 ++++++ .../AdminSetInputTypeAndDesignActionGroup.xml | 22 +++++++ .../AdminSetWidgetNameAndStoreActionGroup.xml | 24 +++++++ .../Widget/Test/Mftf/Data/WidgetsData.xml | 1 + .../Mftf/Section/AdminNewWidgetSection.xml | 4 +- .../Test/Mftf/Test/AdminResetWidgetTest.xml | 65 +++++++++++++++++++ 6 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml create mode 100644 app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetInputTypeAndDesignActionGroup.xml create mode 100644 app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml create mode 100644 app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml new file mode 100644 index 0000000000000..d480ea685736d --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml @@ -0,0 +1,21 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSaveAndContinueWidgetActionGroup"> + <annotations> + <description>Click on the Save an Continue button and check the success message</description> + </annotations> + <scrollToTopOfPage stepKey="scrollToTopOfPage"/> + <click selector="{{AdminNewWidgetSection.saveAndContinue}}" stepKey="clickSaveWidget"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForPageLoad"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> + </actionGroup> +</actionGroups> + + diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetInputTypeAndDesignActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetInputTypeAndDesignActionGroup.xml new file mode 100644 index 0000000000000..3071f60bbc9d6 --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetInputTypeAndDesignActionGroup.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="AdminSetInputTypeAndDesignActionGroup"> + <annotations> + <description>On the widget_instance page select widget type and design</description> + </annotations> + <arguments> + <argument name="widgetType" defaultValue="{{ProductsListWidget.type}}" type="string"/> + <argument name="widgetDesign" defaultValue="{{ProductsListWidget.design_theme}}" type="string"/> + </arguments> + <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widgetType}}" stepKey="setWidgetType"/> + <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widgetDesign}}" stepKey="setWidgetDesignTheme"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml new file mode 100644 index 0000000000000..80546b9d5e6df --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.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="AdminSetWidgetNameAndStoreActionGroup"> + <annotations> + <description>On the widget creation page page set widget name, store add sort order.</description> + </annotations> + <arguments> + <argument name="widgetName" defaultValue="{{ProductsListWidget.name}}" type="string"/> + <argument name="widgetStore" defaultValue="{{ProductsListWidget.store_ids}}" type="string"/> + <argument name="widgetSortOrder" defaultValue="{{ProductsListWidget.sort_order}}" type="string"/> + </arguments> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widgetName}}" stepKey="fillTitle"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" parameterArray="[{{widgetStore}}]" stepKey="setWidgetStoreId"/> + <fillField selector="{{AdminNewWidgetSection.widgetSortOrder}}" userInput="{{widgetSortOrder}}" stepKey="fillSortOrder"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml index ffd2d025548cf..0bac99731ef7b 100644 --- a/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml +++ b/app/code/Magento/Widget/Test/Mftf/Data/WidgetsData.xml @@ -12,6 +12,7 @@ <data key="type">Catalog Products List</data> <data key="design_theme">Magento Luma</data> <data key="name" unique="suffix">TestWidget</data> + <data key="sort_order">0</data> <array key="store_ids"> <item>All Store Views</item> </array> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index c1ff351823823..2e455f4a3470b 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -12,6 +12,7 @@ <element name="widgetType" type="select" selector="#code"/> <element name="widgetDesignTheme" type="select" selector="#theme_id"/> <element name="continue" type="button" timeout="30" selector="#continue_button"/> + <element name="resetBtn" type="button" selector="#reset" timeout="30"/> <element name="widgetTitle" type="input" selector="#title"/> <element name="widgetStoreIds" type="select" selector="#store_ids"/> <element name="widgetSortOrder" type="input" selector="#sort_order"/> @@ -38,10 +39,11 @@ <element name="searchBlock" type="button" selector="//div[@class='admin__filter-actions']/button[@title='Search']"/> <element name="blockStatus" type="select" selector="//select[@name='chooser_is_active']"/> <element name="searchedBlock" type="button" selector="//*[@class='magento-message']//tbody/tr/td[1]"/> - <element name="saveWidget" type="select" selector="#save"/> + <element name="saveWidget" type="button" selector="#save"/> <element name="displayMode" type="select" selector="select[id*='display_mode']"/> <element name="restrictTypes" type="select" selector="select[id*='types']"/> <element name="saveAndContinue" type="button" selector="#save_and_edit_button" timeout="30"/> + <element name="widgetInstanceType" type="select" selector="#instance_code" /> <!-- Catalog Product List Widget Options --> <element name="title" type="input" selector="[name='parameters[title]']"/> <element name="displayPageControl" type="select" selector="[name='parameters[show_pager]']"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml new file mode 100644 index 0000000000000..3a94871d06e1c --- /dev/null +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.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="AdminResetWidgetTest"> + <annotations> + <features value="Widget"/> + <stories value="Reset widget"/> + <title value="[CMS Widgets] Reset Widget"/> + <description value="Check that admin user can reset widget form after filling out all information"/> + <severity value="MAJOR"/> + <testCaseId value="MC-37892"/> + <group value="widget"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteWidget"> + <argument name="widget" value="ProductsListWidget"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="amOnAdminNewWidgetPage"/> + <actionGroup ref="AdminSetInputTypeAndDesignActionGroup" stepKey="firstSetTypeAndDesign"> + <argument name="widgetType" value="{{ProductsListWidget.type}}"/> + <argument name="widgetDesign" value="{{ProductsListWidget.design_theme}}"/> + </actionGroup> + <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetInstance"/> + <dontSeeInField userInput="{{ProductsListWidget.type}}" selector="{{AdminNewWidgetSection.widgetType}}" stepKey="dontSeeTypeAfterReset"/> + <dontSeeInField userInput="{{ProductsListWidget.design_theme}}" selector="{{AdminNewWidgetSection.widgetDesignTheme}}" stepKey="dontSeeDesignAfterReset"/> + <actionGroup ref="AdminSetInputTypeAndDesignActionGroup" stepKey="setTypeAndDesignAfterReset"> + <argument name="widgetType" value="{{ProductsListWidget.type}}"/> + <argument name="widgetDesign" value="{{ProductsListWidget.design_theme}}"/> + </actionGroup> + <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> + <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStore"> + <argument name="widgetName" value="{{ProductsListWidget.name}}"/> + <argument name="widgetStore" value="{{ProductsListWidget.store_ids}}"/> + <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> + </actionGroup> + <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetNameAndStore"/> + <dontSeeInField userInput="{{ProductsListWidget.name}}" selector="{{AdminNewWidgetSection.widgetTitle}}" stepKey="dontSeeNameAfterReset"/> + <dontSeeInField userInput="{{ProductsListWidget.store_ids[0]}}" selector="{{AdminNewWidgetSection.widgetStoreIds}}" stepKey="dontSeeStoreAfterReset"/> + <dontSeeInField userInput="{{ProductsListWidget.sort_order}}" selector="{{AdminNewWidgetSection.widgetSortOrder}}" stepKey="dontSeeSortOrderAfterReset"/> + <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStoreAfterReset"> + <argument name="widgetName" value="{{ProductsListWidget.name}}"/> + <argument name="widgetStore" value="{{ProductsListWidget.store_ids}}"/> + <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> + </actionGroup> + <actionGroup ref="AdminSaveAndContinueWidgetActionGroup" stepKey="saveWidget"/> + <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetWidget"/> + <seeInField userInput="{{ProductsListWidget.name}}" selector="{{AdminNewWidgetSection.widgetTitle}}" stepKey="seeNameAfterReset"/> + <seeInField userInput="{{ProductsListWidget.store_ids[0]}}" selector="{{AdminNewWidgetSection.widgetStoreIds}}" stepKey="seeStoreAfterReset"/> + <seeInField userInput="{{ProductsListWidget.sort_order}}" selector="{{AdminNewWidgetSection.widgetSortOrder}}" stepKey="seeSortOrderAfterReset"/> + <seeInField userInput="{{ProductsListWidget.type}}" selector="{{AdminNewWidgetSection.widgetInstanceType}}" stepKey="seeTypeAfterReset"/> + <seeInField userInput="{{ProductsListWidget.design_theme}}" selector="{{AdminNewWidgetSection.widgetDesignTheme}}" stepKey="seeThemeAfterReset"/> + </test> +</tests> From fceacabac975f59d29ef75271c4ad638a349810c Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Tue, 13 Oct 2020 17:31:58 +0300 Subject: [PATCH 0804/1013] MC-36960: Create automated test for "Create product for "all" store views using API service" --- .../ProductRepositoryAllStoreViewsTest.php | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php new file mode 100644 index 0000000000000..1d3b4ca591c08 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php @@ -0,0 +1,236 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Api; + +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Website\Link; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Tests for products creation for all store views. + * + * @magentoAppIsolation enabled + */ +class ProductRepositoryAllStoreViewsTest extends WebapiAbstract +{ + const PRODUCT_SERVICE_NAME = 'catalogProductRepositoryV1'; + const SERVICE_VERSION = 'V1'; + const PRODUCTS_RESOURCE_PATH = '/V1/products'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var Registry + */ + private $registry; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var string + */ + private $productSku = 'simple'; + + /** + * @var Link + */ + private $productWebsiteLink; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->registry = $this->objectManager->get(Registry::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->productWebsiteLink = $this->objectManager->get(Link::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->productRepository->delete( + $this->productRepository->get($this->productSku) + ); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testCreateProduct(): void + { + $productData = $this->getProductData(); + $resultData = $this->saveProduct($productData); + $this->assertProductWebsites($this->productSku, $this->getAllWebsiteIds()); + $this->assertProductData($productData, $resultData, $this->getAllWebsiteIds()); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + */ + public function testCreateProductOnMultipleWebsites(): void + { + $productData = $this->getProductData(); + $resultData = $this->saveProduct($productData); + $this->assertProductWebsites($this->productSku, $this->getAllWebsiteIds()); + $this->assertProductData($productData, $resultData, $this->getAllWebsiteIds()); + } + + /** + * Saves Product via API. + * + * @param $product + * @return array + */ + private function saveProduct($product): array + { + $serviceInfo = [ + 'rest' => ['resourcePath' =>self::PRODUCTS_RESOURCE_PATH, 'httpMethod' => Request::HTTP_METHOD_POST], + 'soap' => [ + 'service' => self::PRODUCT_SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::PRODUCT_SERVICE_NAME . 'Save' + ] + ]; + $requestData = ['product' => $product]; + return $this->_webApiCall($serviceInfo, $requestData, null, 'all'); + } + + /** + * Returns product data. + * + * @return array + */ + private function getProductData(): array + { + return [ + 'sku' => $this->productSku, + 'name' => 'simple', + 'type_id' => Type::TYPE_SIMPLE, + 'weight' => 1, + 'attribute_set_id' => 4, + 'price' => 10, + 'status' => 1, + 'visibility' => 4, + 'extension_attributes' => [ + 'stock_item' => ['is_in_stock' => true, 'qty' => 1000] + ], + 'custom_attributes' => [ + ['attribute_code' => 'url_key', 'value' => 'simple'], + ['attribute_code' => 'tax_class_id', 'value' => 2], + ['attribute_code' => 'category_ids', 'value' => [333]] + ] + ]; + } + + /** + * Asserts that product is linked to websites in 'catalog_product_website' table. + * + * @param string $sku + * @param array $websiteIds + * @return void + */ + private function assertProductWebsites(string $sku, array $websiteIds): void + { + $productId = $this->productRepository->get($sku)->getId(); + $this->assertEquals($websiteIds, $this->productWebsiteLink->getWebsiteIdsByProductId($productId)); + } + + /** + * Asserts result after product creation. + * + * @param array $productData + * @param array $resultData + * @param array $websiteIds + * @return void + */ + private function assertProductData(array $productData, array $resultData, array $websiteIds): void + { + foreach ($productData as $key => $value) { + if ($key == 'extension_attributes' || $key == 'custom_attributes') { + continue; + } + $this->assertEquals($value, $resultData[$key]); + } + foreach ($productData['custom_attributes'] as $attribute) { + $resultAttribute = $this->getCustomAttributeByCode( + $resultData['custom_attributes'], + $attribute['attribute_code'] + ); + $this->assertEquals($attribute['value'], $resultAttribute['value']); + } + foreach ($productData['extension_attributes']['stock_item'] as $key => $value) { + $this->assertEquals($value, $resultData['extension_attributes']['stock_item'][$key]); + } + $this->assertEquals($websiteIds, $resultData['extension_attributes']['website_ids']); + } + + /** + * Get list of all websites IDs. + * + * @return array + */ + private function getAllWebsiteIds(): array + { + $websiteIds = []; + $websites = $this->storeManager->getWebsites(); + foreach ($websites as $website) { + $websiteIds[] = $website->getId(); + } + + return $websiteIds; + } + + /** + * Returns custom attribute data by given code. + * + * @param array $attributes + * @param string $attributeCode + * @return array + */ + private function getCustomAttributeByCode(array $attributes, string $attributeCode): array + { + $items = array_filter( + $attributes, + function ($attribute) use ($attributeCode) { + return $attribute['attribute_code'] == $attributeCode; + } + ); + + return reset($items); + } +} From 4c1243c56ba28eb8e68a111b34115a91993066f4 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Tue, 13 Oct 2020 17:43:50 +0300 Subject: [PATCH 0805/1013] MC-36960: Create automated test for "Create product for "all" store views using API service" --- .../ProductRepositoryAllStoreViewsTest.php | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php index 1d3b4ca591c08..2950dda4b3c52 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php @@ -7,8 +7,12 @@ namespace Magento\Catalog\Api; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ResourceModel\Product\Website\Link; +use Magento\Eav\Model\Config; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\Webapi\Rest\Request; @@ -48,14 +52,18 @@ class ProductRepositoryAllStoreViewsTest extends WebapiAbstract private $storeManager; /** - * @var string + * @var Link */ - private $productSku = 'simple'; + private $productWebsiteLink; + /** + * @var Config + */ + private $eavConfig; /** - * @var Link + * @var string */ - private $productWebsiteLink; + private $productSku = 'simple'; /** * @inheritdoc @@ -66,6 +74,7 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $this->productRepository->cleanCache(); + $this->eavConfig = $this->objectManager->get(Config::class); $this->registry = $this->objectManager->get(Registry::class); $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->productWebsiteLink = $this->objectManager->get(Link::class); @@ -137,15 +146,18 @@ private function saveProduct($product): array */ private function getProductData(): array { + $setId =(int)$this->eavConfig->getEntityType(ProductAttributeInterface::ENTITY_TYPE_CODE) + ->getDefaultAttributeSetId(); + return [ 'sku' => $this->productSku, 'name' => 'simple', 'type_id' => Type::TYPE_SIMPLE, 'weight' => 1, - 'attribute_set_id' => 4, + 'attribute_set_id' => $setId, 'price' => 10, - 'status' => 1, - 'visibility' => 4, + 'status' => Status::STATUS_ENABLED, + 'visibility' => Visibility::VISIBILITY_BOTH, 'extension_attributes' => [ 'stock_item' => ['is_in_stock' => true, 'qty' => 1000] ], From 1c2d999aa225cb0f06c80ca34df6b0eb561e68eb Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Tue, 13 Oct 2020 18:17:17 +0300 Subject: [PATCH 0806/1013] MC-29405: PHPStan: "Class does not have a constructor and must be instantiated without any parameters" errors --- .../Attribute/Backend/BillingTest.php | 5 +--- .../Attribute/Backend/ShippingTest.php | 5 +--- .../Design/Config/Edit/SaveButtonTest.php | 23 +------------------ .../Test/Unit/Topology/QueueInstallerTest.php | 3 +-- .../Module/Dependency/ServiceLocator.php | 2 +- 5 files changed, 5 insertions(+), 33 deletions(-) diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php index cd7154de14858..4f318948097cc 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php @@ -23,10 +23,7 @@ class BillingTest extends TestCase protected function setUp(): void { - $logger = $this->getMockBuilder(LoggerInterface::class) - ->getMock(); - /** @var LoggerInterface $logger */ - $this->testable = new Billing($logger); + $this->testable = new Billing(); } public function testBeforeSave() diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php index 3947a01582313..6270905ca2e85 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php @@ -23,10 +23,7 @@ class ShippingTest extends TestCase protected function setUp(): void { - $logger = $this->getMockBuilder(LoggerInterface::class) - ->getMock(); - /** @var LoggerInterface $logger */ - $this->testable = new Shipping($logger); + $this->testable = new Shipping(); } public function testBeforeSave() diff --git a/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php b/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php index 65e2b934741ee..0ce450df39b59 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php @@ -20,16 +20,9 @@ class SaveButtonTest extends TestCase */ protected $block; - /** - * @var Context|MockObject - */ - protected $context; - protected function setUp(): void { - $this->initContext(); - - $this->block = new SaveButton($this->context); + $this->block = new SaveButton(); } public function testGetButtonData() @@ -41,18 +34,4 @@ public function testGetButtonData() $this->assertArrayHasKey('data_attribute', $result); $this->assertIsArray($result['data_attribute']); } - - protected function initContext() - { - $this->urlBuilder = $this->getMockBuilder(UrlInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->context = $this->getMockBuilder(Context::class) - ->disableOriginalConstructor() - ->getMock(); - $this->context->expects($this->any()) - ->method('getUrlBuilder') - ->willReturn($this->urlBuilder); - } } diff --git a/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php index 7493d1691699a..44d33362ae26e 100644 --- a/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php +++ b/lib/internal/Magento/Framework/Amqp/Test/Unit/Topology/QueueInstallerTest.php @@ -16,8 +16,7 @@ class QueueInstallerTest extends TestCase { public function testInstall() { - $bindingInstaller = $this->getMockForAbstractClass(QueueConfigItemInterface::class); - $model = new QueueInstaller($bindingInstaller); + $model = new QueueInstaller(); $channel = $this->createMock(AMQPChannel::class); $queue = $this->getMockForAbstractClass(QueueConfigItemInterface::class); diff --git a/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php b/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php index 27f0c7e8e616f..d162d07b38cf8 100644 --- a/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php +++ b/setup/src/Magento/Setup/Module/Dependency/ServiceLocator.php @@ -95,7 +95,7 @@ public static function getCircularDependenciesReportBuilder() self::$circularDependenciesReportBuilder = new CircularReport\Builder( self::getComposerJsonParser(), new CircularReport\Writer(self::getCsvWriter()), - new CircularTool([], null) + new CircularTool() ); } return self::$circularDependenciesReportBuilder; From 19da476f971c476909713ac6425f454b7ac4cc79 Mon Sep 17 00:00:00 2001 From: DmytroPaidych <dimonovp@gmail.com> Date: Tue, 13 Oct 2020 17:21:26 +0200 Subject: [PATCH 0807/1013] MC-37896: Create automated test for "Reset Widget" --- .../ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml | 2 -- .../ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml | 4 ++-- .../Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml index d480ea685736d..cd9774f3b13ba 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml @@ -17,5 +17,3 @@ <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> </actionGroup> </actionGroups> - - diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml index 80546b9d5e6df..f1faadb1e434e 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml @@ -14,11 +14,11 @@ </annotations> <arguments> <argument name="widgetName" defaultValue="{{ProductsListWidget.name}}" type="string"/> - <argument name="widgetStore" defaultValue="{{ProductsListWidget.store_ids}}" type="string"/> + <argument name="widgetStoreIds" defaultValue="{{ProductsListWidget.store_ids}}" type="string"/> <argument name="widgetSortOrder" defaultValue="{{ProductsListWidget.sort_order}}" type="string"/> </arguments> <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widgetName}}" stepKey="fillTitle"/> - <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" parameterArray="[{{widgetStore}}]" stepKey="setWidgetStoreId"/> + <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" parameterArray="{{widgetStoreIds}}" stepKey="setWidgetStoreId"/> <fillField selector="{{AdminNewWidgetSection.widgetSortOrder}}" userInput="{{widgetSortOrder}}" stepKey="fillSortOrder"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml index 3a94871d06e1c..88610d9143bb4 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml @@ -42,7 +42,7 @@ <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStore"> <argument name="widgetName" value="{{ProductsListWidget.name}}"/> - <argument name="widgetStore" value="{{ProductsListWidget.store_ids}}"/> + <argument name="widgetStoreIds" value="{{ProductsListWidget.store_ids}}"/> <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> </actionGroup> <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetNameAndStore"/> @@ -51,7 +51,7 @@ <dontSeeInField userInput="{{ProductsListWidget.sort_order}}" selector="{{AdminNewWidgetSection.widgetSortOrder}}" stepKey="dontSeeSortOrderAfterReset"/> <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStoreAfterReset"> <argument name="widgetName" value="{{ProductsListWidget.name}}"/> - <argument name="widgetStore" value="{{ProductsListWidget.store_ids}}"/> + <argument name="widgetStoreIds" value="{{ProductsListWidget.store_ids}}"/> <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> </actionGroup> <actionGroup ref="AdminSaveAndContinueWidgetActionGroup" stepKey="saveWidget"/> From 6442ec3100fe3b3f420602821e95e68cbb22aaae Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Tue, 13 Oct 2020 21:08:22 +0300 Subject: [PATCH 0808/1013] MC-29414: PHPStan: "Method invoked with X parameters, Y required" errors --- .../Model/Widget/Grid/AbstractTotals.php | 8 ++--- .../Backend/Test/Unit/Helper/DataTest.php | 1 - .../Catalog/Block/Product/View/Details.php | 3 +- .../Model/ResourceModel/Category/Tree.php | 9 +++++- .../Model/ResourceModel/Product/Action.php | 2 +- .../Product/Attribute/RepositoryTest.php | 11 +------ .../Product/Option/Validator/SelectTest.php | 2 +- .../Product/Price/PricePersistenceTest.php | 2 +- .../Model/Quote/Item/QuantityValidator.php | 2 +- .../Model/Plugin/AfterProductLoadTest.php | 16 +--------- .../Observer/ProcessCronQueueObserverTest.php | 4 ++- app/code/Magento/Customer/Model/Address.php | 4 +-- .../Customer/Model/ResourceModel/Customer.php | 2 +- .../Adminhtml/Index/ViewfileTest.php | 6 +--- .../Model/Product/TypeHandler/SampleTest.php | 2 +- .../Unit/Model/Entity/AttributeLoaderTest.php | 6 ++-- .../Widget/Grid/Column/Renderer/Link.php | 7 ++-- .../Integration/Model/Oauth/Consumer.php | 26 +++++++-------- .../Magento/Integration/Model/Oauth/Token.php | 24 +++++++------- .../Unit/Controller/Partners/IndexTest.php | 6 ++-- .../Test/Unit/Model/PartnersTest.php | 6 ++-- .../Model/Cron/ReportNewRelicCronTest.php | 11 +------ .../Subscriber/Grid/Collection.php | 3 +- .../Product/Gallery/RetrieveImageTest.php | 32 +++---------------- app/code/Magento/Quote/Model/QuoteIdMask.php | 2 +- .../Model/ResourceModel/Order/Collection.php | 2 +- .../Magento/Review/Block/Adminhtml/Grid.php | 15 ++++----- app/code/Magento/Sales/Model/Order.php | 16 +++++----- .../Controller/Adminhtml/Order/ViewTest.php | 5 ++- .../Sales/Test/Unit/Helper/DataTest.php | 27 +++------------- .../Swatches/Test/Unit/Helper/DataTest.php | 28 +++------------- .../Unit/Model/SwatchAttributeCodesTest.php | 9 ++---- .../System/Design/Theme/Edit/Tab/Css.php | 6 ++-- .../Design/Config/Collection.php | 7 ++-- .../System/Design/Theme/SaveTest.php | 2 +- .../frontend/templates/html/sections.phtml | 4 +-- .../Adapter/Rest/DocumentationGenerator.php | 2 +- .../Framework/Cache/Backend/MongoDbTest.php | 2 +- .../Magento/Framework/Session/ConfigTest.php | 10 +++--- .../Magento/Test/Legacy/ObsoleteCodeTest.php | 4 ++- .../Magento/Framework/Backup/Filesystem.php | 6 +++- lib/internal/Magento/Framework/DB/Tree.php | 12 +++++++ .../Indexer/Test/Unit/IndexStructureTest.php | 2 +- .../Test/Unit/TypeProcessorTest.php | 6 ++-- .../TestFramework/Unit/Block/Adminhtml.php | 11 ++++--- lib/internal/Magento/Framework/Url.php | 2 +- .../Magento/Framework/View/Context.php | 7 ++-- .../View/Test/Unit/Asset/ConfigTest.php | 10 +----- .../Message/MessageConfigurationsPoolTest.php | 12 +++---- .../Test/Unit/Model/ModuleUninstallerTest.php | 12 +------ .../Di/App/Task/OperationFactoryTest.php | 3 +- 51 files changed, 160 insertions(+), 259 deletions(-) diff --git a/app/code/Magento/Backend/Model/Widget/Grid/AbstractTotals.php b/app/code/Magento/Backend/Model/Widget/Grid/AbstractTotals.php index bb68853dcc08d..dd92661eb9452 100644 --- a/app/code/Magento/Backend/Model/Widget/Grid/AbstractTotals.php +++ b/app/code/Magento/Backend/Model/Widget/Grid/AbstractTotals.php @@ -117,7 +117,7 @@ protected function _countExpr($expr, $collection) foreach ($parsedExpression as $operand) { if ($this->_parser->isOperation($operand)) { $this->_checkOperandsSet($firstOperand, $secondOperand, $tmpResult, $result); - $result = $this->_operate($firstOperand, $secondOperand, $operand, $tmpResult, $result); + $result = $this->_operate($firstOperand, $secondOperand, $operand); $firstOperand = $secondOperand = null; } else { if (null === $firstOperand) { @@ -133,9 +133,9 @@ protected function _countExpr($expr, $collection) /** * Check if operands in not null and set operands values if they are empty * - * @param float|int &$firstOperand - * @param float|int &$secondOperand - * @param float|int &$tmpResult + * @param float|int $firstOperand + * @param float|int $secondOperand + * @param float|int $tmpResult * @param float|int $result * @return void */ diff --git a/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php b/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php index cfeed42b11ba1..58fec039a90fe 100644 --- a/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Backend/Test/Unit/Helper/DataTest.php @@ -42,7 +42,6 @@ protected function setUp(): void $this->createMock(Auth::class), $this->_frontResolverMock, $this->createMock(Random::class), - $this->getMockForAbstractClass(RequestInterface::class) ); } diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php index 67303d177e71e..8a569c268dd1d 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Details.php +++ b/app/code/Magento/Catalog/Block/Product/View/Details.php @@ -27,10 +27,11 @@ class Details extends \Magento\Framework\View\Element\Template * * @return array * @since 103.0.1 + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getGroupSortedChildNames(string $groupName, string $callback): array { - $groupChildNames = $this->getGroupChildNames($groupName, $callback); + $groupChildNames = $this->getGroupChildNames($groupName); $layout = $this->getLayout(); $childNamesSortOrder = []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php index 972f11db7aae3..bcfa6ba3f8d0c 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Tree.php @@ -24,6 +24,11 @@ class Tree extends Dbp const LEVEL_FIELD = 'level'; + /** + * @var array + */ + private $_inactiveItems; + /** * @var \Magento\Framework\Event\ManagerInterface */ @@ -290,7 +295,7 @@ protected function _getDisabledIds($collection, $allIds) foreach ($allIds as $id) { $parents = $this->getNodeById($id)->getPath(); foreach ($parents as $parent) { - if (!$this->_getItemIsActive($parent->getId(), $storeId)) { + if (!$this->_getItemIsActive($parent->getId())) { $disabledIds[] = $id; continue; } @@ -680,6 +685,8 @@ public function getExistingCategoryIdsBySpecifiedIds($ids) } /** + * Get entity methadata pool. + * * @return \Magento\Framework\EntityManager\MetadataPool */ private function getMetadataPool() diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Action.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Action.php index 71ab9413a0d09..85f6269f63af0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Action.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Action.php @@ -160,7 +160,7 @@ protected function _saveAttributeValue($object, $attribute, $value) $storeId = (int) $this->_storeManager->getStore($object->getStoreId())->getId(); $table = $attribute->getBackend()->getTable(); - $entityId = $this->resolveEntityId($object->getId(), $table); + $entityId = $this->resolveEntityId($object->getId()); /** * If we work in single store mode all values should be saved just diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php index 0c2e5f361413e..673e12a5b42b5 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/RepositoryTest.php @@ -83,11 +83,6 @@ class RepositoryTest extends TestCase */ protected $searchResultMock; - /** - * @var AttributeOptionManagementInterface|MockObject - */ - private $optionManagementMock; - /** * @inheritdoc */ @@ -116,8 +111,6 @@ protected function setUp(): void ['getItems', 'getSearchCriteria', 'getTotalCount', 'setItems', 'setSearchCriteria', 'setTotalCount'] ) ->getMockForAbstractClass(); - $this->optionManagementMock = - $this->getMockForAbstractClass(ProductAttributeOptionManagementInterface::class); $this->model = new Repository( $this->attributeResourceMock, @@ -126,8 +119,7 @@ protected function setUp(): void $this->eavAttributeRepositoryMock, $this->eavConfigMock, $this->validatorFactoryMock, - $this->searchCriteriaBuilderMock, - $this->optionManagementMock + $this->searchCriteriaBuilderMock ); } @@ -291,7 +283,6 @@ public function testSaveDoesNotSaveAttributeOptionsIfOptionsAreAbsentInPayload() // Attribute code must not be changed after attribute creation $attributeMock->expects($this->once())->method('setAttributeCode')->with($attributeCode); $this->attributeResourceMock->expects($this->once())->method('save')->with($attributeMock); - $this->optionManagementMock->expects($this->never())->method('add'); $this->model->save($attributeMock); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php index 11412324b8363..d10f4931a928f 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Validator/SelectTest.php @@ -66,7 +66,7 @@ protected function setUp(): void ]; $configMock->expects($this->once())->method('getAll')->willReturn($config); $methods = ['getTitle', 'getType', 'getPriceType', 'getPrice', 'getData']; - $this->valueMock = $this->createPartialMock(Option::class, $methods, []); + $this->valueMock = $this->createPartialMock(Option::class, $methods); $this->validator = new Select( $configMock, $priceConfigMock, diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/PricePersistenceTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/PricePersistenceTest.php index cdd5f4d91b653..6f70f7973c10c 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/PricePersistenceTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Price/PricePersistenceTest.php @@ -301,7 +301,7 @@ public function testDeleteWithException() ->willReturn($idsBySku); $this->attributeRepository->expects($this->once())->method('get')->willReturn($this->productAttribute); $this->productAttribute->expects($this->once())->method('getAttributeId')->willReturn($attributeId); - $this->attributeResource->expects($this->atLeastOnce(2))->method('getConnection') + $this->attributeResource->expects($this->atLeastOnce())->method('getConnection') ->willReturn($this->connection); $this->connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); $this->attributeResource diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index 2ccb726f2c625..317a573a653e9 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -193,7 +193,7 @@ public function validate(Observer $observer) $option->setHasError(true); //Setting this to false, so no error statuses are cleared $removeError = false; - $this->addErrorInfoToQuote($result, $quoteItem, $removeError); + $this->addErrorInfoToQuote($result, $quoteItem); } } if ($removeError) { diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php index d0e34c89b897c..215b31e1b4dad 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/Plugin/AfterProductLoadTest.php @@ -30,11 +30,6 @@ class AfterProductLoadTest extends TestCase */ protected $productMock; - /** - * @var ProductExtensionFactory|MockObject - */ - protected $productExtensionFactoryMock; - /** * @var ProductExtensionInterface|MockObject */ @@ -43,16 +38,9 @@ class AfterProductLoadTest extends TestCase protected function setUp(): void { $stockRegistryMock = $this->getMockForAbstractClass(StockRegistryInterface::class); - $this->productExtensionFactoryMock = $this->getMockBuilder( - ProductExtensionFactory::class - ) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); $this->plugin = new AfterProductLoad( - $stockRegistryMock, - $this->productExtensionFactoryMock + $stockRegistryMock ); $productId = 5494; @@ -88,8 +76,6 @@ public function testAfterLoad() $this->productMock->expects($this->once()) ->method('getExtensionAttributes') ->willReturn($this->productExtensionMock); - $this->productExtensionFactoryMock->expects($this->never()) - ->method('create'); $this->assertEquals( $this->productMock, diff --git a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php index e1e28ff6f06a3..a48a3dd76b884 100644 --- a/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php +++ b/app/code/Magento/Cron/Test/Unit/Observer/ProcessCronQueueObserverTest.php @@ -679,6 +679,8 @@ public function dispatchExceptionInCallbackDataProvider() /** * Test case, successfully run job + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testDispatchRunJob() { @@ -764,7 +766,7 @@ function ($callback) { ->method('getCollection')->willReturn($this->scheduleCollectionMock); $scheduleMock->expects($this->any()) ->method('getResource')->willReturn($this->scheduleResourceMock); - $this->scheduleFactoryMock->expects($this->once(2)) + $this->scheduleFactoryMock->expects($this->once()) ->method('create')->willReturn($scheduleMock); $testCronJob = $this->getMockBuilder('CronJob') diff --git a/app/code/Magento/Customer/Model/Address.php b/app/code/Magento/Customer/Model/Address.php index 241abbb06f8a1..c89821d5c984a 100644 --- a/app/code/Magento/Customer/Model/Address.php +++ b/app/code/Magento/Customer/Model/Address.php @@ -15,8 +15,8 @@ * Customer address model * * @api - * @method int getParentId() getParentId() - * @method \Magento\Customer\Model\Address setParentId() setParentId(int $parentId) + * @method int getParentId() + * @method \Magento\Customer\Model\Address setParentId(int $parentId) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 */ diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index 1477287f79f4b..6ebd6b9410a0c 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -85,7 +85,7 @@ public function __construct( $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() ->get(AccountConfirmation::class); $this->setType('customer'); - $this->setConnection('customer_read', 'customer_write'); + $this->setConnection('customer_read'); $this->storeManager = $storeManager; } diff --git a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php index 8cb5957cbd672..ea8a37127e85d 100644 --- a/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php +++ b/app/code/Magento/Customer/Test/Unit/Controller/Adminhtml/Index/ViewfileTest.php @@ -257,11 +257,7 @@ public function testExecuteInvalidFile() $this->urlDecoderMock->expects($this->once())->method('decode')->with($decodedFile)->willReturn($file); $fileFactoryMock = $this->createMock( - FileFactory::class, - [], - [], - '', - false + FileFactory::class ); $controller = $this->objectManager->getObject( diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeHandler/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeHandler/SampleTest.php index a77ebf9ba7edb..675f900a8b9c1 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeHandler/SampleTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Product/TypeHandler/SampleTest.php @@ -96,7 +96,7 @@ protected function setUp(): void */ public function testSave($product, array $data, array $modelData) { - $link = $this->createSampleModel($product, $modelData, true); + $link = $this->createSampleModel($product, $modelData); $this->metadataMock->expects($this->once())->method('getLinkField')->willReturn('id'); $this->sampleFactory->expects($this->once()) ->method('create') diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeLoaderTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeLoaderTest.php index af6ce1bca8f58..6c3c61c4a6ec6 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeLoaderTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/AttributeLoaderTest.php @@ -48,13 +48,13 @@ class AttributeLoaderTest extends TestCase protected function setUp(): void { - $this->configMock = $this->createMock(Config::class, [], [], '', false); + $this->configMock = $this->createMock(Config::class); $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->entityMock = $this->createMock(AbstractEntity::class, [], [], '', false); - $this->entityTypeMock = $this->createMock(Type::class, [], [], '', false); + $this->entityMock = $this->createMock(AbstractEntity::class); + $this->entityTypeMock = $this->createMock(Type::class); $this->attributeLoader = new AttributeLoader( $this->configMock, $this->objectManagerMock diff --git a/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Link.php b/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Link.php index 9a406945f3b96..76667f6060853 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Link.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Link.php @@ -81,8 +81,9 @@ public function isDisabled() } /** - * Return URL pattern for action associated with the link e.g. "(star)(slash)(star)(slash)activate" -> - * will be translated to http://.../admin/integration/activate/id/X + * Return URL pattern for action associated with the link e.g. "(star)(slash)(star)(slash)activate" + * + * Will be translated to http://.../admin/integration/activate/id/X * * @return string */ @@ -164,6 +165,6 @@ protected function _getDataAttributes() */ protected function _getUrl(DataObject $row) { - return $this->isDisabled($row) ? '#' : $this->getUrl($this->getUrlPattern(), ['id' => $row->getId()]); + return $this->isDisabled() ? '#' : $this->getUrl($this->getUrlPattern(), ['id' => $row->getId()]); } } diff --git a/app/code/Magento/Integration/Model/Oauth/Consumer.php b/app/code/Magento/Integration/Model/Oauth/Consumer.php index 4442be310b967..b436defcd0cfa 100644 --- a/app/code/Magento/Integration/Model/Oauth/Consumer.php +++ b/app/code/Magento/Integration/Model/Oauth/Consumer.php @@ -13,15 +13,15 @@ * @api * @author Magento Core Team <core@magentocommerce.com> * @method string getName() - * @method Consumer setName() setName(string $name) - * @method Consumer setKey() setKey(string $key) - * @method Consumer setSecret() setSecret(string $secret) - * @method Consumer setCallbackUrl() setCallbackUrl(string $url) - * @method Consumer setCreatedAt() setCreatedAt(string $date) + * @method Consumer setName(string $name) + * @method Consumer setKey(string $key) + * @method Consumer setSecret(string $secret) + * @method Consumer setCallbackUrl(string $url) + * @method Consumer setCreatedAt(string $date) * @method string getUpdatedAt() - * @method Consumer setUpdatedAt() setUpdatedAt(string $date) + * @method Consumer setUpdatedAt(string $date) * @method string getRejectedCallbackUrl() - * @method Consumer setRejectedCallbackUrl() setRejectedCallbackUrl(string $rejectedCallbackUrl) + * @method Consumer setRejectedCallbackUrl(string $rejectedCallbackUrl) * @since 100.0.2 */ class Consumer extends \Magento\Framework\Model\AbstractModel implements ConsumerInterface @@ -112,7 +112,7 @@ public function beforeSave() } /** - * {@inheritdoc} + * @inheritDoc */ public function validate() { @@ -158,7 +158,7 @@ public function loadByKey($key) } /** - * {@inheritdoc} + * @inheritDoc */ public function getKey() { @@ -166,7 +166,7 @@ public function getKey() } /** - * {@inheritdoc} + * @inheritDoc */ public function getSecret() { @@ -174,7 +174,7 @@ public function getSecret() } /** - * {@inheritdoc} + * @inheritDoc */ public function getCallbackUrl() { @@ -182,7 +182,7 @@ public function getCallbackUrl() } /** - * {@inheritdoc} + * @inheritDoc */ public function getCreatedAt() { @@ -190,7 +190,7 @@ public function getCreatedAt() } /** - * {@inheritdoc} + * @inheritDoc */ public function isValidForTokenExchange() { diff --git a/app/code/Magento/Integration/Model/Oauth/Token.php b/app/code/Magento/Integration/Model/Oauth/Token.php index 53d9ec300a862..7a3580b8bd402 100644 --- a/app/code/Magento/Integration/Model/Oauth/Token.php +++ b/app/code/Magento/Integration/Model/Oauth/Token.php @@ -15,27 +15,27 @@ * * @method string getName() Consumer name (joined from consumer table) * @method int getConsumerId() - * @method Token setConsumerId() setConsumerId(int $consumerId) + * @method Token setConsumerId(int $consumerId) * @method int getAdminId() - * @method Token setAdminId() setAdminId(int $adminId) + * @method Token setAdminId(int $adminId) * @method int getCustomerId() - * @method Token setCustomerId() setCustomerId(int $customerId) + * @method Token setCustomerId(int $customerId) * @method int getUserType() - * @method Token setUserType() setUserType(int $userType) + * @method Token setUserType(int $userType) * @method string getType() - * @method Token setType() setType(string $type) + * @method Token setType(string $type) * @method string getCallbackUrl() - * @method Token setCallbackUrl() setCallbackUrl(string $callbackUrl) + * @method Token setCallbackUrl(string $callbackUrl) * @method string getCreatedAt() - * @method Token setCreatedAt() setCreatedAt(string $createdAt) + * @method Token setCreatedAt(string $createdAt) * @method string getToken() - * @method Token setToken() setToken(string $token) + * @method Token setToken(string $token) * @method string getSecret() - * @method Token setSecret() setSecret(string $tokenSecret) + * @method Token setSecret(string $tokenSecret) * @method int getRevoked() - * @method Token setRevoked() setRevoked(int $revoked) + * @method Token setRevoked(int $revoked) * @method int getAuthorized() - * @method Token setAuthorized() setAuthorized(int $authorized) + * @method Token setAuthorized(int $authorized) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * @since 100.0.2 @@ -200,7 +200,7 @@ public function createAdminToken($userId) public function createCustomerToken($userId) { $this->setCustomerId($userId); - return $this->saveAccessToken(UserContextInterface::USER_TYPE_CUSTOMER, $userId); + return $this->saveAccessToken(UserContextInterface::USER_TYPE_CUSTOMER); } /** diff --git a/app/code/Magento/Marketplace/Test/Unit/Controller/Partners/IndexTest.php b/app/code/Magento/Marketplace/Test/Unit/Controller/Partners/IndexTest.php index ae4042536eb0c..3f44c5616d597 100644 --- a/app/code/Magento/Marketplace/Test/Unit/Controller/Partners/IndexTest.php +++ b/app/code/Magento/Marketplace/Test/Unit/Controller/Partners/IndexTest.php @@ -93,7 +93,7 @@ public function getControllerIndexMock($methods = null) */ public function getLayoutFactoryMock($methods = null) { - return $this->createPartialMock(LayoutFactory::class, $methods, []); + return $this->createPartialMock(LayoutFactory::class, $methods); } /** @@ -109,7 +109,7 @@ public function getLayoutMock() */ public function getResponseMock($methods = null) { - return $this->createPartialMock(Response::class, $methods, []); + return $this->createPartialMock(Response::class, $methods); } /** @@ -117,7 +117,7 @@ public function getResponseMock($methods = null) */ public function getRequestMock($methods = null) { - return $this->createPartialMock(Http::class, $methods, []); + return $this->createPartialMock(Http::class, $methods); } /** diff --git a/app/code/Magento/Marketplace/Test/Unit/Model/PartnersTest.php b/app/code/Magento/Marketplace/Test/Unit/Model/PartnersTest.php index 76bb7be6e8b19..59928f40ca599 100644 --- a/app/code/Magento/Marketplace/Test/Unit/Model/PartnersTest.php +++ b/app/code/Magento/Marketplace/Test/Unit/Model/PartnersTest.php @@ -140,7 +140,7 @@ public function getPartnersBlockMock($methods = null) */ public function getPartnersModelMock($methods) { - return $this->createPartialMock(Partners::class, $methods, []); + return $this->createPartialMock(Partners::class, $methods); } /** @@ -150,7 +150,7 @@ public function getPartnersModelMock($methods) */ public function getCurlMock($methods) { - return $this->createPartialMock(Curl::class, $methods, []); + return $this->createPartialMock(Curl::class, $methods); } /** @@ -160,6 +160,6 @@ public function getCurlMock($methods) */ public function getCacheMock($methods) { - return $this->createPartialMock(Cache::class, $methods, []); + return $this->createPartialMock(Cache::class, $methods); } } diff --git a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php index 2a86a9d3018dd..b7ab4bda8da53 100644 --- a/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php +++ b/app/code/Magento/NewRelicReporting/Test/Unit/Model/Cron/ReportNewRelicCronTest.php @@ -17,7 +17,6 @@ use Magento\NewRelicReporting\Model\Module\Collect; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; class ReportNewRelicCronTest extends TestCase { @@ -61,11 +60,6 @@ class ReportNewRelicCronTest extends TestCase */ protected $deploymentsModel; - /** - * @var LoggerInterface|MockObject - */ - private $logger; - /** * Setup * @@ -117,15 +111,13 @@ protected function setUp(): void $this->deploymentsFactory->expects($this->any()) ->method('create') ->willReturn($this->deploymentsModel); - $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); $this->model = new ReportNewRelicCron( $this->config, $this->collect, $this->counter, $this->cronEventFactory, - $this->deploymentsFactory, - $this->logger + $this->deploymentsFactory ); } @@ -215,7 +207,6 @@ public function testReportNewRelicCronRequestFailed() ->method('sendRequest'); $this->cronEventModel->expects($this->once())->method('sendRequest')->willThrowException(new \Exception()); - $this->logger->expects($this->never())->method('critical'); $this->deploymentsModel->expects($this->any()) ->method('setDeployment'); diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Grid/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Grid/Collection.php index e16effb8c7e12..d0da06553a5f3 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Grid/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber/Grid/Collection.php @@ -17,7 +17,8 @@ class Collection extends \Magento\Newsletter\Model\ResourceModel\Subscriber\Coll protected function _initSelect() { parent::_initSelect(); - $this->showCustomerInfo(true)->addSubscriberTypeField()->showStoreInfo(); + $this->showCustomerInfo()->addSubscriberTypeField()->showStoreInfo(); + return $this; } } diff --git a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php index 519a8cba014f2..f3d21e97eb9ee 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php @@ -181,20 +181,8 @@ public function testExecuteInvalidFileImage() $this->request->expects($this->any())->method('getParam')->willReturn( 'https://example.com/test.jpg' ); - $readInterface = $this->createMock( - ReadInterface::class, - [], - [], - '', - false - ); - $writeInterface = $this->createMock( - WriteInterface::class, - [], - [], - '', - false - ); + $readInterface = $this->createMock(ReadInterface::class); + $writeInterface = $this->createMock(WriteInterface::class); $this->filesystemMock->expects($this->any())->method('getDirectoryRead')->willReturn($readInterface); $readInterface->expects($this->any())->method('getAbsolutePath')->willReturn(''); $this->abstractAdapter->expects($this->any()) @@ -217,20 +205,8 @@ public function testExecuteInvalidFileType() $this->request->expects($this->any())->method('getParam')->willReturn( 'https://example.com/test.php' ); - $readInterface = $this->createMock( - ReadInterface::class, - [], - [], - '', - false - ); - $writeInterface = $this->createMock( - WriteInterface::class, - [], - [], - '', - false - ); + $readInterface = $this->createMock(ReadInterface::class); + $writeInterface = $this->createMock(WriteInterface::class); $this->filesystemMock->expects($this->any())->method('getDirectoryRead')->willReturn($readInterface); $readInterface->expects($this->any())->method('getAbsolutePath')->willReturn(''); $this->abstractAdapter->expects($this->never())->method('validateUploadFile'); diff --git a/app/code/Magento/Quote/Model/QuoteIdMask.php b/app/code/Magento/Quote/Model/QuoteIdMask.php index 47b02db7650df..8fa0b1fbba80c 100644 --- a/app/code/Magento/Quote/Model/QuoteIdMask.php +++ b/app/code/Magento/Quote/Model/QuoteIdMask.php @@ -10,7 +10,7 @@ * QuoteIdMask model * * @method string getMaskedId() - * @method QuoteIdMask setMaskedId() + * @method QuoteIdMask setMaskedId(string $id) */ class QuoteIdMask extends \Magento\Framework\Model\AbstractModel { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 44571550459c2..47583c27370fb 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -159,7 +159,7 @@ public function prepareSummary($range, $customStart, $customEnd, $isFilter = 0) if ($this->_isLive) { $this->_prepareSummaryLive($range, $customStart, $customEnd, $isFilter); } else { - $this->_prepareSummaryAggregated($range, $customStart, $customEnd, $isFilter); + $this->_prepareSummaryAggregated($range, $customStart, $customEnd); } return $this; diff --git a/app/code/Magento/Review/Block/Adminhtml/Grid.php b/app/code/Magento/Review/Block/Adminhtml/Grid.php index e20cb7554e094..798d6ae7148af 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Grid.php +++ b/app/code/Magento/Review/Block/Adminhtml/Grid.php @@ -10,12 +10,11 @@ /** * Adminhtml reviews grid * - * @method int getProductId() getProductId() - * @method \Magento\Review\Block\Adminhtml\Grid setProductId() setProductId(int $productId) - * @method int getCustomerId() getCustomerId() - * @method \Magento\Review\Block\Adminhtml\Grid setCustomerId() setCustomerId(int $customerId) - * @method \Magento\Review\Block\Adminhtml\Grid setMassactionIdFieldOnlyIndexValue() - * setMassactionIdFieldOnlyIndexValue(bool $onlyIndex) + * @method int getProductId() + * @method Grid setProductId(int $productId) + * @method int getCustomerId() + * @method Grid setCustomerId(int $customerId) + * @method Grid setMassactionIdFieldOnlyIndexValue(bool $onlyIndex) */ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { @@ -110,9 +109,7 @@ protected function _afterLoadCollection() } /** - * Prepare collection - * - * @return \Magento\Review\Block\Adminhtml\Grid + * @inheritDoc */ protected function _prepareCollection() { diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 0af42b0a99d09..7cb99bc75ba03 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -42,17 +42,17 @@ * * @api * @method int getGiftMessageId() - * @method \Magento\Sales\Model\Order setGiftMessageId(int $value) + * @method Order setGiftMessageId(int $value) * @method bool hasBillingAddressId() - * @method \Magento\Sales\Model\Order unsBillingAddressId() + * @method Order unsBillingAddressId() * @method bool hasShippingAddressId() - * @method \Magento\Sales\Model\Order unsShippingAddressId() + * @method Order unsShippingAddressId() * @method int getShippigAddressId() * @method bool hasCustomerNoteNotify() * @method bool hasForcedCanCreditmemo() * @method bool getIsInProcess() * @method \Magento\Customer\Model\Customer|null getCustomer() - * @method \Magento\Sales\Model\Order setSendEmail(bool $value) + * @method Order setSendEmail(bool $value) * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -494,7 +494,7 @@ public function setCanSendNewEmailFlag($flag) * Load order by system increment identifier * * @param string $incrementId - * @return \Magento\Sales\Model\Order + * @return Order */ public function loadByIncrementId($incrementId) { @@ -506,7 +506,7 @@ public function loadByIncrementId($incrementId) * * @param string $incrementId * @param string $storeId - * @return \Magento\Sales\Model\Order + * @return Order */ public function loadByIncrementIdAndStoreId($incrementId, $storeId) { @@ -1818,7 +1818,7 @@ public function getTotalDue() $total = $this->priceCurrency->round($total); return max($total, 0); } - + /** * Retrieve order total due value * @@ -2052,7 +2052,7 @@ public function getStoreGroupName() { $storeId = $this->getStoreId(); if ($storeId === null) { - return $this->getStoreName(1); + return $this->getStoreName(); } return $this->getStore()->getGroup()->getName(); } diff --git a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/ViewTest.php b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/ViewTest.php index 69aa43ceca1e0..2ae5be872e6c4 100644 --- a/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/ViewTest.php +++ b/app/code/Magento/Sales/Test/Unit/Controller/Adminhtml/Order/ViewTest.php @@ -203,7 +203,7 @@ public function testExecute() $id = 111; $titlePart = '#111'; $this->initOrder(); - $this->initOrderSuccess($id); + $this->initOrderSuccess(); $this->prepareRedirect(); $this->initAction(); @@ -264,10 +264,9 @@ public function testExecuteNoOrder() */ public function testGlobalException() { - $id = 111; $exception = new \Exception(); $this->initOrder(); - $this->initOrderSuccess($id); + $this->initOrderSuccess(); $this->prepareRedirect(); $this->resultPageFactoryMock->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Helper/DataTest.php b/app/code/Magento/Sales/Test/Unit/Helper/DataTest.php index 9b2f818aeebb3..75ab7ff0378da 100644 --- a/app/code/Magento/Sales/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Sales/Test/Unit/Helper/DataTest.php @@ -9,8 +9,6 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Helper\Context; -use Magento\Framework\App\State; -use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Sales\Helper\Data; use Magento\Sales\Model\Order\Email\Container\CreditmemoCommentIdentity; use Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity; @@ -21,7 +19,7 @@ use Magento\Sales\Model\Order\Email\Container\ShipmentCommentIdentity; use Magento\Sales\Model\Order\Email\Container\ShipmentIdentity; use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -41,7 +39,7 @@ class DataTest extends TestCase protected $scopeConfigMock; /** - * @var MockObject|\Magento\Sales\Model\Store + * @var MockObject|Store */ protected $storeMock; @@ -60,26 +58,9 @@ protected function setUp(): void ->method('getScopeConfig') ->willReturn($this->scopeConfigMock); - $storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $appStateMock = $this->getMockBuilder(State::class) - ->disableOriginalConstructor() - ->getMock(); - - $pricingCurrencyMock = $this->getMockBuilder(PriceCurrencyInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->helper = new Data( - $contextMock, - $storeManagerMock, - $appStateMock, - $pricingCurrencyMock - ); + $this->helper = new Data($contextMock); - $this->storeMock = $this->getMockBuilder(\Magento\Sales\Model\Store::class) + $this->storeMock = $this->getMockBuilder(Store::class) ->disableOriginalConstructor() ->getMock(); } diff --git a/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php b/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php index 880fbca71dce3..4c1ceecf153dd 100644 --- a/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Helper/DataTest.php @@ -255,7 +255,7 @@ public function dataForAssembleEavAttribute() */ public function testLoadFirstVariationWithSwatchImage($imageTypes, $expected, $requiredAttributes) { - $this->getSwatchAttributes($this->productMock); + $this->getSwatchAttributes(); $this->getUsedProducts($imageTypes + $requiredAttributes, $imageTypes); $result = $this->swatchHelperObject->loadFirstVariationWithSwatchImage($this->productMock, $requiredAttributes); @@ -295,16 +295,13 @@ public function dataForVariationWithSwatchImage() ]; } - /** - * @dataProvider dataForCreateSwatchProductByFallback - */ - public function testLoadVariationByFallback($product) + public function testLoadVariationByFallback() { $metadataMock = $this->getMockForAbstractClass(EntityMetadataInterface::class); $this->metaDataPoolMock->expects($this->once())->method('getMetadata')->willReturn($metadataMock); $metadataMock->expects($this->once())->method('getLinkField')->willReturn('id'); - $this->getSwatchAttributes($product); + $this->getSwatchAttributes(); $this->prepareVariationCollection(); @@ -321,7 +318,7 @@ public function testLoadVariationByFallback($product) */ public function testLoadFirstVariationWithImage($imageTypes, $expected, $requiredAttributes) { - $this->getSwatchAttributes($this->productMock); + $this->getSwatchAttributes(); $this->getUsedProducts($imageTypes + $requiredAttributes, $imageTypes); $result = $this->swatchHelperObject->loadFirstVariationWithImage($this->productMock, $requiredAttributes); @@ -592,23 +589,6 @@ public function dataForCreateSwatchProduct() ]; } - /** - * @return array - */ - public function dataForCreateSwatchProductByFallback() - { - $productMock = $this->createMock(Product::class); - - return [ - [ - 95, - ], - [ - $productMock, - ], - ]; - } - /** * @dataProvider dataForGettingSwatchAsArray */ diff --git a/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributeCodesTest.php b/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributeCodesTest.php index 29eb752bb3c57..c952cd3c2e6a2 100644 --- a/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributeCodesTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Model/SwatchAttributeCodesTest.php @@ -90,17 +90,12 @@ public function testGetCodes($swatchAttributeCodesCache, $expected) ->withConsecutive( [ self::identicalTo( - ['a' => self::ATTRIBUTE_TABLE], - [ - 'attribute_id' => 'a.attribute_id', - 'attribute_code' => 'a.attribute_code', - ] + ['a' => self::ATTRIBUTE_TABLE] ) ], [ self::identicalTo( - ['o' => self::ATTRIBUTE_OPTION_TABLE], - ['attribute_id' => 'o.attribute_id'] + ['o' => self::ATTRIBUTE_OPTION_TABLE] ) ] ) diff --git a/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit/Tab/Css.php b/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit/Tab/Css.php index 2d55bbce2ec2c..5e3bb8774d246 100644 --- a/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit/Tab/Css.php +++ b/app/code/Magento/Theme/Block/Adminhtml/System/Design/Theme/Edit/Tab/Css.php @@ -194,8 +194,7 @@ protected function _addCustomCssFieldset() Storage::PARAM_CONTENT_TYPE => \Magento\Theme\Model\Wysiwyg\Storage::TYPE_IMAGE ] ) . "', null, null,'" . $this->escapeJs( - __('Upload Images'), - true + __('Upload Images') ) . "');", ] ); @@ -222,8 +221,7 @@ protected function _addCustomCssFieldset() Storage::PARAM_CONTENT_TYPE => \Magento\Theme\Model\Wysiwyg\Storage::TYPE_FONT ] ) . "', null, null,'" . $this->escapeJs( - __('Upload Fonts'), - true + __('Upload Fonts') ) . "');", ] ); diff --git a/app/code/Magento/Theme/Model/ResourceModel/Design/Config/Collection.php b/app/code/Magento/Theme/Model/ResourceModel/Design/Config/Collection.php index 3a19ff99a9270..6db521978cfab 100644 --- a/app/code/Magento/Theme/Model/ResourceModel/Design/Config/Collection.php +++ b/app/code/Magento/Theme/Model/ResourceModel/Design/Config/Collection.php @@ -77,12 +77,13 @@ protected function _afterLoad() 'value', $this->valueProcessor->process( $item->getData('value'), - $this->getData('scope'), - $this->getData('scope_id'), + $item->getData('scope'), + $item->getData('scope_id'), $item->getData('path') ) ); } - parent::_afterLoad(); + + return parent::_afterLoad(); } } diff --git a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/SaveTest.php b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/SaveTest.php index b2641d304fb84..f14b176e95e5c 100644 --- a/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/SaveTest.php +++ b/app/code/Magento/Theme/Test/Unit/Controller/Adminhtml/System/Design/Theme/SaveTest.php @@ -57,7 +57,7 @@ public function testSaveAction() ->with('js_order') ->willReturn($jsOrder); - $this->_request->expects($this->once(5))->method('getPostValue')->willReturn(true); + $this->_request->expects($this->once())->method('getPostValue')->willReturn(true); $themeMock = $this->getMockBuilder(Theme::class) ->addMethods(['setCustomization']) diff --git a/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml b/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml index 7f1128b3b07c7..9c6cf4fa07cf2 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/sections.phtml @@ -11,12 +11,12 @@ $group = $block->getGroupName(); $groupCss = $block->getGroupCss(); ?> -<?php if ($detailedInfoGroup = $block->getGroupChildNames($group, 'getChildHtml')) :?> +<?php if ($detailedInfoGroup = $block->getGroupChildNames($group)):?> <div class="sections <?= $block->escapeHtmlAttr($groupCss) ?>"> <?php $layout = $block->getLayout(); ?> <div class="section-items <?= $block->escapeHtmlAttr($groupCss) ?>-items" data-mage-init='{"tabs":{"openedState":"active"}}'> - <?php foreach ($detailedInfoGroup as $name) :?> + <?php foreach ($detailedInfoGroup as $name):?> <?php $html = $layout->renderElement($name); if (!trim($html) && ($block->getUseForce() != true)) { diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Rest/DocumentationGenerator.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Rest/DocumentationGenerator.php index 32f7f4aa3a8a6..945dc935d440c 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Rest/DocumentationGenerator.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/Webapi/Adapter/Rest/DocumentationGenerator.php @@ -23,7 +23,7 @@ class DocumentationGenerator public function generateDocumentation($httpMethod, $resourcePath, $arguments, $response) { $content = $this->generateHtmlContent($httpMethod, $resourcePath, $arguments, $response); - $filePath = $this->generateFileName($resourcePath); + $filePath = $this->generateFileName(); if ($filePath === null) { return; } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Cache/Backend/MongoDbTest.php b/dev/tests/integration/testsuite/Magento/Framework/Cache/Backend/MongoDbTest.php index f4cec6e80aa36..82512c0bd37ff 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Cache/Backend/MongoDbTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Cache/Backend/MongoDbTest.php @@ -203,7 +203,7 @@ public function testSave() $actualData = $this->_model->load($cacheId); $this->assertEquals($data, $actualData); $actualMetadata = $this->_model->getMetadatas($cacheId); - $this->arrayHasKey('tags', $actualMetadata); + $this->arrayHasKey('tags'); $this->assertEquals($tags, $actualMetadata['tags']); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Session/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/Session/ConfigTest.php index bed9a33c73148..095e70a6d0e1a 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Session/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Session/ConfigTest.php @@ -12,8 +12,6 @@ use Magento\Framework\App\Filesystem\DirectoryList; - // @codingStandardsIgnoreEnd - /** * Mock ini_get global function * @@ -37,6 +35,8 @@ function ini_get($varName) return call_user_func_array('\ini_get', func_get_args()); } + // @codingStandardsIgnoreEnd + /** * @magentoAppIsolation enabled */ @@ -181,7 +181,7 @@ public function testSettingInvalidCookieLifetime() $model->setCookieLifetime('foobar_bogus'); $this->assertEquals($preVal, $model->getCookieLifetime()); } - + public function testSettingInvalidCookieLifetime2() { $model = $this->getModel(); @@ -193,8 +193,8 @@ public function testSettingInvalidCookieLifetime2() public function testWrongMethodCall() { $model = $this->getModel(); - $this->expectException( - '\BadMethodCallException', + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage( 'Method "methodThatNotExist" does not exist in Magento\Framework\Session\Config' ); $model->methodThatNotExist(); diff --git a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php index 62baa3ab07568..cd1cb7bf14b98 100644 --- a/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Legacy/ObsoleteCodeTest.php @@ -169,7 +169,7 @@ public function testXmlFiles() $invoker( function ($file) { $content = file_get_contents($file); - $this->_testObsoleteClasses($content, $file); + $this->_testObsoleteClasses($content); $this->_testObsoleteNamespaces($content); $this->_testObsoletePaths($file); }, @@ -950,8 +950,10 @@ private function getBlacklistFiles($absolutePath = false) $appPath = BP; foreach ($blackList as $file) { if ($absolutePath) { + // phpcs:ignore $ignored = array_merge($ignored, glob($appPath . DIRECTORY_SEPARATOR . $file, GLOB_NOSORT)); } else { + // phpcs:ignore $ignored = array_merge($ignored, $this->processPattern($appPath, $file)); } } diff --git a/lib/internal/Magento/Framework/Backup/Filesystem.php b/lib/internal/Magento/Framework/Backup/Filesystem.php index 72996cdd28fda..299bf88c1c409 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem.php @@ -294,7 +294,7 @@ protected function _checkBackupsDir() } mkdir($backupsDir); - chmod($backupsDir); + chmod($backupsDir, 0755); } if (!is_writable($backupsDir)) { @@ -316,6 +316,8 @@ protected function _getTarTmpPath() } /** + * Get rollback FTP + * * @return Ftp * @deprecated 101.0.0 */ @@ -332,6 +334,8 @@ protected function getRollBackFtp() } /** + * Get rollback FS + * * @return Fs * @deprecated 101.0.0 */ diff --git a/lib/internal/Magento/Framework/DB/Tree.php b/lib/internal/Magento/Framework/DB/Tree.php index 6fbd014213bc8..3718c55f61c8a 100644 --- a/lib/internal/Magento/Framework/DB/Tree.php +++ b/lib/internal/Magento/Framework/DB/Tree.php @@ -93,6 +93,7 @@ public function __construct($config = []) // use an object from the registry? if (is_string($connection)) { + /** @phpstan-ignore-next-line */ $connection = \Zend::registry($connection); } @@ -358,6 +359,7 @@ public function appendChild($nodeId, $data) } catch (\Exception $e) { $this->_db->rollBack(); echo $e->getMessage(); + /** @phpstan-ignore-next-line */ echo $sql; exit; } @@ -833,6 +835,7 @@ public function moveNodes($eId, $pId, $aId = 0) if ($pId == 0) { $levelUp = 0; } else { + /** @phpstan-ignore-next-line */ $levelUp = $pInfo[$this->_level]; } @@ -844,15 +847,20 @@ public function moveNodes($eId, $pId, $aId = 0) $rightKeyNear = $this->_db->fetchOne('SELECT MAX(' . $this->_right . ') FROM ' . $this->_table); } elseif ($aId != 0 && $pId == $eInfo[$this->_pid]) { // if we have after ID + /** @phpstan-ignore-next-line */ $rightKeyNear = $aInfo[$this->_right]; + /** @phpstan-ignore-next-line */ $leftKeyNear = $aInfo[$this->_left]; } elseif ($aId == 0 && $pId == $eInfo[$this->_pid]) { // if we do not have after ID + /** @phpstan-ignore-next-line */ $rightKeyNear = $pInfo[$this->_left]; } elseif ($pId != $eInfo[$this->_pid]) { + /** @phpstan-ignore-next-line */ $rightKeyNear = $pInfo[$this->_right] - 1; } + /** @phpstan-ignore-next-line */ $skewLevel = $pInfo[$this->_level] - $eInfo[$this->_level] + 1; $skewTree = $eInfo[$this->_right] - $eInfo[$this->_left] + 1; @@ -996,12 +1004,14 @@ public function moveNodes($eId, $pId, $aId = 0) $this->_db->beginTransaction(); try { + /** @phpstan-ignore-next-line */ $this->_db->query($sql); $this->_db->commit(); } catch (\Exception $e) { $this->_db->rollBack(); echo $e->getMessage(); echo "<br>\r\n"; + /** @phpstan-ignore-next-line */ echo $sql; echo "<br>\r\n"; exit; @@ -1052,6 +1062,7 @@ public function getChildren($nodeId, $startLevel = 0, $endLevel = 0) exit; } + /** @phpstan-ignore-next-line */ $dbSelect = new Select($this->_db); $dbSelect->from( $this->_table @@ -1092,6 +1103,7 @@ public function getChildren($nodeId, $startLevel = 0, $endLevel = 0) */ public function getNode($nodeId) { + /** @phpstan-ignore-next-line */ $dbSelect = new Select($this->_db); $dbSelect->from($this->_table)->where($this->_table . '.' . $this->_id . ' >= :id'); diff --git a/lib/internal/Magento/Framework/Indexer/Test/Unit/IndexStructureTest.php b/lib/internal/Magento/Framework/Indexer/Test/Unit/IndexStructureTest.php index da286bd8cd6e8..c03875f0a2fd2 100644 --- a/lib/internal/Magento/Framework/Indexer/Test/Unit/IndexStructureTest.php +++ b/lib/internal/Magento/Framework/Indexer/Test/Unit/IndexStructureTest.php @@ -167,7 +167,7 @@ public function testCreateWithEmptyFields() ->method('resolve') ->with($index, $dimensions) ->willReturn($index . '_flat'); - $position = $this->mockFulltextTable($position, $expectedTable, true); + $position = $this->mockFulltextTable($position, $expectedTable); $this->mockFlatTable($position, $index . '_flat'); $this->target->create($index, $fields, $dimensions); diff --git a/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php b/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php index f1707fff7786b..a955e5cff2df8 100644 --- a/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php +++ b/lib/internal/Magento/Framework/Reflection/Test/Unit/TypeProcessorTest.php @@ -221,9 +221,9 @@ public function testProcessSimpleTypeStringArrayToIntArray() */ public function testProcessSimpleTypeException($value, $type) { - $this->expectException( - SerializationException::class, - 'Invalid type for value: "' . $value . '". Expected Type: "' . $type . '"' + $this->expectException(SerializationException::class); + $this->expectExceptionMessage( + "The \"$value\" value's type is invalid. The \"$type\" type was expected. Verify and try again." ); $this->typeProcessor->processSimpleAndAnyType($value, $type); } diff --git a/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php b/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php index f8bb912ea6c85..5b86d46452e33 100644 --- a/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php +++ b/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php @@ -108,6 +108,7 @@ class Adminhtml extends \PHPUnit\Framework\TestCase protected $_formKey; /** + * @inheritDoc */ protected function setUp(): void { @@ -150,6 +151,7 @@ protected function setUp(): void [$this, 'translateCallback'] ); + /** @phpstan-ignore-next-line */ $this->_context = new \Magento\Backend\Block\Template\Context( $this->_requestMock, $this->_layoutMock, @@ -194,11 +196,10 @@ protected function _makeMock($className) /** * Sets up a stubbed method with specified behavior and expectations * - * @param \PHPUnit_Framework_MockObject_MockObject $object - * @param string $stubName - * @param mixed $return - * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount|null $expects - * + * @param \PHPUnit_Framework_MockObject_MockObject $object + * @param string $stubName + * @param mixed $return + * @param \PHPUnit\Framework\MockObject\Matcher\InvokedCount|null $expects * @return \PHPUnit\Framework\MockObject\Builder\InvocationMocker */ protected function _setStub( diff --git a/lib/internal/Magento/Framework/Url.php b/lib/internal/Magento/Framework/Url.php index 1d00b732c5795..d63632fafc7d3 100644 --- a/lib/internal/Magento/Framework/Url.php +++ b/lib/internal/Magento/Framework/Url.php @@ -935,7 +935,7 @@ private function createUrl($routePath = null, array $routeParams = null) if (is_string($query)) { $this->_setQuery($query); } elseif (is_array($query)) { - $this->addQueryParams($query, !empty($routeParams['_current'])); + $this->addQueryParams($query); } if ($query === false) { $this->addQueryParams([]); diff --git a/lib/internal/Magento/Framework/View/Context.php b/lib/internal/Magento/Framework/View/Context.php index 8503c48d135c2..f781dc40ba39c 100644 --- a/lib/internal/Magento/Framework/View/Context.php +++ b/lib/internal/Magento/Framework/View/Context.php @@ -27,6 +27,7 @@ * * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @api * @since 100.0.2 */ @@ -383,7 +384,7 @@ public function getFullActionName() public function getAcceptType() { // TODO: do intelligence here - $type = $this->getHeader('Accept', 'html'); + $type = $this->getHeader('Accept') ?: 'html'; if (strpos($type, 'json') !== false) { return 'json'; } elseif (strpos($type, 'soap') !== false) { @@ -486,7 +487,9 @@ protected function getPhysicalTheme(Design\ThemeInterface $theme) $result = $result->getParentTheme(); } if (!$result) { - throw new \Exception("Unable to find a physical ancestor for a theme '{$theme->getThemeTitle()}'."); + throw new \InvalidArgumentException( + "Unable to find a physical ancestor for a theme '{$theme->getThemeTitle()}'." + ); } return $result; } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Asset/ConfigTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Asset/ConfigTest.php index 3f7a700ffb212..a3ded21f2c907 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Asset/ConfigTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Asset/ConfigTest.php @@ -24,11 +24,6 @@ class ConfigTest extends BaseTestCase */ private $scopeConfigMock; - /** - * @var MockObject|State - */ - private $appStateMock; - /** * @var Config */ @@ -41,10 +36,7 @@ protected function setUp(): void { $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) ->getMockForAbstractClass(); - $this->appStateMock = $this->getMockBuilder(State::class) - ->disableOriginalConstructor() - ->getMock(); - $this->model = new Config($this->scopeConfigMock, $this->appStateMock); + $this->model = new Config($this->scopeConfigMock); } /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Message/MessageConfigurationsPoolTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Message/MessageConfigurationsPoolTest.php index 5974042ef9777..467f9485f77b6 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Message/MessageConfigurationsPoolTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Message/MessageConfigurationsPoolTest.php @@ -41,10 +41,8 @@ public function testGetMessageConfiguration() */ public function testConstructNoRendererException(array $configuration) { - static::expectException( - '\InvalidArgumentException', - 'Renderer should be defined.' - ); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Renderer should be defined.'); new MessageConfigurationsPool($configuration); } @@ -67,10 +65,8 @@ public function wrongRenderersDataProvider() */ public function testConstructWrongDataException(array $configuration) { - static::expectException( - '\InvalidArgumentException', - 'Data should be of array type.' - ); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Data should be of array type.'); new MessageConfigurationsPool($configuration); } diff --git a/setup/src/Magento/Setup/Test/Unit/Model/ModuleUninstallerTest.php b/setup/src/Magento/Setup/Test/Unit/Model/ModuleUninstallerTest.php index bc0050513ad85..b5e616a95437c 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/ModuleUninstallerTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/ModuleUninstallerTest.php @@ -15,7 +15,6 @@ use Magento\Framework\Setup\Patch\PatchApplier; use Magento\Framework\Setup\UninstallInterface; use Magento\Setup\Model\ModuleContext; -use Magento\Setup\Model\ModuleRegistryUninstaller; use Magento\Setup\Model\ModuleUninstaller; use Magento\Setup\Model\ObjectManagerProvider; use Magento\Setup\Model\UninstallCollector; @@ -60,11 +59,6 @@ class ModuleUninstallerTest extends TestCase */ private $output; - /** - * @var MockObject|ModuleRegistryUninstaller - */ - private $moduleRegistryUninstaller; - /** * @var PatchApplier|MockObject */ @@ -72,7 +66,6 @@ class ModuleUninstallerTest extends TestCase protected function setUp(): void { - $this->moduleRegistryUninstaller = $this->createMock(ModuleRegistryUninstaller::class); $this->objectManager = $this->getMockForAbstractClass( ObjectManagerInterface::class, [], @@ -94,8 +87,7 @@ protected function setUp(): void $objectManagerProvider, $this->remove, $this->collector, - $setupFactory, - $this->moduleRegistryUninstaller + $setupFactory ); $this->output = $this->getMockForAbstractClass(OutputInterface::class); @@ -103,7 +95,6 @@ protected function setUp(): void public function testUninstallRemoveData() { - $this->moduleRegistryUninstaller->expects($this->never())->method($this->anything()); $uninstall = $this->getMockForAbstractClass(UninstallInterface::class, [], '', false); $uninstall->expects($this->atLeastOnce()) ->method('uninstall') @@ -136,7 +127,6 @@ public function testUninstallRemoveData() public function testUninstallRemoveCode() { - $this->moduleRegistryUninstaller->expects($this->never())->method($this->anything()); $this->output->expects($this->once())->method('writeln'); $packageInfoFactory = $this->createMock(PackageInfoFactory::class); $packageInfo = $this->createMock(PackageInfo::class); diff --git a/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/OperationFactoryTest.php b/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/OperationFactoryTest.php index 4682b0b035798..b2af18b34959a 100644 --- a/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/OperationFactoryTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Module/Di/App/Task/OperationFactoryTest.php @@ -65,8 +65,7 @@ public function testCreateException() $notRegisteredOperation = 'coffee'; $this->expectException(OperationException::class); $this->expectExceptionMessage( - sprintf('Unrecognized operation "%s"', $notRegisteredOperation), - OperationException::UNAVAILABLE_OPERATION + sprintf('Unrecognized operation "%s"', $notRegisteredOperation) ); $this->factory->create($notRegisteredOperation); } From cd42c9d4a41512cf4a978120fcbeaac3d17d9e24 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 13 Oct 2020 22:57:16 +0300 Subject: [PATCH 0809/1013] MC-38074: Report - Products in Carts not following user roles scope --- .../Reports/Block/Adminhtml/Grid/Shopcart.php | 5 + .../Block/Adminhtml/Grid/ShopcartTest.php | 110 ++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php index afa0ce79aca6e..1d65dd5874c6e 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php @@ -28,6 +28,7 @@ class Shopcart extends \Magento\Backend\Block\Widget\Grid\Extended /** * StoreIds setter + * * @codeCoverageIgnore * * @param array $storeIds @@ -46,6 +47,10 @@ public function setStoreIds($storeIds) */ public function getCurrentCurrencyCode() { + if (empty($this->_storeIds)) { + $this->setStoreIds(array_keys($this->_storeManager->getStores())); + } + if ($this->_currentCurrencyCode === null) { reset($this->_storeIds); $this->_currentCurrencyCode = count( diff --git a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php new file mode 100644 index 0000000000000..25dcccdb1ef7a --- /dev/null +++ b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Test\Unit\Block\Adminhtml\Grid; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Reports\Block\Adminhtml\Grid\Shopcart; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for class \Magento\Reports\Block\Adminhtml\Grid\Shopcart. + */ +class ShopcartTest extends TestCase +{ + /** + * @var Shopcart|MockObject + */ + private $model; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->storeManagerMock = $this->getMockForAbstractClass( + StoreManagerInterface::class, + [], + '', + true, + true, + true, + ['getStore'] + ); + + $this->model = $objectManager->getObject( + Shopcart::class, + ['_storeManager' => $this->storeManagerMock] + ); + } + + /** + * @param $storeIds + * + * @dataProvider getCurrentCurrencyCodeDataProvider + */ + public function testGetCurrentCurrencyCode($storeIds) + { + $storeMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + true, + true, + true, + ['getBaseCurrencyCode'] + ); + + $this->model->setStoreIds($storeIds); + + if ($storeIds) { + $expectedCurrencyCode = 'EUR'; + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with($storeIds[0]) + ->willReturn($storeMock); + $storeMock->expects($this->once()) + ->method('getBaseCurrencyCode') + ->willReturn($expectedCurrencyCode); + } else { + $expectedCurrencyCode = 'USD'; + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with(1) + ->willReturn($storeMock); + $this->storeManagerMock->expects($this->once()) + ->method('getStores') + ->willReturn([1 => $storeMock]); + $storeMock->expects($this->once()) + ->method('getBaseCurrencyCode') + ->willReturn($expectedCurrencyCode); + } + + $currencyCode = $this->model->getCurrentCurrencyCode(); + $this->assertEquals($expectedCurrencyCode, $currencyCode); + } + + /** + * DataProvider for testGetCurrentCurrencyCode. + * + * @return array + */ + public function getCurrentCurrencyCodeDataProvider() + { + return [ + [[]], + [[2]], + ]; + } +} From ab42ff6465bf58a53b83810278b37085ff4e93aa Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 13 Oct 2020 16:15:04 -0500 Subject: [PATCH 0810/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Indexer/Product/Price/Action/Rows.php | 1 - .../Indexer/Product/Price/Action/RowsTest.php | 161 +++++++++++++++++- 2 files changed, 156 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php index ce2f1ff75adbe..dfeb5b6bfea26 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php @@ -13,7 +13,6 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Store\Model\StoreManagerInterface; diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php index 4242ab7b2e914..9fd86e81c7a51 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php @@ -7,27 +7,178 @@ namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Price\Action; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Directory\Model\CurrencyFactory; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\Factory; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\DefaultPrice; +use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\TierPrice; +use Magento\Catalog\Model\Indexer\Product\Price\DimensionCollectionFactory; +use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; use Magento\Catalog\Model\Indexer\Product\Price\Action\Rows; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Stdlib\DateTime; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\Indexer\MultiDimensionProvider; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; class RowsTest extends TestCase { /** * @var Rows */ - protected $_model; + private $actionRows; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $config; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var CurrencyFactory|MockObject + */ + private $currencyFactory; + + /** + * @var TimezoneInterface|MockObject + */ + private $localeDate; + + /** + * @var DateTime|MockObject + */ + private $dateTime; + + /** + * @var Type|MockObject + */ + private $catalogProductType; + + /** + * @var Factory|MockObject + */ + private $indexerPriceFactory; + + /** + * @var DefaultPrice|MockObject + */ + private $defaultIndexerResource; + + /** + * @var TierPrice|MockObject + */ + private $tierPriceIndexResource; + + /** + * @var DimensionCollectionFactory|MockObject + */ + private $dimensionCollectionFactory; + + /** + * @var TableMaintainer|MockObject + */ + private $tableMaintainer; protected function setUp(): void { - $objectManager = new ObjectManager($this); - $this->_model = $objectManager->getObject(Rows::class); + $this->config = $this->getMockBuilder(ScopeConfigInterface::class) + ->getMockForAbstractClass(); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->currencyFactory = $this->getMockBuilder(CurrencyFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->localeDate = $this->getMockBuilder(TimezoneInterface::class) + ->getMockForAbstractClass(); + $this->dateTime = $this->getMockBuilder(DateTime::class) + ->disableOriginalConstructor() + ->getMock(); + $this->catalogProductType = $this->getMockBuilder(Type::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexerPriceFactory = $this->getMockBuilder(Factory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->defaultIndexerResource = $this->getMockBuilder(DefaultPrice::class) + ->disableOriginalConstructor() + ->getMock(); + $this->tierPriceIndexResource = $this->getMockBuilder(TierPrice::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dimensionCollectionFactory = $this->getMockBuilder(DimensionCollectionFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $this->tableMaintainer = $this->getMockBuilder(TableMaintainer::class) + ->disableOriginalConstructor() + ->getMock(); + $batchSize = 2; + + $this->actionRows = new Rows( + $this->config, + $this->storeManager, + $this->currencyFactory, + $this->localeDate, + $this->dateTime, + $this->catalogProductType, + $this->indexerPriceFactory, + $this->defaultIndexerResource, + $this->tierPriceIndexResource, + $this->dimensionCollectionFactory, + $this->tableMaintainer, + $batchSize + ); } public function testEmptyIds() { $this->expectException('Magento\Framework\Exception\InputException'); $this->expectExceptionMessage('Bad value was supplied.'); - $this->_model->execute(null); + $this->actionRows->execute(null); + } + + public function testBatchProcessing() + { + $ids = [1, 2, 3, 4]; + $select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $select->expects($this->any())->method('from')->willReturnSelf(); + $select->expects($this->any())->method('where')->willReturnSelf(); + $select->expects($this->any())->method('join')->willReturnSelf(); + $adapter = $this->getMockBuilder(AdapterInterface::class)->getMockForAbstractClass(); + $adapter->expects($this->any())->method('select')->willReturn($select); + $this->defaultIndexerResource->expects($this->any()) + ->method('getConnection') + ->willReturn($adapter); + $adapter->expects($this->any()) + ->method('fetchAll') + ->with($select) + ->willReturn([]); + $adapter->expects($this->any()) + ->method('fetchPairs') + ->with($select) + ->willReturn([]); + $multiDimensionProvider = $this->getMockBuilder(MultiDimensionProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $this->dimensionCollectionFactory->expects($this->exactly(2)) + ->method('create') + ->willReturn($multiDimensionProvider); + $iterator = new \ArrayObject([]); + $multiDimensionProvider->expects($this->exactly(2)) + ->method('getIterator') + ->willReturn($iterator); + $this->catalogProductType->expects($this->any()) + ->method('getTypesByPriority') + ->willReturn([]); + $this->actionRows->execute($ids); } } From 90372d9caa114b021447b0da1d793756617f9f1f Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Tue, 13 Oct 2020 19:16:30 -0500 Subject: [PATCH 0811/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php index dfeb5b6bfea26..acbe20721ee9e 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Rows.php @@ -20,6 +20,7 @@ /** * Class Rows reindex action for mass actions * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) to preserve compatibility with parent class */ class Rows extends \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction { From e3fd2463dba30032f6777c926bba96a40f53730c Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Wed, 14 Oct 2020 10:52:52 +0100 Subject: [PATCH 0812/1013] Small changes to increase performance --- .../Magento/Wishlist/Block/AddToWishlist.php | 73 ++++++++----------- .../frontend/layout/catalog_category_view.xml | 6 +- .../layout/catalogsearch_result_index.xml | 7 +- .../view/frontend/web/js/add-to-wishlist.js | 36 +++++---- 4 files changed, 56 insertions(+), 66 deletions(-) diff --git a/app/code/Magento/Wishlist/Block/AddToWishlist.php b/app/code/Magento/Wishlist/Block/AddToWishlist.php index dffd8cb027e74..0d4d403034f2a 100644 --- a/app/code/Magento/Wishlist/Block/AddToWishlist.php +++ b/app/code/Magento/Wishlist/Block/AddToWishlist.php @@ -6,7 +6,11 @@ namespace Magento\Wishlist\Block; +use Magento\Catalog\Api\Data\ProductTypeInterface; +use Magento\Catalog\Api\ProductTypeListInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; /** * Wishlist js plugin initialization block @@ -24,63 +28,50 @@ class AddToWishlist extends Template private $productTypes; /** - * Returns wishlist widget options - * - * @return array - * @since 100.1.0 + * @var ProductTypeListInterface */ - public function getWishlistOptions() - { - return [ - 'productType' => $this->getProductTypes(), - 'isProductList' => (bool)$this->getData('is_product_list') - ]; - } + private $productTypeList; /** - * Returns an array of product types - * - * @return array|null - * @throws \Magento\Framework\Exception\LocalizedException + * AddToWishlist constructor. + * @param ProductTypeListInterface $productTypeList + * @param Context $context + * @param array $data */ - private function getProductTypes() - { - if ($this->productTypes === null) { - $this->productTypes = []; - $block = $this->getLayout()->getBlock($this->getProductListBlockName()); - if ($block) { - $productCollection = $block->getLoadedProductCollection(); - $productTypes = []; - /** @var $product \Magento\Catalog\Model\Product */ - foreach ($productCollection as $product) { - $productTypes[] = $this->escapeHtml($product->getTypeId()); - } - $this->productTypes = array_unique($productTypes); - } - } - return $this->productTypes; + public function __construct( + Context $context, + array $data = [], + ?ProductTypeListInterface $productTypeList = null + ) { + parent::__construct($context, $data); + $this->productTypes = []; + $this->productTypeList = $productTypeList ?: ObjectManager::getInstance()->get(ProductTypeListInterface::class); } /** - * Get product list block name in layout + * Returns wishlist widget options * - * @return string + * @return array + * @since 100.1.0 */ - private function getProductListBlockName(): string + public function getWishlistOptions() { - return $this->getData('product_list_block') ?: 'category.products.list'; + return ['productType' => $this->getProductTypes()]; } /** - * @inheritDoc + * Returns an array of product types * - * @since 100.1.0 + * @return array */ - protected function _toHtml() + private function getProductTypes(): array { - if (!$this->getProductTypes()) { - return ''; + if (count($this->productTypes) === 0) { + /** @var ProductTypeInterface productTypes */ + $this->productTypes = array_map(function ($productType) { + return $productType->getName(); + }, $this->productTypeList->getProductTypes()); } - return parent::_toHtml(); + return $this->productTypes; } } diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml index c305b7c489d59..81bd966b904d7 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalog_category_view.xml @@ -23,11 +23,7 @@ <referenceContainer name="category.product.list.additional"> <block class="Magento\Wishlist\Block\AddToWishlist" name="category.product.list.additional.wishlist_addto" - template="Magento_Wishlist::addto.phtml"> - <arguments> - <argument name="is_product_list" xsi:type="boolean">true</argument> - </arguments> - </block> + template="Magento_Wishlist::addto.phtml"/> </referenceContainer> </referenceContainer> </body> diff --git a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml index b36a7cc2347af..b26aa64ad89b1 100644 --- a/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml +++ b/app/code/Magento/Wishlist/view/frontend/layout/catalogsearch_result_index.xml @@ -17,12 +17,7 @@ <referenceBlock name="wishlist_page_head_components"> <block class="Magento\Wishlist\Block\AddToWishlist" name="catalogsearch.wishlist_addto" - template="Magento_Wishlist::addto.phtml"> - <arguments> - <argument name="is_product_list" xsi:type="boolean">true</argument> - <argument name="product_list_block" xsi:type="string">search_result_list</argument> - </arguments> - </block> + template="Magento_Wishlist::addto.phtml"/> </referenceBlock> </body> </page> diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 727a9751cc2f6..38ed1f62cea66 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -18,8 +18,8 @@ define([ customOptionsInfo: '.product-custom-option', qtyInfo: '#qty', actionElement: '[data-action="add-to-wishlist"]', - productListItem: '.item.product-item', - isProductList: false + productListWrapper: '.product-item-info', + productPageWrapper: '.product-info-main' }, /** @inheritdoc */ @@ -67,23 +67,17 @@ define([ _updateWishlistData: function (event) { var dataToAdd = {}, isFileUploaded = false, - productItem = null, handleObjSelector = null, self = this; if (event.handleObj.selector == this.options.qtyInfo) { //eslint-disable-line eqeqeq - this._updateAddToWishlistButton({}, productItem); + this._updateAddToWishlistButton({}, event); event.stopPropagation(); return; } - if (this.options.isProductList) { - productItem = $(event.target).closest(this.options.productListItem); - handleObjSelector = productItem.find(event.handleObj.selector); - } else { - handleObjSelector = $(event.handleObj.selector); - } + handleObjSelector = $(event.currentTarget).closest('form').find(event.handleObj.selector) handleObjSelector.each(function (index, element) { if ($(element).is('input[type=text]') || @@ -110,18 +104,18 @@ define([ if (isFileUploaded) { this.bindFormSubmit(); } - this._updateAddToWishlistButton(dataToAdd, productItem); + this._updateAddToWishlistButton(dataToAdd, event); event.stopPropagation(); }, /** * @param {Object} dataToAdd - * @param {Object} productItem + * @param {jQuery.Event} event * @private */ - _updateAddToWishlistButton: function (dataToAdd, productItem) { + _updateAddToWishlistButton: function (dataToAdd, event) { var self = this, - buttons = productItem ? productItem.find(this.options.actionElement) : $(this.options.actionElement); + buttons = this._getAddToWishlistButton(event); buttons.each(function (index, element) { var params = $(element).data('post'); @@ -139,6 +133,20 @@ define([ }); }, + /** + * @param {jQuery.Event} event + * @private + */ + _getAddToWishlistButton: function (event) { + var productListWrapper = $(event.currentTarget).closest(this.options.productListWrapper); + + if (productListWrapper.length) { + return productListWrapper.find(this.options.actionElement); + } + + return $(event.currentTarget).closest(this.options.productPageWrapper).find(this.options.actionElement); + }, + /** * @param {Object} array1 * @param {Object} array2 From dd159ef462103a5b923a5115f5dfccd3e179a0e4 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Wed, 14 Oct 2020 16:11:14 +0530 Subject: [PATCH 0813/1013] 30349: Product filter with category_id does not work as expected - fixed query returns missing items --- .../CollectionProcessor/FilterProcessor/CategoryFilter.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php index 92888a2775e17..7fd4b4bfa8e35 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/SearchCriteria/CollectionProcessor/FilterProcessor/CategoryFilter.php @@ -51,6 +51,11 @@ public function __construct( */ public function apply(Filter $filter, AbstractDb $collection) { + $conditionType = $filter->getConditionType(); + if ($conditionType !== 'eq') { + return true; + } + $categoryIds = $filter->getValue(); if (!is_array($categoryIds)) { $categoryIds = [$categoryIds]; @@ -61,6 +66,7 @@ public function apply(Filter $filter, AbstractDb $collection) $category = $this->categoryFactory->create(); $this->categoryResourceModel->load($category, $categoryId); $categoryProducts[$categoryId] = $category->getProductCollection()->getAllIds(); + $collection->addCategoryFilter($category); } $categoryProductIds = array_unique(array_merge(...$categoryProducts)); From c40f065d7d518b8893188b11b7dd41fa46ad032c Mon Sep 17 00:00:00 2001 From: Munkh-Ulzii Balidar <mbalidar@comwrap.com> Date: Wed, 14 Oct 2020 13:44:26 +0200 Subject: [PATCH 0814/1013] 29251 fix product loading based row_id --- .../Magento/ConfigurableProduct/Helper/Data.php | 10 +++++++--- .../Model/Options/Collection.php | 15 ++++++++++----- .../Model/Variant/Collection.php | 17 ++++++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Helper/Data.php b/app/code/Magento/ConfigurableProduct/Helper/Data.php index a5fdcd62c7aa1..54b95bcd7bd90 100644 --- a/app/code/Magento/ConfigurableProduct/Helper/Data.php +++ b/app/code/Magento/ConfigurableProduct/Helper/Data.php @@ -7,9 +7,11 @@ namespace Magento\ConfigurableProduct\Helper; use Magento\Catalog\Model\Product\Image\UrlBuilder; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Framework\App\ObjectManager; use Magento\Catalog\Helper\Image as ImageHelper; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Image; /** @@ -73,7 +75,7 @@ public function getGalleryImages(ProductInterface $product) /** * Get Options for Configurable Product Options * - * @param \Magento\Catalog\Model\Product $currentProduct + * @param Product $currentProduct * @param array $allowedProducts * @return array */ @@ -100,11 +102,13 @@ public function getOptions($currentProduct, $allowedProducts) /** * Get allowed attributes * - * @param \Magento\Catalog\Model\Product $product + * @param Product $product * @return array */ public function getAllowAttributes($product) { - return $product->getTypeInstance()->getConfigurableAttributes($product); + return ($product->getTypeId() == Configurable::TYPE_CODE) + ? $product->getTypeInstance()->getConfigurableAttributes($product) + : []; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index c5c66a194503a..7a27964897828 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -186,8 +186,8 @@ private function fetch() : array = $this->selectionUidFormatter->encode((int)$attribute->getId(), (int)$value['value_index']); $this->attributeMap[$productId][$attribute->getId()]['values'][$index] ['is_available_for_selection'] = - isset($options[$attribute['attribute_id']][$value['value_index']]) - && $options[$attribute['attribute_id']][$value['value_index']]; + isset($options[$attribute->getAttributeId()][$value['value_index']]) + && $options[$attribute->getAttributeId()][$value['value_index']]; } } } @@ -196,16 +196,21 @@ private function fetch() : array } /** - * Load products by entity ids + * Load products by link field ids * * @param int[] $productIds * @return ProductInterface[] */ private function getProducts($productIds) { - $this->searchCriteriaBuilder->addFilter('entity_id', $productIds, 'in'); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $this->searchCriteriaBuilder->addFilter($linkField, $productIds, 'in'); $searchCriteria = $this->searchCriteriaBuilder->create(); $products = $this->productRepository->getList($searchCriteria)->getItems(); - return $products; + $productsLinkFieldMap = []; + foreach ($products as $product) { + $productsLinkFieldMap[$product->getData($linkField)] = $product; + } + return $productsLinkFieldMap; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index b60a660251f4d..cd6d78e5c3ffb 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection as ChildCollection; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; use Magento\Framework\EntityManager\MetadataPool; @@ -175,19 +176,21 @@ private function fetch(ContextInterface $context = null) : array } /** - * Get attributes code + * Get attributes codes for given product * - * @param \Magento\Catalog\Model\Product $currentProduct + * @param Product $currentProduct * @return array */ private function getAttributesCodes(Product $currentProduct): array { $attributeCodes = $this->attributeCodes; - $allowAttributes = $currentProduct->getTypeInstance()->getConfigurableAttributes($currentProduct); - foreach ($allowAttributes as $attribute) { - $productAttribute = $attribute->getProductAttribute(); - if (!\in_array($productAttribute->getAttributeCode(), $attributeCodes)) { - $attributeCodes[] = $productAttribute->getAttributeCode(); + if ($currentProduct->getTypeId() == Configurable::TYPE_CODE) { + $allowAttributes = $currentProduct->getTypeInstance()->getConfigurableAttributes($currentProduct); + foreach ($allowAttributes as $attribute) { + $productAttribute = $attribute->getProductAttribute(); + if (!\in_array($productAttribute->getAttributeCode(), $attributeCodes)) { + $attributeCodes[] = $productAttribute->getAttributeCode(); + } } } From 8d025339e6df3bce298c231e920ddb243d44c95b Mon Sep 17 00:00:00 2001 From: Gabriel Galvao da Gama <galvaoda@adobe.com> Date: Wed, 14 Oct 2020 14:25:00 +0100 Subject: [PATCH 0815/1013] Fixed static tests --- app/code/Magento/Wishlist/Block/AddToWishlist.php | 3 ++- .../Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Wishlist/Block/AddToWishlist.php b/app/code/Magento/Wishlist/Block/AddToWishlist.php index 0d4d403034f2a..7997a6ed99031 100644 --- a/app/code/Magento/Wishlist/Block/AddToWishlist.php +++ b/app/code/Magento/Wishlist/Block/AddToWishlist.php @@ -34,9 +34,10 @@ class AddToWishlist extends Template /** * AddToWishlist constructor. - * @param ProductTypeListInterface $productTypeList + * * @param Context $context * @param array $data + * @param ProductTypeListInterface|null $productTypeList */ public function __construct( Context $context, diff --git a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js index 38ed1f62cea66..62756f7211cee 100644 --- a/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js +++ b/app/code/Magento/Wishlist/view/frontend/web/js/add-to-wishlist.js @@ -77,7 +77,7 @@ define([ return; } - handleObjSelector = $(event.currentTarget).closest('form').find(event.handleObj.selector) + handleObjSelector = $(event.currentTarget).closest('form').find(event.handleObj.selector); handleObjSelector.each(function (index, element) { if ($(element).is('input[type=text]') || From 59c1bc64ece9510009383c7b21196f9054bd32c7 Mon Sep 17 00:00:00 2001 From: Munkh-Ulzii Balidar <mbalidar@comwrap.com> Date: Wed, 14 Oct 2020 16:03:51 +0200 Subject: [PATCH 0816/1013] 29251 update unit test belongs to change --- .../Test/Unit/Helper/DataTest.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php index db72e1ca6ab4c..963322e2f2c57 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php @@ -49,7 +49,7 @@ protected function setUp(): void ->getMock(); $this->_imageHelperMock = $this->createMock(Image::class); $this->_productMock = $this->createMock(Product::class); - + $this->_productMock->setTypeId(Configurable::TYPE_CODE); $this->_model = $objectManager->getObject( Data::class, [ @@ -66,6 +66,10 @@ public function testGetAllowAttributes() ->method('getConfigurableAttributes') ->with($this->_productMock); + $this->_productMock->expects($this->once()) + ->method('getTypeId') + ->willReturn(Configurable::TYPE_CODE); + $this->_productMock->expects($this->once()) ->method('getTypeInstance') ->willReturn($typeInstanceMock); @@ -119,7 +123,10 @@ public function getOptionsDataProvider() { $currentProductMock = $this->createPartialMock( Product::class, - ['getTypeInstance'] + [ + 'getTypeInstance', + 'getTypeId' + ] ); $provider = []; $provider[] = [ @@ -156,6 +163,9 @@ public function getOptionsDataProvider() $typeInstanceMock->expects($this->any()) ->method('getConfigurableAttributes') ->willReturn($attributes); + $currentProductMock->expects($this->any()) + ->method('getTypeId') + ->willReturn(Configurable::TYPE_CODE); $currentProductMock->expects($this->any()) ->method('getTypeInstance') ->willReturn($typeInstanceMock); From f4b7937cba82d62caf780f18bd7e51e93738ecf5 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 14 Oct 2020 10:05:08 -0500 Subject: [PATCH 0817/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Model/Indexer/Product/Price/Action/RowsTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php index 9fd86e81c7a51..44ac8b67d392b 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php @@ -25,6 +25,11 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Framework\MockObject\MockObject; +/** + * Test coverage for the rows action + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) to preserve compatibility with parent class + */ class RowsTest extends TestCase { /** @@ -172,13 +177,16 @@ public function testBatchProcessing() $this->dimensionCollectionFactory->expects($this->exactly(2)) ->method('create') ->willReturn($multiDimensionProvider); - $iterator = new \ArrayObject([]); + $iterator = new \ArrayIterator([]); $multiDimensionProvider->expects($this->exactly(2)) ->method('getIterator') ->willReturn($iterator); $this->catalogProductType->expects($this->any()) ->method('getTypesByPriority') ->willReturn([]); + $adapter->expects($this->any()) + ->method('getPrimaryKeyName') + ->willReturn(['COLUMNS_LIST'=>['entity_id']]); $this->actionRows->execute($ids); } } From 22692c74455f87e4a987e10d8686c7cd49f0426d Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 14 Oct 2020 12:19:42 -0500 Subject: [PATCH 0818/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Unit/Model/Indexer/Product/Price/Action/RowsTest.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php index 44ac8b67d392b..e6ef516ac0663 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php @@ -184,9 +184,12 @@ public function testBatchProcessing() $this->catalogProductType->expects($this->any()) ->method('getTypesByPriority') ->willReturn([]); - $adapter->expects($this->any()) + $adapter->expects($this->once()) + ->method('getIndexList') + ->willReturn(['entity_id'=>['COLUMNS_LIST'=>['test']]]); + $adapter->expects($this->once()) ->method('getPrimaryKeyName') - ->willReturn(['COLUMNS_LIST'=>['entity_id']]); + ->willReturn('entity_id'); $this->actionRows->execute($ids); } } From d10f1c881daed60d3d7bfae6cadf9fda7d54adb2 Mon Sep 17 00:00:00 2001 From: Yaroslav Bogutsky <y.bogutsky@vveb.pro> Date: Wed, 14 Oct 2020 21:04:55 +0300 Subject: [PATCH 0819/1013] Fixed placeholder translation in Magento UI grid search component --- app/code/Magento/Ui/view/base/web/js/grid/search/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js index ce53b23b79e11..3f5434761ba18 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/search/search.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/search/search.js @@ -19,7 +19,7 @@ define([ return Element.extend({ defaults: { template: 'ui/grid/search/search', - placeholder: 'Search by keyword', + placeholder: $t('Search by keyword'), label: $t('Keyword'), value: '', previews: [], From a1bf66a893c244293840a17365cdee3fac2b6fc9 Mon Sep 17 00:00:00 2001 From: Oleksandr Iegorov <oiegorov@adobe.com> Date: Wed, 14 Oct 2020 13:33:54 -0500 Subject: [PATCH 0820/1013] MC-38038: Partial reindex of prices causes empty categories (missed products) --- .../Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php index e6ef516ac0663..816dc923ebc0a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Price/Action/RowsTest.php @@ -184,10 +184,10 @@ public function testBatchProcessing() $this->catalogProductType->expects($this->any()) ->method('getTypesByPriority') ->willReturn([]); - $adapter->expects($this->once()) + $adapter->expects($this->exactly(2)) ->method('getIndexList') ->willReturn(['entity_id'=>['COLUMNS_LIST'=>['test']]]); - $adapter->expects($this->once()) + $adapter->expects($this->exactly(2)) ->method('getPrimaryKeyName') ->willReturn('entity_id'); $this->actionRows->execute($ids); From a56f2c5ea7fa7c67644b079d4048c737edabaacf Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Mon, 21 Sep 2020 01:33:14 -0500 Subject: [PATCH 0821/1013] MC-36954: [Magento Cloud] Coupon Code with single payment option - Adding key check with the PO --- .../OfflinePayments/Model/Purchaseorder.php | 5 +-- .../Plugin/ValidatePurchaseOrderNumber.php | 43 ++++++++++++++++++ .../Test/Unit/Model/PurchaseorderTest.php | 6 +-- .../Magento/OfflinePayments/composer.json | 3 +- app/code/Magento/OfflinePayments/etc/di.xml | 3 ++ ...etPurchaseOrderPaymentMethodOnCartTest.php | 17 ++++--- ...etPurchaseOrderPaymentMethodOnCartTest.php | 17 ++++--- .../Quote/Model/QuoteManagementTest.php | 44 +++++++++++++++++++ .../_files/quote_with_purchase_order.php | 43 ++++++++++++++++++ .../quote_with_purchase_order_rollback.php | 21 +++++++++ 10 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 app/code/Magento/OfflinePayments/Plugin/ValidatePurchaseOrderNumber.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order_rollback.php diff --git a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php index fe30570aba50d..21790f3ac20bb 100644 --- a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php +++ b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php @@ -10,6 +10,7 @@ /** * Class Purchaseorder * + * Update additional payments fields and validate the payment data * @method \Magento\Quote\Api\Data\PaymentMethodExtensionInterface getExtensionAttributes() * * @api @@ -68,10 +69,6 @@ public function validate() { parent::validate(); - if (empty($this->getInfoInstance()->getPoNumber())) { - throw new LocalizedException(__('Purchase order number is a required field.')); - } - return $this; } } diff --git a/app/code/Magento/OfflinePayments/Plugin/ValidatePurchaseOrderNumber.php b/app/code/Magento/OfflinePayments/Plugin/ValidatePurchaseOrderNumber.php new file mode 100644 index 0000000000000..18e80864f434b --- /dev/null +++ b/app/code/Magento/OfflinePayments/Plugin/ValidatePurchaseOrderNumber.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\OfflinePayments\Plugin; + +use Magento\Framework\Exception\LocalizedException; +use Magento\OfflinePayments\Model\Purchaseorder; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteManagement; + +/** + * Class ValidatePurchaseOrderNumber + * + * Validate purchase order number before submit order + */ +class ValidatePurchaseOrderNumber +{ + /** + * Before submitOrder plugin. + * + * @param QuoteManagement $subject + * @param Quote $quote + * @param array $orderData + * @return void + * @throws LocalizedException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSubmit( + QuoteManagement $subject, + Quote $quote, + array $orderData = [] + ): void { + $payment = $quote->getPayment(); + if ($payment->getMethod() === Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE + && empty($payment->getPoNumber())) { + throw new LocalizedException(__('Purchase order number is a required field.')); + } + } +} diff --git a/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php b/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php index c4c717550dbae..2bbaad03d4b87 100644 --- a/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php +++ b/app/code/Magento/OfflinePayments/Test/Unit/Model/PurchaseorderTest.php @@ -66,9 +66,6 @@ public function testAssignData() public function testValidate() { - $this->expectException(LocalizedException::class); - $this->expectExceptionMessage('Purchase order number is a required field.'); - $data = new DataObject([]); $addressMock = $this->getMockForAbstractClass(OrderAddressInterface::class); @@ -84,6 +81,7 @@ public function testValidate() $this->object->setData('info_instance', $instance); $this->object->assignData($data); - $this->object->validate(); + $result = $this->object->validate(); + $this->assertEquals($result, $this->object); } } diff --git a/app/code/Magento/OfflinePayments/composer.json b/app/code/Magento/OfflinePayments/composer.json index 56c7eb2778c48..237812a205130 100644 --- a/app/code/Magento/OfflinePayments/composer.json +++ b/app/code/Magento/OfflinePayments/composer.json @@ -8,7 +8,8 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-checkout": "*", - "magento/module-payment": "*" + "magento/module-payment": "*", + "magento/module-quote": "*" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/OfflinePayments/etc/di.xml b/app/code/Magento/OfflinePayments/etc/di.xml index 1e3d7cba3b86a..e0a2e250eadcd 100644 --- a/app/code/Magento/OfflinePayments/etc/di.xml +++ b/app/code/Magento/OfflinePayments/etc/di.xml @@ -13,4 +13,7 @@ </argument> </arguments> </type> + <type name="\Magento\Quote\Model\QuoteManagement"> + <plugin name="validate_purchase_order_number" type="Magento\OfflinePayments\Plugin\ValidatePurchaseOrderNumber"/> + </type> </config> diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php index 1777289afe5bc..69dc78b9d08d9 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -7,7 +7,6 @@ namespace Magento\GraphQl\Quote\Customer; -use Exception; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\OfflinePayments\Model\Purchaseorder; @@ -100,9 +99,6 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithSimpleProduct() */ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumber() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Purchase order number is a required field.'); - $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -122,7 +118,18 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumbe } } QUERY; - $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals( + $methodCode, + $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] + ); + self::assertArrayNotHasKey( + 'purchase_order_number', + $response['setPaymentMethodOnCart']['cart']['selected_payment_method'] + ); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php index 2c93a27012a01..121b04cc8ed11 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPurchaseOrderPaymentMethodOnCartTest.php @@ -7,7 +7,6 @@ namespace Magento\GraphQl\Quote\Guest; -use Exception; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\OfflinePayments\Model\Purchaseorder; use Magento\TestFramework\Helper\Bootstrap; @@ -91,9 +90,6 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithSimpleProduct() */ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumber() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Purchase order number is a required field.'); - $methodCode = Purchaseorder::PAYMENT_METHOD_PURCHASEORDER_CODE; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -113,7 +109,18 @@ public function testSetPurchaseOrderPaymentMethodOnCartWithoutPurchaseOrderNumbe } } QUERY; - $this->graphQlMutation($query); + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('setPaymentMethodOnCart', $response); + self::assertArrayHasKey('cart', $response['setPaymentMethodOnCart']); + self::assertArrayHasKey('selected_payment_method', $response['setPaymentMethodOnCart']['cart']); + self::assertEquals( + $methodCode, + $response['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] + ); + self::assertArrayNotHasKey( + 'purchase_order_number', + $response['setPaymentMethodOnCart']['cart']['selected_payment_method'] + ); } /** diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php index 26ae82120b2c7..c02d32430939a 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php @@ -13,6 +13,7 @@ use Magento\Customer\Model\Vat; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\StateException; use Magento\Framework\ObjectManagerInterface; @@ -134,6 +135,49 @@ public function testSubmitGuestCustomer(): void self::assertEquals(3, $quoteAfterOrderPlaced->getCustomerTaxClassId()); } + /** + * Creates order with purchase_order payment method + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Sales/_files/quote_with_purchase_order.php + * + * @return void + * @throws CouldNotSaveException + */ + public function testSubmitWithPurchaseOrder(): void + { + $paymentMethodName = 'purchaseorder'; + $poNumber = '12345678'; + $quote = $this->getQuoteByReservedOrderId->execute('test_order_1'); + $quote->getPayment()->setPoNumber($poNumber); + $quote->collectTotals()->save(); + $orderId = $this->cartManagement->placeOrder($quote->getId()); + $order = $this->orderRepository->get($orderId); + $orderItems = $order->getItems(); + $this->assertCount(1, $orderItems); + $payment = $order->getPayment(); + $this->assertEquals($paymentMethodName, $payment->getMethod()); + $this->assertEquals($poNumber, $payment->getPoNumber()); + } + + /** + * Creates order with purchase_order payment method without po_number + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Sales/_files/quote_with_purchase_order.php + * + * @return void + * @throws CouldNotSaveException + */ + public function testSubmitWithPurchaseOrderWithException(): void + { + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Purchase order number is a required field.'); + + $quote = $this->getQuoteByReservedOrderId->execute('test_order_1'); + $this->cartManagement->placeOrder($quote->getId()); + } + /** * Tries to create order with product that has child items and one of them was deleted. * diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order.php new file mode 100644 index 0000000000000..96a5484746da2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Model\Quote\Address\Rate; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address.php'); +/** @var QuoteFactory $quoteFactory */ +$quoteFactory = Bootstrap::getObjectManager()->get(QuoteFactory::class); +/** @var QuoteResource $quoteResource */ +$quoteResource = Bootstrap::getObjectManager()->get(QuoteResource::class); +$quote = $quoteFactory->create(); +$quoteResource->load($quote, 'test_order_1', 'reserved_order_id'); + +/** @var $rate Rate */ +$rate = Bootstrap::getObjectManager()->create( + Rate::class +); +$rate->setCode('freeshipping_freeshipping'); +$rate->getPrice(100); + +$quote->getShippingAddress()->setShippingMethod('freeshipping_freeshipping'); +$quote->getShippingAddress()->addShippingRate($rate); +$quote->getPayment()->setMethod('purchaseorder'); + +$quote->collectTotals(); +$quote->save(); +$quote->getPayment()->setMethod('purchaseorder'); + +$quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); +$quoteIdMask->setQuoteId($quote->getId()); +$quoteIdMask->setDataChanges(true); +$quoteIdMask->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order_rollback.php new file mode 100644 index 0000000000000..84d5ef051519c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote_with_purchase_order_rollback.php @@ -0,0 +1,21 @@ +<?php +/** + * Rollback for quote_with_purchase_order.php fixture. + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); + +/** @var $objectManager \Magento\TestFramework\ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); +$objectManager->get(Registry::class)->unregister('quote'); +$quote = $objectManager->create(Quote::class); +$quote->load('test_order_1', 'reserved_order_id')->delete(); From aa34315ab9c12729b6936905394d0f1280a115f9 Mon Sep 17 00:00:00 2001 From: Arnob Saha <arnobsh@gmail.com> Date: Wed, 23 Sep 2020 15:07:44 -0500 Subject: [PATCH 0822/1013] MC-37794: [REST] Order has wrong number of items shipped via REST API - Reverting the changes of MC-35633 in scope of MC-37794 --- app/code/Magento/Sales/Model/ShipOrder.php | 38 +-- .../Plugin/ProcessOrderAndShipmentViaAPI.php | 241 -------------- .../Sales/Test/Unit/Model/ShipOrderTest.php | 2 +- app/code/Magento/Sales/etc/webapi_rest/di.xml | 3 - app/code/Magento/Sales/etc/webapi_soap/di.xml | 3 - .../Sales/Service/V1/ShipmentCreateTest.php | 307 +++--------------- .../order_with_bundle_shipped_separately.php | 4 - 7 files changed, 65 insertions(+), 533 deletions(-) delete mode 100644 app/code/Magento/Sales/Plugin/ProcessOrderAndShipmentViaAPI.php diff --git a/app/code/Magento/Sales/Model/ShipOrder.php b/app/code/Magento/Sales/Model/ShipOrder.php index f955f6574a7b2..3bb8527d6e516 100644 --- a/app/code/Magento/Sales/Model/ShipOrder.php +++ b/app/code/Magento/Sales/Model/ShipOrder.php @@ -5,25 +5,15 @@ */ namespace Magento\Sales\Model; -use DomainException; use Magento\Framework\App\ResourceConnection; -use Magento\Sales\Api\Data\ShipmentCommentCreationInterface; -use Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface; -use Magento\Sales\Api\Data\ShipmentItemCreationInterface; -use Magento\Sales\Api\Data\ShipmentPackageCreationInterface; -use Magento\Sales\Api\Data\ShipmentTrackCreationInterface; -use Magento\Sales\Api\Exception\CouldNotShipExceptionInterface; -use Magento\Sales\Api\Exception\DocumentValidationExceptionInterface; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Api\ShipmentRepositoryInterface; use Magento\Sales\Api\ShipOrderInterface; -use Magento\Sales\Exception\CouldNotShipException; -use Magento\Sales\Exception\DocumentValidationException; use Magento\Sales\Model\Order\Config as OrderConfig; use Magento\Sales\Model\Order\OrderStateResolverInterface; +use Magento\Sales\Model\Order\ShipmentDocumentFactory; use Magento\Sales\Model\Order\Shipment\NotifierInterface; use Magento\Sales\Model\Order\Shipment\OrderRegistrarInterface; -use Magento\Sales\Model\Order\ShipmentDocumentFactory; use Magento\Sales\Model\Order\Validation\ShipOrderInterface as ShipOrderValidator; use Psr\Log\LoggerInterface; @@ -126,27 +116,29 @@ public function __construct( * Process the shipment and save shipment and order data * * @param int $orderId - * @param ShipmentItemCreationInterface[] $items + * @param \Magento\Sales\Api\Data\ShipmentItemCreationInterface[] $items * @param bool $notify * @param bool $appendComment - * @param ShipmentCommentCreationInterface|null $comment - * @param ShipmentTrackCreationInterface[] $tracks - * @param ShipmentPackageCreationInterface[] $packages - * @param ShipmentCreationArgumentsInterface|null $arguments + * @param \Magento\Sales\Api\Data\ShipmentCommentCreationInterface|null $comment + * @param \Magento\Sales\Api\Data\ShipmentTrackCreationInterface[] $tracks + * @param \Magento\Sales\Api\Data\ShipmentPackageCreationInterface[] $packages + * @param \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface|null $arguments * @return int - * @throws DocumentValidationExceptionInterface - * @throws CouldNotShipExceptionInterface - * @throws DomainException + * @throws \Magento\Sales\Api\Exception\DocumentValidationExceptionInterface + * @throws \Magento\Sales\Api\Exception\CouldNotShipExceptionInterface + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \DomainException */ public function execute( $orderId, array $items = [], $notify = false, $appendComment = false, - ShipmentCommentCreationInterface $comment = null, + \Magento\Sales\Api\Data\ShipmentCommentCreationInterface $comment = null, array $tracks = [], array $packages = [], - ShipmentCreationArgumentsInterface $arguments = null + \Magento\Sales\Api\Data\ShipmentCreationArgumentsInterface $arguments = null ) { $connection = $this->resourceConnection->getConnection('sales'); $order = $this->orderRepository->get($orderId); @@ -170,7 +162,7 @@ public function execute( $packages ); if ($validationMessages->hasMessages()) { - throw new DocumentValidationException( + throw new \Magento\Sales\Exception\DocumentValidationException( __("Shipment Document Validation Error(s):\n" . implode("\n", $validationMessages->getMessages())) ); } @@ -189,7 +181,7 @@ public function execute( } catch (\Exception $e) { $this->logger->critical($e); $connection->rollBack(); - throw new CouldNotShipException( + throw new \Magento\Sales\Exception\CouldNotShipException( __('Could not save a shipment, see error log for details') ); } diff --git a/app/code/Magento/Sales/Plugin/ProcessOrderAndShipmentViaAPI.php b/app/code/Magento/Sales/Plugin/ProcessOrderAndShipmentViaAPI.php deleted file mode 100644 index 2f81de65fad74..0000000000000 --- a/app/code/Magento/Sales/Plugin/ProcessOrderAndShipmentViaAPI.php +++ /dev/null @@ -1,241 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Sales\Plugin; - -use Exception; -use Magento\Framework\DB\Transaction; -use Magento\Framework\Exception\LocalizedException; -use Magento\Sales\Api\Data\ShipmentInterface; -use Magento\Sales\Model\Order; -use Magento\Sales\Model\Order\Shipment\Item; -use Magento\Sales\Model\Order\ShipmentRepository; -use Magento\Shipping\Controller\Adminhtml\Order\ShipmentLoader; - -/** - * Plugin to update order data before and after saving shipment via API - */ -class ProcessOrderAndShipmentViaAPI -{ - /** - * @var ShipmentLoader - */ - private $shipmentLoader; - - /** - * @var Transaction - */ - private $transaction; - - /** - * Init plugin - * - * @param ShipmentLoader $shipmentLoader - * @param Transaction $transaction - */ - public function __construct( - ShipmentLoader $shipmentLoader, - Transaction $transaction - ) { - $this->shipmentLoader = $shipmentLoader; - $this->transaction = $transaction; - } - - /** - * Process shipping details before saving shipment via API - * - * @param ShipmentRepository $shipmentRepository - * @param ShipmentInterface $shipmentData - * @return array - * @throws LocalizedException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @SuppressWarnings(PHPMD.NPathComplexity) - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - public function beforeSave( - ShipmentRepository $shipmentRepository, - ShipmentInterface $shipmentData - ): array { - $this->shipmentLoader->setOrderId($shipmentData->getOrderId()); - $trackData = !empty($shipmentData->getTracks()) ? - $this->getShipmentTracking($shipmentData) : []; - $this->shipmentLoader->setTracking($trackData); - $shipmentItems = !empty($shipmentData) ? - $this->getShipmentItems($shipmentData) : []; - $orderItems = []; - if (!empty($shipmentData)) { - $order = $shipmentData->getOrder(); - $orderItems = $order ? $this->getOrderItems($order) : []; - } - $data = (!empty($shipmentItems) && !empty($orderItems)) ? - $this->getShippingData($shipmentItems, $orderItems) : []; - $this->shipmentLoader->setShipment($data); - $shipment = $this->shipmentLoader->load(); - $shipment = empty($shipment) ? $shipmentData - : $this->processShippingDetails($shipmentData, $shipment); - return [$shipment]; - } - - /** - * Save order data after saving shipment via API - * - * @param ShipmentRepository $shipmentRepository - * @param ShipmentInterface $shipment - * @return ShipmentInterface - * @throws Exception - */ - public function afterSave( - ShipmentRepository $shipmentRepository, - ShipmentInterface $shipment - ): ShipmentInterface { - $shipmentDetails = $shipmentRepository->get($shipment->getEntityId()); - $order = $shipmentDetails->getOrder(); - $shipmentItems = !empty($shipment) ? - $this->getShipmentItems($shipment) : []; - $this->processOrderItems($order, $shipmentItems); - $order->setIsInProcess(true); - $this->transaction - ->addObject($order) - ->save(); - return $shipment; - } - - /** - * Process shipment items - * - * @param ShipmentInterface $shipment - * @return array - * @throws LocalizedException - */ - private function getShipmentItems(ShipmentInterface $shipment): array - { - $shipmentItems = []; - foreach ($shipment->getItems() as $item) { - $sku = $item->getSku(); - if (isset($sku)) { - $shipmentItems[$sku]['qty'] = $item->getQty(); - } - } - return $shipmentItems; - } - - /** - * Get shipment tracking data from the shipment array - * - * @param ShipmentInterface $shipment - * @return array - */ - private function getShipmentTracking(ShipmentInterface $shipment): array - { - $trackData = []; - foreach ($shipment->getTracks() as $key => $track) { - $trackData[$key]['number'] = $track->getTrackNumber(); - $trackData[$key]['title'] = $track->getTitle(); - $trackData[$key]['carrier_code'] = $track->getCarrierCode(); - } - return $trackData; - } - - /** - * Get orderItems from shipment order - * - * @param Order $order - * @return array - */ - private function getOrderItems(Order $order): array - { - $orderItems = []; - foreach ($order->getItems() as $item) { - $orderItems[$item->getSku()] = $item->getItemId(); - } - return $orderItems; - } - - /** - * Get available shipping data from shippingItems and orderItems - * - * @param array $shipmentItems - * @param array $orderItems - * @return array - * @throws LocalizedException - */ - private function getShippingData(array $shipmentItems, array $orderItems): array - { - $data = []; - foreach ($shipmentItems as $shippingItemSku => $shipmentItem) { - if (isset($orderItems[$shippingItemSku])) { - $itemId = (int) $orderItems[$shippingItemSku]; - $data['items'][$itemId] = $shipmentItem['qty']; - } - } - return $data; - } - - /** - * Process shipping comments if available - * - * @param ShipmentInterface $shipmentData - * @param ShipmentInterface $shipment - * @return void - */ - private function processShippingComments(ShipmentInterface $shipmentData, ShipmentInterface $shipment): void - { - foreach ($shipmentData->getComments() as $comment) { - $shipment->addComment( - $comment->getComment(), - $comment->getIsCustomerNotified(), - $comment->getIsVisibleOnFront() - ); - $shipment->setCustomerNote($comment->getComment()); - $shipment->setCustomerNoteNotify((bool) $comment->getIsCustomerNotified()); - } - } - - /** - * Process shipping details - * - * @param ShipmentInterface $shipmentData - * @param ShipmentInterface $shipment - * @return ShipmentInterface - */ - private function processShippingDetails( - ShipmentInterface $shipmentData, - ShipmentInterface $shipment - ): ShipmentInterface { - if (empty($shipment->getItems())) { - $shipment->setItems($shipmentData->getItems()); - } - if (!empty($shipmentData->getComments())) { - $this->processShippingComments($shipmentData, $shipment); - } - if ((int) $shipment->getTotalQty() < 1) { - $shipment->setTotalQty($shipmentData->getTotalQty()); - } - return $shipment; - } - - /** - * Process order items data and set the proper item qty - * - * @param Order $order - * @param array $shipmentItems - * @throws LocalizedException - */ - private function processOrderItems(Order $order, array $shipmentItems): void - { - /** @var Item $item */ - foreach ($order->getAllItems() as $item) { - if (isset($shipmentItems[$item->getSku()])) { - $qty = (float)$shipmentItems[$item->getSku()]['qty']; - $item->setQty($qty); - if ((float)$item->getQtyToShip() > 0) { - $item->setQtyShipped((float)$item->getQtyToShip()); - } - } - } - } -} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php index 77cd6a058df6f..a31b79fcb0c5c 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ShipOrderTest.php @@ -189,7 +189,7 @@ protected function setUp(): void ->getMockForAbstractClass(); $this->shipOrderValidatorMock = $this->getMockBuilder(ShipOrderInterface::class) ->disableOriginalConstructor() - ->getMock(); + ->getMockForAbstractClass(); $this->validationMessagesMock = $this->getMockBuilder(ValidatorResultInterface::class) ->disableOriginalConstructor() ->setMethods(['hasMessages', 'getMessages', 'addMessage']) diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 1a8478438b04a..5d7838297a7c7 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -19,7 +19,4 @@ </argument> </arguments> </type> - <type name="Magento\Sales\Model\Order\ShipmentRepository"> - <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> - </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 1a8478438b04a..5d7838297a7c7 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -19,7 +19,4 @@ </argument> </arguments> </type> - <type name="Magento\Sales\Model\Order\ShipmentRepository"> - <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> - </type> </config> diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php index 29a11f9d68e8f..dab4ad05f84d3 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/ShipmentCreateTest.php @@ -6,10 +6,6 @@ namespace Magento\Sales\Service\V1; -use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\Webapi\Rest\Request; -use Magento\Sales\Model\Order; -use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -26,78 +22,23 @@ class ShipmentCreateTest extends WebapiAbstract const SERVICE_VERSION = 'V1'; /** - * @var ObjectManagerInterface + * @var \Magento\Framework\ObjectManagerInterface */ protected $objectManager; protected function setUp(): void { - $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); } /** - * Test save shipment return valid result with multiple tracks with multiple comments - * * @magentoApiDataFixture Magento/Sales/_files/order.php */ - public function testInvokeWithMultipleTrackAndComments() + public function testInvoke() { - $data = $this->getEntityData(); - $result = $this->_webApiCall( - $this->getServiceInfo(), - [ - 'entity' => $data['shipment data with multiple tracking and multiple comments']] - ); - $this->assertNotEmpty($result); - $this->assertEquals(3, count($result['tracks'])); - $this->assertEquals(3, count($result['comments'])); - } - - /** - * Test save shipment return valid result with multiple tracks with no comments - * - * @magentoApiDataFixture Magento/Sales/_files/order.php - */ - public function testInvokeWithMultipleTrackAndNoComments() - { - $data = $this->getEntityData(); - $result = $this->_webApiCall( - $this->getServiceInfo(), - [ - 'entity' => $data['shipment data with multiple tracking']] - ); - $this->assertNotEmpty($result); - $this->assertEquals(3, count($result['tracks'])); - $this->assertEquals(0, count($result['comments'])); - } - - /** - * Test save shipment return valid result with no tracks with multiple comments - * - * @magentoApiDataFixture Magento/Sales/_files/order.php - */ - public function testInvokeWithNoTrackAndMultipleComments() - { - $data = $this->getEntityData(); - $result = $this->_webApiCall( - $this->getServiceInfo(), - [ - 'entity' => $data['shipment data with multiple comments']] - ); - $this->assertNotEmpty($result); - $this->assertEquals(0, count($result['tracks'])); - $this->assertEquals(3, count($result['comments'])); - } - - /** - * @return array - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function getEntityData() - { - $existingOrder = $this->getOrder('100000001'); - $orderItem = current($existingOrder->getAllItems()); - + /** @var \Magento\Sales\Model\Order $order */ + $order = $this->objectManager->create(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); + $orderItem = current($order->getAllItems()); $items = [ [ 'order_item_id' => $orderItem->getId(), @@ -114,201 +55,10 @@ public function getEntityData() 'weight' => null, ], ]; - return [ - 'shipment data with multiple tracking and multiple comments' => [ - 'order_id' => $existingOrder->getId(), - 'entity_id' => null, - 'store_id' => null, - 'total_weight' => null, - 'total_qty' => null, - 'email_sent' => null, - 'customer_id' => null, - 'shipping_address_id' => null, - 'billing_address_id' => null, - 'shipment_status' => null, - 'increment_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'shipping_label' => null, - 'tracks' => [ - [ - 'carrier_code' => 'UPS', - 'order_id' => $existingOrder->getId(), - 'title' => 'ground', - 'description' => null, - 'track_number' => '12345678', - 'parent_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'qty' => null, - 'weight' => null - ], - [ - 'carrier_code' => 'UPS', - 'order_id' => $existingOrder->getId(), - 'title' => 'ground', - 'description' => null, - 'track_number' => '654563221', - 'parent_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'qty' => null, - 'weight' => null - ], - [ - 'carrier_code' => 'USPS', - 'order_id' => $existingOrder->getId(), - 'title' => 'ground', - 'description' => null, - 'track_number' => '789654565', - 'parent_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'qty' => null, - 'weight' => null - ] - ], - 'items' => $items, - 'comments' => [ - [ - 'comment' => 'Shipment-related comment-1.', - 'is_customer_notified' => null, - 'is_visible_on_front' => null, - 'parent_id' => null - ], - [ - 'comment' => 'Shipment-related comment-2.', - 'is_customer_notified' => null, - 'is_visible_on_front' => null, - 'parent_id' => null - ], - [ - 'comment' => 'Shipment-related comment-3.', - 'is_customer_notified' => null, - 'is_visible_on_front' => null, - 'parent_id' => null - ] - - ] - ], - 'shipment data with multiple tracking' => [ - 'order_id' => $existingOrder->getId(), - 'entity_id' => null, - 'store_id' => null, - 'total_weight' => null, - 'total_qty' => null, - 'email_sent' => null, - 'customer_id' => null, - 'shipping_address_id' => null, - 'billing_address_id' => null, - 'shipment_status' => null, - 'increment_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'shipping_label' => null, - 'tracks' => [ - [ - 'carrier_code' => 'UPS', - 'order_id' => $existingOrder->getId(), - 'title' => 'ground', - 'description' => null, - 'track_number' => '12345678', - 'parent_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'qty' => null, - 'weight' => null - ], - [ - 'carrier_code' => 'UPS', - 'order_id' => $existingOrder->getId(), - 'title' => 'ground', - 'description' => null, - 'track_number' => '654563221', - 'parent_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'qty' => null, - 'weight' => null - ], - [ - 'carrier_code' => 'USPS', - 'order_id' => $existingOrder->getId(), - 'title' => 'ground', - 'description' => null, - 'track_number' => '789654565', - 'parent_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'qty' => null, - 'weight' => null - ] - ], - 'items' => $items, - 'comments' => [] - ], - 'shipment data with multiple comments' => [ - 'order_id' => $existingOrder->getId(), - 'entity_id' => null, - 'store_id' => null, - 'total_weight' => null, - 'total_qty' => null, - 'email_sent' => null, - 'customer_id' => null, - 'shipping_address_id' => null, - 'billing_address_id' => null, - 'shipment_status' => null, - 'increment_id' => null, - 'created_at' => null, - 'updated_at' => null, - 'shipping_label' => null, - 'tracks' => [], - 'items' => $items, - 'comments' => [ - [ - 'comment' => 'Shipment-related comment-1.', - 'is_customer_notified' => null, - 'is_visible_on_front' => null, - 'parent_id' => null - ], - [ - 'comment' => 'Shipment-related comment-2.', - 'is_customer_notified' => null, - 'is_visible_on_front' => null, - 'parent_id' => null - ], - [ - 'comment' => 'Shipment-related comment-3.', - 'is_customer_notified' => null, - 'is_visible_on_front' => null, - 'parent_id' => null - ] - - ] - ] - ]; - } - - /** - * Returns order by increment id. - * - * @param string $incrementId - * @return Order - */ - private function getOrder(string $incrementId): Order - { - return $this->objectManager->create(Order::class)->loadByIncrementId($incrementId); - } - - /** - * @return array - */ - private function getServiceInfo(): array - { - return [ + $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => Request::HTTP_METHOD_POST, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_READ_NAME, @@ -316,5 +66,46 @@ private function getServiceInfo(): array 'operation' => self::SERVICE_READ_NAME . 'save', ], ]; + $data = [ + 'order_id' => $order->getId(), + 'entity_id' => null, + 'store_id' => null, + 'total_weight' => null, + 'total_qty' => null, + 'email_sent' => null, + 'customer_id' => null, + 'shipping_address_id' => null, + 'billing_address_id' => null, + 'shipment_status' => null, + 'increment_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'shipping_label' => null, + 'tracks' => [ + [ + 'carrier_code' => 'UPS', + 'order_id' => $order->getId(), + 'title' => 'ground', + 'description' => null, + 'track_number' => '12345678', + 'parent_id' => null, + 'created_at' => null, + 'updated_at' => null, + 'qty' => null, + 'weight' => null + ] + ], + 'items' => $items, + 'comments' => [ + [ + 'comment' => 'Shipment-related comment.', + 'is_customer_notified' => null, + 'is_visible_on_front' => null, + 'parent_id' => null + ] + ], + ]; + $result = $this->_webApiCall($serviceInfo, ['entity' => $data]); + $this->assertNotEmpty($result); } } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php index e5f089ae9637c..b91d479cdf1ef 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/order_with_bundle_shipped_separately.php @@ -155,8 +155,6 @@ /** @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()); @@ -174,8 +172,6 @@ /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); $orderItem->setProductId($productId); - $orderItem->setSku($selectedProduct->getSku()); - $orderItem->setName($selectedProduct->getName()); $orderItem->setQtyOrdered(1); $orderItem->setBasePrice($selectedProduct->getPrice()); $orderItem->setPrice($selectedProduct->getPrice()); From 6b39ef29532e070857bae7172daf444199db8dc2 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Thu, 15 Oct 2020 09:38:03 +0300 Subject: [PATCH 0823/1013] MC-38269: [CLARIFICATION] [Magento Cloud] - Persistent Shopping cart Header Weelcome & Not you? --- .../Persistent/view/frontend/web/js/view/additional-welcome.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js index 2f5c42f090d18..8e69325860167 100644 --- a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -40,7 +40,7 @@ define([ $(this).attr('data-bind', html); $(this).html(html); - $(this).after('<span><a ' + window.notYouLink + '>' + $t('Not you?') + '</a></span>'); + $(this).after(' <span><a ' + window.notYouLink + '>' + $t('Not you?') + '</a></span>'); }); } } From 959047dcf84fea2a9bd0af28e6aaf6b55f38ff4c Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Thu, 15 Oct 2020 09:58:26 +0300 Subject: [PATCH 0824/1013] MC-36956: Create automated test for "Upload Category Image" --- .../AdminUploadCategoryImageTest.xml | 44 ++++++++++++++++++ .../AdminEnhancedMediaGalleryImageData.xml | 4 ++ .../acceptance/tests/_data/magento-logo_2.png | Bin 0 -> 3885 bytes 3 files changed, 48 insertions(+) create mode 100644 app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml create mode 100644 dev/tests/acceptance/tests/_data/magento-logo_2.png diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.xml new file mode 100644 index 0000000000000..d431a0c3e40ed --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminUploadCategoryImageTest.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="AdminUploadCategoryImageTest"> + <annotations> + <features value="Catalog"/> + <stories value="Add/remove images and videos for all product types and category"/> + <title value="Upload Category Image"/> + <description value="The test verifies uploading images including a special case of image name with spaces"/> + <severity value="MAJOR"/> + <testCaseId value="MC-26112"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!--Go to created category admin page and upload image--> + <actionGroup ref="GoToAdminCategoryPageByIdActionGroup" stepKey="goToAdminCategoryPage"> + <argument name="id" value="$createCategory.id$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="CheckCategoryImageInAdminActionGroup" stepKey="checkCategoryImageInAdmin"/> + <!--Remove and upload new image--> + <actionGroup ref="RemoveCategoryImageActionGroup" stepKey="removeCategoryImage"/> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImageAgain"> + <argument name="image" value="ImageUploadPngTwo"/> + </actionGroup> + <actionGroup ref="CheckCategoryImageInAdminActionGroup" stepKey="checkCategoryImageInAdminAgain"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml index 4adf92b1c4c09..7948ed13be088 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml @@ -40,4 +40,8 @@ <data key="extension">jpg</data> <data key="keywords">magento, mediagallerymetadata</data> </entity> + <entity name="ImageUploadPngTwo" type="image"> + <data key="file">magento-logo_2.png</data> + <data key="extension">png</data> + </entity> </entities> diff --git a/dev/tests/acceptance/tests/_data/magento-logo_2.png b/dev/tests/acceptance/tests/_data/magento-logo_2.png new file mode 100644 index 0000000000000000000000000000000000000000..24640e7e37e3d9a74b0cb393c570b98bc3ce3379 GIT binary patch literal 3885 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 literal 0 HcmV?d00001 From 603dba357ddaeaf3c6767595ccd22d491c351eb0 Mon Sep 17 00:00:00 2001 From: SmVladyslav <vlatame.tsg@gmail.com> Date: Thu, 15 Oct 2020 10:40:58 +0300 Subject: [PATCH 0825/1013] MC-35783: "1 item(s) need your attention." still visible in mini cart after product remove --- .../Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml index 668d33d26f37a..dcab48dbc5368 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml @@ -39,6 +39,7 @@ <element name="emptyMiniCart" type="text" selector="//div[@class='minicart-wrapper']//span[@class='counter qty empty']/../.."/> <element name="minicartContent" type="block" selector="#minicart-content-wrapper"/> <element name="messageEmptyCart" type="text" selector="//*[@id='minicart-content-wrapper']//*[contains(@class,'subtitle empty')]"/> + <element name="emptyCartMessageContent" type="text" selector="#minicart-content-wrapper .minicart.empty.text"/> <element name="visibleItemsCountText" type="text" selector="//div[@class='items-total']"/> <element name="productQuantity" type="input" selector="//*[@id='mini-cart']//a[contains(text(),'{{productName}}')]/../..//div[@class='details-qty qty']//input[@data-item-qty='{{qty}}']" parameterized="true"/> <element name="productImage" type="text" selector="//ol[@id='mini-cart']//img[@class='product-image-photo']"/> From e4c227d3caf825c787d4d84dc1e25f55177d4656 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 15 Oct 2020 11:44:07 +0300 Subject: [PATCH 0826/1013] MC-37070: Create automated test for "Import products with shared images" --- .../Catalog/Model/Product/Gallery/UpdateHandlerTest.php | 5 ++++- .../Model/Import/ImportWithSharedImagesTest.php | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index c5221f1ae5e76..2d94466939dbe 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -86,12 +86,15 @@ class UpdateHandlerTest extends \PHPUnit\Framework\TestCase * @var StoreManagerInterface */ private $storeManager; + /** * @var int */ private $currentStoreId; - /** @var MetadataPool */ + /** + * @var MetadataPool + */ private $metadataPool; /** diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php index 4c04e5a8814e5..35d4cceb50845 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ImportWithSharedImagesTest.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product as ProductEntity; use Magento\Catalog\Model\Product\Media\ConfigInterface; +use Magento\Framework\App\Bootstrap as AppBootstrap; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; @@ -82,7 +83,7 @@ protected function setUp(): void $this->csvFactory = $this->objectManager->get(CsvFactory::class); $this->importDataResource = $this->objectManager->get(Data::class); $this->appParams = Bootstrap::getInstance()->getBootstrap()->getApplication() - ->getInitParams()[\Magento\Framework\App\Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS]; + ->getInitParams()[AppBootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS]; } /** From 43e5b5c901f27478134fe6b2203648684498bb79 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Thu, 15 Oct 2020 11:50:32 +0300 Subject: [PATCH 0827/1013] MC-36960: Create automated test for "Create product for "all" store views using API service" --- .../ProductRepositoryAllStoreViewsTest.php | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php index 2950dda4b3c52..2814a6ab05321 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php @@ -8,11 +8,13 @@ namespace Magento\Catalog\Api; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ResourceModel\Product\Website\Link; use Magento\Eav\Model\Config; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\Webapi\Rest\Request; @@ -24,6 +26,7 @@ * Tests for products creation for all store views. * * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductRepositoryAllStoreViewsTest extends WebapiAbstract { @@ -55,6 +58,7 @@ class ProductRepositoryAllStoreViewsTest extends WebapiAbstract * @var Link */ private $productWebsiteLink; + /** * @var Config */ @@ -87,9 +91,11 @@ protected function tearDown(): void { $this->registry->unregister('isSecureArea'); $this->registry->register('isSecureArea', true); - $this->productRepository->delete( - $this->productRepository->get($this->productSku) - ); + try { + $this->productRepository->deleteById($this->productSku); + } catch (NoSuchEntityException $e) { + //already deleted + } $this->registry->unregister('isSecureArea'); $this->registry->register('isSecureArea', false); @@ -98,6 +104,7 @@ protected function tearDown(): void /** * @magentoApiDataFixture Magento/Catalog/_files/category.php + * @return void */ public function testCreateProduct(): void { @@ -110,6 +117,7 @@ public function testCreateProduct(): void /** * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @return void */ public function testCreateProductOnMultipleWebsites(): void { @@ -120,12 +128,12 @@ public function testCreateProductOnMultipleWebsites(): void } /** - * Saves Product via API. + * Saves product via API. * - * @param $product + * @param array $product * @return array */ - private function saveProduct($product): array + private function saveProduct(array $product): array { $serviceInfo = [ 'rest' => ['resourcePath' =>self::PRODUCTS_RESOURCE_PATH, 'httpMethod' => Request::HTTP_METHOD_POST], @@ -146,22 +154,22 @@ private function saveProduct($product): array */ private function getProductData(): array { - $setId =(int)$this->eavConfig->getEntityType(ProductAttributeInterface::ENTITY_TYPE_CODE) + $setId = (int)$this->eavConfig->getEntityType(ProductAttributeInterface::ENTITY_TYPE_CODE) ->getDefaultAttributeSetId(); return [ - 'sku' => $this->productSku, - 'name' => 'simple', - 'type_id' => Type::TYPE_SIMPLE, - 'weight' => 1, - 'attribute_set_id' => $setId, - 'price' => 10, - 'status' => Status::STATUS_ENABLED, - 'visibility' => Visibility::VISIBILITY_BOTH, - 'extension_attributes' => [ + ProductInterface::SKU => $this->productSku, + ProductInterface::NAME => 'simple', + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::WEIGHT => 1, + ProductInterface::ATTRIBUTE_SET_ID => $setId, + ProductInterface::PRICE => 10, + ProductInterface::STATUS => Status::STATUS_ENABLED, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::EXTENSION_ATTRIBUTES_KEY => [ 'stock_item' => ['is_in_stock' => true, 'qty' => 1000] ], - 'custom_attributes' => [ + ProductInterface::CUSTOM_ATTRIBUTES => [ ['attribute_code' => 'url_key', 'value' => 'simple'], ['attribute_code' => 'tax_class_id', 'value' => 2], ['attribute_code' => 'category_ids', 'value' => [333]] @@ -219,8 +227,7 @@ private function assertProductData(array $productData, array $resultData, array private function getAllWebsiteIds(): array { $websiteIds = []; - $websites = $this->storeManager->getWebsites(); - foreach ($websites as $website) { + foreach ($this->storeManager->getWebsites() as $website) { $websiteIds[] = $website->getId(); } From 0587310da6dd45e255df1b48aa4c6c829b166107 Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Thu, 15 Oct 2020 12:57:59 +0300 Subject: [PATCH 0828/1013] MC-37665: Updating a category through the REST API will uncheck "Use Default Value" on a bunch of attributes --- .../Catalog/Model/CategoryRepository.php | 4 +- .../CategoryRepository/PopulateWithValues.php | 56 ++++++++++++------- .../Unit/Model/CategoryRepositoryTest.php | 9 ++- .../Catalog/Api/CategoryRepositoryTest.php | 20 +++---- 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository.php b/app/code/Magento/Catalog/Model/CategoryRepository.php index fe3ae4cc468a1..7082fa4747fdc 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository.php @@ -71,13 +71,13 @@ class CategoryRepository implements \Magento\Catalog\Api\CategoryRepositoryInter * @param CategoryFactory $categoryFactory * @param CategoryResource $categoryResource * @param StoreManagerInterface $storeManager - * @param PopulateWithValues $populateWithValues + * @param PopulateWithValues|null $populateWithValues */ public function __construct( CategoryFactory $categoryFactory, CategoryResource $categoryResource, StoreManagerInterface $storeManager, - PopulateWithValues $populateWithValues + ?PopulateWithValues $populateWithValues ) { $this->categoryFactory = $categoryFactory; $this->categoryResource = $categoryResource; diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php index 410aa3db1f255..2a313119f9c8e 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -69,30 +69,46 @@ public function __construct( public function execute(CategoryInterface $category, array $existingData): void { $storeId = $existingData['store_id']; - $overriddenValues = array_filter($category->getData(), function ($key) use ($category, $storeId) { - /** @var Category $category */ - return $this->scopeOverriddenValue->containsValue( - CategoryInterface::class, - $category, - $key, - $storeId - ); - }, ARRAY_FILTER_USE_KEY); + $overriddenValues = array_filter( + $category->getData(), + function ($key) use ($category, $storeId) { + /** @var Category $category */ + return $this->scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + $key, + $storeId + ); + }, + ARRAY_FILTER_USE_KEY + ); $defaultValues = array_diff_key($category->getData(), $overriddenValues); - array_walk($defaultValues, function (&$value, $key) { - $attributes = $this->getAttributes(); - if (isset($attributes[$key]) && !$attributes[$key]->isStatic()) { - $value = null; + array_walk( + $defaultValues, + function (&$value, $key) { + $attributes = $this->getAttributes(); + if (isset($attributes[$key]) && !$attributes[$key]->isStatic()) { + $value = null; + } } - }); + ); $category->addData($defaultValues); $category->addData($existingData); - $useDefaultAttributes = array_filter($category->getData(), function ($attributeValue) { - return null === $attributeValue; - }); - $category->setData('use_default', array_map(function () { - return true; - }, $useDefaultAttributes)); + $useDefaultAttributes = array_filter( + $category->getData(), + function ($attributeValue) { + return null === $attributeValue; + } + ); + $category->setData( + 'use_default', + array_map( + function () { + return true; + }, + $useDefaultAttributes + ) + ); } /** diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php index 61e8133da5759..8274ed9da5f32 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryRepositoryTest.php @@ -65,10 +65,13 @@ class CategoryRepositoryTest extends TestCase protected $metadataPoolMock; /** - * @var MockObject + * @var PopulateWithValues|MockObject */ - protected $populateWithValuesMock; + private $populateWithValuesMock; + /** + * @inheridoc + */ protected function setUp(): void { $this->categoryFactoryMock = $this->createPartialMock( @@ -102,7 +105,7 @@ protected function setUp(): void $this->populateWithValuesMock = $this ->getMockBuilder(PopulateWithValues::class) - ->setMethods(['execute']) + ->onlyMethods(['execute']) ->disableOriginalConstructor() ->getMock(); 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 e7d47ff64a109..6ce922eab21f1 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -12,6 +12,7 @@ use Magento\Authorization\Model\RulesFactory; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; +use Magento\Catalog\Model\Category; use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\Store\Model\Store; @@ -193,7 +194,7 @@ public function testDeleteSystemOrRoot() public function deleteSystemOrRootDataProvider() { return [ - [\Magento\Catalog\Model\Category::TREE_ROOT_ID], + [Category::TREE_ROOT_ID], [2] //Default root category ]; } @@ -216,8 +217,8 @@ public function testUpdate() ]; $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); + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(Category::class); $category = $model->load($categoryId); $this->assertFalse((bool)$category->getIsActive(), 'Category "is_active" must equal to false'); $this->assertEquals("Update Category Test", $category->getName()); @@ -244,8 +245,8 @@ public function testUpdateWithDefaultSortByAttribute() ]; $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); + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(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()); @@ -288,8 +289,8 @@ public function testUpdateUrlKey() ]; $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); + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(Category::class); $category = $model->load($categoryId); $this->assertEquals("Update Category Test New Name", $category->getName()); @@ -564,7 +565,6 @@ public function testSaveDesign(): void /** * Check if repository does not override default values for attributes out of request * - * @throws \Exception * @magentoApiDataFixture Magento/Catalog/_files/category.php */ public function testUpdateScopeAttribute() @@ -576,8 +576,8 @@ public function testUpdateScopeAttribute() $result = $this->updateCategoryForSpecificStore($categoryId, $categoryData); $this->assertEquals($categoryId, $result['id']); - /** @var \Magento\Catalog\Model\Category $model */ - $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); + /** @var Category $model */ + $model = Bootstrap::getObjectManager()->get(Category::class); $category = $model->load($categoryId); /** @var ScopeOverriddenValue $scopeOverriddenValue */ From 3bacacefdd32ea4665e7d185689ecd345e43c50f Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 15 Oct 2020 15:27:46 +0300 Subject: [PATCH 0829/1013] MC-29402: PHPStan: "Anonymous function has an unused use" errors --- .../ProductRepository/TransactionWrapperTest.php | 2 +- .../Model/Resolver/TierPrices.php | 2 +- .../Magento/Customer/Controller/Adminhtml/Index.php | 2 +- .../CustomerRepository/TransactionWrapperTest.php | 2 +- .../Magento/Swatches/Model/AttributeCreateTest.php | 4 ++-- .../Swatches/Model/SwatchAttributeOptionAddTest.php | 2 +- .../Magento/Framework/Setup/ExternalFKSetup.php | 6 +++--- .../Setup/Console/Command/AdminUserCreateCommand.php | 7 ++++--- .../Setup/Console/Command/RollbackCommand.php | 6 +++--- .../Fixtures/AttributeSet/SwatchesGenerator.php | 3 ++- .../Magento/Setup/Fixtures/EavVariationsFixture.php | 12 ++++++++---- 11 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php index c60ef266b7ebb..89243ea30c9dc 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Plugin/ProductRepository/TransactionWrapperTest.php @@ -62,7 +62,7 @@ protected function setUp(): void $this->closureMock = function () use ($productMock) { return $productMock; }; - $this->rollbackClosureMock = function () use ($productMock) { + $this->rollbackClosureMock = function () { throw new \Exception(self::ERROR_MSG); }; diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php index c449d0a2ba30b..675bdaa5f1db0 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/TierPrices.php @@ -87,7 +87,7 @@ public function resolve( $this->tiers->addProductFilter($productId); return $this->valueFactory->create( - function () use ($productId, $context) { + function () use ($productId) { $tierPrices = $this->tiers->getProductTierPrices($productId); return $tierPrices ?? []; diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index.php index 51dc39a2fc658..f03f55b16e0c7 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index.php @@ -291,7 +291,7 @@ protected function _addSessionErrorMessages($messages) $messages = (array)$messages; $session = $this->_getSession(); - $callback = function ($error) use ($session) { + $callback = function ($error) { if (!$error instanceof Error) { $error = new Error($error); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php index c00b5cce02146..634b0d73219db 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Plugin/CustomerRepository/TransactionWrapperTest.php @@ -62,7 +62,7 @@ protected function setUp(): void $this->closureMock = function () use ($customerMock) { return $customerMock; }; - $this->rollbackClosureMock = function () use ($customerMock) { + $this->rollbackClosureMock = function () { throw new \Exception(self::ERROR_MSG); }; diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php index 98297cd43041f..b9ec091003267 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Model/AttributeCreateTest.php @@ -52,7 +52,7 @@ function ($values, $index) use ($optionsPerAttribute) { ); $data['optionvisual']['value'] = array_reduce( range(1, $optionsPerAttribute), - function ($values, $index) use ($optionsPerAttribute) { + function ($values, $index) { $values['option_' . $index] = ['option ' . $index]; return $values; }, @@ -61,7 +61,7 @@ function ($values, $index) use ($optionsPerAttribute) { $data['options']['option'] = array_reduce( range(1, $optionsPerAttribute), - function ($values, $index) use ($optionsPerAttribute) { + function ($values, $index) { $values[] = [ 'label' => 'option ' . $index, 'value' => 'option_' . $index diff --git a/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php b/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php index ccf25fd15c529..06ba28932eeb5 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/Model/SwatchAttributeOptionAddTest.php @@ -40,7 +40,7 @@ public function testSwatchOptionAdd() $data['options']['option'] = array_reduce( range(10, $optionsPerAttribute), - function ($values, $index) use ($optionsPerAttribute) { + function ($values, $index) { $values[] = [ 'label' => 'option ' . $index, 'value' => 'option_' . $index diff --git a/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php b/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php index 4247b7b1aab2f..3d3bb5b6578a9 100644 --- a/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php +++ b/lib/internal/Magento/Framework/Setup/ExternalFKSetup.php @@ -92,10 +92,10 @@ protected function execute() /** * Get foreign keys for tables and columns * - * @param string $refTable - * @param string $refColumn * @param string $targetTable * @param string $targetColumn + * @param string $refTable + * @param string $refColumn * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -110,7 +110,7 @@ protected function getForeignKeys( ); $foreignKeys = array_filter( $foreignKeys, - function ($key) use ($targetColumn, $refTable, $refColumn) { + function ($key) use ($targetColumn, $refTable) { return $key['COLUMN_NAME'] == $targetColumn && $key['REF_TABLE_NAME'] == $refTable; } diff --git a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php index 173ea9e49a8a4..8e64aae20573c 100644 --- a/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php +++ b/setup/src/Magento/Setup/Console/Command/AdminUserCreateCommand.php @@ -7,6 +7,7 @@ namespace Magento\Setup\Console\Command; use Magento\Framework\Setup\ConsoleLogger; +use Magento\Framework\Validation\ValidationException; use Magento\Setup\Model\AdminAccount; use Magento\Setup\Model\InstallerFactory; use Magento\User\Model\UserValidationRules; @@ -81,7 +82,7 @@ protected function interact(InputInterface $input, OutputInterface $output) $question = new Question('<question>Admin password:</question> ', ''); $question->setHidden(true); - $question->setValidator(function ($value) use ($output) { + $question->setValidator(function ($value) { $user = new \Magento\Framework\DataObject(); $user->setPassword($value); @@ -90,7 +91,7 @@ protected function interact(InputInterface $input, OutputInterface $output) $validator->isValid($user); foreach ($validator->getMessages() as $message) { - throw new \Exception($message); + throw new ValidationException(__($message)); } return $value; @@ -143,7 +144,7 @@ private function addNotEmptyValidator(Question $question) { $question->setValidator(function ($value) { if (trim($value) == '') { - throw new \Exception('The value cannot be empty'); + throw new ValidationException(__('The value cannot be empty')); } return $value; diff --git a/setup/src/Magento/Setup/Console/Command/RollbackCommand.php b/setup/src/Magento/Setup/Console/Command/RollbackCommand.php index a9138b9faefa1..e4616ae5e271b 100644 --- a/setup/src/Magento/Setup/Console/Command/RollbackCommand.php +++ b/setup/src/Magento/Setup/Console/Command/RollbackCommand.php @@ -80,7 +80,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ protected function configure() { @@ -111,7 +111,7 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritDoc */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -123,7 +123,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return \Magento\Framework\Console\Cli::RETURN_FAILURE; } $returnValue = $this->maintenanceModeEnabler->executeInMaintenanceMode( - function () use ($input, $output, &$returnValue) { + function () use ($input, $output) { try { $helper = $this->getHelper('question'); $question = new ConfirmationQuestion( diff --git a/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php b/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php index 26e7857703b4f..56263d0ec0adb 100644 --- a/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/AttributeSet/SwatchesGenerator.php @@ -91,7 +91,7 @@ function ($values, $index) use ($optionCount, $data, $type) { ); $attribute['optionvisual']['value'] = array_reduce( range(1, $optionCount), - function ($values, $index) use ($optionCount) { + function ($values, $index) { $values['option_' . $index] = ['option ' . $index]; return $values; }, @@ -129,6 +129,7 @@ private function generateSwatchImage($data) $this->imagesGenerator = $this->imagesGeneratorFactory->create(); } + // phpcs:ignore Magento2.Security.InsecureFunction $imageName = md5($data) . '.jpg'; $this->imagesGenerator->generate([ 'image-width' => self::GENERATED_SWATCH_WIDTH, diff --git a/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php b/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php index f143685f1903d..671627bcea8a9 100644 --- a/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php +++ b/setup/src/Magento/Setup/Fixtures/EavVariationsFixture.php @@ -77,7 +77,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function execute() { @@ -93,7 +93,7 @@ public function execute() } /** - * {@inheritdoc} + * @inheritDoc */ public function getActionTitle() { @@ -101,7 +101,7 @@ public function getActionTitle() } /** - * {@inheritdoc} + * @inheritDoc */ public function introduceParamLabels() { @@ -109,6 +109,8 @@ public function introduceParamLabels() } /** + * Generate Attribute + * * @param int $optionCount * @return void */ @@ -169,7 +171,7 @@ function ($values, $index) use ($optionCount) { ); $data['optionvisual']['value'] = array_reduce( range(1, $optionCount), - function ($values, $index) use ($optionCount) { + function ($values, $index) { $values['option_' . $index] = ['option ' . $index]; return $values; }, @@ -194,6 +196,8 @@ function ($values, $index) use ($optionCount) { } /** + * Get attribute code + * * @return string */ private function getAttributeCode() From 64978b41efdaf05ef59961278b0c882ac87c209b Mon Sep 17 00:00:00 2001 From: DmytroPaidych <dimonovp@gmail.com> Date: Thu, 15 Oct 2020 15:12:27 +0200 Subject: [PATCH 0830/1013] MC-37896: Create automated test for "Reset Widget" --- .../AdminSaveAndContinueWidgetActionGroup.xml | 2 +- .../AdminSetWidgetNameAndStoreActionGroup.xml | 7 ++++--- ...AdminSetWidgetTypeAndDesignActionGroup.xml} | 5 +++-- .../Mftf/Section/AdminNewWidgetSection.xml | 6 +++--- .../Test/Mftf/Test/AdminResetWidgetTest.xml | 18 +++++++++--------- 5 files changed, 20 insertions(+), 18 deletions(-) rename app/code/Magento/Widget/Test/Mftf/ActionGroup/{AdminSetInputTypeAndDesignActionGroup.xml => AdminSetWidgetTypeAndDesignActionGroup.xml} (77%) diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml index cd9774f3b13ba..6d17a5c687b1a 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSaveAndContinueWidgetActionGroup.xml @@ -13,7 +13,7 @@ </annotations> <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminNewWidgetSection.saveAndContinue}}" stepKey="clickSaveWidget"/> - <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessageAppeared"/> <see selector="{{AdminMessagesSection.success}}" userInput="The widget instance has been saved" stepKey="seeSuccess"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml index f1faadb1e434e..ce19c1b086328 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetNameAndStoreActionGroup.xml @@ -10,14 +10,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminSetWidgetNameAndStoreActionGroup"> <annotations> - <description>On the widget creation page page set widget name, store add sort order.</description> + <description>Set widget name, store IDs and sort order on Widget edit page</description> </annotations> <arguments> - <argument name="widgetName" defaultValue="{{ProductsListWidget.name}}" type="string"/> + <argument name="widgetTitle" defaultValue="{{ProductsListWidget.name}}" type="string"/> <argument name="widgetStoreIds" defaultValue="{{ProductsListWidget.store_ids}}" type="string"/> <argument name="widgetSortOrder" defaultValue="{{ProductsListWidget.sort_order}}" type="string"/> </arguments> - <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widgetName}}" stepKey="fillTitle"/> + <waitForElementVisible selector="{{AdminNewWidgetSection.widgetTitle}}" stepKey="waitForWidgetTitleInputVisible"/> + <fillField selector="{{AdminNewWidgetSection.widgetTitle}}" userInput="{{widgetTitle}}" stepKey="fillTitle"/> <selectOption selector="{{AdminNewWidgetSection.widgetStoreIds}}" parameterArray="{{widgetStoreIds}}" stepKey="setWidgetStoreId"/> <fillField selector="{{AdminNewWidgetSection.widgetSortOrder}}" userInput="{{widgetSortOrder}}" stepKey="fillSortOrder"/> </actionGroup> diff --git a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetInputTypeAndDesignActionGroup.xml b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetTypeAndDesignActionGroup.xml similarity index 77% rename from app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetInputTypeAndDesignActionGroup.xml rename to app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetTypeAndDesignActionGroup.xml index 3071f60bbc9d6..3a9b4c53572c7 100644 --- a/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetInputTypeAndDesignActionGroup.xml +++ b/app/code/Magento/Widget/Test/Mftf/ActionGroup/AdminSetWidgetTypeAndDesignActionGroup.xml @@ -8,14 +8,15 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="AdminSetInputTypeAndDesignActionGroup"> + <actionGroup name="AdminSetWidgetTypeAndDesignActionGroup"> <annotations> - <description>On the widget_instance page select widget type and design</description> + <description>Select type and design on Widget edit page</description> </annotations> <arguments> <argument name="widgetType" defaultValue="{{ProductsListWidget.type}}" type="string"/> <argument name="widgetDesign" defaultValue="{{ProductsListWidget.design_theme}}" type="string"/> </arguments> + <waitForElementVisible selector="{{AdminNewWidgetSection.widgetType}}" stepKey="waitForTypeInputVisible"/> <selectOption selector="{{AdminNewWidgetSection.widgetType}}" userInput="{{widgetType}}" stepKey="setWidgetType"/> <selectOption selector="{{AdminNewWidgetSection.widgetDesignTheme}}" userInput="{{widgetDesign}}" stepKey="setWidgetDesignTheme"/> </actionGroup> diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 2e455f4a3470b..805c55f34ce9a 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -12,7 +12,7 @@ <element name="widgetType" type="select" selector="#code"/> <element name="widgetDesignTheme" type="select" selector="#theme_id"/> <element name="continue" type="button" timeout="30" selector="#continue_button"/> - <element name="resetBtn" type="button" selector="#reset" timeout="30"/> + <element name="resetBtn" type="button" selector="//*[@class='page-actions-buttons']/button[@id='reset']" timeout="30"/> <element name="widgetTitle" type="input" selector="#title"/> <element name="widgetStoreIds" type="select" selector="#store_ids"/> <element name="widgetSortOrder" type="input" selector="#sort_order"/> @@ -39,11 +39,11 @@ <element name="searchBlock" type="button" selector="//div[@class='admin__filter-actions']/button[@title='Search']"/> <element name="blockStatus" type="select" selector="//select[@name='chooser_is_active']"/> <element name="searchedBlock" type="button" selector="//*[@class='magento-message']//tbody/tr/td[1]"/> - <element name="saveWidget" type="button" selector="#save"/> + <element name="saveWidget" type="button" selector="#save" timeout="30"/> <element name="displayMode" type="select" selector="select[id*='display_mode']"/> <element name="restrictTypes" type="select" selector="select[id*='types']"/> <element name="saveAndContinue" type="button" selector="#save_and_edit_button" timeout="30"/> - <element name="widgetInstanceType" type="select" selector="#instance_code" /> + <element name="widgetInstanceType" type="select" selector="//*[@class='admin__field-control control']/select[@id='instance_code']" /> <!-- Catalog Product List Widget Options --> <element name="title" type="input" selector="[name='parameters[title]']"/> <element name="displayPageControl" type="select" selector="[name='parameters[show_pager]']"/> diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml index 88610d9143bb4..fd9ce8f3c37e9 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml @@ -11,8 +11,8 @@ <test name="AdminResetWidgetTest"> <annotations> <features value="Widget"/> - <stories value="Reset widget"/> - <title value="[CMS Widgets] Reset Widget"/> + <stories value="CMS Widgets"/> + <title value="Reset Widget"/> <description value="Check that admin user can reset widget form after filling out all information"/> <severity value="MAJOR"/> <testCaseId value="MC-37892"/> @@ -23,25 +23,25 @@ </before> <after> <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteWidget"> - <argument name="widget" value="ProductsListWidget"/> + <argument name="widget" value="{{ProductsListWidget}}"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <amOnPage url="{{AdminNewWidgetPage.url}}" stepKey="amOnAdminNewWidgetPage"/> - <actionGroup ref="AdminSetInputTypeAndDesignActionGroup" stepKey="firstSetTypeAndDesign"> + <actionGroup ref="AdminSetWidgetTypeAndDesignActionGroup" stepKey="firstSetTypeAndDesign"> <argument name="widgetType" value="{{ProductsListWidget.type}}"/> <argument name="widgetDesign" value="{{ProductsListWidget.design_theme}}"/> </actionGroup> <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetInstance"/> <dontSeeInField userInput="{{ProductsListWidget.type}}" selector="{{AdminNewWidgetSection.widgetType}}" stepKey="dontSeeTypeAfterReset"/> <dontSeeInField userInput="{{ProductsListWidget.design_theme}}" selector="{{AdminNewWidgetSection.widgetDesignTheme}}" stepKey="dontSeeDesignAfterReset"/> - <actionGroup ref="AdminSetInputTypeAndDesignActionGroup" stepKey="setTypeAndDesignAfterReset"> + <actionGroup ref="AdminSetWidgetTypeAndDesignActionGroup" stepKey="setTypeAndDesignAfterReset"> <argument name="widgetType" value="{{ProductsListWidget.type}}"/> <argument name="widgetDesign" value="{{ProductsListWidget.design_theme}}"/> </actionGroup> <click selector="{{AdminNewWidgetSection.continue}}" stepKey="clickContinue"/> <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStore"> - <argument name="widgetName" value="{{ProductsListWidget.name}}"/> + <argument name="widgetTitle" value="{{ProductsListWidget.name}}"/> <argument name="widgetStoreIds" value="{{ProductsListWidget.store_ids}}"/> <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> </actionGroup> @@ -50,12 +50,12 @@ <dontSeeInField userInput="{{ProductsListWidget.store_ids[0]}}" selector="{{AdminNewWidgetSection.widgetStoreIds}}" stepKey="dontSeeStoreAfterReset"/> <dontSeeInField userInput="{{ProductsListWidget.sort_order}}" selector="{{AdminNewWidgetSection.widgetSortOrder}}" stepKey="dontSeeSortOrderAfterReset"/> <actionGroup ref="AdminSetWidgetNameAndStoreActionGroup" stepKey="setNameAndStoreAfterReset"> - <argument name="widgetName" value="{{ProductsListWidget.name}}"/> + <argument name="widgetTitle" value="{{ProductsListWidget.name}}"/> <argument name="widgetStoreIds" value="{{ProductsListWidget.store_ids}}"/> <argument name="widgetSortOrder" value="{{ProductsListWidget.sort_order}}"/> </actionGroup> - <actionGroup ref="AdminSaveAndContinueWidgetActionGroup" stepKey="saveWidget"/> - <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetWidget"/> + <actionGroup ref="AdminSaveAndContinueWidgetActionGroup" stepKey="saveWidgetAndContinue"/> + <click selector="{{AdminNewWidgetSection.resetBtn}}" stepKey="resetWidgetForm"/> <seeInField userInput="{{ProductsListWidget.name}}" selector="{{AdminNewWidgetSection.widgetTitle}}" stepKey="seeNameAfterReset"/> <seeInField userInput="{{ProductsListWidget.store_ids[0]}}" selector="{{AdminNewWidgetSection.widgetStoreIds}}" stepKey="seeStoreAfterReset"/> <seeInField userInput="{{ProductsListWidget.sort_order}}" selector="{{AdminNewWidgetSection.widgetSortOrder}}" stepKey="seeSortOrderAfterReset"/> From e133e1f5ae6ba3dcc660da8fe823dfef46f69f33 Mon Sep 17 00:00:00 2001 From: Mykhailo Matiola <mykhailo.matiola@transoftgroup.com> Date: Thu, 15 Oct 2020 17:52:32 +0300 Subject: [PATCH 0831/1013] MC-36960: Create automated test for "Create product for "all" store views using API service" --- .../ProductRepositoryAllStoreViewsTest.php | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php index 2814a6ab05321..fd815c6d2241b 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductRepositoryAllStoreViewsTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; use Magento\Catalog\Model\ResourceModel\Product\Website\Link; +use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\Eav\Model\Config; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\ObjectManagerInterface; @@ -167,7 +168,31 @@ private function getProductData(): array ProductInterface::STATUS => Status::STATUS_ENABLED, ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, ProductInterface::EXTENSION_ATTRIBUTES_KEY => [ - 'stock_item' => ['is_in_stock' => true, 'qty' => 1000] + 'stock_item' => [ + StockItemInterface::IS_IN_STOCK => 1, + StockItemInterface::QTY => 1000, + StockItemInterface::IS_QTY_DECIMAL => 0, + StockItemInterface::SHOW_DEFAULT_NOTIFICATION_MESSAGE => 0, + StockItemInterface::USE_CONFIG_MIN_QTY => 0, + StockItemInterface::USE_CONFIG_MIN_SALE_QTY => 0, + StockItemInterface::MIN_QTY => 1, + StockItemInterface::MIN_SALE_QTY => 1, + StockItemInterface::MAX_SALE_QTY => 100, + StockItemInterface::USE_CONFIG_MAX_SALE_QTY => 0, + StockItemInterface::USE_CONFIG_BACKORDERS => 0, + StockItemInterface::BACKORDERS => 0, + StockItemInterface::USE_CONFIG_NOTIFY_STOCK_QTY => 0, + StockItemInterface::NOTIFY_STOCK_QTY => 0, + StockItemInterface::USE_CONFIG_QTY_INCREMENTS => 0, + StockItemInterface::QTY_INCREMENTS => 0, + StockItemInterface::USE_CONFIG_ENABLE_QTY_INC => 0, + StockItemInterface::ENABLE_QTY_INCREMENTS => 0, + StockItemInterface::USE_CONFIG_MANAGE_STOCK => 1, + StockItemInterface::MANAGE_STOCK => 1, + StockItemInterface::LOW_STOCK_DATE => null, + StockItemInterface::IS_DECIMAL_DIVIDED => 0, + StockItemInterface::STOCK_STATUS_CHANGED_AUTO => 0, + ], ], ProductInterface::CUSTOM_ATTRIBUTES => [ ['attribute_code' => 'url_key', 'value' => 'simple'], @@ -211,6 +236,10 @@ private function assertProductData(array $productData, array $resultData, array $resultData['custom_attributes'], $attribute['attribute_code'] ); + if ($attribute['attribute_code'] == 'category_ids') { + $this->assertEquals(array_values($attribute['value']), array_values($resultAttribute['value'])); + continue; + } $this->assertEquals($attribute['value'], $resultAttribute['value']); } foreach ($productData['extension_attributes']['stock_item'] as $key => $value) { From 5cec5498d29d23e1a19d66f69e085eed9896e935 Mon Sep 17 00:00:00 2001 From: DmytroPaidych <dimonovp@gmail.com> Date: Thu, 15 Oct 2020 17:13:46 +0200 Subject: [PATCH 0832/1013] MC-37896: Create automated test for "Reset Widget" --- .../Widget/Test/Mftf/Section/AdminNewWidgetSection.xml | 5 +++-- .../Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 805c55f34ce9a..49eaf6b377859 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -12,7 +12,7 @@ <element name="widgetType" type="select" selector="#code"/> <element name="widgetDesignTheme" type="select" selector="#theme_id"/> <element name="continue" type="button" timeout="30" selector="#continue_button"/> - <element name="resetBtn" type="button" selector="//*[@class='page-actions-buttons']/button[@id='reset']" timeout="30"/> + <element name="resetBtn" type="button" selector=".page-actions-buttons .reset" timeout="30"/> <element name="widgetTitle" type="input" selector="#title"/> <element name="widgetStoreIds" type="select" selector="#store_ids"/> <element name="widgetSortOrder" type="input" selector="#sort_order"/> @@ -43,7 +43,7 @@ <element name="displayMode" type="select" selector="select[id*='display_mode']"/> <element name="restrictTypes" type="select" selector="select[id*='types']"/> <element name="saveAndContinue" type="button" selector="#save_and_edit_button" timeout="30"/> - <element name="widgetInstanceType" type="select" selector="//*[@class='admin__field-control control']/select[@id='instance_code']" /> + <element name="widgetInstanceType" type="select" selector=".admin__field-control .admin__control-select" /> <!-- Catalog Product List Widget Options --> <element name="title" type="input" selector="[name='parameters[title]']"/> <element name="displayPageControl" type="select" selector="[name='parameters[show_pager]']"/> @@ -51,3 +51,4 @@ <element name="cacheLifetime" type="input" selector="[name='parameters[cache_lifetime]']"/> </section> </sections> + diff --git a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml index fd9ce8f3c37e9..5e053778fe7ed 100644 --- a/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml +++ b/app/code/Magento/Widget/Test/Mftf/Test/AdminResetWidgetTest.xml @@ -23,7 +23,7 @@ </before> <after> <actionGroup ref="AdminDeleteWidgetActionGroup" stepKey="deleteWidget"> - <argument name="widget" value="{{ProductsListWidget}}"/> + <argument name="widget" value="ProductsListWidget"/> </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> From 1ff20ba12e960d937b7ebe49592a9b5619cb4035 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Thu, 15 Oct 2020 18:27:31 +0300 Subject: [PATCH 0833/1013] MC-37546: Create automated test for "Create new Category Update" --- ...torefrontCheckPresentSubCategoryActionGroup.xml | 6 +++--- .../Test/Mftf/Page/AdminCategoryEditPage.xml | 1 + .../AdminCategoryScheduleDesingUpdateSection.xml | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesingUpdateSection.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml index 7cb3287614433..1799f6339a84d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontCheckPresentSubCategoryActionGroup.xml @@ -16,8 +16,8 @@ <argument name="childCategoryName" type="string"/> </arguments> - <waitForElement selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(parenCategoryName)}}" stepKey="waitForTopMenuLoaded"/> - <moveMouseOver selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(parenCategoryName)}}" stepKey="moveMouseToParentCategory"/> - <seeElement selector="{{AdminCategorySidebarTreeSection.categoryHighlighted(childCategoryName)}}" stepKey="seeSubcategoryInTree"/> + <waitForElementVisible selector="{{StorefrontHeaderSection.NavigationCategoryByName(parenCategoryName)}}" stepKey="waitForTopMenuLoaded"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName(parenCategoryName)}}" stepKey="moveMouseToParentCategory"/> + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(childCategoryName)}}" stepKey="seeSubcategoryInTree"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml index e1c8e5c75e9ac..5c5dfe8901563 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml @@ -19,5 +19,6 @@ <section name="AdminCategoryModalSection"/> <section name="AdminCategoryMessagesSection"/> <section name="AdminCategoryContentSection"/> + <section name="AdminCategoryScheduleDesingUpdateSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesingUpdateSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesingUpdateSection.xml new file mode 100644 index 0000000000000..e1b66b3c18260 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesingUpdateSection.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="AdminCategoryScheduleDesingUpdateSection"> + <element name="sectionHeader" type="button" selector="div[data-index='schedule_design_update'] .fieldset-wrapper-title" timeout="30"/> + <element name="sectionBody" type="text" selector="div[data-index='schedule_design_update'] .admin__fieldset-wrapper-content"/> + </section> +</sections> From 15390c45ed1ffdfa48002157990013e45206099d Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Thu, 15 Oct 2020 11:26:34 -0500 Subject: [PATCH 0834/1013] MC-37460: Support by Magento CMS (#6202) * MC-37479: Support by Magento Content Design --- app/code/Magento/AwsS3/Driver/AwsS3.php | 145 +++++++++++-- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 51 +++++ .../Magento/Backup/Model/Fs/Collection.php | 16 +- .../Test/Unit/Model/Fs/CollectionTest.php | 18 +- .../Block/Adminhtml/Wysiwyg/Images/Tree.php | 5 +- .../Adminhtml/Wysiwyg/Directive.php | 19 +- .../Cms/Model/Wysiwyg/Images/Storage.php | 14 +- .../Wysiwyg/Images/Storage/Collection.php | 5 +- .../Adminhtml/Wysiwyg/DirectiveTest.php | 34 +-- .../Unit/Model/Wysiwyg/Images/StorageTest.php | 2 +- app/code/Magento/Cms/etc/di.xml | 16 +- .../Magento/Downloadable/Helper/Download.php | 37 ++-- .../Test/Unit/Helper/DownloadTest.php | 23 +- .../Magento/RemoteStorage/Plugin/Image.php | 204 ++++++++++++++++++ .../Test/Unit/Plugin/ImageTest.php | 174 +++++++++++++++ app/code/Magento/RemoteStorage/composer.json | 4 +- app/code/Magento/RemoteStorage/etc/di.xml | 18 ++ .../App/Filesystem/DirectoryResolver.php | 3 +- .../Unit/Filesystem/DirectoryResolverTest.php | 3 +- .../Framework/Data/Collection/Filesystem.php | 47 +++- lib/internal/Magento/Framework/File/Mime.php | 23 +- .../Framework/File/Test/Unit/MimeTest.php | 26 ++- .../Image/Adapter/AbstractAdapter.php | 30 ++- 23 files changed, 828 insertions(+), 89 deletions(-) create mode 100644 app/code/Magento/RemoteStorage/Plugin/Image.php create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 8a862a6812107..320e3f9c43a54 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -14,6 +14,8 @@ /** * Driver for AWS S3 IO operations. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ class AwsS3 implements DriverInterface { @@ -173,10 +175,7 @@ public function filePutContents($path, $content, $mode = null, $context = null): */ public function readDirectoryRecursively($path = null): array { - return $this->adapter->listContents( - $this->normalizeRelativePath($path), - true - ); + return $this->readPath($path, true); } /** @@ -184,10 +183,7 @@ public function readDirectoryRecursively($path = null): array */ public function readDirectory($path): array { - return $this->adapter->listContents( - $this->normalizeRelativePath($path), - false - ); + return $this->readPath($path, false); } /** @@ -402,11 +398,11 @@ public function stat($path): array 'ctime' => 0, 'blksize' => 0, 'blocks' => 0, - 'size' => $metaInfo['size'], - 'type' => $metaInfo['type'], - 'mtime' => $metaInfo['timestamp'], + 'size' => $metaInfo['size'] ?? 0, + 'type' => $metaInfo['type'] ?? 0, + 'mtime' => $metaInfo['timestamp'] ?? 0, 'disposition' => null, - 'mimetype' => $metaInfo['mimetype'] + 'mimetype' => $metaInfo['mimetype'] ?? 0 ]; } @@ -415,7 +411,36 @@ public function stat($path): array */ public function search($pattern, $path): array { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return $this->glob(rtrim($path, '/') . '/' . ltrim($pattern, '/')); + } + + /** + * Emulate php glob function for AWS S3 storage + * + * @param string $pattern + * @return array + * @throws FileSystemException + */ + private function glob(string $pattern): array + { + $directoryContent = []; + + $patternFound = preg_match('(\*|\?|\[.+\])', $pattern, $parentPattern, PREG_OFFSET_CAPTURE); + if ($patternFound) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $parentDirectory = \dirname(substr($pattern, 0, $parentPattern[0][1] + 1)); + $leftover = substr($pattern, $parentPattern[0][1]); + $index = strpos($leftover, '/'); + $searchPattern = $this->getSearchPattern($pattern, $parentPattern, $parentDirectory, $index); + + if ($this->isDirectory($parentDirectory . '/')) { + $directoryContent = $this->getDirectoryContent($parentDirectory, $searchPattern, $leftover, $index); + } + } elseif ($this->isDirectory($pattern) || $this->isFile($pattern)) { + $directoryContent[] = $pattern; + } + + return $directoryContent; } /** @@ -630,4 +655,98 @@ private function getWarningMessage(): ?string return null; } + + /** + * Read directory by path and is recursive flag + * + * @param string $path + * @param bool $isRecursive + * @return array + */ + private function readPath(string $path, $isRecursive = false): array + { + $relativePath = $this->normalizeRelativePath($path); + $contentsList = $this->adapter->listContents( + $relativePath, + $isRecursive + ); + $itemsList = []; + foreach ($contentsList as $item) { + if (isset($item['path']) + && $item['path'] !== $relativePath + && strpos($item['path'], $relativePath) === 0) { + $itemsList[] = $item['path']; + } + } + + return $itemsList; + } + + /** + * Get search pattern for directory + * + * @param string $pattern + * @param array $parentPattern + * @param string $parentDirectory + * @param int|bool $index + * @return string + */ + private function getSearchPattern(string $pattern, array $parentPattern, string $parentDirectory, $index): string + { + $parentLength = \strlen($parentDirectory); + if ($index !== false) { + $searchPattern = substr( + $pattern, + $parentLength + 1, + $parentPattern[0][1] - $parentLength + $index - 1 + ); + } else { + $searchPattern = substr($pattern, $parentLength + 1); + } + + $replacement = [ + '/\*/' => '.*', + '/\?/' => '.', + '/\//' => '\/' + ]; + return preg_replace(array_keys($replacement), array_values($replacement), $searchPattern); + } + + /** + * Get directory content by given search pattern + * + * @param string $parentDirectory + * @param string $searchPattern + * @param string $leftover + * @param int|bool $index + * @return array + * @throws FileSystemException + */ + private function getDirectoryContent( + string $parentDirectory, + string $searchPattern, + string $leftover, + $index + ): array { + $items = $this->readDirectory($parentDirectory . '/'); + $directoryContent = []; + foreach ($items as $item) { + if (preg_match('/' . $searchPattern . '$/', $item) + // phpcs:ignore Magento2.Functions.DiscouragedFunction + && strpos(basename($item), '.') !== 0) { + if ($index === false || \strlen($leftover) === $index + 1) { + $directoryContent[] = $this->isDirectory($item) + ? rtrim($item, '/') . '/' + : $item; + } elseif (strlen($leftover) > $index + 1) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $directoryContent = array_merge( + $directoryContent, + $this->glob("{$parentDirectory}/{$item}" . substr($leftover, $index)) + ); + } + } + } + return $directoryContent; + } } diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index b3de684ed67dd..b70149e26225c 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -363,4 +363,55 @@ public function getRealPathSafetyDataProvider(): array ] ]; } + + /** + * @throws FileSystemException + */ + public function testSearchDirectory(): void + { + $expression = '/*'; + $path = 'path/'; + $subPaths = [ + ['path' => 'path/1'], + ['path' => 'path/2'] + ]; + $expectedResult = ['path/1', 'path/2']; + $this->adapterMock->expects(self::atLeastOnce())->method('has') + ->willReturnMap([ + [$path, true] + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('getMetadata') + ->willReturnMap([ + [$path, ['type' => AwsS3::TYPE_DIR]] + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('listContents')->with($path, false) + ->willReturn($subPaths); + self::assertEquals($expectedResult, $this->driver->search($expression, $path)); + } + + /** + * @throws FileSystemException + */ + public function testSearchFiles(): void + { + $expression = "/*"; + $path = 'path/'; + $subPaths = [ + ['path' => 'path/1.jpg'], + ['path' => 'path/2.png'] + ]; + $expectedResult = ['path/1.jpg', 'path/2.png']; + + $this->adapterMock->expects(self::atLeastOnce())->method('has') + ->willReturnMap([ + [$path, true], + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('getMetadata') + ->willReturnMap([ + [$path, ['type' => AwsS3::TYPE_DIR]], + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('listContents')->with($path, false) + ->willReturn($subPaths); + self::assertEquals($expectedResult, $this->driver->search($expression, $path)); + } } diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index b17c17f7074fb..94f555e4054e3 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -6,6 +6,8 @@ namespace Magento\Backup\Model\Fs; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Config\DocumentRoot; +use Magento\Framework\Filesystem\Directory\TargetDirectory; /** * Backup data collection @@ -40,20 +42,30 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem */ protected $_backup = null; + /** + * @var \Magento\Framework\Filesystem + */ + protected $_filesystem; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Magento\Backup\Helper\Data $backupData * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Backup\Model\Backup $backup + * @param TargetDirectory|null $targetDirectory + * @param DocumentRoot|null $documentRoot + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, \Magento\Backup\Helper\Data $backupData, \Magento\Framework\Filesystem $filesystem, - \Magento\Backup\Model\Backup $backup + \Magento\Backup\Model\Backup $backup, + TargetDirectory $targetDirectory = null, + DocumentRoot $documentRoot = null ) { $this->_backupData = $backupData; - parent::__construct($entityFactory); + parent::__construct($entityFactory, $targetDirectory, $documentRoot); $this->_filesystem = $filesystem; $this->_backup = $backup; diff --git a/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php b/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php index 69e2fcb6e1f25..cec0ccff70ce6 100644 --- a/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php +++ b/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php @@ -10,6 +10,7 @@ use Magento\Backup\Helper\Data; use Magento\Backup\Model\Fs\Collection; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; @@ -36,10 +37,23 @@ public function testConstructor() $directoryWrite->expects($this->any())->method('create')->with('backups'); $directoryWrite->expects($this->any())->method('getAbsolutePath')->with('backups'); - + $directoryWrite->expects($this->any())->method('isDirectory')->willReturn(true); + $targetDirectory = $this->getMockBuilder(TargetDirectory::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDirectoryWrite->expects($this->any())->method('isDirectory')->willReturn(true); + $targetDirectory->expects($this->any())->method('getDirectoryWrite')->willReturn($targetDirectoryWrite); $classObject = $helper->getObject( Collection::class, - ['filesystem' => $filesystem, 'backupData' => $backupData] + [ + 'filesystem' => $filesystem, + 'backupData' => $backupData, + 'directoryWrite' => $directoryWrite, + 'targetDirectory' => $targetDirectory + ] ); $this->assertNotNull($classObject); } diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php index 41e9358e160cf..c033e09ca8db0 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php @@ -58,6 +58,7 @@ public function __construct( * Json tree builder * * @return string + * @throws \Magento\Framework\Exception\ValidatorException */ public function getTreeJson() { @@ -75,8 +76,8 @@ public function getTreeJson() 'path' => substr($item->getFilename(), strlen($storageRoot)), 'cls' => 'folder', ]; - - $hasNestedDirectories = count(glob($item->getFilename() . '/*', GLOB_ONLYDIR)) > 0; + $nestedDirectories = $this->getMediaDirectory()->readRecursively($item->getFilename()); + $hasNestedDirectories = count($nestedDirectories) > 0; // if no nested directories inside dir, add 'leaf' state so that jstree hides dropdown arrow next to dir if (!$hasNestedDirectories) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php index a3370b2666264..5172ff8088bf8 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php @@ -13,6 +13,8 @@ use Magento\Cms\Model\Template\Filter; use Magento\Cms\Model\Wysiwyg\Config; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\Framework\Image\Adapter\AdapterInterface; use Magento\Framework\Image\AdapterFactory; use Psr\Log\LoggerInterface; @@ -27,6 +29,7 @@ * Process template text for wysiwyg editor. * * Class Directive + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) usage of $this->file eliminated, but it's still there due to BC */ class Directive extends Action implements HttpGetActionInterface { @@ -70,8 +73,13 @@ class Directive extends Action implements HttpGetActionInterface /** * @var File + * @deprecated use $filesystem instead */ private $file; + /** + * @var Filesystem|null + */ + private $filesystem; /** * Constructor @@ -84,6 +92,7 @@ class Directive extends Action implements HttpGetActionInterface * @param Config|null $config * @param Filter|null $filter * @param File|null $file + * @param Filesystem|null $filesystem */ public function __construct( Context $context, @@ -93,7 +102,8 @@ public function __construct( LoggerInterface $logger = null, Config $config = null, Filter $filter = null, - File $file = null + File $file = null, + Filesystem $filesystem = null ) { parent::__construct($context); $this->urlDecoder = $urlDecoder; @@ -103,17 +113,21 @@ public function __construct( $this->config = $config ?: ObjectManager::getInstance()->get(Config::class); $this->filter = $filter ?: ObjectManager::getInstance()->get(Filter::class); $this->file = $file ?: ObjectManager::getInstance()->get(File::class); + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** * Template directives callback * * @return Raw + * @throws \Magento\Framework\Exception\FileSystemException */ public function execute() { $directive = $this->getRequest()->getParam('___directive'); $directive = $this->urlDecoder->decode($directive); + $image = null; + $resultRaw = null; try { /** @var Filter $filter */ $imagePath = $this->filter->filter($directive); @@ -141,7 +155,8 @@ public function execute() // To avoid issues with PNG images with alpha blending we return raw file // after validation as an image source instead of generating the new PNG image // with image adapter - $content = $this->file->fileGetContents($imagePath); + $content = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->getDriver() + ->fileGetContents($imagePath); $resultRaw->setHeader('Content-Type', $mimeType); $resultRaw->setContents($content); diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 0cc108e5bed8b..8b170ecdd5c04 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -369,9 +369,11 @@ public function getFilesCollection($path, $type = null) $item->setName($item->getBasename()); $item->setShortName($this->_cmsWysiwygImages->getShortFilename($item->getBasename())); $item->setUrl($this->_cmsWysiwygImages->getCurrentUrl() . $item->getBasename()); - $itemStats = $this->file->stat($item->getFilename()); + $driver = $this->_directory->getDriver(); + $itemStats = $driver->stat($item->getFilename()); $item->setSize($itemStats['size']); - $item->setMimeType($this->mime->getMimeType($item->getFilename())); + $mimeType = $itemStats['mimetype'] ?? $this->mime->getMimeType($item->getFilename()); + $item->setMimeType($mimeType); if ($this->isImage($item->getBasename())) { $thumbUrl = $this->getThumbnailUrl($item->getFilename(), true); @@ -438,7 +440,7 @@ public function createDirectory($name, $path) $path = $this->_cmsWysiwygImages->getStorageRoot(); } - $newPath = $path . '/' . $name; + $newPath = rtrim($path, '/') . '/' . $name; $relativeNewPath = $this->_directory->getRelativePath($newPath); if ($this->_directory->isDirectory($relativeNewPath)) { throw new \Magento\Framework\Exception\LocalizedException( @@ -571,7 +573,7 @@ public function uploadFile($targetPath, $type = null) } // create thumbnail - $this->resizeFile($targetPath . '/' . $uploader->getUploadedFileName(), true); + $this->resizeFile($targetPath . '/' . ltrim($uploader->getUploadedFileName(), '/'), true); return $result; } @@ -759,7 +761,7 @@ public function getAllowedExtensions($type = null) */ public function getThumbnailRoot() { - return $this->_cmsWysiwygImages->getStorageRoot() . '/' . self::THUMBS_DIRECTORY_NAME; + return rtrim($this->_cmsWysiwygImages->getStorageRoot(), '/') . '/' . self::THUMBS_DIRECTORY_NAME; } /** @@ -844,7 +846,7 @@ protected function _sanitizePath($path) { return rtrim( preg_replace( - '~[/\\\]+~', + '~[/\\\]+(?<![htps?]://)~', '/', $this->_directory->getDriver()->getRealPathSafety( $this->_directory->getAbsolutePath($path) diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php index f66c0f6b06d91..ac60420713b26 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php @@ -41,10 +41,11 @@ public function __construct( */ protected function _generateRow($filename) { - $filename = preg_replace('~[/\\\]+~', '/', $filename); + $filename = preg_replace('~[/\\\]+(?<![htps?]://)~', '/', $filename); $path = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); return [ - 'filename' => $filename, + 'filename' => rtrim($filename, '/'), + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'basename' => basename($filename), 'mtime' => $path->stat($path->getRelativePath($filename))['mtime'] ]; diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php index 5791ecea4e4e3..70dd95521f040 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php @@ -15,7 +15,10 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Raw; use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Image\Adapter\AdapterInterface; use Magento\Framework\Image\AdapterFactory; use Magento\Framework\ObjectManagerInterface; @@ -78,11 +81,6 @@ class DirectiveTest extends TestCase */ protected $responseMock; - /** - * @var File|MockObject - */ - protected $fileMock; - /** * @var Config|MockObject */ @@ -103,6 +101,11 @@ class DirectiveTest extends TestCase */ protected $rawMock; + /** + * @var DriverInterface|MockObject + */ + private $driverMock; + protected function setUp(): void { $this->actionContextMock = $this->getMockBuilder(Context::class) @@ -146,10 +149,6 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['setHeader', 'setBody', 'sendResponse']) ->getMockForAbstractClass(); - $this->fileMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['fileGetContents']) - ->getMock(); $this->wysiwygConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); @@ -173,6 +172,17 @@ protected function setUp(): void $this->actionContextMock->expects($this->any()) ->method('getObjectManager') ->willReturn($this->objectManagerMock); + $this->driverMock = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWrite->expects($this->any())->method('getDriver')->willReturn($this->driverMock); + $filesystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($directoryWrite); $objectManager = new ObjectManager($this); $this->wysiwygDirective = $objectManager->getObject( @@ -185,7 +195,7 @@ protected function setUp(): void 'logger' => $this->loggerMock, 'config' => $this->wysiwygConfigMock, 'filter' => $this->templateFilterMock, - 'file' => $this->fileMock, + 'filesystem' => $filesystemMock ] ); } @@ -216,7 +226,7 @@ public function testExecute() $this->imageAdapterMock->expects($this->once()) ->method('getImage') ->willReturn($imageBody); - $this->fileMock->expects($this->once()) + $this->driverMock->expects($this->once()) ->method('fileGetContents') ->willReturn($imageBody); $this->rawFactoryMock->expects($this->any()) @@ -267,7 +277,7 @@ public function testExecuteException() $this->imageAdapterMock->expects($this->any()) ->method('getImage') ->willReturn($imageBody); - $this->fileMock->expects($this->once()) + $this->driverMock->expects($this->once()) ->method('fileGetContents') ->willReturn($imageBody); $this->loggerMock->expects($this->once()) 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 c2c748dcc7633..b03dbb8f0c888 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 @@ -494,7 +494,7 @@ public function testUploadFile() $targetPath = self::STORAGE_ROOT_DIR . $path; $fileName = 'image.gif'; $realPath = $targetPath . '/' . $fileName; - $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs' . $path; + $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '.thumbs' . $path; $thumbnailDestination = $thumbnailTargetPath . '/' . $fileName; $type = 'image'; $result = [ diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 7fc8268eea5e0..355848830dab6 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -57,35 +57,35 @@ <item name="exclude" xsi:type="array"> <item name="captcha" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+captcha[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+captcha[/\\]*$</item> </item> <item name="catalog/product" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+catalog[/\\]+product[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+catalog[/\\]+product[/\\]*$</item> </item> <item name="customer" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+customer[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+customer[/\\]*$</item> </item> <item name="downloadable" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+downloadable[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+downloadable[/\\]*$</item> </item> <item name="import" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+import[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+import[/\\]*$</item> </item> <item name="theme" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+theme[/\\]*$</item> </item> <item name="theme_customization" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme_customization[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+theme_customization[/\\]*$</item> </item> <item name="tmp" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+tmp[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+tmp[/\\]*$</item> </item> </item> <item name="include" xsi:type="array"/> diff --git a/app/code/Magento/Downloadable/Helper/Download.php b/app/code/Magento/Downloadable/Helper/Download.php index 6b7db3af51195..1425f71f2fd8a 100644 --- a/app/code/Magento/Downloadable/Helper/Download.php +++ b/app/code/Magento/Downloadable/Helper/Download.php @@ -7,6 +7,8 @@ namespace Magento\Downloadable\Helper; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Exception\LocalizedException as CoreException; @@ -18,12 +20,12 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper { /** - * Link type url + * Link type for url */ const LINK_TYPE_URL = 'url'; /** - * Link type file + * Link type for file */ const LINK_TYPE_FILE = 'file'; @@ -109,6 +111,11 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper */ protected $_session; + /** + * @var Mime + */ + private $mime; + /** * @param \Magento\Framework\App\Helper\Context $context * @param File $downloadableFile @@ -116,6 +123,7 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper * @param Filesystem $filesystem * @param \Magento\Framework\Session\SessionManagerInterface $session * @param Filesystem\File\ReadFactory $fileReadFactory + * @param Mime|null $mime */ public function __construct( \Magento\Framework\App\Helper\Context $context, @@ -123,7 +131,8 @@ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb, \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Session\SessionManagerInterface $session, - \Magento\Framework\Filesystem\File\ReadFactory $fileReadFactory + \Magento\Framework\Filesystem\File\ReadFactory $fileReadFactory, + Mime $mime = null ) { parent::__construct($context); $this->_downloadableFile = $downloadableFile; @@ -131,6 +140,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->_session = $session; $this->fileReadFactory = $fileReadFactory; + $this->mime = $mime ?? ObjectManager::getInstance()->get(Mime::class); } /** @@ -148,6 +158,7 @@ protected function _getHandle() if ($this->_handle === null) { if ($this->_linkType == self::LINK_TYPE_URL) { $path = $this->_resourceFile; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $protocol = strtolower(parse_url($path, PHP_URL_SCHEME)); if ($protocol) { // Strip down protocol from path @@ -188,14 +199,8 @@ public function getContentType() { $this->_getHandle(); if ($this->_linkType === self::LINK_TYPE_FILE) { - if (function_exists('mime_content_type') - && ($contentType = mime_content_type( - $this->_workingDirectory->getAbsolutePath($this->_resourceFile) - )) - ) { - return $contentType; - } - return $this->_downloadableFile->getFileType($this->_resourceFile); + $absolutePath = $this->_workingDirectory->getAbsolutePath($this->_resourceFile); + return $this->mime->getMimeType($absolutePath); } if ($this->_linkType === self::LINK_TYPE_URL) { return (is_array($this->_handle->stat($this->_resourceFile)['type']) @@ -209,6 +214,8 @@ public function getContentType() * Return name of the file * * @return string + * phpcs:disable Magento2.Functions.DiscouragedFunction + * phpcs:disable Generic.PHP.NoSilencedErrors */ public function getFilename() { @@ -254,20 +261,21 @@ public function setResource($resourceFile, $linkType = self::LINK_TYPE_FILE) ); } } - + $this->_resourceFile = $resourceFile; - + /** * check header for urls */ if ($linkType === self::LINK_TYPE_URL) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $headers = array_change_key_case(get_headers($this->_resourceFile, 1), CASE_LOWER); if (isset($headers['location'])) { $this->_resourceFile = is_array($headers['location']) ? current($headers['location']) : $headers['location']; } } - + $this->_linkType = $linkType; return $this; } @@ -282,6 +290,7 @@ public function output() $handle = $this->_getHandle(); $this->_session->writeClose(); while (true == ($buffer = $handle->read(1024))) { + // phpcs:ignore Magento2.Security.LanguageConstruct echo $buffer; //@codingStandardsIgnoreLine } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php index 59de5b0139ff6..da89efac59fa8 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php @@ -10,6 +10,7 @@ use Magento\Downloadable\Helper\Download as DownloadHelper; use Magento\Downloadable\Helper\File as DownloadableFile; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface as DirReadInterface; use Magento\Framework\Filesystem\File\ReadFactory; @@ -62,6 +63,11 @@ class DownloadTest extends TestCase const URL = 'http://example.com'; + /** + * @var Mime|MockObject + */ + private $mime; + protected function setUp(): void { require_once __DIR__ . '/../_files/download_mock.php'; @@ -77,6 +83,7 @@ protected function setUp(): void SessionManagerInterface::class ); $this->fileReadFactory = $this->createMock(ReadFactory::class); + $this->mime = $this->createMock(Mime::class); $this->_helper = (new ObjectManager($this))->getObject( \Magento\Downloadable\Helper\Download::class, @@ -85,6 +92,7 @@ protected function setUp(): void 'filesystem' => $this->_filesystemMock, 'session' => $this->sessionManager, 'fileReadFactory' => $this->fileReadFactory, + 'mime' => $this->mime ] ); } @@ -132,8 +140,17 @@ public function testGetFileSizeNoFile() public function testGetContentType() { + $this->mime->expects( + self::once() + )->method( + 'getMimeType' + )->willReturn( + self::MIME_TYPE + ); $this->_setupFileMocks(); $this->_downloadableFileMock->expects($this->never())->method('getFileType'); + $this->_workingDirectoryMock->expects($this->once())->method('getAbsolutePath') + ->willReturn('/path/to/file.txt'); $this->assertEquals(self::MIME_TYPE, $this->_helper->getContentType()); } @@ -146,10 +163,10 @@ public function testGetContentTypeThroughHelper($functionExistsResult, $mimeCont self::$functionExists = $functionExistsResult; self::$mimeContentType = $mimeContentTypeResult; - $this->_downloadableFileMock->expects( - $this->once() + $this->mime->expects( + self::once() )->method( - 'getFileType' + 'getMimeType' )->willReturn( self::MIME_TYPE ); diff --git a/app/code/Magento/RemoteStorage/Plugin/Image.php b/app/code/Magento/RemoteStorage/Plugin/Image.php new file mode 100644 index 0000000000000..8f554a3d8f8c3 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Image.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Image\Adapter\AbstractAdapter; +use Magento\RemoteStorage\Model\Config; +use Psr\Log\LoggerInterface; + +/** + * @see AbstractAdapter + */ +class Image +{ + /** + * @var Filesystem\Directory\WriteInterface + */ + private $tmpDirectoryWrite; + + /** + * @var Filesystem\Directory\WriteInterface + */ + private $targetDirectoryWrite; + + /** + * @var array + */ + private $tmpFiles = []; + + /** + * @var bool + */ + private $isEnabled; + + /** + * @var File + */ + private $ioFile; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Filesystem $filesystem + * @param File $ioFile + * @param TargetDirectory $targetDirectory + * @param Config $config + * @param LoggerInterface $logger + * @throws FileSystemException + * @throws RuntimeException + */ + public function __construct( + Filesystem $filesystem, + File $ioFile, + TargetDirectory $targetDirectory, + Config $config, + LoggerInterface $logger + ) { + $this->tmpDirectoryWrite = $filesystem->getDirectoryWrite(DirectoryList::TMP); + $this->targetDirectoryWrite = $targetDirectory->getDirectoryWrite(DirectoryList::ROOT); + $this->isEnabled = $config->isEnabled(); + $this->ioFile = $ioFile; + $this->logger = $logger; + } + + /** + * Copy file from remote server to tmp directory of Magento + * + * @param AbstractAdapter $subject + * @param string $filename + * @return array + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeOpen(AbstractAdapter $subject, $filename): array + { + if ($this->isEnabled) { + $filename = $this->copyFileToTmp($filename); + } + return [$filename]; + } + + /** + * Get filesystem tmp path for file and provide it to save() function + * + * @param AbstractAdapter $subject + * @param callable $proceed + * @param string|null $destination + * @param string|null $newName + * @return void + * @throws FileSystemException + */ + public function aroundSave( + AbstractAdapter $subject, + callable $proceed, + $destination = null, + $newName = null + ): void { + if ($this->isEnabled) { + $relativePath = $this->targetDirectoryWrite->getRelativePath($destination); + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath($relativePath); + + $proceed($tmpPath, $newName); + + $destination = $this->prepareDestination($subject, $destination, $newName); + $this->tmpDirectoryWrite->getDriver()->rename( + $tmpPath, + $destination, + $this->targetDirectoryWrite->getDriver() + ); + } else { + $proceed($destination, $newName); + } + } + + /** + * Remove created tmp files + */ + public function __destruct() + { + try { + foreach ($this->tmpFiles as $tmpFile) { + $this->tmpDirectoryWrite->delete($tmpFile); + } + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } + } + + /** + * Move files from storage to tmp folder + * + * @param string $filePath + * @return string + * @throws FileSystemException + */ + private function copyFileToTmp($filePath): string + { + $absolutePath = $this->targetDirectoryWrite->getAbsolutePath($filePath); + if ($this->targetDirectoryWrite->isFile($absolutePath)) { + $this->tmpDirectoryWrite->create(); + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath() . basename($filePath); + $this->storeTmpName($tmpPath); + $content = $this->targetDirectoryWrite->getDriver()->fileGetContents($filePath); + $filePath = $this->tmpDirectoryWrite->getDriver()->filePutContents($tmpPath, $content) + ? $tmpPath + : $filePath; + } + return $filePath; + } + + /** + * Store created tmp image path + * + * @param string $path + */ + private function storeTmpName(string $path): void + { + $this->tmpFiles[] = $path; + } + + /** + * Prepare destination path + * + * @param AbstractAdapter $image + * @param string|null $destination + * @param string|null $newName + * @return string + */ + private function prepareDestination( + AbstractAdapter $image, + string $destination = null, + string $newName = null + ): string { + if (empty($destination)) { + $destination = $image->getFileSrcPath(); + } elseif (empty($newName)) { + $info = $this->ioFile->getPathInfo($destination); + $newName = $info['basename']; + $destination = $info['dirname']; + } + + if (empty($newName)) { + $newFileName = $image->getFileSrcName(); + } else { + $newFileName = $newName; + } + return rtrim($destination, '/') . '/' . $newFileName; + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php new file mode 100644 index 0000000000000..4055422a8aa4e --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php @@ -0,0 +1,174 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\RemoteStorage\Test\Unit\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Image\Adapter\AbstractAdapter; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Plugin\Image; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class ImageTest extends TestCase +{ + /** + * @var File|MockObject + */ + private $ioFile; + + /** + * @var Image + */ + private $plugin; + + /** + * @var WriteInterface|MockObject + */ + private $tmpDirectoryWrite; + + /** + * @var WriteInterface|MockObject + */ + private $targetDirectoryWrite; + + /** + * @throws \Magento\Framework\Exception\FileSystemException + * @return void + */ + protected function setUp(): void + { + /** @var Filesystem|MockObject $filesystem */ + $filesystem = $this->getMockBuilder(Filesystem::class)->disableOriginalConstructor()->getMock(); + $this->ioFile = $this->getMockBuilder(File::class)->disableOriginalConstructor()->getMock(); + /** @var TargetDirectory|MockObject $targetDirectory */ + $targetDirectory = $this->getMockBuilder(TargetDirectory::class)->disableOriginalConstructor()->getMock(); + /** @var Config|MockObject $config */ + $config = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $config->expects(self::atLeastOnce())->method('isEnabled')->willReturn(true); + $this->tmpDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->targetDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor()->getMock(); + $filesystem->expects(self::atLeastOnce())->method('getDirectoryWrite')->with(DirectoryList::TMP) + ->willReturn($this->tmpDirectoryWrite); + $targetDirectory->expects(self::atLeastOnce())->method('getDirectoryWrite')->with(DirectoryList::ROOT) + ->willReturn($this->targetDirectoryWrite); + /** @var LoggerInterface|MockObject $logger */ + $logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->plugin = new Image( + $filesystem, + $this->ioFile, + $targetDirectory, + $config, + $logger + ); + } + + /** + * @dataProvider aroundSaveDataProvider + * @param string $destination + * @param string $newDestination + * @param string|null $newName + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function testAroundSaveWithNewName(string $destination, string $newDestination, ?string $newName): void + { + $tmpDestination = '/tmp/' . $destination; + /** @var AbstractAdapter $subject */ + $subject = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $proceed = function () { + }; + $targetDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getRelativePath') + ->willReturn($destination); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($targetDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') + ->willReturn($tmpDestination); + $driver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $driver->expects(self::atLeastOnce())->method('rename') + ->with($tmpDestination, $newDestination, $driver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver')->willReturn($driver); + $this->ioFile->expects(self::any())->method('getPathInfo')->with($destination) + ->willReturn(['dirname' => 'destination/', 'basename' => 'old_name.file']); + $this->plugin->aroundSave($subject, $proceed, $destination, $newName); + } + + /** + * @return array + */ + public function aroundSaveDataProvider(): array + { + return [ + 'with_new_name' => [ + 'destination' => 'destination/', + 'new_destination' => 'destination/new_name.file', + 'new_name' => 'new_name.file' + ], + 'with_old_name' => [ + 'destination' => 'destination/old_name.file', + 'new_destination' => 'destination/old_name.file', + 'new_name' => null + ] + ]; + } + + /** + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function testBeforeOpen(): void + { + /** @var AbstractAdapter $subject */ + $subject = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $filename = '/path/file_name.file'; + $absolutePath = 'absolute' . $filename; + $tmpAbsolutePath = '/var/www/magento2/tmp'; + $tmpFilePath = $tmpAbsolutePath . 'file_name.file'; + $content = 'Just a test'; + + $targetDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDriver->expects(self::atLeastOnce())->method('fileGetContents')->with($filename) + ->willReturn($content); + $tmpDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $tmpDriver->expects(self::atLeastOnce())->method('filePutContents')->with($tmpFilePath, $content) + ->willReturn(true); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath')->with($filename) + ->willReturn($absolutePath); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('isFile')->with($absolutePath) + ->willReturn(true); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($targetDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($tmpDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('create'); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') + ->willReturn($tmpAbsolutePath); + + self::assertEquals([$tmpFilePath], $this->plugin->beforeOpen($subject, $filename)); + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index 105b2b2b21a46..d82c47a7caf5e 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -7,7 +7,9 @@ }, "suggest": { "magento/module-backend": "*", - "magento/module-sitemap": "*" + "magento/module-sitemap": "*", + "magento/module-cms": "*", + "magento/module-downloadable": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index d9124326e65c2..586f07fc9ca83 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -74,4 +74,22 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> + <type name="Magento\Framework\Data\Collection\Filesystem"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\File\Mime"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\Image\Adapter\AbstractAdapter"> + <plugin name="remoteImageFile" type="Magento\RemoteStorage\Plugin\Image" sortOrder="10"/> + </type> </config> diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php index 5ad3d888ffb57..c756fb43cf584 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php @@ -16,6 +16,7 @@ class DirectoryResolver { /** * @var DirectoryList + * @deprecated $this->filesystem->getDirectoryWrite() can be used for getting directory */ private $directoryList; @@ -51,7 +52,7 @@ public function validatePath($path, $directoryConfig = DirectoryList::MEDIA) { $directory = $this->filesystem->getDirectoryWrite($directoryConfig); $realPath = $directory->getDriver()->getRealPathSafety($path); - $root = $this->directoryList->getPath($directoryConfig); + $root = $directory->getAbsolutePath(); return strpos($realPath, $root) === 0; } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php index 5549c34fa7701..2763dea8ef1e1 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php @@ -75,8 +75,7 @@ public function testValidatePath(string $path, bool $expectedResult): void ->willReturnArgument(0); $this->filesystem->expects($this->atLeastOnce())->method('getDirectoryWrite')->with($directoryConfig) ->willReturn($directory); - $this->directoryList->expects($this->atLeastOnce())->method('getPath')->with($directoryConfig) - ->willReturn($rootPath); + $directory->expects($this->atLeastOnce())->method('getAbsolutePath')->willReturn($rootPath); $this->assertEquals($expectedResult, $this->directoryResolver->validatePath($path, $directoryConfig)); } diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 6103a7df5bf0d..767cda60c0d35 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -7,7 +7,10 @@ namespace Magento\Framework\Data\Collection; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Data\Collection; +use Magento\Framework\Filesystem\Directory\TargetDirectory; /** * Filesystem items collection @@ -126,6 +129,32 @@ class Filesystem extends \Magento\Framework\Data\Collection */ protected $_collectedFiles = []; + /** + * @var TargetDirectory|null + */ + private $targetDirectory; + + /** + * @var DocumentRoot|null + */ + private $documentRoot; + + /** + * @param EntityFactoryInterface|null $_entityFactory + * @param TargetDirectory|null $targetDirectory + * @param DocumentRoot|null $documentRoot + */ + public function __construct( + EntityFactoryInterface $_entityFactory = null, + TargetDirectory $targetDirectory = null, + DocumentRoot $documentRoot = null + ) { + $this->_entityFactory = $_entityFactory ?? ObjectManager::getInstance()->get(EntityFactoryInterface::class); + $this->targetDirectory = $targetDirectory ?? ObjectManager::getInstance()->get(TargetDirectory::class); + $this->documentRoot = $documentRoot ?? ObjectManager::getInstance()->get(DocumentRoot::class); + parent::__construct($this->_entityFactory); + } + /** * Allowed dirs mask setter. Set empty to not filter. * @@ -208,7 +237,9 @@ public function setCollectRecursively($value) public function addTargetDir($value) { $value = (string)$value; - if (!is_dir($value)) { + $directory = $this->targetDirectory->getDirectoryWrite($this->documentRoot->getPath()); + + if (!$directory->isDirectory($value)) { // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception('Unable to set target directory.'); } @@ -235,17 +266,19 @@ public function setDirsFirst($value) * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _collectRecursive($dir) { + $directory = $this->targetDirectory->getDirectoryRead($this->documentRoot->getPath()); $collectedResult = []; if (!is_array($dir)) { $dir = [$dir]; } foreach ($dir as $folder) { - if ($nodes = glob($folder . '/*', GLOB_NOSORT)) { + if ($nodes = $directory->search('/*', $folder)) { foreach ($nodes as $node) { - $collectedResult[] = $node; + $collectedResult[] = $directory->getAbsolutePath($node); } } } @@ -254,7 +287,9 @@ protected function _collectRecursive($dir) } foreach ($collectedResult as $item) { - if (is_dir($item) && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item)))) { + if ($directory->isDirectory($item) + && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item))) + ) { if ($this->_collectDirs) { if ($this->_dirsFirst) { $this->_collectedDirs[] = $item; @@ -265,7 +300,7 @@ protected function _collectRecursive($dir) if ($this->_collectRecursively) { $this->_collectRecursive($item); } - } elseif ($this->_collectFiles && is_file( + } elseif ($this->_collectFiles && $directory->isFile( $item ) && (!$this->_allowedFilesMask || preg_match( $this->_allowedFilesMask, @@ -369,7 +404,7 @@ private function _generateAndFilterAndSort($attributeName) * * @param array $a * @param array $b - * @return int + * @return int|void */ protected function _usort($a, $b) { diff --git a/lib/internal/Magento/Framework/File/Mime.php b/lib/internal/Magento/Framework/File/Mime.php index e0b22e4c944d9..fe23969f32ce3 100644 --- a/lib/internal/Magento/Framework/File/Mime.php +++ b/lib/internal/Magento/Framework/File/Mime.php @@ -6,6 +6,9 @@ namespace Magento\Framework\File; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; + /** * Utility for mime type retrieval */ @@ -90,6 +93,20 @@ class Mime 'inode/x-empty', ]; + /** + * @var Filesystem + */ + private $filesystem; + + /** + * Mime constructor. + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem = null) + { + $this->filesystem = $filesystem; + } + /** * Get mime type of a file * @@ -99,14 +116,16 @@ class Mime */ public function getMimeType($file) { - if (!file_exists($file)) { + $directoryRead = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); + $fileExistsLocally = file_exists($file); + if (!$fileExistsLocally && !$directoryRead->isExist($file)) { throw new \InvalidArgumentException("File '$file' doesn't exist"); } $result = null; $extension = $this->getFileExtension($file); - if (function_exists('mime_content_type')) { + if (function_exists('mime_content_type') && $fileExistsLocally) { $result = $this->getNativeMimeType($file); } else { $imageInfo = getimagesize($file); diff --git a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php index 7a54a7966b500..ff70f0fb9b0c9 100644 --- a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php +++ b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php @@ -7,7 +7,11 @@ namespace Magento\Framework\File\Test\Unit; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -20,15 +24,29 @@ class MimeTest extends TestCase */ private $object; + /** + * @var ReadInterface|MockObject + */ + private $readInterface; + /** * @inheritDoc */ protected function setUp(): void { - $this->object = new Mime(); + $this->readInterface = $this->getMockBuilder(ReadInterface::class) + ->disableOriginalConstructor() + ->getMock(); + /** @var Filesystem|MockObject $filesystem */ + $filesystem = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $filesystem->expects(self::any())->method('getDirectoryRead')->with(DirectoryList::ROOT) + ->willReturn($this->readInterface); + $this->object = new Mime($filesystem); } - public function testGetMimeTypeNonexistentFileException() + public function testGetMimeTypeNonexistentFileException(): void { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); @@ -42,10 +60,10 @@ public function testGetMimeTypeNonexistentFileException() * * @dataProvider getMimeTypeDataProvider */ - public function testGetMimeType($file, $expectedType) + public function testGetMimeType($file, $expectedType): void { $actualType = $this->object->getMimeType($file); - $this->assertSame($expectedType, $actualType); + self::assertSame($expectedType, $actualType); } /** diff --git a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php index 8b983809e643f..dc34fb3fcddc9 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php +++ b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php @@ -675,12 +675,10 @@ protected function _prepareDestination($destination = null, $newName = null) { if (empty($destination)) { $destination = $this->_fileSrcPath; - } else { - if (empty($newName)) { - $info = pathinfo((string) $destination); - $newName = $info['basename']; - $destination = $info['dirname']; - } + } elseif (empty($newName)) { + $info = pathinfo((string) $destination); + $newName = $info['basename']; + $destination = $info['dirname']; } if (empty($newName)) { @@ -751,4 +749,24 @@ public function validateUploadFile($filePath) return $this->getImageType() !== null; } + + /** + * Get file source path + * + * @return string + */ + public function getFileSrcPath(): string + { + return $this->_fileSrcPath ?? ''; + } + + /** + * Get file source name + * + * @return string + */ + public function getFileSrcName(): string + { + return $this->_fileSrcName ?? ''; + } } From 6f8021aef1a1cebc2c9ab5d0799dfc13a9638940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= <jhuenig@maxcluster.de> Date: Thu, 15 Oct 2020 20:01:53 +0200 Subject: [PATCH 0835/1013] add --no-tablespaces parameters to mysqldump --- .../integration/framework/Magento/TestFramework/Db/Mysql.php | 3 ++- .../Framework/Crontab/Test/Unit/CrontabManagerTest.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php b/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php index 7627d78df12dc..017669cba7e13 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php @@ -123,8 +123,9 @@ public function storeDbDump() } $format = sprintf( - '%s %s %s', + '%s %s %s %s', 'mysqldump --defaults-file=%s --host=%s --port=%s', + '--no-tablespaces' $additionalArguments, '%s > %s' ); diff --git a/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php b/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php index 904b76a1285e6..f990566f612a1 100644 --- a/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php +++ b/lib/internal/Magento/Framework/Crontab/Test/Unit/CrontabManagerTest.php @@ -375,13 +375,13 @@ public function saveTasksDataProvider(): array ], [ 'tasks' => [ - ['command' => '{magentoRoot}run.php mysqldump db > db-$(date +%F).sql'] + ['command' => '{magentoRoot}run.php mysqldump --no-tablespaces db > db-$(date +%F).sql'] ], 'content' => '* * * * * /bin/php /var/www/cron.php', 'contentToSave' => '* * * * * /bin/php /var/www/cron.php' . PHP_EOL . CrontabManagerInterface::TASKS_BLOCK_START . ' ' . hash("sha256", BP) . PHP_EOL . '* * * * * ' . PHP_BINARY . ' /var/www/magento2/run.php' - . ' mysqldump db > db-\$(date +%%F).sql' . PHP_EOL + . ' mysqldump --no-tablespaces db > db-\$(date +%%F).sql' . PHP_EOL . CrontabManagerInterface::TASKS_BLOCK_END . ' ' . hash("sha256", BP) . PHP_EOL, ], ]; From 2bc78ad309c0f5b22d65dbfe260abfb739ca3e7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20H=C3=BCnig?= <jonas@huenig.name> Date: Thu, 15 Oct 2020 22:33:20 +0200 Subject: [PATCH 0836/1013] Update Mysql.php add missing colon --- .../integration/framework/Magento/TestFramework/Db/Mysql.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php b/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php index 017669cba7e13..54ae3dcb27a58 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Db/Mysql.php @@ -125,7 +125,7 @@ public function storeDbDump() $format = sprintf( '%s %s %s %s', 'mysqldump --defaults-file=%s --host=%s --port=%s', - '--no-tablespaces' + '--no-tablespaces', $additionalArguments, '%s > %s' ); From 7dcd8e31a54ddd4c7fa9a7dfa887f27c28b8e6a1 Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Thu, 15 Oct 2020 15:54:38 -0500 Subject: [PATCH 0837/1013] MC-38337: [Magento Cloud] Repeat Email Reminders only go to a limited number of customers --- lib/internal/Magento/Framework/Mail/EmailMessage.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Mail/EmailMessage.php b/lib/internal/Magento/Framework/Mail/EmailMessage.php index 5083d5475465e..2bebf6d5f0ae2 100644 --- a/lib/internal/Magento/Framework/Mail/EmailMessage.php +++ b/lib/internal/Magento/Framework/Mail/EmailMessage.php @@ -222,7 +222,11 @@ private function convertAddressArrayToAddressList(array $arrayList): AddressList { $laminasAddressList = new AddressList(); foreach ($arrayList as $address) { - $laminasAddressList->add($address->getEmail(), $address->getName()); + try { + $laminasAddressList->add($address->getEmail(), $address->getName()); + } catch (\InvalidArgumentException $e) { + continue; + } } return $laminasAddressList; From ab41eb8864b6c2980d71b4ee8b3d8e2c4ed93fd8 Mon Sep 17 00:00:00 2001 From: DmytroPaidych <dimonovp@gmail.com> Date: Fri, 16 Oct 2020 09:04:54 +0200 Subject: [PATCH 0838/1013] MC-37896: Create automated test for "Reset Widget" --- .../Widget/Test/Mftf/Section/AdminNewWidgetSection.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml index 49eaf6b377859..4064f8eb394ca 100644 --- a/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml +++ b/app/code/Magento/Widget/Test/Mftf/Section/AdminNewWidgetSection.xml @@ -12,7 +12,7 @@ <element name="widgetType" type="select" selector="#code"/> <element name="widgetDesignTheme" type="select" selector="#theme_id"/> <element name="continue" type="button" timeout="30" selector="#continue_button"/> - <element name="resetBtn" type="button" selector=".page-actions-buttons .reset" timeout="30"/> + <element name="resetBtn" type="button" selector=".page-actions-buttons button#reset" timeout="30"/> <element name="widgetTitle" type="input" selector="#title"/> <element name="widgetStoreIds" type="select" selector="#store_ids"/> <element name="widgetSortOrder" type="input" selector="#sort_order"/> @@ -43,7 +43,7 @@ <element name="displayMode" type="select" selector="select[id*='display_mode']"/> <element name="restrictTypes" type="select" selector="select[id*='types']"/> <element name="saveAndContinue" type="button" selector="#save_and_edit_button" timeout="30"/> - <element name="widgetInstanceType" type="select" selector=".admin__field-control .admin__control-select" /> + <element name="widgetInstanceType" type="select" selector=".admin__field-control select#instance_code" /> <!-- Catalog Product List Widget Options --> <element name="title" type="input" selector="[name='parameters[title]']"/> <element name="displayPageControl" type="select" selector="[name='parameters[show_pager]']"/> From be5dbb9c032daa38628fafbbe3600af6cb4641e2 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Fri, 16 Oct 2020 10:55:38 +0300 Subject: [PATCH 0839/1013] MC-37546: Create automated test for "Create new Category Update" --- .../Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml | 2 +- ...Section.xml => AdminCategoryScheduleDesignUpdateSection.xml} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/code/Magento/Catalog/Test/Mftf/Section/{AdminCategoryScheduleDesingUpdateSection.xml => AdminCategoryScheduleDesignUpdateSection.xml} (90%) diff --git a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml index 5c5dfe8901563..15fcf5f7d4000 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Page/AdminCategoryEditPage.xml @@ -19,6 +19,6 @@ <section name="AdminCategoryModalSection"/> <section name="AdminCategoryMessagesSection"/> <section name="AdminCategoryContentSection"/> - <section name="AdminCategoryScheduleDesingUpdateSection"/> + <section name="AdminCategoryScheduleDesignUpdateSection"/> </page> </pages> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesingUpdateSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml similarity index 90% rename from app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesingUpdateSection.xml rename to app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml index e1b66b3c18260..a65d2c9e63bef 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesingUpdateSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.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="AdminCategoryScheduleDesingUpdateSection"> + <section name="AdminCategoryScheduleDesignUpdateSection"> <element name="sectionHeader" type="button" selector="div[data-index='schedule_design_update'] .fieldset-wrapper-title" timeout="30"/> <element name="sectionBody" type="text" selector="div[data-index='schedule_design_update'] .admin__fieldset-wrapper-content"/> </section> From 56fff4c5a8cf90c0d8914590fde7bd315b504c75 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 16 Oct 2020 11:22:37 +0300 Subject: [PATCH 0840/1013] MC-29405: PHPStan: "Class does not have a constructor and must be instantiated without any parameters" errors --- .../Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php | 1 - .../Unit/Model/Customer/Attribute/Backend/ShippingTest.php | 1 - .../Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php | 3 --- 3 files changed, 5 deletions(-) diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php index 4f318948097cc..65f9b62b426c0 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/BillingTest.php @@ -12,7 +12,6 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\DataObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; class BillingTest extends TestCase { diff --git a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php index 6270905ca2e85..1f5485309cc19 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Customer/Attribute/Backend/ShippingTest.php @@ -12,7 +12,6 @@ use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Framework\DataObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; class ShippingTest extends TestCase { diff --git a/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php b/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php index 0ce450df39b59..b7f2def1c0fbd 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Adminhtml/Design/Config/Edit/SaveButtonTest.php @@ -7,10 +7,7 @@ namespace Magento\Theme\Test\Unit\Block\Adminhtml\Design\Config\Edit; -use Magento\Backend\Block\Widget\Context; -use Magento\Framework\UrlInterface; use Magento\Theme\Block\Adminhtml\Design\Config\Edit\SaveButton; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class SaveButtonTest extends TestCase From 0a02873d0aa0889df7eb53ef5d4834c6c1fdedfd Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Fri, 16 Oct 2020 11:50:18 +0300 Subject: [PATCH 0841/1013] MC-37665: Updating a category through the REST API will uncheck "Use Default Value" on a bunch of attributes --- .../CategoryRepository/PopulateWithValues.php | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php index 2a313119f9c8e..847400ba77c26 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -15,6 +15,7 @@ use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Store\Model\Store; /** * Add data to category entity and populate with default values @@ -68,31 +69,34 @@ public function __construct( */ public function execute(CategoryInterface $category, array $existingData): void { - $storeId = $existingData['store_id']; - $overriddenValues = array_filter( - $category->getData(), - function ($key) use ($category, $storeId) { - /** @var Category $category */ - return $this->scopeOverriddenValue->containsValue( - CategoryInterface::class, - $category, - $key, - $storeId - ); - }, - ARRAY_FILTER_USE_KEY - ); - $defaultValues = array_diff_key($category->getData(), $overriddenValues); - array_walk( - $defaultValues, - function (&$value, $key) { - $attributes = $this->getAttributes(); - if (isset($attributes[$key]) && !$attributes[$key]->isStatic()) { - $value = null; + $storeId = $existingData['store_id'] ?? Store::DEFAULT_STORE_ID; + if ((int)$storeId !== Store::DEFAULT_STORE_ID) { + $overriddenValues = array_filter( + $category->getData(), + function ($key) use ($category, $storeId) { + /** @var Category $category */ + return $this->scopeOverriddenValue->containsValue( + CategoryInterface::class, + $category, + $key, + $storeId + ); + }, + ARRAY_FILTER_USE_KEY + ); + $defaultValues = array_diff_key($category->getData(), $overriddenValues); + array_walk( + $defaultValues, + function (&$value, $key) { + $attributes = $this->getAttributes(); + if (isset($attributes[$key]) && !$attributes[$key]->isStatic()) { + $value = null; + } } - } - ); - $category->addData($defaultValues); + ); + $category->addData($defaultValues); + } + $category->addData($existingData); $useDefaultAttributes = array_filter( $category->getData(), From 8d97104395928f7e328be329c46188992a616a2c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 16 Oct 2020 14:43:20 +0300 Subject: [PATCH 0842/1013] MC-29402: PHPStan: "Anonymous function has an unused use" errors --- app/code/Magento/Customer/Controller/Adminhtml/Index.php | 1 - setup/src/Magento/Setup/Console/Command/RollbackCommand.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index.php index f03f55b16e0c7..9595e473c1869 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index.php @@ -289,7 +289,6 @@ protected function prepareDefaultCustomerTitle(\Magento\Backend\Model\View\Resul protected function _addSessionErrorMessages($messages) { $messages = (array)$messages; - $session = $this->_getSession(); $callback = function ($error) { if (!$error instanceof Error) { diff --git a/setup/src/Magento/Setup/Console/Command/RollbackCommand.php b/setup/src/Magento/Setup/Console/Command/RollbackCommand.php index e4616ae5e271b..e114c84ba79bc 100644 --- a/setup/src/Magento/Setup/Console/Command/RollbackCommand.php +++ b/setup/src/Magento/Setup/Console/Command/RollbackCommand.php @@ -122,7 +122,8 @@ protected function execute(InputInterface $input, OutputInterface $output) // we must have an exit code higher than zero to indicate something was wrong return \Magento\Framework\Console\Cli::RETURN_FAILURE; } - $returnValue = $this->maintenanceModeEnabler->executeInMaintenanceMode( + + return $this->maintenanceModeEnabler->executeInMaintenanceMode( function () use ($input, $output) { try { $helper = $this->getHelper('question'); @@ -152,7 +153,6 @@ function () use ($input, $output) { $output, false ); - return $returnValue; } /** From b09239f9750289ea63880933f0296332445f5d8c Mon Sep 17 00:00:00 2001 From: Buba Suma <soumah@adobe.com> Date: Thu, 15 Oct 2020 14:30:26 -0500 Subject: [PATCH 0843/1013] MC-38309: [CLOUD] empty cart after login - Fix guest session is destroyed after password reset --- .../Customer/Api/SessionCleanerInterface.php | 2 +- .../Customer/Controller/Account/EditPost.php | 10 +- .../Controller/Account/ResetPasswordPost.php | 9 +- .../Customer/Model/Session/SessionCleaner.php | 9 +- .../Customer/Model/AccountManagementTest.php | 134 ++++++++++++++++-- 5 files changed, 143 insertions(+), 21 deletions(-) diff --git a/app/code/Magento/Customer/Api/SessionCleanerInterface.php b/app/code/Magento/Customer/Api/SessionCleanerInterface.php index eb24712105f96..d8534f8b34e83 100644 --- a/app/code/Magento/Customer/Api/SessionCleanerInterface.php +++ b/app/code/Magento/Customer/Api/SessionCleanerInterface.php @@ -13,7 +13,7 @@ interface SessionCleanerInterface { /** - * Destroy all active customer sessions related to given customer id, including current session. + * Destroy all active customer sessions related to given customer except current session. * * @param int $customerId * @return void diff --git a/app/code/Magento/Customer/Controller/Account/EditPost.php b/app/code/Magento/Customer/Controller/Account/EditPost.php index 04b5b72ae776b..6b59986f8ec5f 100644 --- a/app/code/Magento/Customer/Controller/Account/EditPost.php +++ b/app/code/Magento/Customer/Controller/Account/EditPost.php @@ -33,7 +33,8 @@ use Magento\Framework\Phrase; /** - * Class EditPost + * Customer edit account information controller + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EditPost extends AbstractAccount implements CsrfAwareActionInterface, HttpPostActionInterface @@ -185,6 +186,7 @@ public function validateForCsrf(RequestInterface $request): ?bool * Change customer email or password action * * @return \Magento\Framework\Controller\Result\Redirect + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute() { @@ -217,6 +219,12 @@ public function execute() ); $this->dispatchSuccessEvent($customerCandidateDataObject); $this->messageManager->addSuccessMessage(__('You saved the account information.')); + // logout from current session if password changed. + if ($isPasswordChanged) { + $this->session->logout(); + $this->session->start(); + return $resultRedirect->setPath('customer/account/login'); + } return $resultRedirect->setPath('customer/account'); } catch (InvalidEmailOrPasswordException $e) { $this->messageManager->addErrorMessage($this->escaper->escapeHtml($e->getMessage())); diff --git a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php index a127f2acf538f..1bb5aea9b1dc9 100644 --- a/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php +++ b/app/code/Magento/Customer/Controller/Account/ResetPasswordPost.php @@ -14,9 +14,7 @@ use Magento\Customer\Model\Customer\CredentialsValidator; /** - * Class ResetPasswordPost - * - * @package Magento\Customer\Controller\Account + * Customer reset password controller */ class ResetPasswordPost extends \Magento\Customer\Controller\AbstractAccount implements HttpPostActionInterface { @@ -91,6 +89,11 @@ public function execute() $resetPasswordToken, $password ); + // logout from current session if password changed. + if ($this->session->isLoggedIn()) { + $this->session->logout(); + $this->session->start(); + } $this->session->unsRpToken(); $this->messageManager->addSuccessMessage(__('You updated your password.')); $resultRedirect->setPath('*/*/login'); diff --git a/app/code/Magento/Customer/Model/Session/SessionCleaner.php b/app/code/Magento/Customer/Model/Session/SessionCleaner.php index 1423c94782535..5118c20329aaa 100644 --- a/app/code/Magento/Customer/Model/Session/SessionCleaner.php +++ b/app/code/Magento/Customer/Model/Session/SessionCleaner.php @@ -71,13 +71,6 @@ public function __construct( */ 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 @@ -89,6 +82,8 @@ public function clearFor(int $customerId): void $visitorCollection = $this->visitorCollectionFactory->create(); $visitorCollection->addFieldToFilter('customer_id', $customerId); $visitorCollection->addFieldToFilter('last_visit_at', ['from' => $activeSessionsTime]); + $visitorCollection->addFieldToFilter('session_id', ['neq' => $this->sessionManager->getSessionId()]); + /** @var \Magento\Customer\Model\Visitor $visitor */ foreach ($visitorCollection->getItems() as $visitor) { $sessionId = $visitor->getSessionId(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index 6f2cf2d76bd11..3a16d3eafd6ce 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -14,6 +14,7 @@ use Magento\Framework\Exception\State\ExpiredException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Url as UrlBuilder; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -113,6 +114,10 @@ protected function tearDown(): void $customerRegistry->remove(1); $addressRegistry->remove(1); $addressRegistry->remove(2); + /** @var \Magento\Customer\Model\ResourceModel\Visitor $resourceModel */ + $resourceModel = $this->objectManager->get(\Magento\Customer\Model\ResourceModel\Visitor::class); + $resourceModel->getConnection()->delete($resourceModel->getMainTable()); + parent::tearDown(); } /** @@ -158,19 +163,52 @@ public function testChangePassword() { /** @var SessionManagerInterface $session */ $session = $this->objectManager->get(SessionManagerInterface::class); - $oldSessionId = $session->getSessionId(); - $session->setTestData('test'); + $time = time(); + + $session->start(); + $guessSessionId = $session->getSessionId(); + $this->createVisitorSession($guessSessionId); + $session->setTestData('guest_session_data'); + + // open new session + $activeSessionId = uniqid("active-$time-"); + $this->startNewSession($activeSessionId); + $this->createVisitorSession($activeSessionId, 1); + $session->setTestData('customer_session_data_1'); + + // open new session + $currentSessionId = uniqid("current-$time-"); + $this->startNewSession($currentSessionId); + $this->createVisitorSession($currentSessionId, 1); + $session->setTestData('customer_session_data_current'); + + // change password $this->accountManagement->changePassword('customer@example.com', 'password', 'new_Password123'); - - $this->assertTrue( - $oldSessionId !== $session->getSessionId(), - 'Customer session id wasn\'t regenerated after change password' + $this->assertEquals( + $currentSessionId, + $session->getSessionId(), + 'Current session was renewed' ); - $session->destroy(); - $session->setSessionId($oldSessionId); + // open customer active session + $this->startNewSession($activeSessionId); + $this->assertNull($session->getTestData(), 'Customer active session data wasn\'t cleaned up'); + + // open customer current session + $this->startNewSession($currentSessionId); + $this->assertEquals( + 'customer_session_data_current', + $session->getTestData(), + 'Customer current session data was cleaned up' + ); - $this->assertNull($session->getTestData(), 'Customer session data wasn\'t cleaned'); + // open guess session + $this->startNewSession($guessSessionId); + $this->assertEquals( + 'guest_session_data', + $session->getTestData(), + 'Guest session data was cleaned up' + ); $this->accountManagement->authenticate('customer@example.com', 'new_Password123'); } @@ -392,11 +430,58 @@ public function testValidateResetPasswordLinkTokenAmbiguous() */ public function testResetPassword() { + /** @var SessionManagerInterface $session */ + $session = $this->objectManager->get(SessionManagerInterface::class); + $time = time(); + + $session->start(); + $guessSessionId = $session->getSessionId(); + $this->createVisitorSession($guessSessionId); + $session->setTestData('guest_session_data'); + + // open new session + $activeSessionId = uniqid("active-$time-"); + $this->startNewSession($activeSessionId); + $this->createVisitorSession($activeSessionId, 1); + $session->setTestData('customer_session_data_1'); + + // open new session + $currentSessionId = uniqid("current-$time-"); + $this->startNewSession($currentSessionId); + $this->createVisitorSession($currentSessionId, 1); + $session->setTestData('customer_session_data_current'); + $resetToken = 'lsdj579slkj5987slkj595lkj'; $password = 'new_Password123'; $this->setResetPasswordData($resetToken, 'Y-m-d H:i:s'); $this->assertTrue($this->accountManagement->resetPassword('customer@example.com', $resetToken, $password)); + + $this->assertEquals( + $currentSessionId, + $session->getSessionId(), + 'Current session was renewed' + ); + + // open customer active session + $this->startNewSession($activeSessionId); + $this->assertNull($session->getTestData(), 'Customer active session data wasn\'t cleaned up'); + + // open customer current session + $this->startNewSession($currentSessionId); + $this->assertEquals( + 'customer_session_data_current', + $session->getTestData(), + 'Customer current session data was cleaned up' + ); + + // open guess session + $this->startNewSession($guessSessionId); + $this->assertEquals( + 'guest_session_data', + $session->getTestData(), + 'Guest session data was cleaned up' + ); } /** @@ -727,4 +812,35 @@ protected function setResetPasswordData( $customerModel->setRpTokenCreatedAt(date($date)); $customerModel->save(); } + + /** + * @param string $sessionId + */ + private function startNewSession(string $sessionId): void + { + /** @var SessionManagerInterface $session */ + $session = $this->objectManager->get(SessionManagerInterface::class); + // close session and cleanup session variable + $session->writeClose(); + $session->clearStorage(); + // open new session + $session->setSessionId($sessionId); + $session->start(); + } + + /** + * @param string $sessionId + * @param int|null $customerId + * @return Visitor + */ + private function createVisitorSession(string $sessionId, ?int $customerId = null): Visitor + { + /** @var Visitor $visitor */ + $visitor = Bootstrap::getObjectManager()->create(Visitor::class); + $visitor->setCustomerId($customerId); + $visitor->setSessionId($sessionId); + $visitor->setLastVisitAt((new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT)); + $visitor->save(); + return $visitor; + } } From eaa08eef94eabd569ab29fb476df28591ae65e1a Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Fri, 16 Oct 2020 15:32:29 +0300 Subject: [PATCH 0844/1013] MC-37665: Updating a category through the REST API will uncheck "Use Default Value" on a bunch of attributes --- .../CategoryRepository/PopulateWithValues.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php index 847400ba77c26..c6feb049e1a10 100644 --- a/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php +++ b/app/code/Magento/Catalog/Model/CategoryRepository/PopulateWithValues.php @@ -42,6 +42,11 @@ class PopulateWithValues */ private $filterBuilder; + /** + * @var AttributeInterface[] + */ + private $attributes; + /** * @param ScopeOverriddenValue $scopeOverriddenValue * @param AttributeRepository $attributeRepository @@ -120,8 +125,12 @@ function () { * * @return AttributeInterface[] */ - private function getAttributes() + private function getAttributes(): array { + if ($this->attributes) { + return $this->attributes; + } + $searchResult = $this->attributeRepository->getList( $this->searchCriteriaBuilder->addFilters( [ @@ -133,11 +142,12 @@ private function getAttributes() ] )->create() ); - $result = []; + + $this->attributes = []; foreach ($searchResult->getItems() as $attribute) { - $result[$attribute->getAttributeCode()] = $attribute; + $this->attributes[$attribute->getAttributeCode()] = $attribute; } - return $result; + return $this->attributes; } } From 40134a616f101df6e98283259810de813d4f66e7 Mon Sep 17 00:00:00 2001 From: TuNa <ladiesman9x@gmail.com> Date: Fri, 16 Oct 2020 21:49:51 +0700 Subject: [PATCH 0845/1013] Fix missing escape less calc up update up --- .../web/css/source/module/order/_payment-shipping.less | 7 ++++++- .../Magento/backend/web/css/source/forms/_fields.less | 6 ++++-- .../Magento/luma/Magento_Sales/web/css/source/_module.less | 6 +++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less index 2c55d243ebe07..121de6e7b6d8a 100644 --- a/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less +++ b/app/design/adminhtml/Magento/backend/Magento_Sales/web/css/source/module/order/_payment-shipping.less @@ -9,13 +9,15 @@ .admin__payment-method-wrapper { margin: 0; - width: calc(50% - @indent__l); + width: ~'calc(50% - @{indent__l})'; + .admin__field { margin-left: 0; &:first-child { margin-top: 1.5rem; } } + .admin__payment-methods { margin: 0; } @@ -62,6 +64,7 @@ position: absolute; right: 0; top: 0; + span { background-color: @color-white; display: block; @@ -71,6 +74,7 @@ position: absolute; top: 43px; } + .order-shipping-address & { span { top: 0; @@ -102,6 +106,7 @@ + .order-payment-currency { margin-top: @indent__s; } + .admin__table-secondary { margin-top: @indent__s; &:extend(.abs-admin__table-secondary-edit-order all); diff --git a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less index 256ac453578df..b86b0005e88fb 100644 --- a/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less +++ b/app/design/adminhtml/Magento/backend/web/css/source/forms/_fields.less @@ -163,6 +163,7 @@ .admin__field-control { padding-top: 7px; } + .admin__field-option { padding-top: 0; } @@ -361,6 +362,7 @@ cursor: inherit; opacity: 1; outline: inherit; + .admin__action-multiselect-wrap { .admin__action-multiselect { .__form-control-pattern__disabled(); @@ -433,7 +435,7 @@ font-size: 1.7rem; font-weight: @font-weight__bold; padding: 1.7rem 0; - width: calc(100% - @indent__l); + width: ~'calc(100% - @{indent__l})'; } .admin__field-option { @@ -704,6 +706,7 @@ width: 100%; } } + & > .admin__field-label { text-align: left; } @@ -819,4 +822,3 @@ overflow: hidden; } } - diff --git a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less index bab8a2abb9b93..f8ab8ddb088ec 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Sales/web/css/source/_module.less @@ -210,6 +210,7 @@ .items-qty { &:extend(.abs-reset-list all); + .item { white-space: nowrap; } @@ -347,13 +348,15 @@ .product-item-name { float: left; - width: calc(100% - 20px); + width: calc(~'100% - 20px'); } + .product-item::after { clear: both; content: ''; display: table; } + .product-item { .label { &:extend(.abs-visually-hidden all); @@ -491,6 +494,7 @@ .data.table .col.options { padding: 0 10px 15px; + &:before { display: none; } From f882de3751ca673841eba17cfc7b7ea458314b2c Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 16 Oct 2020 10:17:18 -0500 Subject: [PATCH 0846/1013] 29251 revert is_available_for_selection --- .../Model/Options/Collection.php | 83 +------------------ .../etc/schema.graphqls | 2 - 2 files changed, 1 insertion(+), 84 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php index 7a27964897828..5e3666407a383 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Options/Collection.php @@ -10,14 +10,10 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ProductFactory; -use Magento\Catalog\Model\ProductRepository; -use Magento\ConfigurableProduct\Helper\Data; use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\Collection as AttributeCollection; use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Attribute\CollectionFactory; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; /** @@ -35,36 +31,11 @@ class Collection */ private $productFactory; - /** - * @var ProductRepository - */ - private $productRepository; - /** * @var MetadataPool */ private $metadataPool; - /** - * @var Data - */ - private $configurableProductHelper; - - /** - * @var Metadata - */ - private $optionsMetadata; - - /** - * @var SelectionUidFormatter - */ - private $selectionUidFormatter; - - /** - * @var SearchCriteriaBuilder - */ - private $searchCriteriaBuilder; - /** * @var int[] */ @@ -78,32 +49,16 @@ class Collection /** * @param CollectionFactory $attributeCollectionFactory * @param ProductFactory $productFactory - * @param ProductRepository $productRepository * @param MetadataPool $metadataPool - * @param Data $configurableProductHelper - * @param Metadata $optionsMetadata - * @param SelectionUidFormatter $selectionUidFormatter - * @param SearchCriteriaBuilder $searchCriteriaBuilder */ public function __construct( CollectionFactory $attributeCollectionFactory, ProductFactory $productFactory, - ProductRepository $productRepository, - MetadataPool $metadataPool, - Data $configurableProductHelper, - Metadata $optionsMetadata, - SelectionUidFormatter $selectionUidFormatter, - SearchCriteriaBuilder $searchCriteriaBuilder + MetadataPool $metadataPool ) { $this->attributeCollectionFactory = $attributeCollectionFactory; $this->productFactory = $productFactory; - $this->productRepository = $productRepository; $this->metadataPool = $metadataPool; - $this->configurableProductHelper = $configurableProductHelper; - $this->optionsMetadata = $optionsMetadata; - $this->selectionUidFormatter = $selectionUidFormatter; - $this->searchCriteriaBuilder = $searchCriteriaBuilder ?? - ObjectManager::getInstance()->get(SearchCriteriaBuilder::class); } /** @@ -156,8 +111,6 @@ private function fetch() : array $attributeCollection->setProductFilter($product); } - $products = $this->getProducts($this->productIds); - /** @var Attribute $attribute */ foreach ($attributeCollection->getItems() as $attribute) { $productId = (int)$attribute->getProductId(); @@ -175,42 +128,8 @@ private function fetch() : array $this->attributeMap[$productId][$attribute->getId()]['values'] = $attributeData['options']; $this->attributeMap[$productId][$attribute->getId()]['label'] = $attribute->getProductAttribute()->getStoreLabel(); - - if (isset($products[$productId])) { - $options = $this->configurableProductHelper->getOptions( - $products[$productId], - $this->optionsMetadata->getAllowProducts($products[$productId]) - ); - foreach ($attributeData['options'] as $index => $value) { - $this->attributeMap[$productId][$attribute->getId()]['values'][$index]['uid'] - = $this->selectionUidFormatter->encode((int)$attribute->getId(), (int)$value['value_index']); - $this->attributeMap[$productId][$attribute->getId()]['values'][$index] - ['is_available_for_selection'] = - isset($options[$attribute->getAttributeId()][$value['value_index']]) - && $options[$attribute->getAttributeId()][$value['value_index']]; - } - } } return $this->attributeMap; } - - /** - * Load products by link field ids - * - * @param int[] $productIds - * @return ProductInterface[] - */ - private function getProducts($productIds) - { - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); - $this->searchCriteriaBuilder->addFilter($linkField, $productIds, 'in'); - $searchCriteria = $this->searchCriteriaBuilder->create(); - $products = $this->productRepository->getList($searchCriteria)->getItems(); - $productsLinkFieldMap = []; - foreach ($products as $product) { - $productsLinkFieldMap[$product->getData($linkField)] = $product; - } - return $productsLinkFieldMap; - } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 88d0f8e212acf..6a58f7a77dfe2 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -35,8 +35,6 @@ type ConfigurableProductOptions @doc(description: "ConfigurableProductOptions de } type ConfigurableProductOptionsValues @doc(description: "ConfigurableProductOptionsValues contains the index number assigned to a configurable product option") { - uid: ID! - is_available_for_selection: Boolean! value_index: Int @doc(description: "A unique index number assigned to the configurable product option") label: String @doc(description: "The label of the product") default_label: String @doc(description: "The label of the product on the default store") From ab0f3177ab48775b6de76cc2be4d7b89b593dfdc Mon Sep 17 00:00:00 2001 From: Roman Flowers <flowers@adobe.com> Date: Fri, 16 Oct 2020 11:55:32 -0500 Subject: [PATCH 0847/1013] MC-38337: [Magento Cloud] Repeat Email Reminders only go to a limited number of customers --- .../Framework/Mail/TransportBuilderTest.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php index 9dc8aa91237d8..47c8da84902d4 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Mail/TransportBuilderTest.php @@ -116,4 +116,84 @@ public function emailDataProvider(): array ] ]; } + + /** + * Test if invalid email in the queue will not fail the entire queue from being sent + * + * @magentoDataFixture Magento/Email/Model/_files/email_template.php + * @magentoDbIsolation enabled + * + * @param string|array $emails + * @dataProvider invalidEmailDataProvider + * @throws LocalizedException + */ + public function testAddToInvalidEmailInTheQueue($emails) + { + $template = $this->template->load('email_exception_fixture', 'template_code'); + $templateId = $template->getId(); + + switch ($template->getType()) { + case TemplateTypesInterface::TYPE_TEXT: + $templateType = MimeInterface::TYPE_TEXT; + break; + + case TemplateTypesInterface::TYPE_HTML: + $templateType = MimeInterface::TYPE_HTML; + break; + + default: + $templateType = ''; + $this->fail('Unsupported Mime Type'); + } + + $this->builder->setTemplateModel(BackendTemplate::class); + + $vars = ['reason' => 'Reason', 'customer' => 'Customer']; + $options = ['area' => 'frontend', 'store' => 1]; + $this->builder->setTemplateIdentifier($templateId)->setTemplateVars($vars)->setTemplateOptions($options); + + $allEmails = $emails[0]; + $validOnlyEmails = $emails[1]; + + foreach ($allEmails as $email) { + $this->builder->addTo($email); + } + + /** @var EmailMessage $emailMessage */ + $emailMessage = $this->builder->getTransport()->getMessage(); + $this->assertStringContainsStringIgnoringCase($templateType, $emailMessage->getHeaders()['Content-Type']); + + $resultEmails = []; + /** @var Address $toAddress */ + foreach ($emailMessage->getTo() as $address) { + $resultEmails[] = $address->getEmail(); + } + + $this->assertEquals($validOnlyEmails, $resultEmails); + } + + /** + * @return array + */ + public function invalidEmailDataProvider(): array + { + return [ + [ + [ + [ + 'billy.everything@someserver.com', + 'billy.everythingsomeserver.com', + 'billy.everything2@someserver.com', + 'billy.everythin2gsomeserver.com', + 'billy.everything3@someserver.com' + ], + [ + 'billy.everything@someserver.com', + 'billy.everything2@someserver.com', + 'billy.everything3@someserver.com' + ] + ] + ] + ]; + } } From d0249eecfd93e9d5725c77b31c76f31c3f08c6c9 Mon Sep 17 00:00:00 2001 From: Andrii Kasian <akasian@magento.com> Date: Fri, 16 Oct 2020 17:25:51 -0500 Subject: [PATCH 0848/1013] MessageValidator fails on hash arrays that dont have 0 element Magento\Framework\MessageQueue\Test\Unit\MessageValidatorTest::testInvalidMessageType with data set #11 (array('object_interface', 'Magento\Customer\Api\Data\Cus...face[]'), array(23, 545), 'Data in topic "topic" must be...e[]". ') PHPUnit\Framework\Exception: Notice: Undefined offset: 0 in /magento/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php:157. --- .../Magento/Framework/MessageQueue/MessageValidator.php | 4 ++-- .../MessageQueue/Test/Unit/MessageValidatorTest.php | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php b/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php index 45ce351ed97bb..ffa980c882640 100644 --- a/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php +++ b/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php @@ -119,7 +119,7 @@ protected function validatePrimitiveType($message, $messageType, $topic) $realType = $this->getRealType($message); if ($realType == 'array' && count($message) == 0) { return; - } elseif ($realType == 'array' && count($message) > 0) { + } elseif ($realType == 'array' && isset($message[0])) { $realType = $this->getRealType($message[0]); $compareType = preg_replace('/\[\]/', '', $messageType); } @@ -153,7 +153,7 @@ protected function validateClassType($message, $messageType, $topic) $realType = $this->getRealType($message); if ($realType == 'array' && count($message) == 0) { return; - } elseif ($realType == 'array' && count($message) > 0) { + } elseif ($realType == 'array' && isset($message[0])) { $message = $message[0]; $compareType = preg_replace('/\[\]/', '', $messageType); } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageValidatorTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageValidatorTest.php index d324a9dbcfda9..c7d3e35aee3de 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageValidatorTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/MessageValidatorTest.php @@ -255,6 +255,14 @@ public function getQueueConfigRequestType() $customerMock, 'Data in topic "topic" must be of type "Magento\Customer\Api\Data\CustomerInterface[]". ' ], + [ + [ + CommunicationConfig::TOPIC_REQUEST_TYPE => CommunicationConfig::TOPIC_REQUEST_TYPE_CLASS, + CommunicationConfig::TOPIC_REQUEST => 'Magento\Customer\Api\Data\CustomerInterface[]' + ], + [1=>23, 3=>545], + 'Data in topic "topic" must be of type "Magento\Customer\Api\Data\CustomerInterface[]". ' + ], ]; } } From 52a288b24c774db2c82b56d07e10d7116fe4d1ef Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Sat, 17 Oct 2020 18:17:57 +0300 Subject: [PATCH 0849/1013] revert changes --- .../Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml index 99c60eba67854..4f0e9bb000a27 100644 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml +++ b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/AdminCheckAnalyticsTrackingTest.xml @@ -22,13 +22,14 @@ <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> <argument name="tags" value="config full_page"/> </actionGroup> + <reloadPage stepKey="pageReload"/> </before> <after> <magentoCLI command="config:set admin/usage/enabled 0" stepKey="disableAdminUsageTracking"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <actionGroup ref="ReloadPageActionGroup" stepKey="pageReload"/> + <waitForPageLoad stepKey="waitForPageReloaded"/> <seeInPageSource html="var adminAnalyticsMetadata =" stepKey="seeInPageSource"/> </test> </tests> From f4d2ff81c74a62007cf6430b8371d06a54a0caa3 Mon Sep 17 00:00:00 2001 From: mage2pratik <magepratik@gmail.com> Date: Sun, 18 Oct 2020 01:30:55 +0530 Subject: [PATCH 0850/1013] XSD URN format --- .../TestModuleDefaultHydrator/etc/extension_attributes.xml | 2 +- .../_files/Magento/TestModuleDefaultHydrator/etc/module.xml | 2 +- .../Magento/Framework/View/_files/UiComponent/theme/theme.xml | 2 +- .../Magento/Framework/View/_files/static/theme/theme.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/extension_attributes.xml b/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/extension_attributes.xml index 96dd60809754a..e1154f407e857 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/extension_attributes.xml +++ b/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/extension_attributes.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Api/etc/extension_attributes.xsd"> +<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="extension_attribute" type="Magento\TestModuleDefaultHydrator\Api\Data\ExtensionAttributeInterface" /> </extension_attributes> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/module.xml b/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/module.xml index ca4ded8ff3190..a8acf9c4acc77 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/module.xml +++ b/dev/tests/api-functional/_files/Magento/TestModuleDefaultHydrator/etc/module.xml @@ -5,7 +5,7 @@ * See COPYING.txt for license details. */ --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd"> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> <module name="Magento_TestModuleDefaultHydrator"> </module> </config> diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/_files/UiComponent/theme/theme.xml b/dev/tests/integration/testsuite/Magento/Framework/View/_files/UiComponent/theme/theme.xml index 532670df30dfe..f1360542ff011 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/_files/UiComponent/theme/theme.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/View/_files/UiComponent/theme/theme.xml @@ -4,6 +4,6 @@ * See COPYING.txt for license details. */ --> -<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Config/etc/theme.xsd"> +<theme xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Config/etc/theme.xsd"> <title>Test theme diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/theme.xml b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/theme.xml index 532670df30dfe..f1360542ff011 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/theme.xml +++ b/dev/tests/integration/testsuite/Magento/Framework/View/_files/static/theme/theme.xml @@ -4,6 +4,6 @@ * See COPYING.txt for license details. */ --> - + Test theme From 3132e0ae2bceeb2de38c357b3072f927ea0f94a0 Mon Sep 17 00:00:00 2001 From: Kate Kyzyma Date: Mon, 19 Oct 2020 09:53:31 +0300 Subject: [PATCH 0851/1013] fix minor code style issues --- .../AdminOpenCmsBlocksGridActionGroup.xml | 27 +++++++++---------- ...dminSelectCMSBlockStoreViewActionGroup.xml | 3 ++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.xml index fca85651f7fda..4b57e0c1274f6 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCmsBlocksGridActionGroup.xml @@ -1,19 +1,18 @@ - + - - - Goes to the Cms Blocks grid page. - - - - - + + + Goes to the Cms Blocks grid page. + + + + diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.xml index 8c543e29c1ed7..e5b8caf1e209f 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSelectCMSBlockStoreViewActionGroup.xml @@ -5,6 +5,7 @@ * See COPYING.txt for license details. */ --> + @@ -12,6 +13,6 @@ - + From b0a3cf71e5809ac1037ab803736126e0039b4460 Mon Sep 17 00:00:00 2001 From: SmVladyslav Date: Mon, 19 Oct 2020 11:12:58 +0300 Subject: [PATCH 0852/1013] MC-35783: "1 item(s) need your attention." still visible in mini cart after product remove --- .../Test/Mftf/Data/NonexistentProductData.xml | 19 +++++++++++++++++++ .../Section/StorefrontMinicartSection.xml | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/Data/NonexistentProductData.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/NonexistentProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/NonexistentProductData.xml new file mode 100644 index 0000000000000..73b0765394333 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Data/NonexistentProductData.xml @@ -0,0 +1,19 @@ + + + + + + NonexistentProductSku + 1 + + + SecondNonexistentProductSku + 1 + + diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml index dcab48dbc5368..1ecf97c50c81a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontMinicartSection.xml @@ -39,7 +39,7 @@ - + From 937295074db740e43fe83c3622300af176a4a6ba Mon Sep 17 00:00:00 2001 From: Namrata Vora Date: Mon, 19 Oct 2020 14:06:38 +0530 Subject: [PATCH 0853/1013] Removed sortOrder from messages, authentication, progressBar, estimation, and sidebar checkout components as they are already been rendered without those sortOrders and individual getRegions at app/code/Magento/Checkout/view/frontend/web/template/onepage.html --- .../Checkout/view/frontend/layout/checkout_index_index.xml | 5 ----- 1 file changed, 5 deletions(-) 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 192f20653f8c3..9b2497efb12d0 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 @@ -32,12 +32,10 @@ - 0 Magento_Ui/js/view/messages messages - 1 Magento_Checkout/js/view/authentication authentication @@ -50,7 +48,6 @@ - 0 Magento_Checkout/js/view/progress-bar progressBar @@ -61,7 +58,6 @@ - 10 Magento_Checkout/js/view/estimation estimation @@ -335,7 +331,6 @@ - 50 Magento_Checkout/js/view/sidebar sidebar From 59b6780dc888fe4bd8015de7d98c64eede46e8f8 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha Date: Mon, 19 Oct 2020 12:32:06 +0300 Subject: [PATCH 0854/1013] MC-36966: Create automated test for "The type of the State input field for the multistore is incorrect when restricted by country" --- ...stomer_for_second_website_with_address.php | 49 +++++++++++++++++++ ...r_second_website_with_address_rollback.php | 37 ++++++++++++++ .../Adminhtml/Order/Address/FormTest.php | 32 ++++++++++++ .../order_with_customer_on_second_website.php | 35 +++++++++++++ ...th_customer_on_second_website_rollback.php | 13 +++++ 5 files changed, 166 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address_rollback.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website.php create mode 100644 dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address.php new file mode 100644 index 0000000000000..1c2782dbe675b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address.php @@ -0,0 +1,49 @@ +requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$store = $storeManager->getStore('fixture_third_store'); +/** @var AccountManagementInterface $accountManagment */ +$accountManagment = $objectManager->get(AccountManagementInterface::class); +/** @var CustomerFactory $customerFactory */ +$customerFactory = $objectManager->get(CustomerFactory::class); +/** @var AttributeRepository $attributeRepository */ +$attributeRepository = $objectManager->get(AttributeRepository::class); +$gender = $attributeRepository->get(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER, CustomerInterface::GENDER) + ->getSource()->getOptionId('Male'); +$defaultGroupId = $objectManager->get(GroupManagement::class)->getDefaultGroup($store->getStoreId())->getId(); + +$customer = $customerFactory->create(); +$customer->setWebsiteId($websiteId) + ->setEmail('customer_second_ws_with_addr@example.com') + ->setGroupId($defaultGroupId) + ->setStoreId($store->getStoreId()) + ->setFirstname('John') + ->setLastname('Smith') + ->setDefaultBilling(1) + ->setDefaultShipping(1) + ->setGender($gender); + +$accountManagment->createAccount($customer, 'Apassword1'); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address_rollback.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address_rollback.php new file mode 100644 index 0000000000000..48e6f56d83442 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_for_second_website_with_address_rollback.php @@ -0,0 +1,37 @@ +get(Registry::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $customer = $customerRepository->get('customer_second_ws_with_addr@example.com', $websiteId); + $customerRepository->delete($customer); +} catch (NoSuchEntityException $e) { + //customer already deleted +} + +$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/Sales/Block/Adminhtml/Order/Address/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php index 493bf7ec37ec3..8657e62eb267e 100644 --- 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 @@ -7,6 +7,7 @@ namespace Magento\Sales\Block\Adminhtml\Order\Address; +use Magento\Directory\Model\ResourceModel\Country\CollectionFactory; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; use Magento\Framework\View\LayoutInterface; @@ -15,6 +16,7 @@ use Magento\Sales\Api\Data\OrderInterfaceFactory; use Magento\TestFramework\App\Config; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; use PHPUnit\Framework\TestCase; /** @@ -38,6 +40,9 @@ class FormTest extends TestCase /** @var OrderInterfaceFactory */ private $orderFactory; + /** @var CollectionFactory */ + private $countryCollectionFactory; + /** * @inheritdoc */ @@ -50,6 +55,7 @@ protected function setUp(): void $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); $this->registry = $this->objectManager->get(Registry::class); $this->objectManager->removeSharedInstance(Config::class); + $this->countryCollectionFactory = $this->objectManager->get(CollectionFactory::class); } /** @@ -110,6 +116,32 @@ public function testCountryIdInNotAllowedList(): void $this->assertCountryField('CA'); } + /** + * @magentoDbIsolation disabled + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer_on_second_website.php + * + * @magentoConfigFixture default_store general/country/default UA + * @magentoConfigFixture default_store general/country/allow UA + * + * @return void + */ + public function testFormRenderedWithSelectRegionInput(): void + { + $address = $this->getOrderAddress('100000001'); + $this->registerOrderAddress($address); + $form = $this->block->getForm(); + $countryElement = $form->getElement('country_id'); + $this->assertNotNull($countryElement); + $this->assertEquals('US', $countryElement->getEscapedValue()); + $html = $form->toHtml(); + $regionIdSelectXpath = '//select[@id=\'region_id\']'; + $this->assertEquals(1, Xpath::getElementsCountForXpath($regionIdSelectXpath, $html)); + $countryOptionsXpath = '//select[@id=\'country_id\']/option'; + $allowedCountriesCount = count($this->countryCollectionFactory->create()->loadByStore()); + $this->assertEquals($allowedCountriesCount, Xpath::getElementsCountForXpath($countryOptionsXpath, $html)); + } + /** * Prepares address edit from block. * diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website.php new file mode 100644 index 0000000000000..2433d8bd26281 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website.php @@ -0,0 +1,35 @@ +requireDataFixture('Magento/Sales/_files/order.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_for_second_website_with_address.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$storeId = (int)$storeManager->getStore('fixture_third_store')->getId(); +$websiteId = (int)$storeManager->getWebsite('test')->getId(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$customerId = $customerRepository->get( + 'customer_second_ws_with_addr@example.com', + $websiteId +)->getId(); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->get(OrderRepositoryInterface::class); +/** @var OrderInterface $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); +$order->setCustomerId((int)$customerId)->setCustomerIsGuest(false)->setStoreId($storeId); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website_rollback.php new file mode 100644 index 0000000000000..b3a396e69164f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_on_second_website_rollback.php @@ -0,0 +1,13 @@ +requireDataFixture('Magento/Sales/_files/order_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/Customer/_files/customer_for_second_website_with_address_rollback.php' +); From e5759ee020b5d33b242cb3b112d91ff1980d5538 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn Date: Mon, 19 Oct 2020 16:53:20 +0300 Subject: [PATCH 0855/1013] MC-36956: Create automated test for "Upload Category Image" --- .../Catalog/Model/ImageUploaderTest.php | 91 +++++++----- .../Model/ResourceModel/CategoryTest.php | 129 ++++++++++++++++++ .../magento_image with space in name.jpg | Bin 0 -> 4025 bytes 3 files changed, 189 insertions(+), 31 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image with space in name.jpg diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php index 51ebc4b03310e..09d08d23cf3e3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ImageUploaderTest.php @@ -8,47 +8,61 @@ namespace Magento\Catalog\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\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * Tests for the \Magento\Catalog\Model\ImageUploader class */ -class ImageUploaderTest extends \PHPUnit\Framework\TestCase +class ImageUploaderTest extends TestCase { + private const BASE_TMP_PATH = 'catalog/tmp/category'; + + private const BASE_PATH = 'catalog/category'; + /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; /** - * @var \Magento\Catalog\Model\ImageUploader + * @var ImageUploader */ private $imageUploader; /** - * @var \Magento\Framework\Filesystem + * @var Filesystem */ private $filesystem; /** - * @var \Magento\Framework\Filesystem\Directory\WriteInterface + * @var WriteInterface */ private $mediaDirectory; + /** + * @var WriteInterface + */ + private $tmpDirectory; + /** * @inheritdoc */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - /** @var \Magento\Framework\Filesystem $filesystem */ - $this->filesystem = $this->objectManager->get(\Magento\Framework\Filesystem::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->filesystem = $this->objectManager->get(Filesystem::class); $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); - /** @var $uploader \Magento\MediaStorage\Model\File\Uploader */ + $this->tmpDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::SYS_TMP); $this->imageUploader = $this->objectManager->create( - \Magento\Catalog\Model\ImageUploader::class, + ImageUploader::class, [ - 'baseTmpPath' => 'catalog/tmp/category', - 'basePath' => 'catalog/category', + 'baseTmpPath' => self::BASE_TMP_PATH, + 'basePath' => self::BASE_PATH, 'allowedExtensions' => ['jpg', 'jpeg', 'gif', 'png'], 'allowedMimeTypes' => ['image/jpg', 'image/jpeg', 'image/gif', 'image/png'] ] @@ -56,14 +70,15 @@ protected function setUp(): void } /** + * @dataProvider saveFileToTmpDirProvider + * @param string $fileName + * @param string $expectedName * @return void */ - public function testSaveFileToTmpDir(): void + public function testSaveFileToTmpDir(string $fileName, string $expectedName): void { - $fileName = 'magento_small_image.jpg'; - $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); $fixtureDir = realpath(__DIR__ . '/../_files'); - $filePath = $tmpDirectory->getAbsolutePath($fileName); + $filePath = $this->tmpDirectory->getAbsolutePath($fileName); copy($fixtureDir . DIRECTORY_SEPARATOR . $fileName, $filePath); $_FILES['image'] = [ @@ -75,10 +90,27 @@ public function testSaveFileToTmpDir(): void ]; $this->imageUploader->saveFileToTmpDir('image'); - $filePath = $this->imageUploader->getBaseTmpPath() . DIRECTORY_SEPARATOR. $fileName; + $filePath = $this->imageUploader->getBaseTmpPath() . DIRECTORY_SEPARATOR . $expectedName; $this->assertTrue(is_file($this->mediaDirectory->getAbsolutePath($filePath))); } + /** + * @return array + */ + public function saveFileToTmpDirProvider(): array + { + return [ + 'image_default_name' => [ + 'file_name' => 'magento_small_image.jpg', + 'expected_name' => 'magento_small_image.jpg', + ], + 'image_with_space_in_name' => [ + 'file_name' => 'magento_image with space in name.jpg', + 'expected_name' => 'magento_image_with_space_in_name.jpg', + ], + ]; + } + /** * Test that method rename files when move it with the same name into base directory. * @@ -90,7 +122,7 @@ public function testMoveFileFromTmp(): void { $expectedFilePath = $this->imageUploader->getBasePath() . DIRECTORY_SEPARATOR . 'magento_small_image_1.jpg'; - $this->assertFileNotExists($this->mediaDirectory->getAbsolutePath($expectedFilePath)); + $this->assertFileDoesNotExist($this->mediaDirectory->getAbsolutePath($expectedFilePath)); $this->imageUploader->moveFileFromTmp('magento_small_image.jpg'); @@ -102,12 +134,11 @@ public function testMoveFileFromTmp(): void */ public function testSaveFileToTmpDirWithWrongExtension(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('File validation failed.'); $fileName = 'text.txt'; - $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); - $filePath = $tmpDirectory->getAbsolutePath($fileName); + $filePath = $this->tmpDirectory->getAbsolutePath($fileName); $file = fopen($filePath, "wb"); fwrite($file, 'just a text'); @@ -120,7 +151,7 @@ public function testSaveFileToTmpDirWithWrongExtension(): void ]; $this->imageUploader->saveFileToTmpDir('image'); - $filePath = $this->imageUploader->getBaseTmpPath() . DIRECTORY_SEPARATOR. $fileName; + $filePath = $this->imageUploader->getBaseTmpPath() . DIRECTORY_SEPARATOR . $fileName; $this->assertFalse(is_file($this->mediaDirectory->getAbsolutePath($filePath))); } @@ -129,12 +160,11 @@ public function testSaveFileToTmpDirWithWrongExtension(): void */ public function testSaveFileToTmpDirWithWrongFile(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('File validation failed.'); $fileName = 'file.gif'; - $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); - $filePath = $tmpDirectory->getAbsolutePath($fileName); + $filePath = $this->tmpDirectory->getAbsolutePath($fileName); $file = fopen($filePath, "wb"); fwrite($file, 'just a text'); @@ -147,7 +177,7 @@ public function testSaveFileToTmpDirWithWrongFile(): void ]; $this->imageUploader->saveFileToTmpDir('image'); - $filePath = $this->imageUploader->getBaseTmpPath() . DIRECTORY_SEPARATOR. $fileName; + $filePath = $this->imageUploader->getBaseTmpPath() . DIRECTORY_SEPARATOR . $fileName; $this->assertFalse(is_file($this->mediaDirectory->getAbsolutePath($filePath))); } @@ -157,11 +187,10 @@ public function testSaveFileToTmpDirWithWrongFile(): void public static function tearDownAfterClass(): void { parent::tearDownAfterClass(); - $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Filesystem::class - ); - /** @var \Magento\Framework\Filesystem\Directory\WriteInterface $mediaDirectory */ + $filesystem = Bootstrap::getObjectManager()->get(Filesystem::class); + /** @var WriteInterface $mediaDirectory */ $mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); - $mediaDirectory->delete('tmp'); + $mediaDirectory->delete(self::BASE_TMP_PATH); + $mediaDirectory->delete(self::BASE_PATH); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php new file mode 100644 index 0000000000000..0a3944bbb36fd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php @@ -0,0 +1,129 @@ +objectManager = Bootstrap::getObjectManager(); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->categoryResource = $this->objectManager->get(CategoryResource::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->categoryCollection = $this->objectManager->get(CategoryCollection::class); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->mediaDirectory->delete(self::BASE_PATH); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Catalog/_files/catalog_tmp_category_image.php + * @magentoDbIsolation disabled + * @return void + */ + public function testAddImageForCategory(): void + { + $dataImage = [ + 'name' => 'magento_small_image.jpg', + 'type' => 'image/jpg', + 'tmp_name' => '/tmp/phpDstnAx', + 'file' => 'magento_small_image.jpg', + ]; + $this->prepareDataImageUrl($dataImage); + $imageRelativePath = self::BASE_PATH . DIRECTORY_SEPARATOR . $dataImage['file']; + $expectedImage = DIRECTORY_SEPARATOR . $this->storeManager->getStore()->getBaseMediaDir() + . DIRECTORY_SEPARATOR . $imageRelativePath; + /** @var CategoryModel $category */ + $category = $this->categoryRepository->get(333); + $category->setImage([$dataImage]); + + $this->categoryResource->save($category); + + $categoryModel = $this->categoryCollection + ->addAttributeToSelect('image') + ->addIdFilter([$category->getId()]) + ->getFirstItem(); + $this->assertEquals( + $expectedImage, + $categoryModel->getImage(), + 'The path of the expected image does not match the path to the actual image.' + ); + $this->assertFileExists($this->mediaDirectory->getAbsolutePath($imageRelativePath)); + } + + /** + * Add image url to image data + * + * @param array $dataImage + * @return void + */ + private function prepareDataImageUrl(array &$dataImage): void + { + $dataImage['url'] = $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) + . self::BASE_TMP_PATH . DIRECTORY_SEPARATOR . $dataImage['file']; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image with space in name.jpg b/dev/tests/integration/testsuite/Magento/Catalog/_files/magento_image with space in name.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bed66dfbcb1c336ea02db176ce9b36f2e3e82261 GIT binary patch literal 4025 zcmV;q4@U5bP)&i;JzSt=ih!SXNd@MMi07XI)!c@?$Xb zgJbh{PxD_WJ2*M>qm%WqpZnFz^pSS+X*&An;`Dk|_MC(DubuY3uk?s%?{8A{os95T zEAM(>?~8TscwO&mNl`0m%>V!nlSxEDRCt{2U29|7HWvmW2XKvMPNr^n}a&5IQ7nqQT(c~JZiTnuOOcfXvw zYdWY2~Y^A7^;<6s}_#M!)8V*b?NB>O&-|3j^>r$j=sQ}dQg zfWhGVx%>JP;g^(T$F}|3^=ru-82ql**V8~M*#~c~E1H&$p_G_E5ws5y7jrlGrjbJH z519i}V*WovCfV=|pAbsSACFM7OWk*}+U*zF z0-(hFk+}X{;z0(lC2~c3aVHxfCFYMnhGdt^bItl%pteFbG9-JZ1#9_dhe9SkOFj0R zeI8rOxf&>BVy|T1bJ?vD^Zx~MMgBI2B3EMmcw|JN`Ob!AiTQ62-07ze`Fi>__x0MX zG@J8Yt*?8by-c%KP?Tu0p{pw=zZW6z)vCyPVG*7d^VO>2eLo2A)tI#~=(LV#MG*60 zgW0@Krag@5zF+Z4mEN*=N1hCT_lWuZk{uumr8qHvSKZ6|0(lt7-17q-RMB(Y!P5le zoj98;$^Pjb|LIn`!NkNoZ7q}czFWhv*V%pYQ4Q+ky@@A?nD=URAlKcuZ=0`$&&`1} z=NpRoyQr;9-a7`6ibk1z+OaA}c)SO|Y+{aEr*4$v{pMGhvE$<$cM0Aza=zO2!o{{s-!~4Asr0PLdm!=-$eO2U6 z9tzuV_UR?_PIJR)-tlSdwA{?YJM|$Ut4>CZwtlEaspyo?I}3$OZ$a!Zjo84$ec)g=<>x3wocRt~j!Fwv_n}hci z;hktnPZ}4&gfR9my=9_Y-alV(0ca-gIOofiIJKfEyQ=bfg@Ql*mDNIiEWhg^myMhmAbPRwqBWNqGLh>-eCD^2 z3snq6hie)Z((hah6e$5`&MMao5A`YYA@A1EpY$t-7p8-}V`n4D6I74n9rJ19?vs2$ zO2U&#U8~UaSyViWIEkPmeswUBA(VAG2yrcR5P!9SVg+k@AZV=}U&RH#((`H}m}6`S z?bTe*XS|PwmEEv%cwx%Hdua1X-np`Zykogybl5$)Q?ZoD7EbA7X|ZTaw++Ll z_sp?3CFOQF>q8qS)UuQ&b}H6A`A&Bc@*oS1A_t6lcl+TymA6aF&HTd>~k)&YXM z_1JVE-l>MxzEx?g#vb!G;UJK)*$__AeCqpV-zLmQD+}h`-YpS~i>D*nHtEEaxO*TL zKrY+3Fv`KM-`XHLk_ihs3GX+j znYuy#Bzt{vVl25i1zw8YDj~|?O2J*xEHc@>5>m>fK3#Ds??x?Fmykk=+Dpbw+yc0vDnpl7=I^Q5#Mu7K3 zLW0bYXd)(fCxKrwl{&YJdiaugH#us-Npt(#_zh~&l5~`Ka*1TVTq6$A z=>P_L!UUao=D-;!-4#IMC=g3Uc~8U@3^0-f&YhIZd&l4{9Gn0>6ExvnKTV7w?oxll zqb#ww!?V&Q)n z{iAlk@lJfMZbx-VKuHxgq*x3I5LpI`@(u*2Hp;}An*$~DPFzAPra2|u5LQ)ogUtMW znkI2k(CU%kpC6Vfgrsosh$Ino1Ih^RMyH!bi&TFmyt|xZ!N4Il7&$1+xG`Zu)03(w z7_x!r3e#Z=5u>;3w%R1KsIw~}bj?0^b|OEMa&?DLF7I)kSGWyK3-;$+j)f8i3z+4R zMb_n_6_NK!7m`z<`yl_iE~CIt2UP-!@;;TXtF1&RauDa>olBX{Sd0(iowG}mQ->tZ zvG4g{CBb{ezmL~k7qy~-N%9vHI8H~V+Jlw6z-t1^<$aWr+B+&N@kA8@4~M%!(0xLT znB;vD@=lZF!Gg29SvoxbWex|HjF}tJ<8?XNzHoZ2e6|TGJ2~x+a=49C3rVO zKV#2Vj&Dv9BAUeYl7kEE!lHQs$j&D$+eu`i52IY({d7o=2{*RH9S9aZixXp#t3*9R ztx!76vkop$V6oS-#R-f5+C`I!o}H+$Zb z=|#w@NOsd-PQ>=13<<1EHb~E#2PTe+I}&cDc9*uWRbJ?&N9FQP?kMq?9(#H(dD50+iyfO9za(UZwD zm>qUF9E+d&Epok!wo}s_)4UNXcxJp@-rKrldtd=4jgpTl zcH`74eMm%iJr8oYs89AqBHm4WmdW4_@`aVXXftj?-I8wL955XrG$&0T<~^EAx~{_< z8PQWw2JR0vnxaR>%jKPH{3kbSs2WHnH~L%#%8Z}RsnK(GcnB%)4RXc~?VM%z9v=x~ zwjbHxDo7o;V<*OP@IFG(WTK1vY%cUCoJxh(RE%zkQdIooG=(?R!dQ(!FGU7+M zcRifn*iv_1SPJ`1Z5->;b4k|9T1-z7&U6s158U?O4w_M=4s4@ z)nU~KT4JNMmTSZgarZe%>X2U%d7I*1Zh3iPu06OmlB12Ny;{H6Nil%-^70{gzqva7 zdr)Q*$4O{JjD?5QP4fHMTu(WW5pH00-C^pi& z+5{uqN3F}nd{ySV#Y{m^#vrNL zLaD0qp;`Zg&gL|{H|yoTtJcZzCqqv@9*+y!^{e_Rc)z)NS@NyIqaZ26?vaf6$zL0D za%kGe%PRNzINq;*E&pBQkVHToPP$m;hGc6Wy}4s()=TcwalHRH@MhbGKqD|9h((+3 zcsiYqaR!F$gFlY<*A&B1yq^cwhUQ;v!*xBE9^})}eSz)$M}sHCdlM3Kt`uizlSf4gD?VGs1R_kZTvOh;?xBuhT*HE&bA Date: Mon, 19 Oct 2020 17:00:39 +0300 Subject: [PATCH 0856/1013] MC-37665: Updating a category through the REST API will uncheck "Use Default Value" on a bunch of attributes --- .../Catalog/Api/CategoryRepositoryTest.php | 46 +++++++------------ 1 file changed, 17 insertions(+), 29 deletions(-) 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 6ce922eab21f1..5623edca62b9a 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -4,6 +4,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Api; use Magento\Authorization\Model\Role; @@ -157,7 +158,7 @@ public function testDelete() UrlRewrite::ENTITY_ID => $categoryId, UrlRewrite::ENTITY_TYPE => CategoryUrlRewriteGenerator::ENTITY_TYPE ]; - /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $urlRewrite*/ + /** @var \Magento\UrlRewrite\Service\V1\Data\UrlRewrite $urlRewrite */ $urlRewrite = $storage->findOneByData($data); // Assert that a url rewrite is auto-generated for the category created from the data fixture @@ -432,14 +433,9 @@ protected function updateCategory($id, $data, ?string $token = null) if ($token) { $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; } + $data['id'] = $id; - if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { - $data['id'] = $id; - return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data]); - } else { - $data['id'] = $id; - return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data]); - } + return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data]); } /** @@ -619,31 +615,23 @@ protected function updateCategoryForSpecificStore( ?string $token = null, string $storeCode = 'default' ) { - $serviceInfo = - [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $id, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => 'V1', - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $id, + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; if ($token) { $serviceInfo['rest']['token'] = $serviceInfo['soap']['token'] = $token; } + $data['id'] = $id; - if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { - $data['id'] = $id; - - return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data], null, $storeCode); - } else { - $data['id'] = $id; - - return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data], null, $storeCode); - } + return $this->_webApiCall($serviceInfo, ['id' => $id, 'category' => $data], null, $storeCode); } /** From 3de90f72bcfc208e07fa58f79e6be7579a73bc60 Mon Sep 17 00:00:00 2001 From: Roman Flowers Date: Mon, 19 Oct 2020 12:14:35 -0500 Subject: [PATCH 0857/1013] MC-38337: [Magento Cloud] Repeat Email Reminders only go to a limited number of customers --- .../Magento/Framework/Mail/EmailMessage.php | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/internal/Magento/Framework/Mail/EmailMessage.php b/lib/internal/Magento/Framework/Mail/EmailMessage.php index 2bebf6d5f0ae2..184f64e902bc9 100644 --- a/lib/internal/Magento/Framework/Mail/EmailMessage.php +++ b/lib/internal/Magento/Framework/Mail/EmailMessage.php @@ -7,10 +7,12 @@ namespace Magento\Framework\Mail; +use Laminas\Mail\Exception\InvalidArgumentException as LaminasInvalidArgumentException; use Magento\Framework\Mail\Exception\InvalidArgumentException; use Laminas\Mail\Address as LaminasAddress; use Laminas\Mail\AddressList; use Laminas\Mime\Message as LaminasMimeMessage; +use Psr\Log\LoggerInterface; /** * Magento Framework Email message @@ -27,6 +29,11 @@ class EmailMessage extends Message implements EmailMessageInterface */ private $addressFactory; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param MimeMessageInterface $body * @param array $to @@ -39,8 +46,7 @@ class EmailMessage extends Message implements EmailMessageInterface * @param Address|null $sender * @param string|null $subject * @param string|null $encoding - * @throws InvalidArgumentException - * + * @param LoggerInterface $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -56,10 +62,12 @@ public function __construct( ?array $replyTo = null, ?Address $sender = null, ?string $subject = '', - ?string $encoding = 'utf-8' + ?string $encoding = 'utf-8', + LoggerInterface $logger ) { parent::__construct($encoding); $mimeMessage = new LaminasMimeMessage(); + $this->logger = $logger; $mimeMessage->setParts($body->getParts()); $this->zendMessage->setBody($mimeMessage); if ($subject) { @@ -224,7 +232,11 @@ private function convertAddressArrayToAddressList(array $arrayList): AddressList foreach ($arrayList as $address) { try { $laminasAddressList->add($address->getEmail(), $address->getName()); - } catch (\InvalidArgumentException $e) { + } catch (LaminasInvalidArgumentException $e) { + $this->logger->warning( + 'Could not add an invalid email address to the mailing queue', + ['exception' => $e] + ); continue; } } From 9d4c1e55f4ff581a7eb51542b5e1fcd50aff3663 Mon Sep 17 00:00:00 2001 From: sdzhepa Date: Mon, 19 Oct 2020 13:59:56 -0500 Subject: [PATCH 0858/1013] Fix mistake in stale.yml file formatting --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 10589b97ea9b3..0a89a432699e0 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale - daysUntilStale: 76 +daysUntilStale: 76 # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. From a63cf9e824a9bf3d8ec14c409d9ac00e354e5640 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 19 Oct 2020 14:24:43 -0500 Subject: [PATCH 0859/1013] 28550 fix static --- .../ConfigurableProduct/Test/Unit/Helper/DataTest.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php index 963322e2f2c57..87357d3c927da 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php @@ -59,7 +59,7 @@ protected function setUp(): void $objectManager->setBackwardCompatibleProperty($this->_model, 'imageUrlBuilder', $this->imageUrlBuilder); } - public function testGetAllowAttributes() + public function testGetAllowAttributes(): void { $typeInstanceMock = $this->createMock(Configurable::class); $typeInstanceMock->expects($this->once()) @@ -82,7 +82,7 @@ public function testGetAllowAttributes() * @param array $data * @dataProvider getOptionsDataProvider */ - public function testGetOptions(array $expected, array $data) + public function testGetOptions(array $expected, array $data): array { if (count($data['allowed_products'])) { $imageHelper1 = $this->getMockBuilder(Image::class) @@ -118,8 +118,9 @@ public function testGetOptions(array $expected, array $data) /** * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function getOptionsDataProvider() + public function getOptionsDataProvider(): array { $currentProductMock = $this->createPartialMock( Product::class, @@ -225,7 +226,7 @@ public function getOptionsDataProvider() * @param string $key * @return string */ - public function getDataCallback($key) + public function getDataCallback($key): string { $map = []; for ($k = 1; $k < 3; $k++) { @@ -289,7 +290,7 @@ public function testGetGalleryImages() /** * @return Collection */ - private function getImagesCollection() + private function getImagesCollection(): MockObject { $collectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() From ceb44748510f39c4782220c9f907d77247a2e31e Mon Sep 17 00:00:00 2001 From: Roman Flowers Date: Mon, 19 Oct 2020 15:11:15 -0500 Subject: [PATCH 0860/1013] MC-38337: [Magento Cloud] Repeat Email Reminders only go to a limited number of customers --- lib/internal/Magento/Framework/Mail/EmailMessage.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/internal/Magento/Framework/Mail/EmailMessage.php b/lib/internal/Magento/Framework/Mail/EmailMessage.php index 184f64e902bc9..f2c4856af4909 100644 --- a/lib/internal/Magento/Framework/Mail/EmailMessage.php +++ b/lib/internal/Magento/Framework/Mail/EmailMessage.php @@ -8,6 +8,7 @@ namespace Magento\Framework\Mail; use Laminas\Mail\Exception\InvalidArgumentException as LaminasInvalidArgumentException; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Mail\Exception\InvalidArgumentException; use Laminas\Mail\Address as LaminasAddress; use Laminas\Mail\AddressList; @@ -30,7 +31,7 @@ class EmailMessage extends Message implements EmailMessageInterface private $addressFactory; /** - * @var LoggerInterface + * @var LoggerInterface|null */ private $logger; @@ -46,7 +47,7 @@ class EmailMessage extends Message implements EmailMessageInterface * @param Address|null $sender * @param string|null $subject * @param string|null $encoding - * @param LoggerInterface $logger + * @param LoggerInterface|null $logger * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -63,11 +64,11 @@ public function __construct( ?Address $sender = null, ?string $subject = '', ?string $encoding = 'utf-8', - LoggerInterface $logger + ?LoggerInterface $logger = null ) { parent::__construct($encoding); $mimeMessage = new LaminasMimeMessage(); - $this->logger = $logger; + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); $mimeMessage->setParts($body->getParts()); $this->zendMessage->setBody($mimeMessage); if ($subject) { From 6384b9668752a3918cd22f3bfadb0e307055bcb1 Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Mon, 19 Oct 2020 15:42:47 -0500 Subject: [PATCH 0861/1013] 29251 fix static --- .../Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php index 87357d3c927da..d1cd57e59ffe6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Helper/DataTest.php @@ -59,7 +59,7 @@ protected function setUp(): void $objectManager->setBackwardCompatibleProperty($this->_model, 'imageUrlBuilder', $this->imageUrlBuilder); } - public function testGetAllowAttributes(): void + public function testGetAllowAttributes() { $typeInstanceMock = $this->createMock(Configurable::class); $typeInstanceMock->expects($this->once()) @@ -82,7 +82,7 @@ public function testGetAllowAttributes(): void * @param array $data * @dataProvider getOptionsDataProvider */ - public function testGetOptions(array $expected, array $data): array + public function testGetOptions(array $expected, array $data) { if (count($data['allowed_products'])) { $imageHelper1 = $this->getMockBuilder(Image::class) From 0d40b6c42b589ea8310a153680f0dda4224056ce Mon Sep 17 00:00:00 2001 From: Roman Flowers Date: Mon, 19 Oct 2020 15:48:09 -0500 Subject: [PATCH 0862/1013] MC-38337: [Magento Cloud] Repeat Email Reminders only go to a limited number of customers --- lib/internal/Magento/Framework/Mail/EmailMessage.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/internal/Magento/Framework/Mail/EmailMessage.php b/lib/internal/Magento/Framework/Mail/EmailMessage.php index f2c4856af4909..46ba8c118232b 100644 --- a/lib/internal/Magento/Framework/Mail/EmailMessage.php +++ b/lib/internal/Magento/Framework/Mail/EmailMessage.php @@ -51,6 +51,7 @@ class EmailMessage extends Message implements EmailMessageInterface * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ public function __construct( MimeMessageInterface $body, From c66199ceb55d5f338c6950315e00ed6c681e63d9 Mon Sep 17 00:00:00 2001 From: Roman Flowers Date: Mon, 19 Oct 2020 15:57:40 -0500 Subject: [PATCH 0863/1013] MC-38337: [Magento Cloud] Repeat Email Reminders only go to a limited number of customers --- lib/internal/Magento/Framework/Mail/EmailMessage.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/internal/Magento/Framework/Mail/EmailMessage.php b/lib/internal/Magento/Framework/Mail/EmailMessage.php index 46ba8c118232b..726c76ee96850 100644 --- a/lib/internal/Magento/Framework/Mail/EmailMessage.php +++ b/lib/internal/Magento/Framework/Mail/EmailMessage.php @@ -48,6 +48,7 @@ class EmailMessage extends Message implements EmailMessageInterface * @param string|null $subject * @param string|null $encoding * @param LoggerInterface|null $logger + * @throws InvalidArgumentException * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) From 37d086856e3dbd0169ea8c3d7c38db944b3b34c4 Mon Sep 17 00:00:00 2001 From: Roman Flowers Date: Mon, 19 Oct 2020 17:54:15 -0500 Subject: [PATCH 0864/1013] MC-38337: [Magento Cloud] Repeat Email Reminders only go to a limited number of customers --- lib/internal/Magento/Framework/Mail/EmailMessage.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/Mail/EmailMessage.php b/lib/internal/Magento/Framework/Mail/EmailMessage.php index 726c76ee96850..53ea805889db2 100644 --- a/lib/internal/Magento/Framework/Mail/EmailMessage.php +++ b/lib/internal/Magento/Framework/Mail/EmailMessage.php @@ -17,6 +17,8 @@ /** * Magento Framework Email message + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class EmailMessage extends Message implements EmailMessageInterface { @@ -52,7 +54,6 @@ class EmailMessage extends Message implements EmailMessageInterface * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ public function __construct( MimeMessageInterface $body, From 8fa4cf589a08bcbf9d1da9accb67f5694c25bef9 Mon Sep 17 00:00:00 2001 From: IvanPletnyov Date: Tue, 20 Oct 2020 09:46:15 +0300 Subject: [PATCH 0865/1013] MC-37891: Create automated test for "Delete Widget" --- .../Adminhtml/Widget/Instance/DeleteTest.php | 56 +++++++++++++++++++ .../Widget/_files/new_widget_rollback.php | 21 +++++++ 2 files changed, 77 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Widget/_files/new_widget_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php new file mode 100644 index 0000000000000..46ea322953278 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php @@ -0,0 +1,56 @@ +collectionFactory = $this->_objectManager->get(CollectionFactory::class); + } + + /** + * @magentoDataFixture Magento/Widget/_files/new_widget.php + * + * @return void + */ + public function testDeleteWidget(): void + { + $widget = $this->collectionFactory->create() + ->addFieldToFilter('title', 'New Sample widget title')->getFirstItem(); + $this->assertNotNull($widget->getInstanceId()); + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->getRequest()->setParams(['instance_id' => $widget->getInstanceId()]); + $this->dispatch('backend/admin/widget_instance/delete'); + $this->assertSessionMessages( + $this->containsEqual((string)__('The widget instance has been deleted.')), + MessageInterface::TYPE_SUCCESS + ); + $this->assertRedirect($this->stringContains('admin/widget_instance/index')); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Widget/_files/new_widget_rollback.php b/dev/tests/integration/testsuite/Magento/Widget/_files/new_widget_rollback.php new file mode 100644 index 0000000000000..5e729472070ba --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/_files/new_widget_rollback.php @@ -0,0 +1,21 @@ +get(CollectionFactory::class); +/** @var Instance $widgetResourceModel */ +$widgetResourceModel = $objectManager->get(Instance::class); + +$widget = $collectionFactory->create()->addFieldToFilter('title', 'New Sample widget title')->getFirstItem(); +if ($widget->getInstanceId()) { + $widgetResourceModel->delete($widget); +} From fcaa729c390c4a2a4d62bd59a8c14bf21c24e8f1 Mon Sep 17 00:00:00 2001 From: Sudheer S Date: Tue, 20 Oct 2020 12:17:00 +0530 Subject: [PATCH 0866/1013] 30349: Product filter with category_id does not work as expected - added API functional test --- .../GraphQl/Catalog/ProductSearchTest.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) 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 f755a1a1e0282..3b2febe44b215 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -1448,11 +1448,12 @@ public function testFilterProductsForExactMatchingName() */ public function testFilteringForProductsFromMultipleCategories() { + $categoriesIds = ["4","5","12"]; $query = <<graphQlQuery($query); /** @var ProductRepositoryInterface $productRepository */ $this->assertEquals(3, $response['products']['total_count']); + $actualProducts = []; + foreach ($categoriesIds as $categoriesId) { + /** @var CategoryLinkManagement $productLinks */ + $productLinks = ObjectManager::getInstance()->get(CategoryLinkManagement::class); + $links = $productLinks->getAssignedProducts($categoriesId); + $links = array_reverse($links); + foreach ($links as $linkProduct) { + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + /** @var ProductInterface $product */ + $product = $productRepository->get($linkProduct->getSku()); + $actualProducts[$linkProduct->getSku()] = $product->getName(); + } + } + $this->assertEquals(array_column($response['products']['items'],"name","sku"), $actualProducts); } /** From 5448cbc77b74f0badac9dd9c187caf83e49ad4c3 Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin Date: Tue, 20 Oct 2020 09:49:52 +0300 Subject: [PATCH 0867/1013] MC-38476: Can not register customer with correct postal code for Argentina --- app/code/Magento/Directory/etc/zip_codes.xml | 1 + .../Country/Postcode/Config/ReaderTest.php | 20 ++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Directory/etc/zip_codes.xml b/app/code/Magento/Directory/etc/zip_codes.xml index 14d250656d28c..de6c064815d7a 100644 --- a/app/code/Magento/Directory/etc/zip_codes.xml +++ b/app/code/Magento/Directory/etc/zip_codes.xml @@ -19,6 +19,7 @@ ^[0-9]{4}$ + ^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$ diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php index 740afcda11386..9146535ed5181 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php @@ -6,18 +6,20 @@ namespace Magento\Directory\Model\Country\Postcode\Config; -class ReaderTest extends \PHPUnit\Framework\TestCase +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class ReaderTest extends TestCase { /** - * @var \Magento\Directory\Model\Country\Postcode\Config\Reader + * @var Reader */ private $reader; protected function setUp(): void { - $this->reader = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Directory\Model\Country\Postcode\Config\Reader::class - ); + $this->reader = Bootstrap::getObjectManager() + ->create(Reader::class); } public function testRead() @@ -39,5 +41,13 @@ public function testRead() $this->assertEquals('test1', $result['NL_NEW']['pattern_1']['example']); $this->assertEquals('^[0-2]{4}[A-Z]{2}$', $result['NL_NEW']['pattern_1']['pattern']); + + $this->assertArrayHasKey('AR', $result); + $this->assertArrayHasKey('pattern_1', $result['AR']); + $this->assertArrayHasKey('pattern_2', $result['AR']); + $this->assertEquals('1234', $result['AR']['pattern_1']['example']); + $this->assertEquals('^[0-9]{4}$', $result['AR']['pattern_1']['pattern']); + $this->assertEquals('A1234BCD', $result['AR']['pattern_2']['example']); + $this->assertEquals('^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$', $result['AR']['pattern_2']['pattern']); } } From fa2e1bcd49a28e2ed9498c1b1cd6f400d9005df6 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" Date: Tue, 20 Oct 2020 12:38:21 +0300 Subject: [PATCH 0868/1013] MC-38306: [Cloud] Adding new disabled products to Magento flushes categories cache --- .../Category/Product/PositionResolver.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index 44bf153f83697..f7c0ea608123e 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -49,4 +49,30 @@ public function getPositions(int $categoryId): array return array_flip($connection->fetchCol($select)); } + + /** + * Get category product positions + * + * @param int $categoryId + * @return int + */ + public function getLastPosition(int $categoryId): int + { + $connection = $this->getConnection(); + + $select = $connection->select()->from( + ['cpe' => $this->getTable('catalog_product_entity')], + ['position' => new \Zend_Db_Expr('MAX(position)')] + )->joinLeft( + ['ccp' => $this->getTable('catalog_category_product')], + 'ccp.product_id=cpe.entity_id' + )->where( + 'ccp.category_id = ?', + $categoryId + )->order( + 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC + ); + + return (int)$connection->fetchOne($select); + } } From b07c48320f301e6c166266ba437413161b6cc7d9 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun Date: Tue, 20 Oct 2020 12:59:52 +0300 Subject: [PATCH 0869/1013] MC-38488: [MFTF] AdminMediaGalleryAssertUsedInLinkBlocksGridTest failed because of bad design --- ...eryAssertImageUsedInLinkBlocksGridTest.xml | 90 +++++++++++++++++++ ...aGalleryAssertUsedInLinkBlocksGridTest.xml | 4 +- ...nhancedMediaGalleryImageActionsSection.xml | 2 +- 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml new file mode 100644 index 0000000000000..c25e55cd30461 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml @@ -0,0 +1,90 @@ + + + + + + + + + + <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 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"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTest"> + <argument name="tags" value="block_html"/> + </actionGroup> + </after> + + <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage"> + <argument name="CMSBlockPage" value="$$block$$"/> + </actionGroup> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForInitialPageLoad" /> + <waitForPageLoad stepKey="waitForSecondaryPageLoad" /> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"> + <argument name="name" value="blockImage"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"> + <argument name="name" value="blockImage"/> + </actionGroup> + <waitForPageLoad stepKey="waitForGridToLoadAfterNewFolderCreated"/> + <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="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCreatedFolder"> + <argument name="name" value="blockImage"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInBlocks"> + <argument name="entityName" value="Blocks"/> + </actionGroup> + <wait time="5" stepKey="waitForAssertLoads"/> + <reloadPage stepKey="reloadBlocksGridPage"/> + <waitForPageLoad stepKey="waitForBlocksGridPageLoad"/> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilterInBlocksGrid"/> + + <deleteData createDataKey="block" stepKey="deleteBlock"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultViewAgain"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCreatedFolderAgain"> + <argument name="name" value="blockImage"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeImageDetails"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"> + <argument name="name" value="blockImage"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml index c850d09cd8a03..69d0c191fd146 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml @@ -7,11 +7,11 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryAssertUsedInLinkBlocksGridTest"> + <test name="AdminMediaGalleryAssertUsedInLinkBlocksGridTest" deprecated="Use AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest instead"> <annotations> <features value="AdminMediaGalleryUsedInBlocksFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> - <title value="Used in blocks link"/> + <title value="Deprecated. Used in blocks link"/> <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 blocks"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index f36fca88dc760..fb813bd65c9bf 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -10,7 +10,7 @@ <section name="AdminEnhancedMediaGalleryImageActionsSection"> <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> - <element name="viewDetails" type="button" selector="[data-ui-id='action-image-details']"/> + <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[@class='action-menu-item' and contains(text(), 'View 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"/> From 606292dc833efdda124f211488a838d9d4746110 Mon Sep 17 00:00:00 2001 From: hanna_hnida <hanna.hnida@vaimo.com> Date: Tue, 20 Oct 2020 12:07:09 +0200 Subject: [PATCH 0870/1013] magento/magento2-page-builder#558: Developer can style content types output differently per viewport - Added body id in Storefront, in tinymce4 in admin --- app/code/Magento/Theme/view/frontend/layout/default.xml | 1 + lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js | 1 + 2 files changed, 2 insertions(+) diff --git a/app/code/Magento/Theme/view/frontend/layout/default.xml b/app/code/Magento/Theme/view/frontend/layout/default.xml index 8eaac4aa3e794..bf76933b356c0 100644 --- a/app/code/Magento/Theme/view/frontend/layout/default.xml +++ b/app/code/Magento/Theme/view/frontend/layout/default.xml @@ -8,6 +8,7 @@ <page layout="3columns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="default_head_blocks"/> <body> + <attribute name="id" value="html-body"/> <block name="require.js" class="Magento\Framework\View\Element\Template" template="Magento_Theme::page/js/require_js.phtml" /> <referenceContainer name="after.body.start"> <block class="Magento\RequireJs\Block\Html\Head\Config" name="requirejs-config"/> diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js index 4393b6c882039..7e4d4f532f8d3 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js @@ -205,6 +205,7 @@ define([ plugins: this.config.tinymce4.plugins, toolbar: this.config.tinymce4.toolbar, adapter: this, + body_id: "html-body", /** * @param {Object} editor From 09cd013f4f58d2541b61af3e67f5486ad89b2872 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Tue, 20 Oct 2020 16:35:29 +0530 Subject: [PATCH 0871/1013] 30349: Product filter with category_id does not work as expected - added API functional test --- .../testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 3b2febe44b215..1fecc1a76c176 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -1492,7 +1492,8 @@ public function testFilteringForProductsFromMultipleCategories() $actualProducts[$linkProduct->getSku()] = $product->getName(); } } - $this->assertEquals(array_column($response['products']['items'],"name","sku"), $actualProducts); + $expectedProducts = array_column($response['products']['items'],"name","sku"); + $this->assertEquals($expectedProducts, $actualProducts); } /** From 46239525dde56e3fed909a895f04d34c8d86c4de Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Tue, 20 Oct 2020 15:06:01 +0300 Subject: [PATCH 0872/1013] MC-38342: [B2B] Multiple identical warning messages are displayed when adding an unconfigured Product with Customizable Options to a Requisition List from a Category page --- .../Model/Product/Type/AbstractType.php | 3 +- .../Model/Product/Type/AbstractTypeTest.php | 204 +++++++++++------- 2 files changed, 129 insertions(+), 78 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index eb4a71cb90a8c..bfdafacb11aad 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; @@ -620,7 +621,7 @@ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $p } } if (count($results) > 0) { - throw new LocalizedException(__(implode("\n", $results))); + throw new LocalizedException(__(implode("\n", array_unique($results)))); } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php index c72e7e0e1d078..4cd9d74e58418 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php @@ -3,41 +3,60 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AbstractTypeTest extends \PHPUnit\Framework\TestCase +class AbstractTypeTest extends TestCase { /** - * @var \Magento\Catalog\Model\Product\Type\AbstractType + * @var AbstractType */ protected $_model; protected function setUp(): void { - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Api\ProductRepositoryInterface::class + $productRepository = Bootstrap::getObjectManager()->get( + ProductRepositoryInterface::class ); - $catalogProductOption = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Product\Option::class + $catalogProductOption = Bootstrap::getObjectManager()->get( + Option::class ); - $catalogProductType = $this->createMock(\Magento\Catalog\Model\Product\Type::class); - $eventManager = $this->createPartialMock(\Magento\Framework\Event\ManagerInterface::class, ['dispatch']); - $fileStorageDb = $this->createMock(\Magento\MediaStorage\Helper\File\Storage\Database::class); - $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); - $registry = $this->createMock(\Magento\Framework\Registry::class); - $logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $serializer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Serialize\Serializer\Json::class + $catalogProductType = $this->createMock(Type::class); + $eventManager = $this->createPartialMock(ManagerInterface::class, ['dispatch']); + $fileStorageDb = $this->createMock(Database::class); + $filesystem = $this->createMock(Filesystem::class); + $registry = $this->createMock(Registry::class); + $logger = $this->createMock(LoggerInterface::class); + $serializer = Bootstrap::getObjectManager()->get( + Json::class ); $this->_model = $this->getMockForAbstractClass( - \Magento\Catalog\Model\Product\Type\AbstractType::class, + AbstractType::class, [ $catalogProductOption, - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class), + Bootstrap::getObjectManager()->get(Config::class), $catalogProductType, $eventManager, $fileStorageDb, @@ -53,7 +72,7 @@ protected function setUp(): void public function testGetRelationInfo() { $info = $this->_model->getRelationInfo(); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $info); + $this->assertInstanceOf(DataObject::class, $info); $this->assertNotSame($info, $this->_model->getRelationInfo()); } @@ -72,8 +91,8 @@ public function testGetParentIdsByChild() */ public function testGetSetAttributes() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -85,7 +104,7 @@ public function testGetSetAttributes() $this->assertArrayHasKey('name', $attributes); $isTypeExists = false; foreach ($attributes as $attribute) { - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute); $applyTo = $attribute->getApplyTo(); if (count($applyTo) > 0 && !in_array('simple', $applyTo)) { $isTypeExists = true; @@ -97,9 +116,9 @@ public function testGetSetAttributes() public function testAttributesCompare() { - $attribute[1] = new \Magento\Framework\DataObject(['group_sort_path' => 1, 'sort_path' => 10]); - $attribute[2] = new \Magento\Framework\DataObject(['group_sort_path' => 1, 'sort_path' => 5]); - $attribute[3] = new \Magento\Framework\DataObject(['group_sort_path' => 2, 'sort_path' => 10]); + $attribute[1] = new DataObject(['group_sort_path' => 1, 'sort_path' => 10]); + $attribute[2] = new DataObject(['group_sort_path' => 1, 'sort_path' => 5]); + $attribute[3] = new DataObject(['group_sort_path' => 2, 'sort_path' => 10]); $this->assertEquals(1, $this->_model->attributesCompare($attribute[1], $attribute[2])); $this->assertEquals(-1, $this->_model->attributesCompare($attribute[2], $attribute[1])); $this->assertEquals(-1, $this->_model->attributesCompare($attribute[1], $attribute[3])); @@ -110,9 +129,9 @@ public function testAttributesCompare() public function testGetAttributeById() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class )->load( 1 ); @@ -120,8 +139,8 @@ public function testGetAttributeById() $this->assertNull($this->_model->getAttributeById(-1, $product)); $this->assertNull($this->_model->getAttributeById(null, $product)); - $sku = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Eav\Model\Config::class + $sku = Bootstrap::getObjectManager()->get( + Config::class )->getAttribute( 'catalog_product', 'sku' @@ -140,8 +159,8 @@ public function testGetAttributeById() */ public function testIsVirtual() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertFalse($this->_model->isVirtual($product)); } @@ -151,8 +170,8 @@ public function testIsVirtual() */ public function testIsSalable() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertTrue($this->_model->isSalable($product)); @@ -169,20 +188,20 @@ public function testIsSalable() */ public function testPrepareForCart() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->load(10); // fixture $this->assertEmpty($product->getCustomOption('info_buyRequest')); $requestData = ['qty' => 5]; - $result = $this->_model->prepareForCart(new \Magento\Framework\DataObject($requestData), $product); + $result = $this->_model->prepareForCart(new DataObject($requestData), $product); $this->assertArrayHasKey(0, $result); $this->assertSame($product, $result[0]); $buyRequest = $product->getCustomOption('info_buyRequest'); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $buyRequest); + $this->assertInstanceOf(DataObject::class, $buyRequest); $this->assertEquals($product->getId(), $buyRequest->getProductId()); $this->assertSame($product, $buyRequest->getProduct()); $this->assertEquals(json_encode($requestData), $buyRequest->getValue()); @@ -193,15 +212,15 @@ public function testPrepareForCart() */ public function testPrepareForCartOptionsException() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture $this->assertStringContainsString( "The product's required option(s) weren't entered. Make sure the options are entered and try again.", - $this->_model->prepareForCart(new \Magento\Framework\DataObject(), $product) + $this->_model->prepareForCart(new DataObject(), $product) ); } @@ -215,9 +234,9 @@ public function testGetSpecifyOptionMessage() public function testCheckProductBuyState() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->setSkipCheckRequiredOption('_'); $this->_model->checkProductBuyState($product); @@ -228,10 +247,10 @@ public function testCheckProductBuyState() */ public function testCheckProductBuyStateException() { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -243,9 +262,9 @@ public function testCheckProductBuyStateException() */ public function testGetOrderOptions() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertEquals([], $this->_model->getOrderOptions($product)); @@ -283,8 +302,8 @@ public function testGetOrderOptions() */ public function testBeforeSave() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -299,8 +318,8 @@ public function testBeforeSave() */ public function testGetSku() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -312,9 +331,9 @@ public function testGetSku() */ public function testGetOptionSku() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertEmpty($this->_model->getOptionSku($product)); @@ -336,7 +355,7 @@ public function testGetOptionSku() public function testGetWeight() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertEmpty($this->_model->getWeight($product)); $product->setWeight('value'); $this->assertEquals('value', $this->_model->getWeight($product)); @@ -346,16 +365,16 @@ public function testHasOptions() { $this->markTestIncomplete('Bug MAGE-2814'); - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertFalse($this->_model->hasOptions($product)); - $product = new \Magento\Framework\DataObject(['has_options' => true]); + $product = new DataObject(['has_options' => true]); $this->assertTrue($this->_model->hasOptions($product)); } public function testHasRequiredOptions() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertFalse($this->_model->hasRequiredOptions($product)); $product->setRequiredOptions(1); $this->assertTrue($this->_model->hasRequiredOptions($product)); @@ -363,7 +382,7 @@ public function testHasRequiredOptions() public function testGetSetStoreFilter() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertNull($this->_model->getStoreFilter($product)); $store = new \StdClass(); $this->_model->setStoreFilter($store, $product); @@ -374,8 +393,8 @@ public function testGetForceChildItemQtyChanges() { $this->assertFalse( $this->_model->getForceChildItemQtyChanges( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -387,8 +406,8 @@ public function testPrepareQuoteItemQty() 3.0, $this->_model->prepareQuoteItemQty( 3, - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -396,12 +415,12 @@ public function testPrepareQuoteItemQty() public function testAssignProductToOption() { - $product = new \Magento\Framework\DataObject(); - $option = new \Magento\Framework\DataObject(); + $product = new DataObject(); + $option = new DataObject(); $this->_model->assignProductToOption($product, $option, $product); $this->assertSame($product, $option->getProduct()); - $option = new \Magento\Framework\DataObject(); + $option = new DataObject(); $this->_model->assignProductToOption(null, $option, $product); $this->assertSame($product, $option->getProduct()); } @@ -415,8 +434,8 @@ public function testSetConfig() { $this->assertFalse( $this->_model->isComposite( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -425,8 +444,8 @@ public function testSetConfig() $this->_model->setConfig($config); $this->assertTrue( $this->_model->isComposite( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -438,8 +457,8 @@ public function testSetConfig() */ public function testGetSearchableData() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->load(1); // fixture @@ -467,10 +486,41 @@ public function testProcessBuyRequest() public function testCheckProductConfiguration() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); - $buyRequest = new \Magento\Framework\DataObject(['qty' => 5]); + $buyRequest = new DataObject(['qty' => 5]); $this->_model->checkProductConfiguration($product, $buyRequest); } + + /** + * Test that only one exception appears instead of multiple identical exceptions + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * + * @return void + */ + public function testPrepareOptions(): void + { + $exceptionMessage = + "The product's required option(s) weren't entered. Make sure the options are entered and try again."; + $product = Bootstrap::getObjectManager()->create( + Product::class + ); + $product->load(1); + $buyRequest = new DataObject(['product' => 1]); + $method = new \ReflectionMethod( + AbstractType::class, + '_prepareOptions' + ); + $method->setAccessible(true); + $exceptionIsThrown = false; + try { + $method->invoke($this->_model, $buyRequest, $product, 'full'); + } catch (LocalizedException $exception) { + $this->assertEquals($exceptionMessage, $exception->getMessage()); + $exceptionIsThrown = true; + } + $this->assertTrue($exceptionIsThrown); + } } From 2c3b5b994dc94adfcf5051df84039f4d0eec4bc1 Mon Sep 17 00:00:00 2001 From: hanna_hnida <hanna.hnida@vaimo.com> Date: Tue, 20 Oct 2020 15:32:34 +0200 Subject: [PATCH 0873/1013] magento/magento2-page-builder#558: Developer can style content types output differently per viewport - Fixed quote marks --- lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js index 7e4d4f532f8d3..d74838b0c26bf 100644 --- a/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js +++ b/lib/web/mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter.js @@ -205,7 +205,7 @@ define([ plugins: this.config.tinymce4.plugins, toolbar: this.config.tinymce4.toolbar, adapter: this, - body_id: "html-body", + 'body_id': 'html-body', /** * @param {Object} editor From e3789f0906f45b565fdb8d13d28c305917cca1bd Mon Sep 17 00:00:00 2001 From: Pieter Hoste <hoste.pieter@gmail.com> Date: Tue, 20 Oct 2020 15:36:01 +0200 Subject: [PATCH 0874/1013] Make sure the depends definition works for custom widgets. Also converted code from PrototypeJS to jQuery. --- lib/web/mage/adminhtml/form.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/web/mage/adminhtml/form.js b/lib/web/mage/adminhtml/form.js index eae359c4b26a4..054594ff9e9f2 100644 --- a/lib/web/mage/adminhtml/form.js +++ b/lib/web/mage/adminhtml/form.js @@ -496,10 +496,15 @@ define([ } // toggle target row - headElement = $(idTo + '-head'); + headElement = jQuery('#' + idTo + '-head'); isInheritCheckboxChecked = $(idTo + '_inherit') && $(idTo + '_inherit').checked; target = $(idTo); + // Account for the chooser style parameters. + if (target === null && headElement.length === 0 && idTo.substring(0, 16) === 'options_fieldset') { + headElement = jQuery('.field-' + idTo).add('.field-chooser' + idTo); + } + // Target won't always exist (for example, if field type is "label") if (target) { inputs = target.up(this._config['levels_up']).select('input', 'select', 'td'); @@ -529,10 +534,10 @@ define([ }); } - if (headElement) { + if (headElement.length > 0) { headElement.show(); - if (headElement.hasClassName('open') && target) { + if (headElement.hasClass('open') && target) { target.show(); } else if (target) { target.hide(); @@ -567,7 +572,7 @@ define([ }); } - if (headElement) { + if (headElement.length > 0) { headElement.hide(); } From 754eb5a168e6b2bd78b972e482988fa0b695a612 Mon Sep 17 00:00:00 2001 From: hanna_hnida <hanna.hnida@vaimo.com> Date: Tue, 20 Oct 2020 18:11:04 +0200 Subject: [PATCH 0875/1013] magento/magento2-page-builder#558: Developer can style content types output differently per viewport - Fixed tinymce image selector --- .../CmsNewBlockBlockActionsSection/BlockContentSection.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml index 1d5e8541dd497..f4e26938d9008 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="BlockContentSection"> <element name="TextArea" type="input" selector="#cms_block_form_content"/> - <element name="image" type="file" selector="#tinymce img"/> + <element name="image" type="file" selector=".mce-content-body img"/> <element name="contentIframe" type="iframe" selector="cms_block_form_content_ifr"/> </section> </sections> From 7683dece30de99d5df537507487e1129cd85c023 Mon Sep 17 00:00:00 2001 From: hanna_hnida <hanna.hnida@vaimo.com> Date: Tue, 20 Oct 2020 18:12:21 +0200 Subject: [PATCH 0876/1013] magento/magento2-page-builder#558: Developer can style content types output differently per viewport - Fixed tinymce image selector --- .../CmsNewBlockBlockActionsSection/BlockContentSection.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml index 1d5e8541dd497..f4e26938d9008 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="BlockContentSection"> <element name="TextArea" type="input" selector="#cms_block_form_content"/> - <element name="image" type="file" selector="#tinymce img"/> + <element name="image" type="file" selector=".mce-content-body img"/> <element name="contentIframe" type="iframe" selector="cms_block_form_content_ifr"/> </section> </sections> From 0ab061a5da09ba5fc95a6cd3a1d87454c1f2a8d0 Mon Sep 17 00:00:00 2001 From: hanna_hnida <hanna.hnida@vaimo.com> Date: Tue, 20 Oct 2020 18:15:23 +0200 Subject: [PATCH 0877/1013] Revert "magento/magento2-page-builder#558: Developer can style content types output differently per viewport - Fixed tinymce image selector" This reverts commit 754eb5a168e6b2bd78b972e482988fa0b695a612. --- .../CmsNewBlockBlockActionsSection/BlockContentSection.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml index f4e26938d9008..1d5e8541dd497 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsNewBlockBlockActionsSection/BlockContentSection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="BlockContentSection"> <element name="TextArea" type="input" selector="#cms_block_form_content"/> - <element name="image" type="file" selector=".mce-content-body img"/> + <element name="image" type="file" selector="#tinymce img"/> <element name="contentIframe" type="iframe" selector="cms_block_form_content_ifr"/> </section> </sections> From 5543a9f31b13bddba7918f200636c8dc0935a319 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 20 Oct 2020 19:15:36 +0300 Subject: [PATCH 0878/1013] MC-38306: [Cloud] Adding new disabled products to Magento flushes categories cache --- .../Catalog/Model/Category/Product/PositionResolver.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index f7c0ea608123e..320a253a9a1dd 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -51,18 +51,18 @@ public function getPositions(int $categoryId): array } /** - * Get category product positions + * Get category product minimum position * * @param int $categoryId * @return int */ - public function getLastPosition(int $categoryId): int + public function getMinPosition(int $categoryId): int { $connection = $this->getConnection(); $select = $connection->select()->from( ['cpe' => $this->getTable('catalog_product_entity')], - ['position' => new \Zend_Db_Expr('MAX(position)')] + ['position' => new \Zend_Db_Expr('MIN(position)')] )->joinLeft( ['ccp' => $this->getTable('catalog_category_product')], 'ccp.product_id=cpe.entity_id' From bc3d88a026e178162b547dfe933378a8119e9121 Mon Sep 17 00:00:00 2001 From: Sergii Ivashchenko <serg.ivashchenko@gmail.com> Date: Tue, 20 Oct 2020 17:22:49 +0100 Subject: [PATCH 0879/1013] Removed extra spaces --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index 0a89a432699e0..0b9283fde06c7 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -18,7 +18,7 @@ exemptLabels: - "Progress: dev in progress" - "Progress: PR in progress" - "Progress: done" - - "B2B: GraphQL" + - "B2B: GraphQL" - "Progress: PR Created" - "PAP" - "Project: Login as Customer" From f0ea57ee4d879380a1cd8dcb4c738ae84b9a2aef Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Tue, 20 Oct 2020 21:34:50 -0500 Subject: [PATCH 0880/1013] 29251 test web-api test fix --- .../etc/graphql/di.xml | 6 ++-- ...gurableProductToCartSingleMutationTest.php | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index dc672b02e2f96..ace37b54a2bf6 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -48,7 +48,7 @@ <plugin name="used_products_cache_graphql" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> </type> - <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> - <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder_GraphQl" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> - </type> +<!-- <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface">--> +<!-- <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder_GraphQl" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/>--> +<!-- </type>--> </config> diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php index a2b7b54fb875a..fb6b36b883e77 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php @@ -8,9 +8,13 @@ namespace Magento\GraphQl\ConfigurableProduct; use Exception; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\CatalogInventory\Model\Configuration; /** * Add configurable product to cart testcases @@ -22,6 +26,21 @@ class AddConfigurableProductToCartSingleMutationTest extends GraphQlAbstract */ private $getMaskedQuoteIdByReservedOrderId; + /** + * @var Config $config + */ + private $resourceConfig; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + /** * @inheritdoc */ @@ -29,6 +48,9 @@ protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->resourceConfig = $objectManager->get(Config::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); } /** @@ -166,9 +188,20 @@ public function testAddNonExistentConfigurableProductParentToCart() */ public function testOutOfStockVariationToCart() { + $showOutOfStock = $this->scopeConfig->getValue(Configuration::XML_PATH_SHOW_OUT_OF_STOCK); + + // Changing SHOW_OUT_OF_STOCK to show the out of stock option, otherwise graphql won't display it. + $this->resourceConfig->saveConfig(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, 1); + $this->reinitConfig->reinit(); + $product = $this->getConfigurableProductInfo(); $attributeId = (int) $product['configurable_options'][0]['attribute_id']; $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; + // Asserting that the first value is the right option we want to add to cart + $this->assertEquals( + $product['configurable_options'][0]['values'][0]['label'], + 'Option 1' + ); $parentSku = $product['sku']; $configurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); @@ -191,6 +224,8 @@ public function testOutOfStockVariationToCart() $response['addProductsToCart']['user_errors'][0]['message'], $expectedErrorMessages ); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_SHOW_OUT_OF_STOCK, $showOutOfStock); + $this->reinitConfig->reinit(); } /** From 0eb8848fa1d67201d97726f701b7edc6f237e21c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 21 Oct 2020 09:13:21 +0300 Subject: [PATCH 0881/1013] MC-34444: Configurable shows simple products which are no longer assigned to the website --- .../Frontend/UsedProductsWebsiteFilter.php | 34 +++++++++++++++++++ .../ConfigurableProduct/etc/frontend/di.xml | 1 + .../Product/View/Type/ConfigurableTest.php | 25 ++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsWebsiteFilter.php diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsWebsiteFilter.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsWebsiteFilter.php new file mode 100644 index 0000000000000..9e1f3482d3c0f --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/Frontend/UsedProductsWebsiteFilter.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\Plugin\Frontend; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; + +/** + * Filter configurable options by current store plugin. + */ +class UsedProductsWebsiteFilter +{ + /** + * Filter configurable options not assigned to current website. + * + * @param Configurable $subject + * @param ProductInterface $product + * @param array|null $requiredAttributeIds + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeGetUsedProducts( + Configurable $subject, + ProductInterface $product, + array $requiredAttributeIds = null + ): void { + $subject->setStoreFilter($product->getStore(), $product); + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml index f60234453dc60..3942ec52cbb8b 100644 --- a/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/frontend/di.xml @@ -12,5 +12,6 @@ </type> <type name="Magento\ConfigurableProduct\Model\Product\Type\Configurable"> <plugin name="used_products_cache" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> + <plugin name="used_products_website_filter" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsWebsiteFilter" /> </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php index 0344d467a3cc2..214613821afb6 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableTest.php @@ -19,6 +19,8 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\LayoutInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -126,6 +128,29 @@ public function testGetAllowProducts(): void } } + /** + * Verify configurable option not assigned to current website won't be visible. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_two_websites.php + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * + * @return void + */ + public function testGetAllowProductsNonDefaultWebsite(): void + { + // Set current website to non-default. + $storeManager = $this->objectManager->get(StoreManagerInterface::class); + $storeManager->setCurrentStore('fixture_second_store'); + // Un-assign simple product from non-default website. + $simple = $this->productRepository->get('simple_Option_1'); + $simple->setWebsiteIds([1]); + $this->productRepository->save($simple); + // Verify only one configurable option will be visible. + $products = $this->block->getAllowProducts(); + $this->assertEquals(1, count($products)); + } + /** * @return void */ From afe3cd9d4c7f97a8097d08f4636cd99fa50c5671 Mon Sep 17 00:00:00 2001 From: Sudheer S <sudheers@kensium.com> Date: Wed, 21 Oct 2020 11:59:28 +0530 Subject: [PATCH 0882/1013] 30349: Product filter with category_id does not work as expected - added API functional test --- .../testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1fecc1a76c176..b0d7141d55589 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -1492,7 +1492,7 @@ public function testFilteringForProductsFromMultipleCategories() $actualProducts[$linkProduct->getSku()] = $product->getName(); } } - $expectedProducts = array_column($response['products']['items'],"name","sku"); + $expectedProducts = array_column($response['products']['items'], "name", "sku"); $this->assertEquals($expectedProducts, $actualProducts); } From 5df1d69445c7dd3a4d56e6cff66fecd824d93620 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Wed, 21 Oct 2020 10:20:46 +0300 Subject: [PATCH 0883/1013] MC-38488: [MFTF] AdminMediaGalleryAssertUsedInLinkBlocksGridTest failed because of bad design --- .../Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml | 3 +++ .../Section/AdminEnhancedMediaGalleryImageActionsSection.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml index 69d0c191fd146..b360d958aee33 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkBlocksGridTest.xml @@ -17,6 +17,9 @@ <description value="User filters assets used in blocks"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest instead</issueId> + </skip> </annotations> <before> <createData entity="_defaultBlock" stepKey="block" /> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index fb813bd65c9bf..a8e9feaa2d623 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -10,7 +10,7 @@ <section name="AdminEnhancedMediaGalleryImageActionsSection"> <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> - <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[@class='action-menu-item' and contains(text(), 'View Details')]"/> + <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[@class='action-menu-item' and contains(text(), 'View Details')]" timeout="30" /> <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"/> From 320fae74b0edb284d1d430155cf2b798dd1e0a27 Mon Sep 17 00:00:00 2001 From: Serhii Voloshkov <serhii.voloshkov@transoftgroup.com> Date: Wed, 21 Oct 2020 11:00:35 +0300 Subject: [PATCH 0884/1013] MC-36768: Custom attribute value created in integration tests fixtures product not appears in elasticsearch --- .../Indexer/Fulltext/Action/DataProvider.php | 96 ++++--- .../Indexer/Fulltext/Plugin/Attribute.php | 18 +- .../Indexer/Fulltext/Plugin/AttributeTest.php | 35 ++- .../FieldMapper/Product/AttributeProvider.php | 20 +- .../Plugin/Category/Product/Attribute.php | 12 +- .../Catalog/ProductSearchAggregationsTest.php | 6 +- .../GraphQl/Catalog/ProductSearchTest.php | 25 +- .../Swatches/ProductSwatchDataTest.php | 10 +- .../Magento/Catalog/Helper/ProductTest.php | 147 ++++++----- .../Magento/Catalog/_files/categories.php | 141 +++++----- ...th_custom_attribute_layered_navigation.php | 27 +- .../_files/product_boolean_attribute.php | 85 +++--- ...ucts_with_layered_navigation_attribute.php | 242 +++++++++--------- ..._layered_navigation_attribute_rollback.php | 58 +++-- ...th_layered_navigation_custom_attribute.php | 218 ++++++++-------- .../_files/configurable_attribute.php | 106 ++++---- .../_files/configurable_products.php | 68 ++--- .../Indexer/_files/reindex_all_invalid.php | 13 + .../order_configurable_product_rollback.php | 1 + ..._with_visual_swatch_attribute_rollback.php | 7 +- ...e_with_different_options_type_rollback.php | 17 +- 21 files changed, 758 insertions(+), 594 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php 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 8c4690f044764..3a67025230430 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -6,10 +6,24 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\ResourceModel\EngineInterface; +use Magento\CatalogSearch\Model\ResourceModel\EngineProvider; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadata; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Zend_Db; /** * Catalog search full test search data provider. @@ -24,7 +38,7 @@ class DataProvider /** * Searchable attributes cache * - * @var \Magento\Eav\Model\Entity\Attribute[] + * @var Attribute[] */ private $searchableAttributes; @@ -50,40 +64,40 @@ class DataProvider private $productEmulators = []; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory + * @var CollectionFactory */ private $productAttributeCollectionFactory; /** * Eav config * - * @var \Magento\Eav\Model\Config + * @var Config */ private $eavConfig; /** * Catalog product type * - * @var \Magento\Catalog\Model\Product\Type + * @var Type */ private $catalogProductType; /** * Core event manager proxy * - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ private $eventManager; /** * Store manager * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; /** - * @var \Magento\CatalogSearch\Model\ResourceModel\EngineInterface + * @var EngineInterface */ private $engine; @@ -93,12 +107,12 @@ class DataProvider private $resource; /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface + * @var AdapterInterface */ private $connection; /** - * @var \Magento\Framework\EntityManager\EntityMetadata + * @var EntityMetadata */ private $metadata; @@ -126,24 +140,24 @@ class DataProvider /** * @param ResourceConnection $resource - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttributeCollectionFactory - * @param \Magento\CatalogSearch\Model\ResourceModel\EngineProvider $engineProvider - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param Type $catalogProductType + * @param Config $eavConfig + * @param CollectionFactory $prodAttributeCollectionFactory + * @param EngineProvider $engineProvider + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param MetadataPool $metadataPool * @param int $antiGapMultiplier */ public function __construct( ResourceConnection $resource, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttributeCollectionFactory, - \Magento\CatalogSearch\Model\ResourceModel\EngineProvider $engineProvider, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, + Type $catalogProductType, + Config $eavConfig, + CollectionFactory $prodAttributeCollectionFactory, + EngineProvider $engineProvider, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + MetadataPool $metadataPool, int $antiGapMultiplier = 5 ) { $this->resource = $resource; @@ -224,7 +238,7 @@ private function getSelectForSearchableProducts( $batch ) { $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId(); - $lastProductId = (int) $lastProductId; + $lastProductId = (int)$lastProductId; $select = $this->connection->select() ->useStraightJoin(true) @@ -242,7 +256,7 @@ private function getSelectForSearchableProducts( $this->joinAttribute($select, 'status', $storeId, [Status::STATUS_ENABLED]); if ($productIds !== null) { - $select->where('e.entity_id IN (?)', $productIds, \Zend_Db::INT_TYPE); + $select->where('e.entity_id IN (?)', $productIds, Zend_Db::INT_TYPE); } $select->where('e.entity_id > ?', $lastProductId); $select->order('e.entity_id'); @@ -308,14 +322,17 @@ private function joinAttribute(Select $select, $attributeCode, $storeId, array $ */ public function getSearchableAttributes($backendType = null) { + /** TODO: Remove this block in the next minor release and add a new public method instead */ + if ($this->eavConfig->getEntityType(Product::ENTITY)->getNeedRefreshSearchAttributesList()) { + $this->clearSearchableAttributesList(); + } if (null === $this->searchableAttributes) { $this->searchableAttributes = []; - /** @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection $productAttributes */ $productAttributes = $this->productAttributeCollectionFactory->create(); $productAttributes->addToIndexFilter(true); - /** @var \Magento\Eav\Model\Entity\Attribute[] $attributes */ + /** @var Attribute[] $attributes */ $attributes = $productAttributes->getItems(); /** @deprecated */ @@ -329,7 +346,7 @@ public function getSearchableAttributes($backendType = null) ['engine' => $this->engine, 'attributes' => $attributes] ); - $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); + $entity = $this->eavConfig->getEntityType(Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); @@ -355,6 +372,18 @@ public function getSearchableAttributes($backendType = null) return $this->searchableAttributes; } + /** + * Remove searchable attributes list. + * + * @return void + */ + private function clearSearchableAttributesList(): void + { + $this->searchableAttributes = null; + $this->searchableAttributesByBackendType = []; + $this->eavConfig->getEntityType(Product::ENTITY)->unsNeedRefreshSearchAttributesList(); + } + /** * Retrieve searchable attribute by Id or code * @@ -369,7 +398,7 @@ public function getSearchableAttribute($attribute) return $attributes[$attribute]; } - return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->eavConfig->getAttribute(Product::ENTITY, $attribute); } /** @@ -386,6 +415,7 @@ private function unifyField($field, $backendType = 'varchar') } else { $expr = $field; } + return $expr; } @@ -411,7 +441,7 @@ public function getProductAttributes($storeId, array $productIds, array $attribu )->where( 'cpe.entity_id IN (?)', $productIds, - \Zend_Db::INT_TYPE + Zend_Db::INT_TYPE ) ); foreach ($attributeTypes as $backendType => $attributeIds) { @@ -479,6 +509,7 @@ private function getProductTypeInstance($typeId) $this->productTypes[$typeId] = $this->catalogProductType->factory($productEmulator); } + return $this->productTypes[$typeId]; } @@ -513,6 +544,7 @@ public function getProductChildIds($productId, $typeId) if ($relation->getWhere() !== null) { $select->where($relation->getWhere()); } + return $this->connection->fetchCol($select); } @@ -528,10 +560,11 @@ public function getProductChildIds($productId, $typeId) private function getProductEmulator($typeId) { if (!isset($this->productEmulators[$typeId])) { - $productEmulator = new \Magento\Framework\DataObject(); + $productEmulator = new DataObject(); $productEmulator->setTypeId($typeId); $this->productEmulators[$typeId] = $productEmulator; } + return $this->productEmulators[$typeId]; } @@ -660,6 +693,7 @@ function ($value) { $attributeOptionValue .= $this->attributeOptions[$optionKey][$attrValueId] . ' '; } } + return empty($attributeOptionValue) ? null : trim($attributeOptionValue); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php index 7b5d43ece922d..3f0046b918f28 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php @@ -5,12 +5,15 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin; +use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; use Magento\Framework\Search\Request\Config; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Api\Data\EavAttributeInterface; +use Magento\Eav\Model\Config as EavConfig; /** * Catalog search indexer plugin for catalog attribute. @@ -37,16 +40,24 @@ class Attribute extends AbstractPlugin */ private $saveIsNew; + /** + * @var EavConfig + */ + private $eavConfig; + /** * @param IndexerRegistry $indexerRegistry * @param Config $config + * @param EavConfig $eavConfig */ public function __construct( IndexerRegistry $indexerRegistry, - Config $config + Config $config, + EavConfig $eavConfig ) { parent::__construct($indexerRegistry); $this->config = $config; + $this->eavConfig = $eavConfig; } /** @@ -84,6 +95,11 @@ public function afterSave( } if ($this->saveIsNew || $this->saveNeedInvalidation) { $this->config->reset(); + /** + * TODO: Remove this in next minor release and use public method instead. + * @see DataProvider::getSearchableAttributes + */ + $this->eavConfig->getEntityType(Product::ENTITY)->setNeedRefreshSearchAttributesList(true); } return $result; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php index befe462184af6..4d8a7de391356 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php @@ -7,8 +7,10 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Indexer\Fulltext\Plugin; +use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext; use Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Attribute; +use Magento\Eav\Model\Config as EavConfig; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Search\Request\Config; @@ -16,6 +18,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Unit tests for @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Attribute. + */ class AttributeTest extends TestCase { /** @@ -53,6 +58,14 @@ class AttributeTest extends TestCase */ private $config; + /** + * @var EavConfig + */ + private $eavConfig; + + /** + * @inheridoc + */ protected function setUp(): void { $this->objectManager = new ObjectManager($this); @@ -78,11 +91,16 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['reset']) ->getMock(); + $this->eavConfig = $this->createPartialMock( + EavConfig::class, + ['getEntityType'] + ); $this->model = $this->objectManager->getObject( Attribute::class, [ 'indexerRegistry' => $this->indexerRegistryMock, - 'config' => $this->config + 'config' => $this->config, + 'eavConfig' => $this->eavConfig ] ); } @@ -123,21 +141,26 @@ public function testAfterSaveWithInvalidation(bool $saveNeedInvalidation, bool $ [ 'indexerRegistry' => $this->indexerRegistryMock, 'config' => $this->config, + 'eavConfig' => $this->eavConfig, 'saveNeedInvalidation' => $saveNeedInvalidation, 'saveIsNew' => $saveIsNew, ] ); + if ($saveIsNew || $saveNeedInvalidation) { + $this->config->expects($this->once()) + ->method('reset'); + $catalogProductEntity = $this->createMock(Product::class); + $this->eavConfig->expects($this->once()) + ->method('getEntityType') + ->with(Product::ENTITY) + ->willReturn($catalogProductEntity); + } if ($saveNeedInvalidation) { $this->indexerMock->expects($this->once())->method('invalidate'); $this->prepareIndexer(); } - if ($saveIsNew || $saveNeedInvalidation) { - $this->config->expects($this->once()) - ->method('reset'); - } - $this->assertEquals( $this->subjectMock, $model->afterSave($this->subjectMock, $this->subjectMock) diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php index 89c98d29ae03e..75636991e7ee6 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php @@ -10,6 +10,7 @@ use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter\DummyAttribute; +use Magento\Framework\ObjectManagerInterface; use Psr\Log\LoggerInterface; /** @@ -20,7 +21,7 @@ class AttributeProvider /** * Object Manager instance * - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; @@ -49,13 +50,13 @@ class AttributeProvider /** * Factory constructor * - * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @param Config $eavConfig * @param LoggerInterface $logger * @param string $instanceName */ public function __construct( - \Magento\Framework\ObjectManagerInterface $objectManager, + ObjectManagerInterface $objectManager, Config $eavConfig, LoggerInterface $logger, $instanceName = AttributeAdapter::class @@ -87,4 +88,17 @@ public function getByAttributeCode(string $attributeCode): AttributeAdapter return $this->cachedPool[$attributeCode]; } + + /** + * Remove attribute from cache by code. + * + * @param string $attributeCode + * @return void + */ + public function removeAttributeCacheByCode(string $attributeCode): void + { + if (isset($this->cachedPool[$attributeCode])) { + unset($this->cachedPool[$attributeCode]); + } + } } 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 index 53f036a3b8e38..e15d91148b8ce 100644 --- 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 @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Indexer\IndexerHandler as ElasticsearchIndexerHandler; use Magento\Framework\Indexer\DimensionProviderInterface; use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; @@ -41,6 +42,11 @@ class Attribute */ private $indexerHandlerFactory; + /** + * @var AttributeProvider + */ + private $attributeProvider; + /** * @var bool */ @@ -56,17 +62,20 @@ class Attribute * @param Processor $indexerProcessor * @param DimensionProviderInterface $dimensionProvider * @param IndexerHandlerFactory $indexerHandlerFactory + * @param AttributeProvider $attributeProvider */ public function __construct( Config $config, Processor $indexerProcessor, DimensionProviderInterface $dimensionProvider, - IndexerHandlerFactory $indexerHandlerFactory + IndexerHandlerFactory $indexerHandlerFactory, + AttributeProvider $attributeProvider ) { $this->config = $config; $this->indexerProcessor = $indexerProcessor; $this->dimensionProvider = $dimensionProvider; $this->indexerHandlerFactory = $indexerHandlerFactory; + $this->attributeProvider = $attributeProvider; } /** @@ -82,6 +91,7 @@ public function afterSave( AttributeResourceModel $subject, AttributeResourceModel $result ): AttributeResourceModel { + $this->attributeProvider->removeAttributeCacheByCode($this->attributeCode); $indexer = $this->indexerProcessor->getIndexer(); if ($this->isNewObject && !$indexer->isScheduled() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index 9dbd902f1714e..b8e1587fcad71 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -17,10 +17,7 @@ class ProductSearchAggregationsTest extends GraphQlAbstract */ public function testAggregationBooleanAttribute() { - $this->markTestSkipped( - 'MC-22184: Elasticsearch returns incorrect aggregation options for booleans' - . 'MC-36768: Custom attribute not appears in elasticsearch' - ); + $this->markTestSkipped('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); $skus= '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"'; $query = <<<QUERY @@ -64,7 +61,6 @@ function ($a) { $this->assertEquals('boolean_attribute', $booleanAggregation['attribute_code']); $this->assertContainsEquals(['label' => '1', 'value'=> '1', 'count' => '3'], $booleanAggregation['options']); - $this->markTestSkipped('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); $this->assertEquals(2, $booleanAggregation['count']); $this->assertCount(2, $booleanAggregation['options']); $this->assertContainsEquals(['label' => '0', 'value'=> '0', 'count' => '2'], $booleanAggregation['options']); 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 f755a1a1e0282..2355eb281ac38 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -19,6 +19,7 @@ use Magento\Eav\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\ObjectManager; @@ -67,14 +68,11 @@ public function testFilterForNonExistingCategory() * Verify that layered navigation filters and aggregations are correct for product query * * Filter products by an array of skus - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterLn() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); $query = <<<QUERY { products ( @@ -149,14 +147,12 @@ private function compareFilterNames(array $a, array $b) * Layered navigation for Configurable products with out of stock options * Two configurable products each having two variations and one of the child products of one Configurable set to OOS * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testLayeredNavigationForConfigurableProducts() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; @@ -256,12 +252,11 @@ private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $fi * Filter products by custom attribute of dropdown type and filterTypeInput eq * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterProductsByDropDownCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); @@ -455,12 +450,11 @@ private function getDefaultAttributeOptionValue(string $attributeCode): string * Full text search for Products and then filter the results by custom attribute (default sort is relevance) * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSearchAndFilterByCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); $attribute_code = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); @@ -603,18 +597,19 @@ public function testSearchAndFilterByCustomAttribute() * Filter by category and custom attribute * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterByCategoryIdAndCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); - $categoryId = 13; + /** @var GetCategoryByName $getCategoryByName */ + $getCategoryByName = Bootstrap::getObjectManager()->get(GetCategoryByName::class); + $category = $getCategoryByName->execute('Category 1.2'); $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); $query = <<<QUERY { products(filter:{ - category_id : {eq:"{$categoryId}"} + category_id : {eq:"{$category->getId()}"} second_test_configurable: {eq: "{$optionValue}"} }, pageSize: 3 @@ -2368,7 +2363,6 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() /** * Verify that invalid current page return an error * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testInvalidCurrentPage() @@ -2399,7 +2393,6 @@ public function testInvalidCurrentPage() /** * Verify that invalid page size returns an error. * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testInvalidPageSize() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php index 1514613987b40..ae34ea31f0d51 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php @@ -32,8 +32,7 @@ protected function setUp(): void } /** - * @magentoApiDataFixture Magento/Swatches/_files/text_swatch_attribute.php - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @magentoApiDataFixture Magento/Swatches/_files/configurable_product_text_swatch_attribute.php */ public function testTextSwatchDataValues() { @@ -68,14 +67,15 @@ public function testTextSwatchDataValues() $option = $product['configurable_options'][0]; $this->assertArrayHasKey('values', $option); $length = count($option['values']); + $swatchData = ['Swatch 1', 'Swatch 2', 'Swatch 3']; for ($i = 0; $i < $length; $i++) { - $this->assertEquals('option ' . ($i + 1), $option['values'][$i]['swatch_data']['value']); + $swatchValue = $option['values'][$i]['swatch_data']['value']; + $this->assertContains($swatchValue, $swatchData); } } /** - * @magentoApiDataFixture Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @magentoApiDataFixture Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute.php */ public function testVisualSwatchDataValues() { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php index 98f623e5f193b..4c0f74f009330 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php @@ -5,34 +5,69 @@ */ namespace Magento\Catalog\Helper; -class ProductTest extends \PHPUnit\Framework\TestCase +use Exception; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Session; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppIsolation enabled + * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductTest extends TestCase { /** - * @var \Magento\Catalog\Helper\Product + * @var ProductHelper */ protected $helper; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductInterfaceFactory + */ + private $productFactory; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheridoc + */ protected function setUp(): void { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\App\State::class) - ->setAreaCode('frontend'); - $this->helper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Helper\Product::class - ); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $this->productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(ProductHelper::class); + /** @var ProductInterfaceFactory $productInterfaceFactory */ + $this->productFactory = $this->objectManager->get(ProductInterfaceFactory::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->registry = $this->objectManager->get(Registry::class); } /** * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php - * @magentoAppIsolation enabled */ public function testGetProductUrl() { @@ -46,20 +81,16 @@ public function testGetProductUrl() public function testGetPrice() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $product->setPrice(49.95); $this->assertEquals(49.95, $this->helper->getPrice($product)); } public function testGetFinalPrice() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $product->setPrice(49.95); $product->setFinalPrice(49.95); $this->assertEquals(49.95, $this->helper->getFinalPrice($product)); @@ -67,10 +98,8 @@ public function testGetFinalPrice() public function testGetImageUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/image.jpg', $this->helper->getImageUrl($product)); $product->setImage('test_image.png'); @@ -79,10 +108,8 @@ public function testGetImageUrl() public function testGetSmallImageUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/small_image.jpg', $this->helper->getSmallImageUrl($product)); $product->setSmallImage('test_image.png'); @@ -91,10 +118,8 @@ public function testGetSmallImageUrl() public function testGetThumbnailUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/thumbnail.jpg', $this->helper->getThumbnailUrl($product)); $product->setThumbnail('test_image.png'); $this->assertStringEndsWith('/test_image.png', $this->helper->getThumbnailUrl($product)); @@ -102,26 +127,20 @@ public function testGetThumbnailUrl() public function testGetEmailToFriendUrl() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->productFactory->create(); $product->setId(100); - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); + $category = $this->objectManager->create(CategoryInterfaceFactory::class)->create(); $category->setId(10); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); + $this->registry->register('current_category', $category); try { $this->assertStringEndsWith( 'sendfriend/product/send/id/100/cat_id/10/', $this->helper->getEmailToFriendUrl($product) ); - $objectManager->get(\Magento\Framework\Registry::class)->unregister('current_category'); - } catch (\Exception $e) { - $objectManager->get(\Magento\Framework\Registry::class)->unregister('current_category'); + $this->registry->unregister('current_category'); + } catch (Exception $e) { + $this->registry->unregister('current_category'); throw $e; } } @@ -137,17 +156,15 @@ public function testGetStatuses() public function testCanShow() { // non-visible or disabled - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertFalse($this->helper->canShow($product)); $existingProduct = $this->productRepository->get('simple'); // enabled and visible $product->setId($existingProduct->getId()); - $product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); - $product->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH); + $product->setStatus(Status::STATUS_ENABLED); + $product->setVisibility(Visibility::VISIBILITY_BOTH); $this->assertTrue($this->helper->canShow($product)); $this->assertTrue($this->helper->canShow((int)$product->getId())); @@ -193,39 +210,27 @@ public function testGetAttributeSourceModelByInputType() } /** - * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation enabled - * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/categories.php */ public function testInitProduct() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $objectManager->get(\Magento\Catalog\Model\Session::class)->setLastVisitedCategoryId(2); + $this->objectManager->get(Session::class)->setLastVisitedCategoryId(2); $product = $this->productRepository->get('simple'); $this->helper->initProduct($product->getId(), 'view'); - $this->assertInstanceOf( - \Magento\Catalog\Model\Product::class, - $objectManager->get(\Magento\Framework\Registry::class)->registry('current_product') - ); - $this->assertInstanceOf( - \Magento\Catalog\Model\Category::class, - $objectManager->get(\Magento\Framework\Registry::class)->registry('current_category') - ); + $this->assertInstanceOf(Product::class, $this->registry->registry('current_product')); + $this->assertInstanceOf(Category::class, $this->registry->registry('current_category')); } public function testPrepareProductOptions() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $buyRequest = new \Magento\Framework\DataObject(['qty' => 100, 'options' => ['option' => 'value']]); + /** @var $product Product */ + $product = $this->productFactory->create(); + $buyRequest = new DataObject(['qty' => 100, 'options' => ['option' => 'value']]); $this->helper->prepareProductOptions($product, $buyRequest); $result = $product->getPreconfiguredValues(); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $result); + $this->assertInstanceOf(DataObject::class, $result); $this->assertEquals(100, $result->getQty()); $this->assertEquals(['option' => 'value'], $result->getOptions()); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index 4255d7d3c98e5..9b743542b8573 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -3,29 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) - ->getEntityType('catalog_product') - ->getDefaultAttributeSetId(); +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\CategoryLinkRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +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\Eav\Model\Config; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; -$productRepository = $objectManager->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class -); +$objectManager = Bootstrap::getObjectManager(); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); +$rootCategoryId = $baseWebsite->getDefaultStore()->getRootCategoryId(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +$defaultAttributeSet = $objectManager->get(Config::class)->getEntityType(Product::ENTITY)->getDefaultAttributeSetId(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$categoryFactory = $objectManager->get(CategoryInterfaceFactory::class); $categoryLinkRepository = $objectManager->create( - \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + CategoryLinkRepositoryInterface::class, [ - 'productRepository' => $productRepository + 'productRepository' => $productRepository, ] ); /** @var Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ -$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +$categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class); $reflectionClass = new \ReflectionClass(get_class($categoryLinkManagement)); $properties = [ 'productRepository' => $productRepository, - 'categoryLinkRepository' => $categoryLinkRepository + 'categoryLinkRepository' => $categoryLinkRepository, ]; foreach ($properties as $key => $value) { if ($reflectionClass->hasProperty($key)) { @@ -39,7 +58,7 @@ * After installation system has two categories: root one with ID:1 and Default category with ID:2 */ /** @var $category \Magento\Catalog\Model\Category */ -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(3) ->setName('Category 1') @@ -52,7 +71,7 @@ ->setPosition(1) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(4) ->setName('Category 1.1') @@ -67,7 +86,7 @@ ->setDescription('Category 1.1 description.') ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(5) ->setName('Category 1.1.1') @@ -83,7 +102,7 @@ ->setDescription('This is the description for Category 1.1.1') ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(6) ->setName('Category 2') @@ -96,7 +115,7 @@ ->setPosition(2) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(7) ->setName('Movable') @@ -109,7 +128,7 @@ ->setPosition(3) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(8) ->setName('Inactive') @@ -122,7 +141,7 @@ ->setPosition(4) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(9) ->setName('Movable Position 1') @@ -135,7 +154,7 @@ ->setPosition(5) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(10) ->setName('Movable Position 2') @@ -148,7 +167,7 @@ ->setPosition(6) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(11) ->setName('Movable Position 3') @@ -161,7 +180,7 @@ ->setPosition(7) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(12) ->setName('Category 12') @@ -174,7 +193,7 @@ ->setPosition(8) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(13) ->setName('Category 1.2') @@ -189,84 +208,86 @@ ->setPosition(2) ->save(); -/** @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) +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product') ->setSku('simple') ->setPrice(10) ->setWeight(18) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple1 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple1->getSku(), [2, 3, 4, 13] ); -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Two') ->setSku('12345') // SKU intentionally contains digits only ->setPrice(45.67) ->setWeight(56) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple2 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple2->getSku(), [5, 4] ); -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Not Visible On Storefront') ->setSku('simple-3') ->setPrice(15) ->setWeight(2) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED); + +$simple3 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple3->getSku(), [10, 11, 12] ); -/** @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) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Three') ->setSku('simple-4') ->setPrice(10) ->setWeight(18) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple4 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple4->getSku(), [10, 11, 12, 13] ); 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 c2c3782c8cd23..6737aef1eb487 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,29 +5,26 @@ */ declare(strict_types=1); -use Magento\TestFramework\Workaround\Override\Fixture\Resolver; - -Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); - +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\TestFramework\Helper\CacheCleaner; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); -/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$objectManager = Bootstrap::getObjectManager(); -$eavConfig->clear(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var $attribute Attribute */ +$attribute = $attributeRepository->get('test_configurable'); $attribute->setIsSearchable(1) - ->setIsVisibleInAdvancedSearch(1) - ->setIsFilterable(true) - ->setIsFilterableInSearch(true) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) ->setIsVisibleOnFront(1); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); - CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php index 57b918fb5e663..6f81d6b659996 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php @@ -5,51 +5,56 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Setup\CategorySetup; -use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Eav\Model\Entity\Attribute\Source\Boolean; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); -/** @var Attribute $attribute */ -$attribute = $objectManager->create(Attribute::class); -/** @var $installer CategorySetup */ -$installer = $objectManager->create(CategorySetup::class); -try { - $attributeRepository->get(CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, 'boolean_attribute'); -} catch (NoSuchEntityException $e) { - $attribute->setData( - [ - 'attribute_code' => 'boolean_attribute', - 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, - 'is_global' => 0, - 'is_user_defined' => 1, - 'frontend_input' => 'boolean', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 0, - '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' => 0, - 'frontend_label' => ['Boolean Attribute'], - 'backend_type' => 'int', - 'source_model' => Boolean::class - ] - ); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var $installer CategorySetup */ +$installer = $objectManager->get(CategorySetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); - $attributeRepository->save($attribute); +/** @var ProductAttributeInterface $attributeModel */ +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'boolean_attribute', + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'boolean', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + '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' => 0, + 'frontend_label' => ['Boolean Attribute'], + 'backend_type' => 'int', + 'source_model' => Boolean::class + ] +); +$attribute = $attributeRepository->save($attributeModel); - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); -} +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); 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 29812aa942ab5..3bc3fef56e32e 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 @@ -5,125 +5,135 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +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\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Indexer\Model\Indexer; +use Magento\Indexer\Model\Indexer\Collection; +use Magento\Msrp\Model\Product\Attribute\Source\Type as SourceType; +use Magento\Store\Api\WebsiteRepositoryInterface; 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'); - +$objectManager = Bootstrap::getObjectManager(); + +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); + +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + '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' => ['option_0'] + ] +); +$attribute = $attributeRepository->save($attributeModel); + +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); +CacheCleaner::cleanAll(); $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); - - $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' => ['option_0'] - ] - ); - - $attributeRepository->save($attribute); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); - CacheCleaner::cleanAll(); -} - -$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) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->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) + ->setMsrpDisplayActualPriceType(SourceType::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]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('5.99') - ->save(); + ->setSpecialPrice('5.99'); +$simple1 = $productRepository->save($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(11) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->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) + ->setMsrpDisplayActualPriceType(SourceType::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]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('15.99') - ->save(); + ->setSpecialPrice('15.99'); +$simple2 = $productRepository->save($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(12) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product3') ->setSku('simple3') ->setTaxClassId('none') @@ -131,44 +141,42 @@ ->setShortDescription('short description') ->setPrice(30) ->setWeight(1) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) - ->setWebsiteIds([1]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_DISABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 140, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('25.99') - ->save(); + ->setSpecialPrice('25.99'); +$simple3 = $productRepository->save($product); -$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +/** @var CategoryInterfaceFactory $categoryInterfaceFactory */ +$categoryInterfaceFactory = $objectManager->get(CategoryInterfaceFactory::class); + +$category = $categoryInterfaceFactory->create(); $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, 12 => 12] -)->save(); +$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( + [ + $simple1->getId() => 10, + $simple2->getId() => 11, + $simple3->getId() => 12 + ] + ); +$category->save(); -/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ -$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +/** @var Collection $indexerCollection */ +$indexerCollection = $objectManager->get(Collection::class); $indexerCollection->load(); -/** @var \Magento\Indexer\Model\Indexer $indexer */ +/** @var Indexer $indexer */ foreach ($indexerCollection->getItems() as $indexer) { $indexer->reindexAll(); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php index dd89f8974a647..47e6a4e71cb69 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php @@ -5,47 +5,57 @@ */ 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); - +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; +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\Api\ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); foreach (['simple1', 'simple2', 'simple3'] as $sku) { try { $product = $productRepository->get($sku, false, null, true); $productRepository->delete($product); - } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + } catch (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 CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var GetCategoryByName $getCategoryByName */ +$getCategoryByName = $objectManager->get(GetCategoryByName::class); +$category = $getCategoryByName->execute('Category 1'); +try { + if ($category->getId()) { + $categoryRepository->delete($category); + } +} catch (NoSuchEntityException $exception) { + //Category already removed } -/** @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 = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); -$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(); +try { + $attribute = $attributeRepository->get('test_configurable'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $exception) { + //Attribute already removed } $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 4dd088e148d75..76056f2fa9e0d 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,8 +5,15 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Framework\Exception\NoSuchEntityException; 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; @@ -15,136 +22,117 @@ Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/categories.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var GetAttributeSetByName $getAttributeSetByName */ -$getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); -$attributeSet = $getAttributeSetByName->execute('second_attribute_set'); -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); - -$eavConfig->clear(); - -$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 \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 -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(), - $attribute1->getId() - ); -} +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); +/** @var GetAttributeSetByName $getAttributeSetByName */ +$getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); +$secondAttributeSet = $getAttributeSetByName->execute('second_attribute_set'); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$defaultAttributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$defaultGroupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $defaultAttributeSetId); + +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + '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' => ['option_0'] + ] +); +$attribute = $attributeRepository->save($attributeModel); +$installer->addAttributeToGroup( + Product::ENTITY, + $defaultAttributeSetId, + $defaultGroupId, + $attribute->getId() +); $eavConfig->clear(); -/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var ProductAttributeInterface $attribute */ +$attributeModel2 = $attributeFactory->create(); +$attributeModel2->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + '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'], + ] +); +$attribute2 = $attributeRepository->save($attributeModel2); +$installer->addAttributeToGroup( + Product::ENTITY, + $secondAttributeSet->getId(), + $secondAttributeSet->getDefaultGroupId(), + $attribute2->getId() +); /** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ -$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); $productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; foreach ($productsWithNewAttributeSet as $sku) { try { $product = $productRepository->get($sku, false, null, true); - $product->setAttributeSetId($attributeSet->getId()); + $product->setAttributeSetId($secondAttributeSet->getId()); $product->setStockData( - ['use_config_manage_stock' => 1, + [ + 'use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, - 'is_in_stock' => 1] + 'is_in_stock' => 1, + ] ); $productRepository->save($product); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php index 12f63993cb2d3..939c1d261b3c6 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php @@ -4,59 +4,67 @@ * See COPYING.txt for license details. */ +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$objectManager = Bootstrap::getObjectManager(); -$eavConfig->clear(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); -/** @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); - - $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' => 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'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - ] - ); - - $attributeRepository->save($attribute); - - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +try { + $attributeRepository->get('test_configurable'); + Resolver::getInstance() + ->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php'); +} catch (NoSuchEntityException $e) { } +$eavConfig = $objectManager->get(Config::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); +/** @var ProductAttributeInterface $attributeModel */ +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + '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'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] +); + +$attribute = $attributeRepository->save($attributeModel); + +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); $eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php index f6e6261c75662..618b554aaa2cc 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php @@ -3,49 +3,63 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; 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\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\Eav\Setup\EavSetup; +use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager - ->get(ProductRepositoryInterface::class); -/** @var Config $eavConfig */ -$eavConfig = $objectManager->get(Config::class); -$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); -/** @var $installer CategorySetup */ -$installer = $objectManager->create(CategorySetup::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); -/* Create simple products per each option value*/ +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var $attribute Attribute */ +$attribute = $attributeRepository->get('test_configurable'); /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); + +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +/* Create simple products per each option value*/ + $attributeValues = []; -$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); $associatedProductIds = []; $productIds = [10, 20]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { /** @var $product Product */ - $product = $objectManager->create(Product::class); + $product = $productInterfaceFactory->create(); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Option' . $option->getLabel()) ->setSku('simple_' . $productId) ->setPrice($productId) @@ -53,20 +67,18 @@ ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) ->setStatus(Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $product = $productRepository->save($product); + $simple1 = $productRepository->save($product); $attributeValues[] = [ 'label' => 'test', 'attribute_id' => $attribute->getId(), 'value_index' => $option->getValue(), ]; - $associatedProductIds[] = $product->getId(); + $associatedProductIds[] = $simple1->getId(); } /** @var $product Product */ -$product = $objectManager->create(Product::class); -/** @var Factory $optionsFactory */ -$optionsFactory = $objectManager->create(Factory::class); +$product = $productInterfaceFactory->create(); $configurableAttributesData = [ [ 'attribute_id' => $attribute->getId(), @@ -84,7 +96,7 @@ $product->setTypeId(Configurable::TYPE_CODE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Product') ->setSku('configurable') ->setVisibility(Visibility::VISIBILITY_BOTH) @@ -98,18 +110,17 @@ $options = $attribute->getOptions(); $attributeValues = []; -$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); $associatedProductIds = []; $productIds = [30, 40]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { /** @var $product Product */ - $product = $objectManager->create(Product::class); + $product = $productInterfaceFactory->create(); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Option' . $option->getLabel()) ->setSku('simple_' . $productId) ->setPrice($productId) @@ -117,21 +128,18 @@ ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) ->setStatus(Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $product = $productRepository->save($product); + $simple2 = $productRepository->save($product); $attributeValues[] = [ 'label' => 'test', 'attribute_id' => $attribute->getId(), 'value_index' => $option->getValue(), ]; - $associatedProductIds[] = $product->getId(); + $associatedProductIds[] = $simple2->getId(); } /** @var $product Product */ -$product = $objectManager->create(Product::class); - -/** @var Factory $optionsFactory */ -$optionsFactory = $objectManager->create(Factory::class); +$product = $productInterfaceFactory->create(); $configurableAttributesData = [ [ @@ -153,7 +161,7 @@ $product->setTypeId(Configurable::TYPE_CODE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Product 12345') ->setSku('configurable_12345') ->setVisibility(Visibility::VISIBILITY_BOTH) diff --git a/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php b/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php new file mode 100644 index 0000000000000..f243d39c24d26 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Indexer\Model\Processor; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Processor $processor */ +$processor = Bootstrap::getObjectManager()->get(Processor::class); +$processor->reindexAllInvalid(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php index 4e64aa0349b80..1b56a7e8c4448 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php @@ -5,4 +5,5 @@ */ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/product_configurable_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php index 38fade9013cd1..0bc5e2e6e595e 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php @@ -12,9 +12,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture( - 'Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php' -); $objectManager = Bootstrap::getObjectManager(); /** @var Registry $registry */ $registry = $objectManager->get(Registry::class); @@ -42,5 +39,9 @@ //Product already removed } +Resolver::getInstance()->requireDataFixture( + 'Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php' +); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php index c480906619a4a..c5e1e1fc287ba 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php @@ -5,20 +5,33 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Swatches\Helper\Media as SwatchesMedia; use Magento\TestFramework\Helper\Bootstrap; +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + +try { + $attribute = $attributeRepository->get('test_configurable'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $exception) { + //Product already removed +} + /** @var WriteInterface $mediaDirectory */ -$mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) +$mediaDirectory = $objectManager->get(Filesystem::class) ->getDirectoryWrite( DirectoryList::MEDIA ); /** @var SwatchesMedia $swatchesMedia */ -$swatchesMedia = Bootstrap::getObjectManager()->get(SwatchesMedia::class); +$swatchesMedia = $objectManager->get(SwatchesMedia::class); $testImageName = 'visual_swatch_attribute_option_type_image.jpg'; $testImageSwatchPath = $swatchesMedia->getAttributeSwatchPath($testImageName); From 6d05df05c1161e5d7e742da4f9f460a0e4f8a687 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Wed, 21 Oct 2020 11:44:36 +0300 Subject: [PATCH 0885/1013] MC-36956: Create automated test for "Upload Category Image" --- .../Model/ResourceModel/CategoryTest.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php index 0a3944bbb36fd..c57e981f772de 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/CategoryTest.php @@ -12,6 +12,7 @@ use Magento\Catalog\Model\Category as CategoryModel; use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; @@ -64,7 +65,7 @@ protected function setUp(): void $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); $this->categoryResource = $this->objectManager->get(CategoryResource::class); $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); - $this->categoryCollection = $this->objectManager->get(CategoryCollection::class); + $this->categoryCollection = $this->objectManager->get(CategoryCollectionFactory::class)->create(); $this->filesystem = $this->objectManager->get(Filesystem::class); $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); } @@ -92,8 +93,8 @@ public function testAddImageForCategory(): void 'type' => 'image/jpg', 'tmp_name' => '/tmp/phpDstnAx', 'file' => 'magento_small_image.jpg', + 'url' => $this->prepareDataImageUrl('magento_small_image.jpg'), ]; - $this->prepareDataImageUrl($dataImage); $imageRelativePath = self::BASE_PATH . DIRECTORY_SEPARATOR . $dataImage['file']; $expectedImage = DIRECTORY_SEPARATOR . $this->storeManager->getStore()->getBaseMediaDir() . DIRECTORY_SEPARATOR . $imageRelativePath; @@ -116,14 +117,14 @@ public function testAddImageForCategory(): void } /** - * Add image url to image data + * Prepare image url for image data * - * @param array $dataImage - * @return void + * @param string $file + * @return string */ - private function prepareDataImageUrl(array &$dataImage): void + private function prepareDataImageUrl(string $file): string { - $dataImage['url'] = $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) - . self::BASE_TMP_PATH . DIRECTORY_SEPARATOR . $dataImage['file']; + return $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) + . self::BASE_TMP_PATH . DIRECTORY_SEPARATOR . $file; } } From 4f8f9f2ae93caadd2d63d587183d6ac78a3514c3 Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Wed, 21 Oct 2020 12:00:21 +0300 Subject: [PATCH 0886/1013] test coverage --- .../ConfirmCustomerByTokenTest.php | 91 ++++++++++++++++++ .../ConfirmCustomerByTokenTest.php | 92 +++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php diff --git a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php new file mode 100644 index 0000000000000..30aa70e89d2d0 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\ForgotPasswordToken; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Customer\Model\ResourceModel\Customer; +use Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken. + */ +class ConfirmCustomerByTokenTest extends TestCase +{ + private const STUB_RESET_PASSWORD_TOKEN = 'resetPassword'; + + /** + * @var ConfirmCustomerByToken; + */ + private $model; + + /** + * @var CustomerInterface|MockObject + */ + private $customerMock; + + /** + * @var CustomerResource|MockObject + */ + private $customerResourceMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->customerMock = $this->getMockForAbstractClass(CustomerInterface::class); + $this->customerResourceMock = $this->createMock(CustomerResource::class); + + $getCustomerByTokenMock = $this->createMock(GetCustomerByToken::class); + $getCustomerByTokenMock->method('execute')->willReturn($this->customerMock); + + $this->model = new ConfirmCustomerByToken($getCustomerByTokenMock, $this->customerResourceMock); + } + + /** + * Confirm customer with confirmation + * + * @return void + */ + public function testExecuteWithConfirmation(): void + { + $customerId = 777; + + $this->customerMock->expects($this->once()) + ->method('getConfirmation') + ->willReturn('GWz2ik7Kts517MXAgrm4DzfcxKayGCm4'); + $this->customerMock->expects($this->once()) + ->method('getId') + ->willReturn($customerId); + $this->customerResourceMock->expects($this->once()) + ->method('updateColumn') + ->with($customerId, 'confirmation', null); + + $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); + } + + /** + * Confirm customer without confirmation + * + * @return void + */ + public function testExecuteWithoutConfirmation(): void + { + $this->customerMock->expects($this->once()) + ->method('getConfirmation') + ->willReturn(null); + $this->customerResourceMock->expects($this->never()) + ->method('updateColumn'); + + $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php new file mode 100644 index 0000000000000..5399f6903ee9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\ForgotPasswordToken; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken. + */ +class ConfirmCustomerByTokenTest extends TestCase +{ + private const STUB_CUSTOMER_RESET_TOKEN = 'token12345'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ConfirmCustomerByToken + */ + private $confirmCustomerByToken; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + + $resource = $this->objectManager->get(ResourceConnection::class); + $this->connection = $resource->getConnection(); + + $this->confirmCustomerByToken = $this->objectManager->get(ConfirmCustomerByToken::class); + } + + /** + * Customer address shouldn't validate during confirm customer by token + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_address.php + * + * @return void + */ + public function testExecuteWithInvalidAddress(): void + { + $id = 1; + + $customerModel = $this->objectManager->create(Customer::class); + $customerModel->load($id); + $customerModel->setRpToken(self::STUB_CUSTOMER_RESET_TOKEN); + $customerModel->setRpTokenCreatedAt(date('Y-m-d H:i:s')); + $customerModel->setConfirmation($customerModel->getRandomConfirmationKey()); + $customerModel->save(); + + //make city address invalid + $this->makeCityInvalid($id); + + $this->confirmCustomerByToken->execute(self::STUB_CUSTOMER_RESET_TOKEN); + $this->assertNull($customerModel->load($id)->getConfirmation()); + } + + /** + * Set city invalid for customer address + * + * @param int $id + * @return void + */ + private function makeCityInvalid(int $id): void + { + $this->connection->update( + $this->connection->getTableName('customer_address_entity'), + ['city' => ''], + $this->connection->quoteInto('entity_id = ?', $id) + ); + } +} From 36f4183ae372fb1b8a2f51aefe1ee948829ed16c Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Wed, 21 Oct 2020 12:05:40 +0300 Subject: [PATCH 0887/1013] MC-37544: Create automated test for "Category Schedule Design Update not available if CatalogStaging installed" --- .../Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml | 1 + .../Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml index a65d2c9e63bef..fb231cf56b200 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml @@ -10,5 +10,6 @@ <section name="AdminCategoryScheduleDesignUpdateSection"> <element name="sectionHeader" type="button" selector="div[data-index='schedule_design_update'] .fieldset-wrapper-title" timeout="30"/> <element name="sectionBody" type="text" selector="div[data-index='schedule_design_update'] .admin__fieldset-wrapper-content"/> + <element name="customDesignFrom" type="input" selector="input[name='custom_design_from']"/> </section> </sections> diff --git a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml index e2f0a01fc733b..b9940fe42052c 100644 --- a/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml +++ b/app/code/Magento/Theme/Test/Mftf/Section/StorefrontHeaderSection.xml @@ -10,5 +10,6 @@ <section name="StorefrontHeaderSection"> <element name="welcomeMessage" type="text" selector=".greet.welcome"/> <element name="logoLink" type="button" selector=".header .logo"/> + <element name="logoImage" type="button" selector=".header .logo > img[src*='{{filename}}']" parameterized="true"/> </section> </sections> From 6970b19c234d752d2f4137580c877b37d96f77d5 Mon Sep 17 00:00:00 2001 From: Ejaz Alam <ejazalam518@gmail.com> Date: Wed, 21 Oct 2020 14:23:44 +0500 Subject: [PATCH 0888/1013] Updated string with Static key. #30545 Update string with the static key in function. --- app/code/Magento/Quote/Model/Quote/Address.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 5476915d9d649..4773c5a391094 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -139,6 +139,8 @@ class Address extends AbstractAddress implements const ADDRESS_TYPE_BILLING = 'billing'; const ADDRESS_TYPE_SHIPPING = 'shipping'; + + const CACHED_ITEMS_ALL = 'cached_items_all'; /** * Prefix of model events @@ -636,8 +638,7 @@ public function getItemsCollection() public function getAllItems() { // We calculate item list once and cache it in three arrays - all items - $key = 'cached_items_all'; - if (!$this->hasData($key)) { + if (!$this->hasData(self::CACHED_ITEMS_ALL)) { $quoteItems = $this->getQuote()->getItemsCollection(); $addressItems = $this->getItemsCollection(); @@ -676,10 +677,10 @@ public function getAllItems() } // Cache calculated lists - $this->setData('cached_items_all', $items); + $this->setData(self::CACHED_ITEMS_ALL, $items); } - $items = $this->getData($key); + $items = $this->getData(self::CACHED_ITEMS_ALL); return $items; } From 8243e91b186baaa1002cc1cc7f74bbe4f1d219fa Mon Sep 17 00:00:00 2001 From: Viktor Kopin <viktor.kopin@transoftgroup.com> Date: Wed, 21 Oct 2020 14:50:58 +0300 Subject: [PATCH 0889/1013] MC-38413: REST API: catalogCategoryLinkManagement error if same product in multiple subcategories of parent --- .../Catalog/Model/CategoryLinkManagement.php | 6 ++++-- .../Catalog/Model/ResourceModel/Category.php | 6 +++--- .../ResourceModel/Product/Collection.php | 12 ++++++----- .../Unit/Model/CategoryLinkManagementTest.php | 5 +++++ .../Api/CategoryLinkManagementTest.php | 21 ++++++++++++++++--- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/app/code/Magento/Catalog/Model/CategoryLinkManagement.php b/app/code/Magento/Catalog/Model/CategoryLinkManagement.php index 8966848a6d036..591cbc32a0d86 100644 --- a/app/code/Magento/Catalog/Model/CategoryLinkManagement.php +++ b/app/code/Magento/Catalog/Model/CategoryLinkManagement.php @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model; /** - * Class CategoryLinkManagement + * Represents Category Product Link Management class */ class CategoryLinkManagement implements \Magento\Catalog\Api\CategoryLinkManagementInterface { @@ -56,7 +57,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getAssignedProducts($categoryId) { @@ -65,6 +66,7 @@ public function getAssignedProducts($categoryId) /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $products */ $products = $category->getProductCollection(); $products->addFieldToSelect('position'); + $products->groupByAttribute($products->getProductEntityMetadata()->getIdentifierField()); /** @var \Magento\Catalog\Api\Data\CategoryProductLinkInterface[] $links */ $links = []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 917aafb643b47..e19286efc38c0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -13,7 +13,7 @@ namespace Magento\Catalog\Model\ResourceModel; -use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Catalog\Setup\CategorySetup; use Magento\Framework\App\ObjectManager; @@ -1172,11 +1172,11 @@ public function getCategoryWithChildren(int $categoryId): array return []; } - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $linkField = $this->metadataPool->getMetadata(CategoryInterface::class)->getLinkField(); $select = $connection->select() ->from( ['cce' => $this->getTable('catalog_category_entity')], - [$linkField, 'parent_id', 'path'] + [$linkField, 'entity_id', 'parent_id', 'path'] )->join( ['cce_int' => $this->getTable('catalog_category_entity_int')], 'cce.' . $linkField . ' = cce_int.' . $linkField, diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 7dbfe0d5fccea..3f908663c8e5e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; @@ -2130,16 +2131,17 @@ private function getChildrenCategories(int $categoryId): array $firstCategory = array_shift($categories); if ($firstCategory['is_anchor'] == 1) { - $linkField = $this->getProductEntityMetadata()->getLinkField(); - $anchorCategory[] = (int)$firstCategory[$linkField]; + //category hierarchy can not be modified by staging updates + $entityField = $this->metadataPool->getMetadata(CategoryInterface::class)->getIdentifierField(); + $anchorCategory[] = (int)$firstCategory[$entityField]; foreach ($categories as $category) { if (in_array($category['parent_id'], $categoryIds) && in_array($category['parent_id'], $anchorCategory)) { - $categoryIds[] = (int)$category[$linkField]; + $categoryIds[] = (int)$category[$entityField]; // Storefront approach is to treat non-anchor children of anchor category as anchors. - // Adding their's IDs to $anchorCategory for consistency. + // Adding theirs IDs to $anchorCategory for consistency. if ($category['is_anchor'] == 1 || in_array($category['parent_id'], $anchorCategory)) { - $anchorCategory[] = (int)$category[$linkField]; + $anchorCategory[] = (int)$category[$entityField]; } } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php index be79b11cdf2b8..7cb2064d34d20 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php @@ -15,6 +15,7 @@ use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Framework\DataObject; use Magento\Framework\Indexer\IndexerRegistry; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -85,7 +86,11 @@ public function testGetAssignedProducts() $categoryMock->expects($this->once())->method('getProductCollection')->willReturn($productsMock); $categoryMock->expects($this->once())->method('getId')->willReturn($categoryId); $productsMock->expects($this->once())->method('addFieldToSelect')->with('position')->willReturnSelf(); + $productsMock->expects($this->once())->method('groupByAttribute')->with('entity_id')->willReturnSelf(); $productsMock->expects($this->once())->method('getItems')->willReturn($items); + $productsMock->expects($this->once()) + ->method('getProductEntityMetadata') + ->willReturn(new DataObject(['identifier_field' => 'entity_id'])); $this->productLinkFactoryMock->expects($this->once())->method('create')->willReturn($categoryProductLinkMock); $categoryProductLinkMock->expects($this->once()) ->method('setSku') diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php index 629cc077a63ea..85509dabdf415 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php @@ -4,10 +4,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Api; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Represents CategoryLinkManagementTest Class + */ class CategoryLinkManagementTest extends WebapiAbstract { const SERVICE_WRITE_NAME = 'catalogCategoryLinkManagementV1'; @@ -43,11 +48,21 @@ public function testInfoNoSuchEntityException() } } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testDuplicatedProductsInChildCategories() + { + $result = $this->getAssignedProducts(3, 'all'); + $this->assertCount(3, $result); + } + /** * @param int $id category id - * @return string + * @param string|null $storeCode + * @return array|string */ - protected function getAssignedProducts($id) + private function getAssignedProducts(int $id, ?string $storeCode = null) { $serviceInfo = [ 'rest' => [ @@ -60,6 +75,6 @@ protected function getAssignedProducts($id) 'operation' => self::SERVICE_WRITE_NAME . 'GetAssignedProducts', ], ]; - return $this->_webApiCall($serviceInfo, ['categoryId' => $id]); + return $this->_webApiCall($serviceInfo, ['categoryId' => $id], null, $storeCode); } } From 3176d9d703a7859646c2543a564cb58c6c1ac99d Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Wed, 21 Oct 2020 15:19:12 +0300 Subject: [PATCH 0890/1013] MC-37213: [MFTF] AdminMediaGalleryCatalogUiUsedInProductFilterTest is flaky --- .../AdminDisableWYSIWYGActionGroup.xml | 2 +- .../AdminEnableWYSIWYGActionGroup.xml | 2 +- .../Cms/Test/Mftf/Data/WysiwygConfigData.xml | 2 +- ...alleryFilterPlaceHolderGridActionGroup.xml | 2 + ...leryCatalogUiUsedInProductFilterOnTest.xml | 79 +++++++++++++++++++ ...alleryCatalogUiUsedInProductFilterTest.xml | 7 +- ...nhancedMediaGalleryImageActionsSection.xml | 2 +- .../Test/Mftf/Suite/MediaGalleryUiSuite.xml | 12 +-- .../web/js/grid/filters/elements/ui-select.js | 4 +- 9 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDisableWYSIWYGActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDisableWYSIWYGActionGroup.xml index 7e035a47824ee..8407860959184 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDisableWYSIWYGActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDisableWYSIWYGActionGroup.xml @@ -13,6 +13,6 @@ <description>Runs bin/magento command to disable WYSIWYG</description> </annotations> - <magentoCLI stepKey="disableWYSIWYG" command="config:set cms/wysiwyg/enabled disabled"/> + <magentoCLI command="config:set {{WysiwygDisabledByDefault.path}} {{WysiwygDisabledByDefault.value}}" stepKey="disableWYSIWYG"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminEnableWYSIWYGActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminEnableWYSIWYGActionGroup.xml index 6c9b439e2941b..58c219092d85e 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminEnableWYSIWYGActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminEnableWYSIWYGActionGroup.xml @@ -13,6 +13,6 @@ <description>Runs bin/magento command to enable WYSIWYG</description> </annotations> - <magentoCLI stepKey="enableWYSIWYG" command="config:set cms/wysiwyg/enabled enabled"/> + <magentoCLI command="config:set {{WysiwygEnabledByDefault.path}} {{WysiwygEnabledByDefault.value}}" stepKey="enableWYSIWYG"/> </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 ea5e90383511c..66f2983140b45 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml @@ -16,7 +16,7 @@ <entity name="WysiwygDisabledByDefault"> <data key="path">cms/wysiwyg/enabled</data> <data key="scope_id">0</data> - <data key="value">hidden</data> + <data key="value">disabled</data> </entity> <entity name="WysiwygTinyMCE3Enable" deprecated="Use WysiwygTinyMCE4Enable instead"> <data key="path">cms/wysiwyg/editor</data> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup.xml index e21fa89965391..32065da7bb1e7 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup.xml @@ -15,6 +15,8 @@ <argument name="filterPlaceholder" type="string"/> </arguments> + <waitForPageLoad stepKey="waitVisibleFilter"/> + <waitForElementVisible selector="{{AdminProductGridFilterSection.enabledFilters}}" stepKey="waitForRequest"/> <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="{{filterPlaceholder}}" stepKey="seeFilter"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml new file mode 100644 index 0000000000000..32a07d6f6273f --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.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="AdminMediaGalleryCatalogUiUsedInProductFilterOnTest"> + <annotations> + <features value="MediaGalleryCatalogUi"/> + <stories value="Story 58 - User sees entities where asset is used in" /> + <title value="User can open the product entity the asset is associated"/> + <description value="User filters assets used in products"/> + <severity value="CRITICAL"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <magentoCLI command="config:set {{WysiwygEnabledByDefault.path}} {{WysiwygEnabledByDefault.value}}" stepKey="enableWYSIWYG"/> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <magentoCLI command="config:set {{WysiwygDisabledByDefault.path}} {{WysiwygDisabledByDefault.value}}" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToAssertEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView2"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createProduct.id$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <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="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Products"/> + <argument name="optionName" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInProducts"> + <argument name="entityName" value="Products"/> + </actionGroup> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersOnProductGrid"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml index a66009e9d2045..34d51f6533ef5 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCatalogUiUsedInProductFilterTest"> + <test name="AdminMediaGalleryCatalogUiUsedInProductFilterTest" deprecated="Use AdminMediaGalleryCatalogUiUsedInProductFilterOnTest instead"> <annotations> <features value="AdminMediaGalleryUsedInProductsFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> - <title value="User can open product entity the asset is associated"/> + <title value="Deprecated. User can open product entity the asset is associated"/> <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 filters assets used in products"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryCatalogUiUsedInProductFilterOnTest instead</issueId> + </skip> </annotations> <before> <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index f36fca88dc760..32282f4f5e8ca 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -10,7 +10,7 @@ <section name="AdminEnhancedMediaGalleryImageActionsSection"> <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> - <element name="viewDetails" type="button" selector="[data-ui-id='action-image-details']"/> + <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[text()='View 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"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml index e81dc807d0f48..bda9b6ad08e43 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml @@ -11,16 +11,12 @@ <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="AdminMediaGalleryRenditionsEnableActionGroup" stepKey="enableMediaGalleryRenditions"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <magentoCLI command="config:set {{MediaGalleryConfigDataEnabled.path}} {{MediaGalleryConfigDataEnabled.value}}" stepKey="enableEnhancedMediaGallery"/> + <magentoCLI command="config:set {{MediaGalleryRenditionsDataEnabled.path}} {{MediaGalleryRenditionsDataEnabled.value}}" stepKey="enableMediaGalleryRenditions"/> </before> <after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> + <magentoCLI command="config:set {{MediaGalleryRenditionsDataDisabled.path}} {{MediaGalleryRenditionsDataDisabled.value}}" stepKey="disableMediaGalleryRenditions"/> + <magentoCLI command="config:set {{MediaGalleryConfigDataDisabled.path}} {{MediaGalleryConfigDataDisabled.value}}" stepKey="disableEnhancedMediaGallery"/> <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYG" /> </after> <include> diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js index a913f3fa4a042..cddcc7d49ffe8 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js @@ -18,13 +18,15 @@ define([ loadedOption: [], validationLoading: true, imports: { + applied: '${ $.filterChipsProvider }:applied', activeIndex: '${ $.bookmarkProvider }:activeIndex' }, modules: { filterChips: '${ $.filterChipsProvider }' }, listens: { - activeIndex: 'validateInitialValue' + activeIndex: 'validateInitialValue', + applied: 'validateInitialValue' } }, From af0ba2b76e1015481d262eeabd83f2734a62c632 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Wed, 21 Oct 2020 15:32:40 +0300 Subject: [PATCH 0891/1013] MC-37891: Create automated test for "Delete Widget" --- .../Adminhtml/Widget/Instance/DeleteTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php index 46ea322953278..eb4fe3dc3b7a9 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/Instance/DeleteTest.php @@ -10,6 +10,7 @@ use Magento\Framework\App\Request\Http; use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Widget\Model\ResourceModel\Widget\Instance\Collection; use Magento\Widget\Model\ResourceModel\Widget\Instance\CollectionFactory; /** @@ -21,8 +22,8 @@ */ class DeleteTest extends AbstractBackendController { - /** @var CollectionFactory */ - private $collectionFactory; + /** @var Collection */ + private $widgetCollection; /** * @inheritdoc @@ -31,7 +32,7 @@ protected function setUp(): void { parent::setUp(); - $this->collectionFactory = $this->_objectManager->get(CollectionFactory::class); + $this->widgetCollection = $this->_objectManager->get(CollectionFactory::class)->create(); } /** @@ -41,8 +42,7 @@ protected function setUp(): void */ public function testDeleteWidget(): void { - $widget = $this->collectionFactory->create() - ->addFieldToFilter('title', 'New Sample widget title')->getFirstItem(); + $widget = $this->widgetCollection->addFieldToFilter('title', 'New Sample widget title')->getFirstItem(); $this->assertNotNull($widget->getInstanceId()); $this->getRequest()->setMethod(Http::METHOD_POST); $this->getRequest()->setParams(['instance_id' => $widget->getInstanceId()]); From fd4a5a7cb63e2c712e03de61b820f92a47a33a2e Mon Sep 17 00:00:00 2001 From: Ejaz Alam <ejazalam518@gmail.com> Date: Wed, 21 Oct 2020 18:23:14 +0500 Subject: [PATCH 0892/1013] Apply suggestions from code review added scope for the const variable. Co-authored-by: Ihor Sviziev <ihor-sviziev@users.noreply.github.com> --- app/code/Magento/Quote/Model/Quote/Address.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 4773c5a391094..aee86eb1f8935 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -140,7 +140,7 @@ class Address extends AbstractAddress implements const ADDRESS_TYPE_SHIPPING = 'shipping'; - const CACHED_ITEMS_ALL = 'cached_items_all'; + private const CACHED_ITEMS_ALL = 'cached_items_all'; /** * Prefix of model events From f38bcc53503704ed39c7fcee506342ea7b61c0dc Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 21 Oct 2020 12:46:31 +0300 Subject: [PATCH 0893/1013] MC-37128: Create automated test for "[Security] No XSS injections in order comments" MC-37128: Create automated test for "[Security] No XSS injections in order comments" --- ...with_address_and_shipping_method_saved.php | 26 +++++++++++++++++++ ...ess_and_shipping_method_saved_rollback.php | 10 +++++++ 2 files changed, 36 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.php create mode 100644 dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.php new file mode 100644 index 0000000000000..0f8e92e252057 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.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\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address_saved.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CartManagementInterface $quoteManagement */ +$quoteManagement = $objectManager->get(CartManagementInterface::class); +$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('test_order_1'); +$quote->setIsActive(true); +$quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); +$quote->getShippingAddress()->setCollectShippingRates(true); +$quote->getShippingAddress()->collectShippingRates(); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved_rollback.php new file mode 100644 index 0000000000000..919958d9cbcf4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved_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/Checkout/_files/quote_with_address_saved_rollback.php'); From 23df632a70ed050b709cf8f25d8a047ae4e69faa Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Wed, 21 Oct 2020 22:21:47 -0500 Subject: [PATCH 0894/1013] 29251 test web-api test fix --- .../etc/graphql/di.xml | 6 +- .../Model/Cart/AddSimpleProductToCart.php | 70 ++++++++++++++++++- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml index ace37b54a2bf6..dc672b02e2f96 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/graphql/di.xml @@ -48,7 +48,7 @@ <plugin name="used_products_cache_graphql" type="Magento\ConfigurableProduct\Model\Plugin\Frontend\UsedProductsCache" /> </type> -<!-- <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface">--> -<!-- <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder_GraphQl" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/>--> -<!-- </type>--> + <type name="Magento\ConfigurableProduct\Model\ResourceModel\Attribute\OptionSelectBuilderInterface"> + <plugin name="Magento_ConfigurableProduct_Plugin_Model_ResourceModel_Attribute_InStockOptionSelectBuilder_GraphQl" type="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Attribute\InStockOptionSelectBuilder"/> + </type> </config> diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 83c1d03f132db..4b76a1ea5bd15 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -9,11 +9,13 @@ use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestBuilder; +use Magento\CatalogInventory\Api\StockStateInterface; /** * Add simple product to cart @@ -30,16 +32,24 @@ class AddSimpleProductToCart */ private $buyRequestBuilder; + /** + * @var StockStateInterface + */ + private $stockState; + /** * @param ProductRepositoryInterface $productRepository * @param BuyRequestBuilder $buyRequestBuilder + * @param StockStateInterface $stockState */ public function __construct( ProductRepositoryInterface $productRepository, - BuyRequestBuilder $buyRequestBuilder + BuyRequestBuilder $buyRequestBuilder, + StockStateInterface $stockState ) { $this->productRepository = $productRepository; $this->buyRequestBuilder = $buyRequestBuilder; + $this->stockState = $stockState; } /** @@ -53,15 +63,40 @@ public function __construct( public function execute(Quote $cart, array $cartItemData): void { $sku = $this->extractSku($cartItemData); - + $childSku = $this->extractChildSku($cartItemData); + $childSkuQty = $this->extractChildSkuQuantity($cartItemData); try { $product = $this->productRepository->get($sku, false, null, true); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku])); } + if ($childSku) { + $childProduct = $this->productRepository->get($childSku, false, null, true); + + $result = $this->stockState->checkQuoteItemQty( + $childProduct->getId(), $childSkuQty, $childSkuQty, $childSkuQty, $cart->getStoreId() + ); + + if ($result->getHasError() ) { + throw new GraphQlInputException( + __( + 'Could not add the product with SKU %sku to the shopping cart: %message', + ['sku' => $childSku, 'message' => __($result->getMessage())] + ) + ); + } + } + try { - $result = $cart->addProduct($product, $this->buyRequestBuilder->build($cartItemData)); + $buyRequest = $this->buyRequestBuilder->build($cartItemData); + // Some options might be disabled and not available + if (empty($buyRequest['super_attribute'])) { + throw new LocalizedException( + __('The product with SKU %sku is out of stock.', ['sku' => $childSku]) + ); + } + $result = $cart->addProduct($product, $this->buyRequestBuilder->build($cartItemData)); } catch (Exception $e) { throw new GraphQlInputException( __( @@ -99,4 +134,33 @@ private function extractSku(array $cartItemData): string } return (string)$cartItemData['data']['sku']; } + + /** + * Extract option child SKU from cart item data + * + * @param array $cartItemData + * @return string + * @throws GraphQlInputException + */ + private function extractChildSku(array $cartItemData): ?string + { + if (isset($cartItemData['data']['sku'])) { + return (string)$cartItemData['data']['sku']; + } + } + + /** + * Extract option child SKU from cart item data + * + * @param array $cartItemData + * @return string + * @throws GraphQlInputException + */ + private function extractChildSkuQuantity(array $cartItemData): ?string + { + if (empty($cartItemData['data']['quantity'])) { + throw new GraphQlInputException(__('Missed "quantity" in cart item data')); + } + return (string)$cartItemData['data']['quantity']; + } } From 2e4e937b00dfd3fb06a83b3dcd9b92a69edb3f95 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Thu, 22 Oct 2020 09:21:11 +0300 Subject: [PATCH 0895/1013] MC-36964: Create automated test for "[Elasticsearch] Search by multiple attributes" --- .../Block/Navigation/AbstractFiltersTest.php | 23 ++- .../Navigation/Category/FilterScopeTest.php | 3 +- .../Category/MultipleFiltersTest.php | 187 ++++++++++++++++++ .../Navigation/Search/MultipleFiltersTest.php | 58 ++++++ 4 files changed, 263 insertions(+), 8 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultipleFiltersTest.php create mode 100644 dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultipleFiltersTest.php diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php index df542dd622864..608e6c9b53cef 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/AbstractFiltersTest.php @@ -91,7 +91,10 @@ abstract protected function getLayerType(): string; * * @return string */ - abstract protected function getAttributeCode(): string; + protected function getAttributeCode(): string + { + return ''; + } /** * Tests getFilters method from navigation block on category page. @@ -108,7 +111,7 @@ protected function getCategoryFiltersAndAssert( array $expectation, string $categoryName ): void { - $this->updateAttribute($attributeData); + $this->updateAttribute($attributeData, $this->getAttributeCode()); $this->updateProducts($products, $this->getAttributeCode()); $this->clearInstanceAndReindexSearch(); $category = $this->loadCategory($categoryName, Store::DEFAULT_STORE_ID); @@ -141,7 +144,10 @@ protected function getCategoryActiveFiltersAndAssert( string $filterValue, int $productsCount ): void { - $this->updateAttribute(['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS]); + $this->updateAttribute( + ['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS], + $this->getAttributeCode() + ); $this->updateProducts($products, $this->getAttributeCode()); $this->clearInstanceAndReindexSearch(); $this->navigationBlock->getRequest()->setParams($this->getRequestParams($filterValue)); @@ -169,7 +175,7 @@ protected function getSearchFiltersAndAssert( array $attributeData, array $expectation ): void { - $this->updateAttribute($attributeData); + $this->updateAttribute($attributeData, $this->getAttributeCode()); $this->updateProducts($products, $this->getAttributeCode()); $this->clearInstanceAndReindexSearch(); $this->navigationBlock->getRequest()->setParams(['q' => $this->getSearchString()]); @@ -200,7 +206,8 @@ protected function getSearchActiveFiltersAndAssert( int $productsCount ): void { $this->updateAttribute( - ['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS, 'is_filterable_in_search' => 1] + ['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS, 'is_filterable_in_search' => 1], + $this->getAttributeCode() ); $this->updateProducts($products, $this->getAttributeCode()); $this->clearInstanceAndReindexSearch(); @@ -239,12 +246,14 @@ function (AbstractFilter $filter) use ($code) { * Updates attribute data. * * @param array $data + * @param string $attributeCode * @return void */ protected function updateAttribute( - array $data + array $data, + string $attributeCode ): void { - $attribute = $this->attributeRepository->get($this->getAttributeCode()); + $attribute = $this->attributeRepository->get($attributeCode); $attribute->setDataChanges(false); $attribute->addData($data); diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/FilterScopeTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/FilterScopeTest.php index 288128b04669c..266f8735fe48d 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/FilterScopeTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/FilterScopeTest.php @@ -63,7 +63,8 @@ public function testGetFilters(int $scope, array $products, array $expectation): [ 'is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS, 'is_global' => $scope, - ] + ], + $this->getAttributeCode() ); $this->updateProductsOnStore($products); $this->clearInstanceAndReindexSearch(); diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultipleFiltersTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultipleFiltersTest.php new file mode 100644 index 0000000000000..299be3f3c3e88 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/MultipleFiltersTest.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LayeredNavigation\Block\Navigation\Category; + +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\LayeredNavigation\Block\Navigation\AbstractFiltersTest; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; +use Magento\Store\Model\Store; + +/** + * Provides tests for multiple custom select filters in navigation block on category page. + * + * @magentoAppArea frontend + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ +class MultipleFiltersTest extends AbstractFiltersTest +{ + /** + * @magentoDataFixture Magento/Catalog/_files/product_dropdown_attribute.php + * @magentoDataFixture Magento/Catalog/_files/configurable_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @dataProvider getMultipleActiveFiltersDataProvider + * @param array $products + * @param array $filters + * @param array $expectedProducts + * @return void + */ + public function testGetMultipleActiveFilters( + array $products, + array $filters, + array $expectedProducts + ): void { + $this->updateAttributesAndProducts( + $products, + ['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS] + ); + $this->clearInstanceAndReindexSearch(); + $this->navigationBlock->getRequest()->setParams($this->getMultipleRequestParams($filters)); + $this->navigationBlock->getLayer()->setCurrentCategory( + $this->loadCategory('Category 999', Store::DEFAULT_STORE_ID) + ); + $this->navigationBlock->setLayout($this->layout); + $resultProducts = $this->getProductSkus($this->navigationBlock->getLayer()->getProductCollection()); + $this->assertEquals($expectedProducts, $resultProducts); + } + + /** + * @return array + */ + public function getMultipleActiveFiltersDataProvider(): array + { + return [ + 'without_filters' => [ + 'products_data' => [ + 'test_configurable' => [ + 'simple1000' => 'Option 1', + 'simple1001' => 'Option 2', + 'simple1002' => 'Option 2', + ], + 'dropdown_attribute' => [ + 'simple1000' => 'Option 1', + 'simple1001' => 'Option 2', + 'simple1002' => 'Option 3', + ], + ], + 'filters' => [], + 'expected_products' => ['simple1000', 'simple1001', 'simple1002'], + ], + 'applied_first_option_in_both_filters' => [ + 'products_data' => [ + 'test_configurable' => [ + 'simple1000' => 'Option 1', + 'simple1001' => 'Option 1', + 'simple1002' => 'Option 2', + ], + 'dropdown_attribute' => [ + 'simple1000' => 'Option 1', + 'simple1001' => 'Option 1', + 'simple1002' => 'Option 3', + ], + ], + 'filters' => ['test_configurable' => 'Option 1', 'dropdown_attribute' => 'Option 1'], + 'expected_products' => ['simple1000', 'simple1001'], + ], + 'applied_mixed_options_in_filters' => [ + 'products_data' => [ + 'test_configurable' => [ + 'simple1000' => 'Option 1', + 'simple1001' => 'Option 2', + 'simple1002' => 'Option 2', + ], + 'dropdown_attribute' => [ + 'simple1000' => 'Option 1', + 'simple1001' => 'Option 2', + 'simple1002' => 'Option 3', + ], + ], + 'filters' => ['test_configurable' => 'Option 2', 'dropdown_attribute' => 'Option 3'], + 'expected_products' => ['simple1002'], + ], + ]; + } + + /** + * @inheritdoc + */ + protected function getLayerType(): string + { + return Resolver::CATALOG_LAYER_CATEGORY; + } + + /** + * Updates products and product attribute. + * + * @param array $productsData + * @param array $attributesData + * @return void + */ + protected function updateAttributesAndProducts(array $productsData, array $attributesData): void + { + $products = []; + foreach ($productsData as $attributeCode => $data) { + $this->updateAttribute($attributesData, $attributeCode); + $attribute = $this->attributeRepository->get($attributeCode); + + foreach ($data as $productSku => $stringValue) { + if (empty($products[$productSku])) { + $product = $this->productRepository->get($productSku, false, Store::DEFAULT_STORE_ID, true); + $products[$productSku] = $product; + } else { + $product = $products[$productSku]; + } + $productValue = $attribute->usesSource() + ? $attribute->getSource()->getOptionId($stringValue) + : $stringValue; + $product->addData([$attribute->getAttributeCode() => $productValue]); + } + } + foreach ($products as $product) { + $this->productRepository->save($product); + } + } + + /** + * Returns array with multiple filters. + * + * @param array $filters + * @return array + */ + protected function getMultipleRequestParams(array $filters): array + { + $params = []; + foreach ($filters as $attributeCode => $filterValue) { + $attribute = $this->attributeRepository->get($attributeCode); + $filterValue = $attribute->usesSource() + ? $attribute->getSource()->getOptionId($filterValue) + : $filterValue; + + $params[$attributeCode] = $filterValue; + } + + return $params; + } + + /** + * Returns list of product skus from given collection. + * + * @param Collection $getProductCollection + * @return array + */ + protected function getProductSkus(Collection $getProductCollection): array + { + $skus = []; + foreach ($getProductCollection as $product) { + $skus[] = $product->getSku(); + } + + return $skus; + } +} diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultipleFiltersTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultipleFiltersTest.php new file mode 100644 index 0000000000000..d0943b333e1ac --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/MultipleFiltersTest.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LayeredNavigation\Block\Navigation\Search; + +use Magento\Catalog\Model\Layer\Resolver; +use Magento\LayeredNavigation\Block\Navigation\Category\MultipleFiltersTest as CategoryFilterTest; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; + +/** + * Provides tests for multiple custom select filters in navigation block on search page. + * + * @magentoAppArea frontend + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + */ +class MultipleFiltersTest extends CategoryFilterTest +{ + /** + * @magentoDataFixture Magento/Catalog/_files/product_dropdown_attribute.php + * @magentoDataFixture Magento/Catalog/_files/configurable_attribute.php + * @magentoDataFixture Magento/Catalog/_files/category_with_three_products.php + * @dataProvider getMultipleActiveFiltersDataProvider + * @param array $products + * @param array $filters + * @param array $expectedProducts + * @return void + */ + public function testGetMultipleActiveFilters( + array $products, + array $filters, + array $expectedProducts + ): void { + $this->updateAttributesAndProducts( + $products, + ['is_filterable' => AbstractFilter::ATTRIBUTE_OPTIONS_ONLY_WITH_RESULTS, 'is_filterable_in_search' => 1] + ); + $this->clearInstanceAndReindexSearch(); + $this->navigationBlock->getRequest()->setParams( + array_merge($this->getMultipleRequestParams($filters), ['q' => $this->getSearchString()]) + ); + $this->navigationBlock->setLayout($this->layout); + $resultProducts = $this->getProductSkus($this->navigationBlock->getLayer()->getProductCollection()); + $this->assertEquals($expectedProducts, $resultProducts); + } + + /** + * @inheritdoc + */ + protected function getLayerType(): string + { + return Resolver::CATALOG_LAYER_SEARCH; + } +} From 59b7ba8a5845ea8964d62e1ed9c28d18fc5925a9 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 22 Oct 2020 10:56:30 +0300 Subject: [PATCH 0896/1013] MC-38589: [MFTF] AdminEnhancedMediaGalleryVerifyAssetFilterTest failed because of bad design --- .../TinyMCESection/MediaGallerySection.xml | 1 + ...ediaGalleryDeletedAllImagesActionGroup.xml | 30 ++++++++ ...ediaGalleryClickAddSelectedActionGroup.xml | 6 +- ...ediaGalleryFromTinyMce4IconActionGroup.xml | 4 +- .../Test/Mftf/Helper/MediaGalleryUiHelper.php | 77 +++++++++++++++++++ ...nEnhancedMediaGalleryMassActionSection.xml | 1 + .../Section/AdminMediaGalleryGridSection.xml | 2 +- .../AdminMediaGalleryMessagesSection.xml | 13 ++++ ...ancedMediaGalleryVerifyAssetFilterTest.xml | 7 +- ...cedMediaGalleryVerifyFilterByAssetTest.xml | 75 ++++++++++++++++++ 10 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml 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 725d050554f2d..212035fbc575a 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml @@ -36,5 +36,6 @@ <element name="checkIfWysiwygArrowExpand" type="button" selector="//li[@id='d3lzaXd5Zw--' and contains(@class,'jstree-closed')]"/> <element name="confirmDelete" type="button" selector=".action-primary.action-accept"/> <element name="imageBlockByName" type="block" selector="//div[@data-row='file'][contains(., '{{imageName}}')]" parameterized="true"/> + <element name="insertEditImageModalWindow" type="block" selector=".mce-floatpanel.mce-window[aria-label='Insert/edit image']"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml new file mode 100644 index 0000000000000..f77521879c8ea --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.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="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup"> + <annotations> + <description>Open Media Gallery page and delete all images</description> + </annotations> + + <amOnPage url="{{AdminStandaloneMediaGalleryPage.url}}" stepKey="openMediaGalleryPage"/> + <!-- 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\MediaGalleryUi\Test\Mftf\Helper\MediaGalleryUiHelper" method="deleteAllImagesUsingMassAction" stepKey="deleteAllImagesUsingMassAction"> + <argument name="emptyRow">{{AdminMediaGalleryGridSection.noDataMessage}}</argument> + <argument name="deleteImagesButton">{{AdminEnhancedMediaGalleryMassActionSection.deleteImages}}</argument> + <argument name="checkImage">{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckboxAll}}</argument> + <argument name="deleteSelectedButton">{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}</argument> + <argument name="modalAcceptButton">{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}</argument> + <argument name="successMessageContainer">{{AdminMediaGalleryMessagesSection.success}}</argument> + <argument name="successMessage">been successfully deleted</argument> + </helper> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml index 45ab4dc4538e0..f575e346a8ca0 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml @@ -10,8 +10,8 @@ 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"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="clickAddSelected"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{MediaGallerySection.insertEditImageModalWindow}}" stepKey="waitForInsertEditImageWindow"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml index 3143b4ff24fb4..e4fb6aec5c152 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml @@ -11,8 +11,8 @@ <annotations> <description>Opens Enhanced MediaGallery from category page by tyniMce4 image icon</description> </annotations> - - <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="clickExpandContent"/> + + <conditionalClick selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.uploadButton}}" visible="false" stepKey="clickExpandContent"/> <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> <waitForPageLoad stepKey="waitForPageLoad" /> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php b/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php new file mode 100644 index 0000000000000..4059a8460bb51 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\Remote\RemoteWebElement; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for MediaGalleryUi module. + */ +class MediaGalleryUiHelper extends Helper +{ + /** + * Delete all images using mass action. + * + * @param string $emptyRow + * @param string $deleteImagesButton + * @param string $checkImage + * @param string $deleteSelectedButton + * @param string $modalAcceptButton + * @param string $successMessageContainer + * @param string $successMessage + * + * @return void + */ + public function deleteAllImagesUsingMassAction( + string $emptyRow, + string $deleteImagesButton, + string $checkImage, + string $deleteSelectedButton, + string $modalAcceptButton, + 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($emptyRow)); + while (empty($rows)) { + $magentoWebDriver->click($deleteImagesButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($deleteSelectedButton, 10); + + // Check all images + /** @var RemoteWebElement[] $images */ + $imagesCheckboxes = $webDriver->findElements(WebDriverBy::cssSelector($checkImage)); + /** @var RemoteWebElement $image */ + foreach ($imagesCheckboxes as $imageCheckbox) { + $imageCheckbox->click(); + } + + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteSelectedButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForElementVisible($successMessageContainer, 10); + $magentoWebDriver->see($successMessage, $successMessageContainer); + + $rows = $webDriver->findElements(WebDriverBy::cssSelector($emptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml index 07f2dc23530e1..9018ccb4ddd69 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml @@ -13,5 +13,6 @@ <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"/> + <element name="massActionCheckboxAll" type="checkbox" selector="[data-id='media-gallery-masonry-grid'] .mediagallery-massaction-checkbox input[type='checkbox']"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml index f35a32b6d3a37..08be2e61a9d14 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryGridSection.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="AdminMediaGalleryGridSection"> - <element name="noDataMessage" type="text" selector="div.no-data-message-container"/> + <element name="noDataMessage" type="text" selector="[data-id='media-gallery-masonry-grid'] .no-data-message-container"/> <element name="nthImageInGrid" type="text" selector="div[class='masonry-image-column'][data-repeat-index='{{row}}'] img" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml new file mode 100644 index 0000000000000..42a936b6c0ebc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.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="AdminMediaGalleryMessagesSection"> + <element name="success" type="text" selector=".media-gallery-container ul.messages div.message.message-success span"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml index 9a08f7cd0bb9c..847cc398d489d 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminEnhancedMediaGalleryVerifyAssetFilterTest"> + <test name="AdminEnhancedMediaGalleryVerifyAssetFilterTest" deprecated="Use AdminEnhancedMediaGalleryVerifyFilterByAssetTest instead"> <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"/> + <title value="DEPRECATED. 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"/> + <skip> + <issueId value="DEPRECATED">Use AdminEnhancedMediaGalleryVerifyFilterByAssetTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml new file mode 100644 index 0000000000000..24259edbfe669 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AdminEnhancedMediaGalleryVerifyFilterByAssetTest"> + <annotations> + <features value="MediaGallery"/> + <stories value="Story 58: User sees entities where asset is used in"/> + <title value="User sees entities where asset is used in"/> + <description value="User sees entities where asset is used in"/> + <severity value="CRITICAL"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951024"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1292"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImages"/> + </before> + + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImages"/> + + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="GoToAdminCategoryPageByIdActionGroup" stepKey="openCategoryPage"> + <argument name="id" value="$category.id$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <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="AdminSaveCategoryFormActionGroup" stepKey="saveCategory"/> + + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultViewAgain"/> + <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="AssertAdminCategoryGridPageNumberOfRecordsActionGroup" stepKey="assertOneRecordInGrid"> + <argument name="numberOfRecords" value="1 records found"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageImageColumnActionGroup" stepKey="assertCategoryGridPageImageColumn"/> + <actionGroup ref="AssertAdminCategoryGridPageDetailsActionGroup" stepKey="assertCategoryInGrid"> + <argument name="category" value="$category$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + </test> +</tests> From 64322e6e61de7c2733b98ef9e888de9727557987 Mon Sep 17 00:00:00 2001 From: IvanPletnyov <ivan.pletnyov@transoftgroup.com> Date: Thu, 22 Oct 2020 12:16:16 +0300 Subject: [PATCH 0897/1013] MC-37544: Create automated test for "Category Schedule Design Update not available if CatalogStaging installed" --- .../AdminCategoryOpenDesignSectionActionGroup.xml | 15 +++++++++++++++ ...OpenScheduleDesignUpdateSectionActionGroup.xml | 15 +++++++++++++++ .../AdminCategoryScheduleDesignUpdateSection.xml | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenDesignSectionActionGroup.xml create mode 100644 app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenScheduleDesignUpdateSectionActionGroup.xml diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenDesignSectionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenDesignSectionActionGroup.xml new file mode 100644 index 0000000000000..7fd42fd4925e1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenDesignSectionActionGroup.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="AdminCategoryOpenDesignSectionActionGroup"> + <waitForElementVisible selector="{{CategoryDesignSection.DesignTab}}" stepKey="waitForDesignSection"/> + <conditionalClick selector="{{CategoryDesignSection.DesignTab}}" dependentSelector="{{CategoryDesignSection.LayoutDropdown}}" visible="false" stepKey="openDesignSection"/> + <waitForPageLoad stepKey="waitForDesignSectionLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenScheduleDesignUpdateSectionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenScheduleDesignUpdateSectionActionGroup.xml new file mode 100644 index 0000000000000..e0606d159e357 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCategoryOpenScheduleDesignUpdateSectionActionGroup.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="AdminCategoryOpenScheduleDesignUpdateSectionActionGroup"> + <waitForElementVisible selector="{{AdminCategoryScheduleDesignUpdateSection.sectionHeader}}" stepKey="waitForScheduleDesignUpdateSection"/> + <conditionalClick selector="{{AdminCategoryScheduleDesignUpdateSection.sectionHeader}}" dependentSelector="{{AdminCategoryScheduleDesignUpdateSection.customDesignFrom}}" visible="false" stepKey="openScheduleDesignUpdateSection"/> + <waitForPageLoad stepKey="waitForScheduleDesignUpdateSectionLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml index fb231cf56b200..9d9a7f204544d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminCategoryScheduleDesignUpdateSection.xml @@ -10,6 +10,6 @@ <section name="AdminCategoryScheduleDesignUpdateSection"> <element name="sectionHeader" type="button" selector="div[data-index='schedule_design_update'] .fieldset-wrapper-title" timeout="30"/> <element name="sectionBody" type="text" selector="div[data-index='schedule_design_update'] .admin__fieldset-wrapper-content"/> - <element name="customDesignFrom" type="input" selector="input[name='custom_design_from']"/> + <element name="customDesignFrom" type="input" selector="div[data-index='schedule_design_update'] input[name='custom_design_from']"/> </section> </sections> From 8a091cc5ac9f654751fa7495abae33b26ea75e6d Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Thu, 22 Oct 2020 12:18:33 +0300 Subject: [PATCH 0898/1013] MC-34156: Cannot save product in store view scope without Magento_Catalog::edit_product_design ACL --- .../Initialization/Helper/AttributeFilter.php | 2 +- .../Model/Product/AuthorizationTest.php | 170 ++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php index 49165c85f85d7..1d6939acacfd0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php @@ -80,7 +80,7 @@ private function prepareDefaultData(array $attributeList, string $attributeCode, // For non-numeric types set the attributeValue to 'false' to trigger their removal from the db if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { $attribute->setIsRequired(false); - $productData[$attributeCode] = false; + $productData[$attributeCode] = $attribute->getDefaultValue() ?: false; } else { $productData[$attributeCode] = null; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php new file mode 100644 index 0000000000000..80de1b3a19270 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php @@ -0,0 +1,170 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Laminas\Stdlib\Parameters; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Verify additional authorization for product operations + */ +class AuthorizationTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Helper + */ + private $initializationHelper; + + /** + * @var HttpRequest + */ + private $request; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheridoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->initializationHelper = $this->objectManager->get(Helper::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->request = $this->objectManager->get(HttpRequest::class); + } + + /** + * Verify AuthorizedSavingOf + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @param array $data + * + * @dataProvider postRequestData + */ + public function testAuthorizedSavingOf(array $data): void + { + $this->request->setPost(new Parameters($data)); + + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + + $product = $this->initializationHelper->initialize($product); + $this->assertEquals('simple_new', $product->getName()); + $this->assertEquals( + 'container2', + $product->getCustomAttribute('options_container')->getValue() + ); + } + + /** + * @return array + */ + public function postRequestData(): array + { + return [ + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'custom_design' => '', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_layout_update' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout_update_file' => '', + ], + 'use_default' => [ + 'custom_design' => '1', + 'page_layout' => '1', + 'options_container' => '1', + 'custom_layout' => '1', + 'custom_design_from' => '1', + 'custom_design_to' => '1', + 'custom_layout_update_file' => '1', + ], + ] + ], + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout' => '', + 'custom_layout_update_file' => '__no_update__', + ], + 'use_default' => null, + ] + ], + ]; + } + + /** + * Verify AuthorizedSavingOf when change design attributes + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @param array $data + * + * @dataProvider postRequestDataException + * @throws AuthorizationException + */ + public function testAuthorizedSavingOfWithException(array $data): void + { + $this->expectException(AuthorizationException::class); + $this->expectErrorMessage('Not allowed to edit the product\'s design attributes'); + $this->request->setPost(new Parameters($data)); + + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + + $this->initializationHelper->initialize($product); + } + + /** + * @return array + */ + public function postRequestDataException(): array + { + return [ + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'page_layout' => '1column', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout' => '', + 'custom_layout_update_file' => '__no_update__', + ], + 'use_default' => null, + ], + ], + ]; + } +} From e113cd980c0567ded9e1db5cae5e2ac397ffd4dc Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Thu, 22 Oct 2020 15:10:25 +0300 Subject: [PATCH 0899/1013] MC-38433: CSV import ignores "dropdown" and "textarea" for additional attributes --- .../Import/Product/Type/AbstractType.php | 1 + .../Import/Product/Type/AbstractTypeTest.php | 162 +++++++++++------- 2 files changed, 98 insertions(+), 65 deletions(-) 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 6571b16c87565..bd17cfd2cd7f1 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -533,6 +533,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe if ($attrParams['is_static']) { continue; } + $attrCode = mb_strtolower($attrCode); if (isset($rowData[$attrCode]) && strlen(trim($rowData[$attrCode]))) { if (in_array($attrParams['type'], ['select', 'boolean'])) { $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index 08915fb31a8aa..9453075f99e7c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -6,6 +6,7 @@ */ namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType as AbstractType; @@ -13,6 +14,7 @@ use Magento\Eav\Model\Entity\Attribute; use Magento\Eav\Model\Entity\Attribute\Set; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory as AttributeSetCollectionFactory; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; @@ -68,12 +70,12 @@ protected function setUp(): void { $this->entityModel = $this->createMock(Product::class); $attrSetColFactory = $this->createPartialMock( - \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class, + AttributeSetCollectionFactory::class, ['create'] ); $attrSetCollection = $this->createMock(Collection::class); $attrColFactory = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class, + AttributeCollectionFactory::class, ['create'] ); $attributeSet = $this->createMock(Set::class); @@ -100,14 +102,22 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMock(); - $attribute->expects($this->any())->method('getIsVisible')->willReturn(true); - $attribute->expects($this->any())->method('getIsGlobal')->willReturn(true); - $attribute->expects($this->any())->method('getIsRequired')->willReturn(true); - $attribute->expects($this->any())->method('getIsUnique')->willReturn(true); - $attribute->expects($this->any())->method('getFrontendLabel')->willReturn('frontend_label'); - $attribute->expects($this->any())->method('getApplyTo')->willReturn(['simple']); - $attribute->expects($this->any())->method('getDefaultValue')->willReturn('default_value'); - $attribute->expects($this->any())->method('usesSource')->willReturn(true); + $attribute->method('getIsVisible') + ->willReturn(true); + $attribute->method('getIsGlobal') + ->willReturn(true); + $attribute->method('getIsRequired') + ->willReturn(true); + $attribute->method('getIsUnique') + ->willReturn(true); + $attribute->method('getFrontendLabel') + ->willReturn('frontend_label'); + $attribute->method('getApplyTo') + ->willReturn(['simple']); + $attribute->method('getDefaultValue') + ->willReturn('default_value'); + $attribute->method('usesSource') + ->willReturn(true); $entityAttributes = [ [ @@ -123,38 +133,54 @@ protected function setUp(): void $attribute2 = clone $attribute; $attribute3 = clone $attribute; - $attribute1->expects($this->any())->method('getId')->willReturn('1'); - $attribute1->expects($this->any())->method('getAttributeCode')->willReturn('attr_code'); - $attribute1->expects($this->any())->method('getFrontendInput')->willReturn('multiselect'); - $attribute1->expects($this->any())->method('isStatic')->willReturn(true); - - $attribute2->expects($this->any())->method('getId')->willReturn('2'); - $attribute2->expects($this->any())->method('getAttributeCode')->willReturn('boolean_attribute'); - $attribute2->expects($this->any())->method('getFrontendInput')->willReturn('boolean'); - $attribute2->expects($this->any())->method('isStatic')->willReturn(false); - - $attribute3->expects($this->any())->method('getId')->willReturn('3'); - $attribute3->expects($this->any())->method('getAttributeCode')->willReturn('text_attribute'); - $attribute3->expects($this->any())->method('getFrontendInput')->willReturn('text'); - $attribute3->expects($this->any())->method('isStatic')->willReturn(false); - - $this->entityModel->expects($this->any())->method('getEntityTypeId')->willReturn(3); - $this->entityModel->expects($this->any())->method('getAttributeOptions')->willReturnOnConsecutiveCalls( - ['option1', 'option2'], - ['yes' => 1, 'no' => 0] - ); - $attrSetColFactory->expects($this->any())->method('create')->willReturn($attrSetCollection); - $attrSetCollection->expects($this->any())->method('setEntityTypeFilter')->willReturn([$attributeSet]); - $attrColFactory->expects($this->any())->method('create')->willReturn($attrCollection); - $attrCollection->expects($this->any()) - ->method('setAttributeSetFilter') + $attribute1->method('getId') + ->willReturn('1'); + $attribute1->method('getAttributeCode') + ->willReturn('attr_code'); + $attribute1->method('getFrontendInput') + ->willReturn('multiselect'); + $attribute1->method('isStatic') + ->willReturn(true); + + $attribute2->method('getId') + ->willReturn('2'); + $attribute2->method('getAttributeCode') + ->willReturn('boolean_attribute'); + $attribute2->method('getFrontendInput') + ->willReturn('boolean'); + $attribute2->method('isStatic') + ->willReturn(false); + + $attribute3->method('getId') + ->willReturn('3'); + $attribute3->method('getAttributeCode') + ->willReturn('Text_attribute'); + $attribute3->method('getFrontendInput') + ->willReturn('text'); + $attribute3->method('isStatic') + ->willReturn(false); + + $this->entityModel->method('getEntityTypeId') + ->willReturn(3); + $this->entityModel->method('getAttributeOptions') + ->willReturnOnConsecutiveCalls( + ['option1', 'option2'], + ['yes' => 1, 'no' => 0] + ); + $attrSetColFactory->method('create') + ->willReturn($attrSetCollection); + $attrSetCollection->method('setEntityTypeFilter') + ->willReturn([$attributeSet]); + $attrColFactory->method('create') + ->willReturn($attrCollection); + $attrCollection->method('setAttributeSetFilter') ->willReturn([$attribute1, $attribute2, $attribute3]); - $attributeSet->expects($this->any())->method('getId')->willReturn(1); - $attributeSet->expects($this->any())->method('getAttributeSetName')->willReturn('attribute_set_name'); + $attributeSet->method('getId') + ->willReturn(1); + $attributeSet->method('getAttributeSetName') + ->willReturn('attribute_set_name'); - $attrCollection - ->expects($this->any()) - ->method('addFieldToFilter') + $attrCollection->method('addFieldToFilter') ->with( ['main_table.attribute_id', 'main_table.attribute_code'], [ @@ -193,19 +219,26 @@ protected function setUp(): void 'getConnection', ] ); - $this->select->expects($this->any())->method('from')->willReturnSelf(); - $this->select->expects($this->any())->method('where')->willReturnSelf(); - $this->select->expects($this->any())->method('joinLeft')->willReturnSelf(); - $this->connection->expects($this->any())->method('select')->willReturn($this->select); + $this->select->method('from') + ->willReturnSelf(); + $this->select->method('where') + ->willReturnSelf(); + $this->select->method('joinLeft') + ->willReturnSelf(); + $this->connection->method('select') + ->willReturn($this->select); $connection = $this->createMock(Mysql::class); - $connection->expects($this->any())->method('quoteInto')->willReturn('query'); - $this->select->expects($this->any())->method('getConnection')->willReturn($connection); - $this->connection->expects($this->any())->method('insertOnDuplicate')->willReturnSelf(); - $this->connection->expects($this->any())->method('delete')->willReturnSelf(); - $this->connection->expects($this->any())->method('quoteInto')->willReturn(''); - $this->connection - ->expects($this->any()) - ->method('fetchAll') + $connection->method('quoteInto') + ->willReturn('query'); + $this->select->method('getConnection') + ->willReturn($connection); + $this->connection->method('insertOnDuplicate') + ->willReturnSelf(); + $this->connection->method('delete') + ->willReturnSelf(); + $this->connection->method('quoteInto') + ->willReturn(''); + $this->connection->method('fetchAll') ->willReturn($entityAttributes); $this->resource = $this->createPartialMock( @@ -215,12 +248,10 @@ protected function setUp(): void 'getTableName', ] ); - $this->resource->expects($this->any())->method('getConnection')->willReturn( - $this->connection - ); - $this->resource->expects($this->any())->method('getTableName')->willReturn( - 'tableName' - ); + $this->resource->method('getConnection') + ->willReturn($this->connection); + $this->resource->method('getTableName') + ->willReturn('tableName'); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->simpleType = $this->objectManagerHelper->getObject( @@ -233,9 +264,7 @@ protected function setUp(): void ] ); - $this->abstractType = $this->getMockBuilder( - \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class - ) + $this->abstractType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); } @@ -277,8 +306,10 @@ public function testIsRowValidSuccess() { $rowData = ['_attribute_set' => 'attribute_set_name']; $rowNum = 1; - $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(null); - $this->entityModel->expects($this->never())->method('addRowError'); + $this->entityModel->method('getRowScope') + ->willReturn(null); + $this->entityModel->expects($this->never()) + ->method('addRowError'); $this->setPropertyValue( $this->simpleType, '_attributes', @@ -296,8 +327,9 @@ public function testIsRowValidError() 'sku' => 'sku' ]; $rowNum = 1; - $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(1); - $this->entityModel->expects($this->once())->method('addRowError') + $this->entityModel->method('getRowScope') + ->willReturn(1); + $this->entityModel->method('addRowError') ->with( RowValidatorInterface::ERROR_VALUE_IS_REQUIRED, 1, From f43937b0081af90943aeff631447da654e086be4 Mon Sep 17 00:00:00 2001 From: engcom-Echo <engcom-vendorworker-echo@adobe.com> Date: Thu, 22 Oct 2020 15:35:25 +0300 Subject: [PATCH 0900/1013] MC-34156: Cannot save product in store view scope without Magento_Catalog::edit_product_design ACL --- app/code/Magento/Catalog/Model/Product/Authorization.php | 2 +- .../Magento/Catalog/Model/Product/AuthorizationTest.php | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Catalog/Model/Product/Authorization.php b/app/code/Magento/Catalog/Model/Product/Authorization.php index b8aa8f70ba70f..4022eb34e65e3 100644 --- a/app/code/Magento/Catalog/Model/Product/Authorization.php +++ b/app/code/Magento/Catalog/Model/Product/Authorization.php @@ -159,7 +159,7 @@ public function authorizeSavingOf(ProductInterface $product): void if (!$savedProduct->getSku()) { throw NoSuchEntityException::singleField('id', $product->getId()); } - $oldData = $product->getOrigData(); + $oldData = $savedProduct->getData(); } } if ($this->hasProductChanged($product, $oldData)) { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php index 80de1b3a19270..e2b80a975502f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php @@ -19,6 +19,8 @@ /** * Verify additional authorization for product operations + * + * @magentoAppArea adminhtml */ class AuthorizationTest extends TestCase { @@ -43,7 +45,7 @@ class AuthorizationTest extends TestCase private $productRepository; /** - * @inheridoc + * @inheritdoc */ protected function setUp(): void { @@ -103,7 +105,7 @@ public function postRequestData(): array 'custom_design_to' => '1', 'custom_layout_update_file' => '1', ], - ] + ], ], [ [ @@ -118,7 +120,7 @@ public function postRequestData(): array 'custom_layout_update_file' => '__no_update__', ], 'use_default' => null, - ] + ], ], ]; } @@ -130,7 +132,6 @@ public function postRequestData(): array * @param array $data * * @dataProvider postRequestDataException - * @throws AuthorizationException */ public function testAuthorizedSavingOfWithException(array $data): void { From 078919e3771fa662caa8c7d8378e5fd99b131b57 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 22 Oct 2020 17:05:35 +0300 Subject: [PATCH 0901/1013] MC-38589: [MFTF] AdminEnhancedMediaGalleryVerifyAssetFilterTest failed because of bad design --- .../Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml index 24259edbfe669..90ae7a5f10368 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml @@ -9,7 +9,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminEnhancedMediaGalleryVerifyFilterByAssetTest"> <annotations> - <features value="MediaGallery"/> + <features value="MediaGalleryUi"/> <stories value="Story 58: User sees entities where asset is used in"/> <title value="User sees entities where asset is used in"/> <description value="User sees entities where asset is used in"/> From 2a68e712b11d201e00bc4357a8c6e89a43151a4b Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 22 Oct 2020 12:45:34 -0500 Subject: [PATCH 0902/1013] 29251 test web-api test fix --- .../BuyRequest/SuperAttributeDataProvider.php | 39 +++++++++- .../Model/Cart/AddSimpleProductToCart.php | 71 ++----------------- 2 files changed, 41 insertions(+), 69 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php index 4a613254ddf84..d9c8ade39f621 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php @@ -8,6 +8,8 @@ namespace Magento\ConfigurableProductGraphQl\Model\Cart\BuyRequest; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\StockStateInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; @@ -16,6 +18,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Quote\Model\Quote; /** * DataProvider for building super attribute options in buy requests @@ -42,22 +45,30 @@ class SuperAttributeDataProvider implements BuyRequestDataProviderInterface */ private $metadataPool; + /** + * @var StockStateInterface + */ + private $stockState; + /** * @param ArrayManager $arrayManager * @param ProductRepositoryInterface $productRepository * @param OptionCollection $optionCollection * @param MetadataPool $metadataPool + * @param StockStateInterface $stockState */ public function __construct( ArrayManager $arrayManager, ProductRepositoryInterface $productRepository, OptionCollection $optionCollection, - MetadataPool $metadataPool + MetadataPool $metadataPool, + StockStateInterface $stockState ) { $this->arrayManager = $arrayManager; $this->productRepository = $productRepository; $this->optionCollection = $optionCollection; $this->metadataPool = $metadataPool; + $this->stockState = $stockState; } /** @@ -65,18 +76,36 @@ public function __construct( */ public function execute(array $cartItemData): array { + $parentSku = $this->arrayManager->get('parent_sku', $cartItemData); if ($parentSku === null) { return []; } $sku = $this->arrayManager->get('data/sku', $cartItemData); - + $qty = $this->arrayManager->get('data/quantity', $cartItemData); + $cart = $this->arrayManager->get('model', $cartItemData); + if (!$cart instanceof Quote) { + throw new LocalizedException(__('"model" value should be specified')); + } try { $parentProduct = $this->productRepository->get($parentSku); $product = $this->productRepository->get($sku); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('Could not find specified product.')); } + + + // Child stock check has to be performed a catalog by default would not show/check it + $childProduct = $this->productRepository->get($sku, false, null, true); + + $result = $this->stockState->checkQuoteItemQty($childProduct->getId(), $qty, $qty, $qty, $cart->getStoreId()); + + if ($result->getHasError() ) { + throw new LocalizedException( + __($result->getMessage()) + ); + } + $configurableProductLinks = $parentProduct->getExtensionAttributes()->getConfigurableProductLinks(); if (!in_array($product->getId(), $configurableProductLinks)) { throw new GraphQlInputException(__('Could not find specified product.')); @@ -95,6 +124,12 @@ public function execute(array $cartItemData): array } } } + // Some options might be disabled and/or available when parent and child sku are provided + if (empty($superAttributesData)) { + throw new LocalizedException( + __('The product with SKU %sku is out of stock.', ['sku' => $parentSku]) + ); + } return ['super_attribute' => $superAttributesData]; } } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 4b76a1ea5bd15..12eebe9b926e8 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -9,13 +9,11 @@ use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestBuilder; -use Magento\CatalogInventory\Api\StockStateInterface; /** * Add simple product to cart @@ -32,24 +30,16 @@ class AddSimpleProductToCart */ private $buyRequestBuilder; - /** - * @var StockStateInterface - */ - private $stockState; - /** * @param ProductRepositoryInterface $productRepository * @param BuyRequestBuilder $buyRequestBuilder - * @param StockStateInterface $stockState */ public function __construct( ProductRepositoryInterface $productRepository, - BuyRequestBuilder $buyRequestBuilder, - StockStateInterface $stockState + BuyRequestBuilder $buyRequestBuilder ) { $this->productRepository = $productRepository; $this->buyRequestBuilder = $buyRequestBuilder; - $this->stockState = $stockState; } /** @@ -62,41 +52,17 @@ public function __construct( */ public function execute(Quote $cart, array $cartItemData): void { + $cartItemData['model'] = $cart; $sku = $this->extractSku($cartItemData); - $childSku = $this->extractChildSku($cartItemData); - $childSkuQty = $this->extractChildSkuQuantity($cartItemData); + try { $product = $this->productRepository->get($sku, false, null, true); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku])); } - if ($childSku) { - $childProduct = $this->productRepository->get($childSku, false, null, true); - - $result = $this->stockState->checkQuoteItemQty( - $childProduct->getId(), $childSkuQty, $childSkuQty, $childSkuQty, $cart->getStoreId() - ); - - if ($result->getHasError() ) { - throw new GraphQlInputException( - __( - 'Could not add the product with SKU %sku to the shopping cart: %message', - ['sku' => $childSku, 'message' => __($result->getMessage())] - ) - ); - } - } - try { - $buyRequest = $this->buyRequestBuilder->build($cartItemData); - // Some options might be disabled and not available - if (empty($buyRequest['super_attribute'])) { - throw new LocalizedException( - __('The product with SKU %sku is out of stock.', ['sku' => $childSku]) - ); - } - $result = $cart->addProduct($product, $this->buyRequestBuilder->build($cartItemData)); + $result = $cart->addProduct($product, $this->buyRequestBuilder->build($cartItemData)); } catch (Exception $e) { throw new GraphQlInputException( __( @@ -134,33 +100,4 @@ private function extractSku(array $cartItemData): string } return (string)$cartItemData['data']['sku']; } - - /** - * Extract option child SKU from cart item data - * - * @param array $cartItemData - * @return string - * @throws GraphQlInputException - */ - private function extractChildSku(array $cartItemData): ?string - { - if (isset($cartItemData['data']['sku'])) { - return (string)$cartItemData['data']['sku']; - } - } - - /** - * Extract option child SKU from cart item data - * - * @param array $cartItemData - * @return string - * @throws GraphQlInputException - */ - private function extractChildSkuQuantity(array $cartItemData): ?string - { - if (empty($cartItemData['data']['quantity'])) { - throw new GraphQlInputException(__('Missed "quantity" in cart item data')); - } - return (string)$cartItemData['data']['quantity']; - } } From 640cad53009b291334234ccd61ab79f256b43da2 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <duhon@users.noreply.github.com> Date: Thu, 22 Oct 2020 16:21:34 -0500 Subject: [PATCH 0903/1013] [performance] MC-37459: Support by Magento Catalog (#6223) * MC-37459: Support by Magento Catalog --- .htaccess | 400 +-------------- .../Test/Unit/Model/LinkProviderTest.php | 2 +- app/code/Magento/AwsS3/Driver/AwsS3.php | 89 +++- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 30 ++ .../Magento/Backup/Model/Fs/Collection.php | 10 +- .../Captcha/Test/Unit/Helper/DataTest.php | 4 +- .../Captcha/Test/Unit/Model/DefaultTest.php | 6 +- app/code/Magento/Catalog/Helper/Image.php | 27 +- .../Model/Config/CatalogMediaConfig.php | 50 ++ .../Source/Web/CatalogMediaUrlFormat.php | 33 ++ .../Magento/Catalog/Model/Product/Image.php | 21 +- .../Catalog/Model/Product/Media/Config.php | 5 +- .../Magento/Catalog/Model/Template/Filter.php | 2 +- .../Catalog/Model/View/Asset/Image.php | 91 +++- .../Observer/ImageResizeAfterProductSave.php | 21 +- .../Helper/Form/Gallery/ContentTest.php | 454 ------------------ .../Catalog/Test/Unit/Helper/ImageTest.php | 13 +- .../Category/Attribute/Backend/ImageTest.php | 26 +- .../Test/Unit/Model/Category/ImageTest.php | 12 +- .../Test/Unit/Model/ImageUploaderTest.php | 168 ------- .../Model/View/Asset/Image/ContextTest.php | 79 --- .../Test/Unit/Model/View/Asset/ImageTest.php | 213 -------- .../Unit/Model/View/Asset/PlaceholderTest.php | 4 +- .../Product/Listing/Collector/ImageTest.php | 13 +- .../Product/Listing/Collector/Image.php | 16 +- .../Magento/Catalog/etc/adminhtml/system.xml | 7 + app/code/Magento/Catalog/etc/config.xml | 8 +- .../Wysiwyg/Images/Storage/Collection.php | 2 +- .../Test/Unit/Model/Wysiwyg/ConfigTest.php | 8 +- .../Model/Config/Backend/Admin/Robots.php | 8 +- .../Magento/Customer/Model/FileProcessor.php | 3 +- .../Test/Unit/Model/FileProcessorTest.php | 59 +++ .../Unit/Model/Template/Css/ProcessorTest.php | 2 +- .../Test/Unit/Model/UploadImageTest.php | 2 +- .../Unit/Block/Product/View/GalleryTest.php | 2 +- .../Product/Gallery/RetrieveImageTest.php | 15 +- .../RemoteStorage/Plugin/MediaStorage.php | 80 +++ app/code/Magento/RemoteStorage/composer.json | 3 +- app/code/Magento/RemoteStorage/etc/di.xml | 3 + app/code/Magento/RemoteStorage/etc/module.xml | 1 + .../Unit/Model/ItemProvider/ProductTest.php | 2 +- .../Sitemap/Test/Unit/Model/SitemapTest.php | 2 +- .../Test/Unit/Model/_files/sitemap-1-4.xml | 6 +- .../Test/Unit/Model/_files/sitemap-single.xml | 6 +- .../Model/Service/StoreConfigManagerTest.php | 6 +- app/code/Magento/Swatches/Helper/Data.php | 2 +- app/code/Magento/Swatches/Helper/Media.php | 84 +++- .../Swatches/Test/Unit/Helper/MediaTest.php | 42 +- .../Test/Unit/Block/Html/Header/LogoTest.php | 4 +- .../Unit/Model/Design/Backend/FileTest.php | 10 +- .../Config/FileUploader/FileProcessorTest.php | 4 +- .../Test/Unit/Model/Favicon/FaviconTest.php | 2 +- .../dynamic-rows/cells/thumbnail.html | 2 +- .../Test/Unit/Model/Template/FilterTest.php | 4 +- .../Wishlist/CustomerData/Wishlist.php | 27 +- .../Test/Unit/CustomerData/WishlistTest.php | 6 - .../CategoriesQuery/CategoryTreeTest.php | 2 +- .../Magento/GraphQl/Catalog/CategoryTest.php | 2 +- .../Helper/Form/Gallery/ContentTest.php | 4 +- .../Block/Product/View/GalleryTest.php | 102 ++++ .../Adminhtml/Product/Gallery/UploadTest.php | 6 +- .../Catalog/Controller/ProductTest.php | 2 +- .../Catalog/Model/Product/ImageTest.php | 2 +- .../Magento/Cms/Helper/Wysiwyg/ImagesTest.php | 4 +- .../Magento/Cms/Model/Wysiwyg/ConfigTest.php | 2 +- .../Images/GetInsertImageContentTest.php | 2 +- .../Cms/Model/Wysiwyg/Images/StorageTest.php | 11 +- .../Model/Config/Backend/Admin/RobotsTest.php | 13 +- .../Config/Model/_files/no_robots_txt.php | 2 +- .../Config/Model/_files/robots_txt.php | 17 +- .../Magento/Framework/Console/CliTest.php | 130 ----- .../Magento/Framework/Console/_files/env.php | 46 -- .../Framework/Css/_files/css/test-input.html | 2 +- .../_files/_inline_page_expected.html | 14 +- .../testsuite/Magento/Framework/UrlTest.php | 2 +- .../View/Element/AbstractBlockTest.php | 2 +- .../ResourceModel/Catalog/ProductTest.php | 6 +- .../Magento/Store/Model/StoreTest.php | 20 +- .../Widget/Model/Template/FilterTest.php | 4 +- .../Widget/Model/Widget/ConfigTest.php | 2 +- .../TestFramework/Deploy/CliCommand.php | 2 +- index.php | 39 -- .../App/Filesystem/DirectoryList.php | 8 +- .../App/Test/Unit/Config/DocumentRootTest.php | 75 --- .../Magento/Framework/Config/DocumentRoot.php | 6 +- .../Magento/Framework/Console/Cli.php | 23 - .../Framework/Data/Collection/Filesystem.php | 42 +- .../Magento/Framework/File/Uploader.php | 34 +- .../Framework/Filesystem/Directory/Write.php | 2 +- .../Framework/HTTP/PhpEnvironment/Request.php | 3 +- lib/internal/Magento/Framework/Image.php | 4 - .../View/Test/Unit/Design/Theme/ImageTest.php | 14 + nginx.conf.sample | 25 + pub/.htaccess | 102 ++-- pub/index.php | 13 +- .../ImagesGenerator/ImagesGenerator.php | 2 +- .../Model/ConfigOptionsList/Directory.php | 3 +- 97 files changed, 1044 insertions(+), 1953 deletions(-) create mode 100644 app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php create mode 100644 app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php delete mode 100644 app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php create mode 100644 app/code/Magento/RemoteStorage/Plugin/MediaStorage.php delete mode 100644 dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php delete mode 100644 dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php delete mode 100644 index.php delete mode 100644 lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php diff --git a/.htaccess b/.htaccess index c5f3bf034d2fb..ae929f8bc6467 100644 --- a/.htaccess +++ b/.htaccess @@ -1,393 +1,7 @@ -############################################ -## overrides deployment configuration mode value -## use command bin/magento deploy:mode:set to switch modes - -# SetEnv MAGE_MODE developer - -############################################ -## uncomment these lines for CGI mode -## make sure to specify the correct cgi php binary file name -## it might be /cgi-bin/php-cgi - -# Action php5-cgi /cgi-bin/php5-cgi -# AddHandler php5-cgi .php - -############################################ -## GoDaddy specific options - -# Options -MultiViews - -## you might also need to add this line to php.ini -## cgi.fix_pathinfo = 1 -## if it still doesn't work, rename php.ini to php5.ini - -############################################ -## this line is specific for 1and1 hosting - - #AddType x-mapp-php5 .php - #AddHandler x-mapp-php5 .php - -############################################ -## enable usage of methods arguments in backtrace - - SetEnv MAGE_DEBUG_SHOW_ARGS 1 - -############################################ -## default index file - - DirectoryIndex index.php - -<IfModule mod_php7.c> -############################################ -## adjust memory limit - - php_value memory_limit 756M - php_value max_execution_time 18000 - -############################################ -## disable automatic session start -## before autoload was initialized - - php_flag session.auto_start off - -############################################ -## enable resulting html compression - - #php_flag zlib.output_compression on - -########################################### -## disable user agent verification to not break multiple image upload - - php_flag suhosin.session.cryptua off -</IfModule> -<IfModule mod_security.c> -########################################### -## disable POST processing to not break multiple image upload - - SecFilterEngine Off - SecFilterScanPOST Off -</IfModule> - -<IfModule mod_deflate.c> - -############################################ -## enable apache served files compression -## http://developer.yahoo.com/performance/rules.html#gzip - - # Insert filter on all content - ###SetOutputFilter DEFLATE - # Insert filter on selected content types only - #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/x-javascript application/json image/svg+xml - - # Netscape 4.x has some problems... - #BrowserMatch ^Mozilla/4 gzip-only-text/html - - # Netscape 4.06-4.08 have some more problems - #BrowserMatch ^Mozilla/4\.0[678] no-gzip - - # MSIE masquerades as Netscape, but it is fine - #BrowserMatch \bMSIE !no-gzip !gzip-only-text/html - - # Don't compress images - #SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary - - # Make sure proxies don't deliver the wrong content - #Header append Vary User-Agent env=!dont-vary - -</IfModule> - -<IfModule mod_ssl.c> - -############################################ -## make HTTPS env vars available for CGI mode - - SSLOptions StdEnvVars - -</IfModule> - -############################################ -## workaround for Apache 2.4.6 CentOS build when working via ProxyPassMatch with HHVM (or any other) -## Please, set it on virtual host configuration level - -## SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 -############################################ - -<IfModule mod_rewrite.c> - -############################################ -## enable rewrites - - Options +FollowSymLinks - RewriteEngine on - -############################################ -## you can put here your magento root folder -## path relative to web root - - #RewriteBase /magento/ - -############################################ -## workaround for HTTP authorization -## in CGI environment - - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - -############################################ -## TRACE and TRACK HTTP methods disabled to prevent XSS attacks - - RewriteCond %{REQUEST_METHOD} ^TRAC[EK] - RewriteRule .* - [L,R=405] - -############################################ -## redirect for mobile user agents - - #RewriteCond %{REQUEST_URI} !^/mobiledirectoryhere/.*$ - #RewriteCond %{HTTP_USER_AGENT} "android|blackberry|ipad|iphone|ipod|iemobile|opera mobile|palmos|webos|googlebot-mobile" [NC] - #RewriteRule ^(.*)$ /mobiledirectoryhere/ [L,R=302] - -############################################ -## never rewrite for existing files, directories and links - - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-l - -############################################ -## rewrite everything else to index.php - - RewriteRule .* index.php [L] - -</IfModule> - - -############################################ -## Prevent character encoding issues from server overrides -## If you still have problems, use the second line instead - - AddDefaultCharset Off - #AddDefaultCharset UTF-8 - AddType 'text/html; charset=UTF-8' html - -<IfModule mod_expires.c> - -############################################ -## Add default Expires header -## http://developer.yahoo.com/performance/rules.html#expires - - ExpiresDefault "access plus 1 year" - ExpiresByType text/html A0 - ExpiresByType text/plain A0 - -</IfModule> - -########################################### -## Deny access to root files to hide sensitive application information - RedirectMatch 403 /\.git - - <Files composer.json> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files composer.lock> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .gitignore> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .htaccess> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .htaccess.sample> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .php_cs.dist> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files CHANGELOG.md> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files COPYING.txt> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files Gruntfile.js> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files LICENSE.txt> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files LICENSE_AFL.txt> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files nginx.conf.sample> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files package.json> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files php.ini.sample> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files README.md> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files magento_umask> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files auth.json> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - <Files .user.ini> - <IfVersion < 2.4> - order allow,deny - deny from all - </IfVersion> - <IfVersion >= 2.4> - Require all denied - </IfVersion> - </Files> - -# For 404s and 403s that aren't handled by the application, show plain 404 response -ErrorDocument 404 /pub/errors/404.php -ErrorDocument 403 /pub/errors/404.php - -################################ -## If running in cluster environment, uncomment this -## http://developer.yahoo.com/performance/rules.html#etags - - #FileETag none - -# ###################################################################### -# # INTERNET EXPLORER # -# ###################################################################### - -# ---------------------------------------------------------------------- -# | Document modes | -# ---------------------------------------------------------------------- - -# Force Internet Explorer 8/9/10 to render pages in the highest mode -# available in the various cases when it may not. -# -# https://hsivonen.fi/doctype/#ie8 -# -# (!) Starting with Internet Explorer 11, document modes are deprecated. -# If your business still relies on older web apps and services that were -# designed for older versions of Internet Explorer, you might want to -# consider enabling `Enterprise Mode` throughout your company. -# -# https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode -# http://blogs.msdn.com/b/ie/archive/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11.aspx - -<IfModule mod_headers.c> - - Header set X-UA-Compatible "IE=edge" - - # `mod_headers` cannot match based on the content-type, however, - # the `X-UA-Compatible` response header should be send only for - # HTML documents and not for the other resources. - - <FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$"> - Header unset X-UA-Compatible - </FilesMatch> - -</IfModule> +RewriteEngine on +RewriteCond %{REQUEST_URI} !^/pub/ +RewriteCond %{REQUEST_URI} !^/setup/ +RewriteCond %{REQUEST_URI} !^/update/ +RewriteCond %{REQUEST_URI} !^/dev/ +RewriteRule .* /pub/$0 [L] +DirectoryIndex index.php diff --git a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php index e888c38c4e817..50081d6ae1f17 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php @@ -90,7 +90,7 @@ protected function setUp(): void public function testGet() { - $baseUrl = 'http://magento.local/pub/media/'; + $baseUrl = 'http://magento.local/media/'; $fileInfoPath = 'analytics/data.tgz'; $fileInitializationVector = 'er312esq23eqq'; $this->fileInfoManagerMock->expects($this->once()) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 320e3f9c43a54..b7dee36488bb9 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -11,6 +11,7 @@ use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Phrase; /** * Driver for AWS S3 IO operations. @@ -232,14 +233,14 @@ public function getRealPathSafety($path) */ public function getAbsolutePath($basePath, $path, $scheme = null) { + $basePath = $this->normalizeRelativePath((string)$basePath); + $path = $this->normalizeRelativePath((string)$path); if ($basePath && $path && 0 === strpos($path, $basePath)) { - return $this->normalizeAbsolutePath( - $this->normalizeRelativePath($path) - ); + return $this->normalizeAbsolutePath($path); } if ($basePath && $basePath !== '/') { - return $basePath . ltrim((string)$path, '/'); + $path = $basePath . ltrim((string)$path, '/'); } return $this->normalizeAbsolutePath($path); @@ -328,7 +329,10 @@ public function isDirectory($path): bool $path = rtrim($path, '/') . '/'; - return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_DIR; + if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { + return ($meta['type'] ?? null) === self::TYPE_DIR; + } + return false; } /** @@ -383,7 +387,7 @@ public function stat($path): array $metaInfo = $this->adapter->getMetadata($path); if (!$metaInfo) { - throw new FileSystemException(__('Cannot gather stats! %1', (array)$path)); + throw new FileSystemException(__('Cannot gather stats! %1', [$this->getWarningMessage()])); } return [ @@ -486,7 +490,16 @@ public function touch($path, $modificationTime = null) */ public function fileReadLine($resource, $length, $ending = null): string { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + // phpcs:disable + $result = @stream_get_line($resource, $length, $ending); + // phpcs:enable + if (false === $result) { + throw new FileSystemException( + new Phrase('File cannot be read %1', [$this->getWarningMessage()]) + ); + } + + return $result; } /** @@ -521,7 +534,13 @@ public function fileGetCsv($resource, $length = 0, $delimiter = ',', $enclosure */ public function fileTell($resource): int { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @ftell($resource); + if ($result === null) { + throw new FileSystemException( + new Phrase('An error occurred during "%1" execution.', [$this->getWarningMessage()]) + ); + } + return $result; } /** @@ -529,7 +548,16 @@ public function fileTell($resource): int */ public function fileSeek($resource, $offset, $whence = SEEK_SET): int { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @fseek($resource, $offset, $whence); + if ($result === -1) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileSeek execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -537,7 +565,7 @@ public function fileSeek($resource, $offset, $whence = SEEK_SET): int */ public function endOfFile($resource): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return feof($resource); } /** @@ -554,7 +582,16 @@ public function filePutCsv($resource, array $data, $delimiter = ',', $enclosure */ public function fileFlush($resource): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @fflush($resource); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileFlush execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -562,7 +599,16 @@ public function fileFlush($resource): bool */ public function fileLock($resource, $lockMode = LOCK_EX): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @flock($resource, $lockMode); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileLock execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -570,7 +616,16 @@ public function fileLock($resource, $lockMode = LOCK_EX): bool */ public function fileUnlock($resource): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + $result = @flock($resource, LOCK_UN); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileUnlock execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** @@ -627,15 +682,11 @@ public function fileOpen($path, $mode) if (!isset($this->streams[$path])) { $this->streams[$path] = tmpfile(); if ($this->adapter->has($path)) { - $file = tmpfile(); //phpcs:ignore Magento2.Functions.DiscouragedFunction - fwrite($file, $this->adapter->read($path)['contents']); + fwrite($this->streams[$path], $this->adapter->read($path)['contents']); //phpcs:ignore Magento2.Functions.DiscouragedFunction - fseek($file, 0); - } else { - $file = tmpfile(); + rewind($this->streams[$path]); } - $this->streams[$path] = $file; } return $this->streams[$path]; diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index b70149e26225c..e3e3e4208484d 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -105,16 +105,46 @@ public function getAbsolutePathDataProvider(): array self::URL . 'test/test.png', self::URL . 'test/test.png' ], + [ + self::URL, + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], + [ + '', + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], [ self::URL . 'test/', 'test.txt', self::URL . 'test/test.txt' ], + [ + self::URL . 'media/', + 'media/image.jpg', + self::URL . 'media/image.jpg' + ], [ self::URL . 'media/', '/catalog/test.png', self::URL . 'media/catalog/test.png' ], + [ + self::URL, + 'var/import/images', + self::URL . 'var/import/images' + ], + [ + self::URL . 'export/', + null, + self::URL . 'export/' + ], + [ + self::URL . 'var/import/images/product_images/', + self::URL . 'var/import/images/product_images/1.png', + self::URL . 'var/import/images/product_images/1.png' + ], [ '', self::URL . 'media/catalog/test.png', diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index 94f555e4054e3..6102a63ec2f69 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -6,8 +6,6 @@ namespace Magento\Backup\Model\Fs; use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Config\DocumentRoot; -use Magento\Framework\Filesystem\Directory\TargetDirectory; /** * Backup data collection @@ -52,20 +50,16 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem * @param \Magento\Backup\Helper\Data $backupData * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Backup\Model\Backup $backup - * @param TargetDirectory|null $targetDirectory - * @param DocumentRoot|null $documentRoot * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, \Magento\Backup\Helper\Data $backupData, \Magento\Framework\Filesystem $filesystem, - \Magento\Backup\Model\Backup $backup, - TargetDirectory $targetDirectory = null, - DocumentRoot $documentRoot = null + \Magento\Backup\Model\Backup $backup ) { $this->_backupData = $backupData; - parent::__construct($entityFactory, $targetDirectory, $documentRoot); + parent::__construct($entityFactory, $filesystem); $this->_filesystem = $filesystem; $this->_backup = $backup; diff --git a/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php b/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php index ec9f6f03134cc..4b9286f69cce5 100644 --- a/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php @@ -197,7 +197,7 @@ public function testGetImgDir() */ public function testGetImgUrl() { - $this->assertEquals($this->helper->getImgUrl(), 'http://localhost/pub/media/captcha/base/'); + $this->assertEquals($this->helper->getImgUrl(), 'http://localhost/media/captcha/base/'); } /** @@ -223,7 +223,7 @@ protected function _getStoreStub() { $store = $this->createMock(Store::class); - $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/pub/media/'); + $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/media/'); return $store; } diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php index a20ff898c222e..9e3b0e9a8770f 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php @@ -217,7 +217,7 @@ public function testGetImgSrc() { $this->assertEquals( $this->_object->getImgSrc(), - 'http://localhost/pub/media/captcha/base/' . $this->_object->getId() . '.png' + 'http://localhost/media/captcha/base/' . $this->_object->getId() . '.png' ); } @@ -310,7 +310,7 @@ protected function _getHelperStub() )->method( 'getImgUrl' )->willReturn( - 'http://localhost/pub/media/captcha/base/' + 'http://localhost/media/captcha/base/' ); return $helper; @@ -365,7 +365,7 @@ protected function _getStoreStub() ->onlyMethods(['getBaseUrl']) ->disableOriginalConstructor() ->getMock(); - $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/pub/media/'); + $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/media/'); $store->expects($this->any())->method('isAdmin')->willReturn(false); return $store; } diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index ab74b5694ce9f..de32f6b7637d4 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -5,7 +5,10 @@ */ namespace Magento\Catalog\Helper; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Element\Block\ArgumentInterface; /** @@ -133,27 +136,34 @@ class Image extends AbstractHelper implements ArgumentInterface */ private $viewAssetPlaceholderFactory; + /** + * @var CatalogMediaConfig + */ + private $mediaConfig; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Catalog\Model\Product\ImageFactory $productImageFactory * @param \Magento\Framework\View\Asset\Repository $assetRepo * @param \Magento\Framework\View\ConfigInterface $viewConfig * @param \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory + * @param CatalogMediaConfig $mediaConfig */ public function __construct( \Magento\Framework\App\Helper\Context $context, \Magento\Catalog\Model\Product\ImageFactory $productImageFactory, \Magento\Framework\View\Asset\Repository $assetRepo, \Magento\Framework\View\ConfigInterface $viewConfig, - \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory = null + \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory = null, + CatalogMediaConfig $mediaConfig = null ) { $this->_productImageFactory = $productImageFactory; parent::__construct($context); $this->_assetRepo = $assetRepo; $this->viewConfig = $viewConfig; $this->viewAssetPlaceholderFactory = $placeholderFactory - ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class); + ?: ObjectManager::getInstance()->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class); + $this->mediaConfig = $mediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); } /** @@ -532,7 +542,16 @@ protected function isScheduledActionsAllowed() public function getUrl() { try { - $this->applyScheduledActions(); + switch ($this->mediaConfig->getMediaUrlFormat()) { + case CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS: + $this->initBaseFile(); + break; + case CatalogMediaConfig::HASH: + $this->applyScheduledActions(); + break; + default: + throw new LocalizedException(__("The specified Catalog media URL format is not supported.")); + } return $this->_getModel()->getUrl(); } catch (\Exception $e) { return $this->getDefaultPlaceholderUrl(); diff --git a/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php new file mode 100644 index 0000000000000..0ae128b34d348 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Config; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Config for catalog media + */ +class CatalogMediaConfig +{ + private const XML_PATH_CATALOG_MEDIA_URL_FORMAT = 'web/url/catalog_media_url_format'; + + const IMAGE_OPTIMIZATION_PARAMETERS = 'image_optimization_parameters'; + const HASH = 'hash'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get media URL format for catalog images + * + * @param string $scopeType + * @param null|int|string $scopeCode + * @return string + */ + public function getMediaUrlFormat($scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null): string + { + return $this->scopeConfig->getValue( + CatalogMediaConfig::XML_PATH_CATALOG_MEDIA_URL_FORMAT, + $scopeType, + $scopeCode + ); + } +} diff --git a/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php b/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php new file mode 100644 index 0000000000000..bab2d5ccb3f1f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.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\Model\Config\Source\Web; + +use Magento\Catalog\Model\Config\CatalogMediaConfig; + +/** + * Option provider for catalog media URL format system setting. + */ +class CatalogMediaUrlFormat implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * Get a list of supported catalog media URL formats. + * + * @codeCoverageIgnore + * @return array + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS, + 'label' => __('Image optimization based on query parameters') + ], + ['value' => CatalogMediaConfig::HASH, 'label' => __('Unique hash per image variant (Legacy mode)')] + ]; + } +} diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index 3c60d81e9a4d8..842ee197f83fe 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -10,9 +10,11 @@ use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Image as MagentoImage; use Magento\Framework\Serialize\SerializerInterface; use Magento\Catalog\Model\Product\Image\ParamsBuilder; +use Magento\Framework\Filesystem\Driver\File as FilesystemDriver; /** * Image operations @@ -101,6 +103,7 @@ class Image extends \Magento\Framework\Model\AbstractModel /** * @var int + * @deprecated unused */ protected $_angle; @@ -199,6 +202,11 @@ class Image extends \Magento\Framework\Model\AbstractModel */ private $serializer; + /** + * @var FilesystemDriver + */ + private $filesystemDriver; + /** * Constructor * @@ -219,6 +227,8 @@ class Image extends \Magento\Framework\Model\AbstractModel * @param array $data * @param SerializerInterface $serializer * @param ParamsBuilder $paramsBuilder + * @param FilesystemDriver $filesystemDriver + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -239,7 +249,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], SerializerInterface $serializer = null, - ParamsBuilder $paramsBuilder = null + ParamsBuilder $paramsBuilder = null, + FilesystemDriver $filesystemDriver = null ) { $this->_storeManager = $storeManager; $this->_catalogProductMediaConfig = $catalogProductMediaConfig; @@ -254,6 +265,7 @@ public function __construct( $this->viewAssetPlaceholderFactory = $viewAssetPlaceholderFactory; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->paramsBuilder = $paramsBuilder ?: ObjectManager::getInstance()->get(ParamsBuilder::class); + $this->filesystemDriver = $filesystemDriver ?: ObjectManager::getInstance()->get(FilesystemDriver::class); } /** @@ -663,7 +675,12 @@ public function getDestinationSubdir() public function isCached() { $path = $this->imageAsset->getPath(); - return is_array($this->loadImageInfoFromCache($path)) || file_exists($path); + try { + $isCached = is_array($this->loadImageInfoFromCache($path)) || $this->filesystemDriver->isExists($path); + } catch (FileSystemException $e) { + $isCached = false; + } + return $isCached; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php index 33af93db13b4c..71e29515791a7 100644 --- a/app/code/Magento/Catalog/Model/Product/Media/Config.php +++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\Product\Media; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -76,7 +77,7 @@ public function getBaseMediaPath() public function getBaseMediaUrl() { return $this->storeManager->getStore() - ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . 'catalog/product'; + ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $this->getBaseMediaUrlAddition(); } /** @@ -97,7 +98,7 @@ public function getBaseTmpMediaPath() public function getBaseTmpMediaUrl() { return $this->storeManager->getStore()->getBaseUrl( - \Magento\Framework\UrlInterface::URL_TYPE_MEDIA + UrlInterface::URL_TYPE_MEDIA ) . 'tmp/' . $this->getBaseMediaUrlAddition(); } diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index 0a46af3ef021d..bf624c3435103 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -108,7 +108,7 @@ public function viewDirective($construction) * The original intent of _absolute parameter was to simply append specified path to a base URL * bypassing any kind of processing. * For example, normally you would use {{view url="css/styles.css"}} directive which would automatically resolve - * into something like http://example.com/pub/static/area/theme/en_US/css/styles.css + * into something like http://example.com/static/area/theme/en_US/css/styles.css * But with _absolute, the expected behavior is this: {{view url="favicon.ico" _absolute=true}} should resolve * into something like http://example.com/favicon.ico * diff --git a/app/code/Magento/Catalog/Model/View/Asset/Image.php b/app/code/Magento/Catalog/Model/View/Asset/Image.php index c547ec612bb94..0f7082f9df154 100644 --- a/app/code/Magento/Catalog/Model/View/Asset/Image.php +++ b/app/code/Magento/Catalog/Model/View/Asset/Image.php @@ -6,11 +6,16 @@ namespace Magento\Catalog\Model\View\Asset; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product\Media\ConfigInterface; use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Asset\ContextInterface; use Magento\Framework\View\Asset\LocalInterface; +use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; /** * A locally available image file asset that can be referred with a file path @@ -58,6 +63,21 @@ class Image implements LocalInterface */ private $encryptor; + /** + * @var ImageHelper + */ + private $imageHelper; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var string + */ + private $mediaFormatUrl; + /** * Image constructor. * @@ -66,13 +86,19 @@ class Image implements LocalInterface * @param EncryptorInterface $encryptor * @param string $filePath * @param array $miscParams + * @param ImageHelper $imageHelper + * @param CatalogMediaConfig $catalogMediaConfig + * @param StoreManagerInterface $storeManager */ public function __construct( ConfigInterface $mediaConfig, ContextInterface $context, EncryptorInterface $encryptor, $filePath, - array $miscParams + array $miscParams, + ImageHelper $imageHelper = null, + CatalogMediaConfig $catalogMediaConfig = null, + StoreManagerInterface $storeManager = null ) { if (isset($miscParams['image_type'])) { $this->sourceContentType = $miscParams['image_type']; @@ -85,14 +111,73 @@ public function __construct( $this->filePath = $filePath; $this->miscParams = $miscParams; $this->encryptor = $encryptor; + $this->imageHelper = $imageHelper ?: ObjectManager::getInstance()->get(ImageHelper::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + + $catalogMediaConfig = $catalogMediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaFormatUrl = $catalogMediaConfig->getMediaUrlFormat(); } /** - * @inheritdoc + * Get catalog image URL. + * + * @return string + * @throws LocalizedException */ public function getUrl() { - return $this->context->getBaseUrl() . DIRECTORY_SEPARATOR . $this->getImageInfo(); + switch ($this->mediaFormatUrl) { + case CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS: + return $this->getUrlWithTransformationParameters(); + case CatalogMediaConfig::HASH: + return $this->context->getBaseUrl() . DIRECTORY_SEPARATOR . $this->getImageInfo(); + default: + throw new LocalizedException( + __("The specified Catalog media URL format '$this->mediaFormatUrl' is not supported.") + ); + } + } + + /** + * Get image URL with transformation parameters + * + * @return string + */ + private function getUrlWithTransformationParameters() + { + return $this->getOriginalImageUrl() . '?' . http_build_query($this->getImageTransformationParameters()); + } + + /** + * The list of parameters to be used during image transformations (e.g. resizing or applying watermarks). + * + * This method can be used as an extension point. + * + * @return string[] + */ + public function getImageTransformationParameters() + { + return [ + 'width' => $this->miscParams['image_width'], + 'height' => $this->miscParams['image_height'], + 'store' => $this->storeManager->getStore()->getCode(), + 'image-type' => $this->sourceContentType + ]; + } + + /** + * Get URL to the original version of the product image. + * + * @return string + */ + private function getOriginalImageUrl() + { + $originalImageFile = $this->getSourceFile(); + if (!$originalImageFile) { + return $this->imageHelper->getDefaultPlaceholderUrl(); + } else { + return $this->context->getBaseUrl() . $this->getFilePath(); + } } /** diff --git a/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php b/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php index 91d2868afab8c..54b655a217a08 100644 --- a/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php +++ b/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php @@ -10,7 +10,11 @@ use Magento\Framework\Event\ObserverInterface; use Magento\Framework\App\State; use Magento\MediaStorage\Service\ImageResize; +use Magento\Catalog\Model\Config\CatalogMediaConfig; +/** + * Resize product images after the product is saved + */ class ImageResizeAfterProductSave implements ObserverInterface { /** @@ -23,17 +27,26 @@ class ImageResizeAfterProductSave implements ObserverInterface */ private $state; + /** + * @var CatalogMediaConfig + */ + private $catalogMediaConfig; + /** * Product constructor. + * * @param ImageResize $imageResize * @param State $state + * @param CatalogMediaConfig $catalogMediaConfig */ public function __construct( ImageResize $imageResize, - State $state + State $state, + CatalogMediaConfig $catalogMediaConfig ) { $this->imageResize = $imageResize; $this->state = $state; + $this->catalogMediaConfig = $catalogMediaConfig; } /** @@ -44,6 +57,12 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + $catalogMediaUrlFormat = $this->catalogMediaConfig->getMediaUrlFormat(); + if ($catalogMediaUrlFormat == CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS) { + // Skip image resizing on the Magento side when it is offloaded to a web server or CDN + return; + } + /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php deleted file mode 100644 index 572dbc4ca2732..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ /dev/null @@ -1,454 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form\Gallery; - -use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; -use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content; -use Magento\Catalog\Helper\Image; -use Magento\Catalog\Model\Entity\Attribute; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Media\Config; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\Read; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Json\EncoderInterface; -use Magento\Framework\Phrase; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\MediaStorage\Helper\File\Storage\Database; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ContentTest extends TestCase -{ - /** - * @var Filesystem|MockObject - */ - protected $fileSystemMock; - - /** - * @var Read|MockObject - */ - protected $readMock; - - /** - * @var Content|MockObject - */ - protected $content; - - /** - * @var Config|MockObject - */ - protected $mediaConfigMock; - - /** - * @var EncoderInterface|MockObject - */ - protected $jsonEncoderMock; - - /** - * @var Gallery|MockObject - */ - protected $galleryMock; - - /** - * @var Image|MockObject - */ - protected $imageHelper; - - /** - * @var Database|MockObject - */ - protected $databaseMock; - - /** - * @var ObjectManager - */ - protected $objectManager; - - protected function setUp(): void - { - $this->fileSystemMock = $this->getMockBuilder(Filesystem::class) - ->addMethods(['stat']) - ->onlyMethods(['getDirectoryRead']) - ->disableOriginalConstructor() - ->getMock(); - $this->readMock = $this->getMockForAbstractClass(ReadInterface::class); - $this->galleryMock = $this->createMock(Gallery::class); - $this->mediaConfigMock = $this->createPartialMock( - Config::class, - ['getMediaUrl', 'getMediaPath'] - ); - $this->jsonEncoderMock = $this->getMockBuilder(EncoderInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->databaseMock = $this->getMockBuilder(Database::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->objectManager = new ObjectManager($this); - $this->content = $this->objectManager->getObject( - Content::class, - [ - 'mediaConfig' => $this->mediaConfigMock, - 'jsonEncoder' => $this->jsonEncoderMock, - 'filesystem' => $this->fileSystemMock, - 'fileStorageDatabase' => $this->databaseMock - ] - ); - } - - public function testGetImagesJson() - { - $url = [ - ['file_1.jpg', 'url_to_the_image/image_1.jpg'], - ['file_2.jpg', 'url_to_the_image/image_2.jpg'] - ]; - $mediaPath = [ - ['file_1.jpg', 'catalog/product/image_1.jpg'], - ['file_2.jpg', 'catalog/product/image_2.jpg'] - ]; - - $sizeMap = [ - ['catalog/product/image_1.jpg', ['size' => 399659]], - ['catalog/product/image_2.jpg', ['size' => 879394]] - ]; - - $imagesResult = [ - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0', - 'url' => 'url_to_the_image/image_2.jpg', - 'size' => 879394 - ], - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1', - 'url' => 'url_to_the_image/image_1.jpg', - 'size' => 399659 - ] - ]; - - $images = [ - 'images' => [ - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1' - ] , - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn($images); - $this->fileSystemMock->expects($this->once())->method('getDirectoryRead')->willReturn($this->readMock); - - $this->mediaConfigMock->method('getMediaUrl')->willReturnMap($url); - $this->mediaConfigMock->method('getMediaPath')->willReturnMap($mediaPath); - $this->readMock->method('stat')->willReturnMap($sizeMap); - $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); - - $this->readMock->method('isFile')->willReturn(true); - $this->databaseMock->method('checkDbUsage')->willReturn(false); - - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } - - public function testGetImagesJsonWithoutImages() - { - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn(null); - - $this->assertSame('[]', $this->content->getImagesJson()); - } - - public function testGetImagesJsonWithException() - { - $this->imageHelper = $this->getMockBuilder(Image::class) - ->disableOriginalConstructor() - ->setMethods(['getDefaultPlaceholderUrl']) - ->getMock(); - - $this->objectManager->setBackwardCompatibleProperty( - $this->content, - 'imageHelper', - $this->imageHelper - ); - - $placeholderUrl = 'url_to_the_placeholder/placeholder.jpg'; - - $imagesResult = [ - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0', - 'url' => 'url_to_the_placeholder/placeholder.jpg', - 'size' => 0 - ], - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1', - 'url' => 'url_to_the_placeholder/placeholder.jpg', - 'size' => 0 - ] - ]; - - $images = [ - 'images' => [ - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1' - ], - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn($images); - $this->fileSystemMock->method('getDirectoryRead')->willReturn($this->readMock); - $this->mediaConfigMock->method('getMediaUrl'); - $this->mediaConfigMock->method('getMediaPath'); - - $this->readMock - ->method('isFile') - ->willReturn(true); - $this->databaseMock - ->method('checkDbUsage') - ->willReturn(false); - - $this->readMock->method('stat')->willReturnOnConsecutiveCalls( - $this->throwException( - new FileSystemException(new Phrase('test')) - ), - $this->throwException( - new FileSystemException(new Phrase('test')) - ) - ); - $this->imageHelper->method('getDefaultPlaceholderUrl')->willReturn($placeholderUrl); - $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); - - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } - - /** - * Test GetImageTypes() will return value for given attribute from data persistor. - * - * @return void - */ - public function testGetImageTypesFromDataPersistor() - { - $attributeCode = 'thumbnail'; - $value = 'testImageValue'; - $scopeLabel = 'testScopeLabel'; - $label = 'testLabel'; - $name = 'testName'; - $expectedTypes = [ - $attributeCode => [ - 'code' => $attributeCode, - 'value' => $value, - 'label' => $label, - 'name' => $name, - ], - ]; - $product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - $product->expects($this->once()) - ->method('getData') - ->with($this->identicalTo($attributeCode)) - ->willReturn(null); - $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); - $product->expects($this->once()) - ->method('getMediaAttributes') - ->willReturn([$mediaAttribute]); - $this->galleryMock->expects($this->exactly(2)) - ->method('getDataObject') - ->willReturn($product); - $this->galleryMock->expects($this->once()) - ->method('getImageValue') - ->with($this->identicalTo($attributeCode)) - ->willReturn($value); - $this->galleryMock->expects($this->once()) - ->method('getScopeLabel') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($scopeLabel); - $this->galleryMock->expects($this->once()) - ->method('getAttributeFieldName') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($name); - $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); - } - - /** - * Test GetImageTypes() will return value for given attribute from product. - * - * @return void - */ - public function testGetImageTypesFromProduct() - { - $attributeCode = 'thumbnail'; - $value = 'testImageValue'; - $scopeLabel = 'testScopeLabel'; - $label = 'testLabel'; - $name = 'testName'; - $expectedTypes = [ - $attributeCode => [ - 'code' => $attributeCode, - 'value' => $value, - 'label' => $label, - 'name' => $name, - ], - ]; - $product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - $product->expects($this->once()) - ->method('getData') - ->with($this->identicalTo($attributeCode)) - ->willReturn($value); - $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); - $product->expects($this->once()) - ->method('getMediaAttributes') - ->willReturn([$mediaAttribute]); - $this->galleryMock->expects($this->exactly(2)) - ->method('getDataObject') - ->willReturn($product); - $this->galleryMock->expects($this->never()) - ->method('getImageValue'); - $this->galleryMock->expects($this->once()) - ->method('getScopeLabel') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($scopeLabel); - $this->galleryMock->expects($this->once()) - ->method('getAttributeFieldName') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($name); - $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); - } - - /** - * Perform assertions. - * - * @param string $attributeCode - * @param string $scopeLabel - * @param array $expectedTypes - * @return void - */ - private function getImageTypesAssertions(string $attributeCode, string $scopeLabel, array $expectedTypes) - { - $this->content->setElement($this->galleryMock); - $result = $this->content->getImageTypes(); - $scope = $result[$attributeCode]['scope']; - $this->assertSame($scopeLabel, $scope->getText()); - unset($result[$attributeCode]['scope']); - $this->assertSame($expectedTypes, $result); - } - - /** - * Get media attribute mock. - * - * @param string $label - * @param string $attributeCode - * @return MockObject - */ - private function getMediaAttribute(string $label, string $attributeCode) - { - $frontend = $this->getMockBuilder(Product\Attribute\Frontend\Image::class) - ->disableOriginalConstructor() - ->getMock(); - $frontend->expects($this->once()) - ->method('getLabel') - ->willReturn($label); - $mediaAttribute = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - $mediaAttribute - ->method('getAttributeCode') - ->willReturn($attributeCode); - $mediaAttribute->expects($this->once()) - ->method('getFrontend') - ->willReturn($frontend); - - return $mediaAttribute; - } - - /** - * Test GetImagesJson() calls MediaStorage functions to obtain image from DB prior to stat call - * - * @return void - */ - public function testGetImagesJsonMediaStorageMode() - { - $images = [ - 'images' => [ - [ - 'value_id' => '0', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $mediaPath = [ - ['file_1.jpg', 'catalog/product/image_1.jpg'] - ]; - - $this->content->setElement($this->galleryMock); - - $this->galleryMock->expects($this->once()) - ->method('getImages') - ->willReturn($images); - $this->fileSystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->willReturn($this->readMock); - $this->mediaConfigMock - ->method('getMediaPath') - ->willReturnMap($mediaPath); - - $this->readMock - ->method('isFile') - ->willReturn(false); - $this->databaseMock - ->method('checkDbUsage') - ->willReturn(true); - - $this->databaseMock->expects($this->once()) - ->method('saveFileToFilesystem') - ->with('catalog/product/image_1.jpg'); - - $this->readMock->method('stat')->willReturn(['size' => 123]); - - $this->content->getImagesJson(); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php index c606b7537cc44..125fd287cd4ce 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Test\Unit\Helper; use Magento\Catalog\Helper\Image; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\ImageFactory as ProductImageFactory; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; @@ -70,6 +71,11 @@ class ImageTest extends TestCase */ protected $placeholderFactory; + /** + * @var CatalogMediaConfig|MockObject + */ + private $catalogMediaConfigMock; + protected function setUp(): void { $this->mockContext(); @@ -90,12 +96,17 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); + $this->catalogMediaConfigMock = $this->createPartialMock(CatalogMediaConfig::class, ['getMediaUrlFormat']); + $this->catalogMediaConfigMock->method('getMediaUrlFormat')->willReturn(CatalogMediaConfig::HASH); + + $this->helper = new Image( $this->context, $this->imageFactory, $this->assetRepository, $this->viewConfig, - $this->placeholderFactory + $this->placeholderFactory, + $this->catalogMediaConfigMock ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php index 16771214026f0..23136e55a2307 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -14,7 +14,9 @@ use Magento\Framework\DataObject; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -186,6 +188,7 @@ public function testBeforeSaveValueInvalid($value) */ public function testBeforeSaveAttributeFileName() { + $this->setupObjectManagerForCheckImageExist(false); $this->attribute->expects($this->once()) ->method('getName') ->willReturn('test_attribute'); @@ -253,11 +256,23 @@ public function testBeforeSaveAttributeFileNameOutsideOfCategoryDir() ); } + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + /** * Test beforeSaveTemporaryAttribute. */ public function testBeforeSaveTemporaryAttribute() { + $this->setupObjectManagerForCheckImageExist(false); $this->attribute->expects($this->once()) ->method('getName') ->willReturn('test_attribute'); @@ -268,7 +283,7 @@ public function testBeforeSaveTemporaryAttribute() $this->storeMock->expects($this->once()) ->method('getBaseMediaDir') - ->willReturn('pub/media'); + ->willReturn('media'); $model = $this->setUpModelForTests(); $model->setAttribute($this->attribute); @@ -279,7 +294,9 @@ public function testBeforeSaveTemporaryAttribute() ->with(DirectoryList::MEDIA) ->willReturn($mediaDirectoryMock); - $this->imageUploader->expects($this->any())->method('moveFileFromTmp')->willReturn('test123.jpg'); + $mediaDirectoryMock->method('getAbsolutePath')->willReturn('/media/test123.jpg'); + + $this->imageUploader->method('moveFileFromTmp')->willReturn('test123.jpg'); $object = new DataObject( [ @@ -287,7 +304,7 @@ public function testBeforeSaveTemporaryAttribute() [ 'name' => 'test123.jpg', 'tmp_name' => 'abc123', - 'url' => 'http://www.example.com/pub/media/temp/test123.jpg' + 'url' => 'http://www.example.com/media/temp/test123.jpg' ], ], ] @@ -297,7 +314,7 @@ public function testBeforeSaveTemporaryAttribute() $this->assertEquals( [ - ['name' => '/pub/media/test123.jpg', 'tmp_name' => 'abc123', 'url' => '/pub/media/test123.jpg'], + ['name' => '/media/test123.jpg', 'tmp_name' => 'abc123', 'url' => '/media/test123.jpg'], ], $object->getData('_additional_data_test_attribute') ); @@ -418,6 +435,7 @@ public function testBeforeSaveWithoutAdditionalData($value) */ public function testBeforeSaveWithExceptions() { + $this->setupObjectManagerForCheckImageExist(false); $model = $this->setUpModelForTests(); $this->storeManagerInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php index 42a3031ae27e0..676cf07912f1d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php @@ -84,8 +84,8 @@ public function getUrlDataProvider() ], [ 'testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/catalog/category/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/catalog/category/testimage' ], [ 'testimage', @@ -94,8 +94,8 @@ public function getUrlDataProvider() ], [ '/pub/media/catalog/category/testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/catalog/category/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/catalog/category/testimage' ], [ '/pub/media/catalog/category/testimage', @@ -104,8 +104,8 @@ public function getUrlDataProvider() ], [ '/pub/media/posters/testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/posters/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/posters/testimage' ], [ '/pub/media/posters/testimage', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php deleted file mode 100644 index 93bb85abced75..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php +++ /dev/null @@ -1,168 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model; - -use Magento\Catalog\Model\ImageUploader; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use Magento\MediaStorage\Helper\File\Storage\Database; -use Magento\MediaStorage\Model\File\Uploader; -use Magento\MediaStorage\Model\File\UploaderFactory; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -class ImageUploaderTest extends TestCase -{ - /** - * @var ImageUploader - */ - private $imageUploader; - - /** - * Core file storage database - * - * @var Database|MockObject - */ - private $coreFileStorageDatabaseMock; - - /** - * Media directory object (writable). - * - * @var Filesystem|MockObject - */ - private $mediaDirectoryMock; - - /** - * Media directory object (writable). - * - * @var WriteInterface|MockObject - */ - private $mediaWriteDirectoryMock; - - /** - * Uploader factory - * - * @var UploaderFactory|MockObject - */ - private $uploaderFactoryMock; - - /** - * Store manager - * - * @var StoreManagerInterface|MockObject - */ - private $storeManagerMock; - - /** - * @var LoggerInterface|MockObject - */ - private $loggerMock; - - /** - * Base tmp path - * - * @var string - */ - private $baseTmpPath; - - /** - * Base path - * - * @var string - */ - private $basePath; - - /** - * Allowed extensions - * - * @var array - */ - private $allowedExtensions; - - /** - * Allowed mime types - * - * @var array - */ - private $allowedMimeTypes; - - protected function setUp(): void - { - $this->coreFileStorageDatabaseMock = $this->createMock( - Database::class - ); - $this->mediaDirectoryMock = $this->createMock( - Filesystem::class - ); - $this->mediaWriteDirectoryMock = $this->createMock( - WriteInterface::class - ); - $this->mediaDirectoryMock->expects($this->any())->method('getDirectoryWrite')->willReturn( - $this->mediaWriteDirectoryMock - ); - $this->uploaderFactoryMock = $this->createMock( - UploaderFactory::class - ); - $this->storeManagerMock = $this->createMock( - StoreManagerInterface::class - ); - $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $this->baseTmpPath = 'base/tmp/'; - $this->basePath = 'base/real/'; - $this->allowedExtensions = ['.jpg']; - $this->allowedMimeTypes = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']; - - $this->imageUploader = - new ImageUploader( - $this->coreFileStorageDatabaseMock, - $this->mediaDirectoryMock, - $this->uploaderFactoryMock, - $this->storeManagerMock, - $this->loggerMock, - $this->baseTmpPath, - $this->basePath, - $this->allowedExtensions, - $this->allowedMimeTypes - ); - } - - public function testSaveFileToTmpDir() - { - $fileId = 'file.jpg'; - $allowedMimeTypes = [ - 'image/jpg', - 'image/jpeg', - 'image/gif', - 'image/png', - ]; - /** @var \Magento\MediaStorage\Model\File\Uploader|MockObject $uploader */ - $uploader = $this->createMock(Uploader::class); - $this->uploaderFactoryMock->expects($this->once())->method('create')->willReturn($uploader); - $uploader->expects($this->once())->method('setAllowedExtensions')->with($this->allowedExtensions); - $uploader->expects($this->once())->method('setAllowRenameFiles')->with(true); - $this->mediaWriteDirectoryMock->expects($this->once())->method('getAbsolutePath')->with($this->baseTmpPath) - ->willReturn($this->basePath); - $uploader->expects($this->once())->method('save')->with($this->basePath) - ->willReturn(['tmp_name' => $this->baseTmpPath, 'file' => $fileId, 'path' => $this->basePath]); - $uploader->expects($this->atLeastOnce())->method('checkMimeType')->with($allowedMimeTypes)->willReturn(true); - $storeMock = $this->createPartialMock( - Store::class, - ['getBaseUrl'] - ); - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); - $storeMock->expects($this->once())->method('getBaseUrl'); - $this->coreFileStorageDatabaseMock->expects($this->once())->method('saveFile'); - - $result = $this->imageUploader->saveFileToTmpDir($fileId); - - $this->assertArrayNotHasKey('path', $result); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php deleted file mode 100644 index af8245de3525d..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\View\Asset\Image; - -use Magento\Catalog\Model\Product\Media\ConfigInterface; -use Magento\Catalog\Model\View\Asset\Image\Context; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ContextTest extends TestCase -{ - /** - * @var Context - */ - protected $model; - - /** - * @var WriteInterface|MockObject - */ - protected $mediaDirectory; - - /** - * @var ContextInterface|MockObject - */ - protected $mediaConfig; - - /** - * @var Filesystem|MockObject - */ - protected $filesystem; - - protected function setUp(): void - { - $this->mediaConfig = $this->getMockBuilder(ConfigInterface::class) - ->getMockForAbstractClass(); - $this->mediaConfig->expects($this->any())->method('getBaseMediaPath')->willReturn('catalog/product'); - $this->mediaDirectory = $this->getMockBuilder(WriteInterface::class) - ->getMockForAbstractClass(); - $this->mediaDirectory->expects($this->once())->method('create')->with('catalog/product'); - $this->filesystem = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->filesystem->expects($this->once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::MEDIA) - ->willReturn($this->mediaDirectory); - $this->model = new Context( - $this->mediaConfig, - $this->filesystem - ); - } - - public function testGetPath() - { - $path = '/var/www/html/magento2ce/pub/media/catalog/product'; - $this->mediaDirectory->expects($this->once()) - ->method('getAbsolutePath') - ->with('catalog/product') - ->willReturn($path); - - $this->assertEquals($path, $this->model->getPath()); - } - - public function testGetUrl() - { - $baseUrl = 'http://localhost/pub/media/catalog/product'; - $this->mediaConfig->expects($this->once())->method('getBaseMediaUrl')->willReturn($baseUrl); - - $this->assertEquals($baseUrl, $this->model->getBaseUrl()); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php deleted file mode 100644 index 1a61cd4d4eea8..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php +++ /dev/null @@ -1,213 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\View\Asset; - -use Magento\Catalog\Model\Product\Media\ConfigInterface; -use Magento\Catalog\Model\View\Asset\Image; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Framework\View\Asset\ContextInterface; -use Magento\Framework\View\Asset\Repository; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ImageTest extends TestCase -{ - /** - * @var Image - */ - protected $model; - - /** - * @var ContextInterface|MockObject - */ - protected $mediaConfig; - - /** - * @var EncryptorInterface|MockObject - */ - protected $encryptor; - - /** - * @var ContextInterface|MockObject - */ - protected $context; - - /** - * @var Repository|MockObject - */ - private $assetRepo; - - private $objectManager; - - protected function setUp(): void - { - $this->mediaConfig = $this->getMockForAbstractClass(ConfigInterface::class); - $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); - $this->context = $this->getMockForAbstractClass(ContextInterface::class); - $this->assetRepo = $this->createMock(Repository::class); - $this->objectManager = new ObjectManager($this); - $this->model = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'imageContext' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => '/somefile.png', - 'assetRepo' => $this->assetRepo, - 'miscParams' => [ - 'image_width' => 100, - 'image_height' => 50, - 'constrain_only' => false, - 'keep_aspect_ratio' => false, - 'keep_frame' => true, - 'keep_transparency' => false, - 'background' => '255,255,255', - 'image_type' => 'image', //thumbnail,small_image,image,swatch_image,swatch_thumb - 'quality' => 80, - 'angle' => null - ] - ] - ); - } - - public function testModuleAndContentAndContentType() - { - $contentType = 'image'; - $this->assertEquals($contentType, $this->model->getContentType()); - $this->assertEquals($contentType, $this->model->getSourceContentType()); - $this->assertNull($this->model->getContent()); - $this->assertEquals('cache', $this->model->getModule()); - } - - public function testGetFilePath() - { - $this->assertEquals('/somefile.png', $this->model->getFilePath()); - } - - public function testGetSoureFile() - { - $this->mediaConfig->expects($this->once())->method('getBaseMediaPath')->willReturn('catalog/product'); - $this->assertEquals('catalog/product/somefile.png', $this->model->getSourceFile()); - } - - public function testGetContext() - { - $this->assertInstanceOf(ContextInterface::class, $this->model->getContext()); - } - - /** - * @param string $filePath - * @param array $miscParams - * @param string $readableParams - * @dataProvider getPathDataProvider - */ - public function testGetPath($filePath, $miscParams, $readableParams) - { - $imageModel = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'context' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => $filePath, - 'assetRepo' => $this->assetRepo, - 'miscParams' => $miscParams - ] - ); - $absolutePath = '/var/www/html/magento2ce/pub/media/catalog/product'; - $hashPath = 'somehash'; - $this->context->method('getPath')->willReturn($absolutePath); - $this->encryptor->expects(static::once()) - ->method('hash') - ->with($readableParams, $this->anything()) - ->willReturn($hashPath); - static::assertEquals( - $absolutePath . '/cache/' . $hashPath . $filePath, - $imageModel->getPath() - ); - } - - /** - * @param string $filePath - * @param array $miscParams - * @param string $readableParams - * @dataProvider getPathDataProvider - */ - public function testGetUrl($filePath, $miscParams, $readableParams) - { - $imageModel = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'context' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => $filePath, - 'assetRepo' => $this->assetRepo, - 'miscParams' => $miscParams - ] - ); - $absolutePath = 'http://localhost/pub/media/catalog/product'; - $hashPath = 'somehash'; - $this->context->expects(static::once())->method('getBaseUrl')->willReturn($absolutePath); - $this->encryptor->expects(static::once()) - ->method('hash') - ->with($readableParams, $this->anything()) - ->willReturn($hashPath); - static::assertEquals( - $absolutePath . '/cache/' . $hashPath . $filePath, - $imageModel->getUrl() - ); - } - - /** - * @return array - */ - public function getPathDataProvider() - { - return [ - [ - '/some_file.png', - [], //default value for miscParams, - 'h:empty_w:empty_q:empty_r:empty_nonproportional_noframe_notransparency_notconstrainonly_nobackground', - ], - [ - '/some_file_2.png', - [ - 'image_type' => 'thumbnail', - 'image_height' => 75, - 'image_width' => 75, - 'keep_aspect_ratio' => true, - 'keep_frame' => true, - 'keep_transparency' => true, - 'constrain_only' => true, - 'background' => [233,1,0], - 'angle' => null, - 'quality' => 80, - ], - 'h:75_w:75_proportional_frame_transparency_doconstrainonly_rgb233,1,0_r:empty_q:80', - ], - [ - '/some_file_3.png', - [ - 'image_type' => 'thumbnail', - 'image_height' => 75, - 'image_width' => 75, - 'keep_aspect_ratio' => false, - 'keep_frame' => false, - 'keep_transparency' => false, - 'constrain_only' => false, - 'background' => [233,1,0], - 'angle' => 90, - 'quality' => 80, - ], - 'h:75_w:75_nonproportional_noframe_notransparency_notconstrainonly_rgb233,1,0_r:90_q:80', - ], - ]; - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php index f32a7513f236b..401f16831e75a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php @@ -141,10 +141,10 @@ public function testGetUrl($imageType, $placeholderPath) if ($placeholderPath == null) { $this->imageContext->expects($this->never())->method('getBaseUrl'); - $expectedResult = 'http://localhost/pub/media/catalog/product/to_default/placeholder/by_type'; + $expectedResult = 'http://localhost/media/catalog/product/to_default/placeholder/by_type'; $this->repository->expects($this->any())->method('getUrl')->willReturn($expectedResult); } else { - $baseUrl = 'http://localhost/pub/media/catalog/product'; + $baseUrl = 'http://localhost/media/catalog/product'; $this->imageContext->expects($this->any())->method('getBaseUrl')->willReturn($baseUrl); $expectedResult = $baseUrl . DIRECTORY_SEPARATOR . $imageModel->getModule() diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php index 605a5e4fd5e3b..457408e0934af 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php @@ -104,9 +104,6 @@ public function testGet() ->method('create') ->willReturn($image); - $imageHelper->expects($this->once()) - ->method('getResizedImageInfo') - ->willReturn([11, 11]); $this->state->expects($this->once()) ->method('emulateAreaCode') ->with( @@ -116,12 +113,14 @@ public function testGet() ) ->willReturn($imageHelper); + $width = 5; + $height = 10; $imageHelper->expects($this->once()) ->method('getHeight') - ->willReturn(10); + ->willReturn($height); $imageHelper->expects($this->once()) ->method('getWidth') - ->willReturn(10); + ->willReturn($width); $imageHelper->expects($this->once()) ->method('getLabel') ->willReturn('Label'); @@ -137,10 +136,10 @@ public function testGet() ->with(); $image->expects($this->once()) ->method('setResizedHeight') - ->with(11); + ->with($height); $image->expects($this->once()) ->method('setResizedWidth') - ->with(11); + ->with($width); $productRenderInfoDto->expects($this->once()) ->method('setImages') 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 2324ca27ffaaf..2d4f1566a5b6e 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 @@ -118,18 +118,14 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ [$product, $imageCode, (int) $productRender->getStoreId(), $image] ); - try { - $resizedInfo = $helper->getResizedImageInfo(); - } catch (NotLoadInfoImageException $exception) { - $resizedInfo = [$helper->getWidth(), $helper->getHeight()]; - } - $image->setCode($imageCode); - $image->setHeight($helper->getHeight()); - $image->setWidth($helper->getWidth()); + $height = $helper->getHeight(); + $image->setHeight($height); + $width = $helper->getWidth(); + $image->setWidth($width); $image->setLabel($helper->getLabel()); - $image->setResizedHeight($resizedInfo[1]); - $image->setResizedWidth($resizedInfo[0]); + $image->setResizedHeight($height); + $image->setResizedWidth($width); $images[] = $image; } diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 8f8a5f36e516c..4e10453f542bb 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -214,6 +214,13 @@ <source_model>Magento\Catalog\Model\Config\Source\LayoutList</source_model> </field> </group> + <group id="url"> + <field id="catalog_media_url_format" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Catalog media URL format</label> + <source_model>Magento\Catalog\Model\Config\Source\Web\CatalogMediaUrlFormat</source_model> + <comment><![CDATA[Images should be optimized based on query parameters by your CDN or web server. Use the legacy mode for backward compatibility. <a href="https://docs.magento.com/m2/ee/user_guide/configuration/general/web.html#url-options">Learn more</a> about catalog URL formats.<br/><br/><strong style="color:red">Warning!</strong> If you switch back to legacy mode, you must <a href="https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/themes/theme-images.html#resize-catalog-images">use the CLI to regenerate images</a>.]]></comment> + </field> + </group> </section> <section id="system" translate="label" type="text" sortOrder="900" showInDefault="1" showInWebsite="1" showInStore="1"> <class>separator-top</class> diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index aa689c7dd35b2..b8ab4e32ec161 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -67,7 +67,8 @@ <media_storage_configuration> <allowed_resources> <tmp_images_folder>tmp</tmp_images_folder> - <catalog_product_images>media/catalog/product/cache/</catalog_product_images> + <catalog_product_images>media/catalog/product/</catalog_product_images> + <catalog_product_images_tmp>media/tmp/catalog/product/</catalog_product_images_tmp> <catalog_images_folder>catalog</catalog_images_folder> <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> @@ -83,6 +84,11 @@ <thumbnail_position>stretch</thumbnail_position> </watermark> </design> + <web> + <url> + <catalog_media_url_format>hash</catalog_media_url_format> + </url> + </web> <general> <validator_data> <input_types> diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php index ac60420713b26..617c8663d6f80 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php @@ -30,7 +30,7 @@ public function __construct( \Magento\Framework\Filesystem $filesystem ) { $this->_filesystem = $filesystem; - parent::__construct($entityFactory); + parent::__construct($entityFactory, $filesystem); } /** diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php index 33bf352adf6c5..0ba3fada2a072 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php @@ -192,12 +192,12 @@ public function testGetConfig($data, $isAuthorizationAllowed, $expectedResults) ->willReturn('localhost/index.php/'); $this->filesystemMock->expects($this->once()) ->method('getUri') - ->willReturn('pub/static'); + ->willReturn('static'); /** @var ContextInterface|MockObject $contextMock */ $contextMock = $this->getMockForAbstractClass(ContextInterface::class); $contextMock->expects($this->once()) ->method('getBaseUrl') - ->willReturn('localhost/pub/static/'); + ->willReturn('localhost/static/'); $this->assetRepoMock->expects($this->once()) ->method('getStaticViewFileContext') ->willReturn($contextMock); @@ -217,8 +217,8 @@ public function testGetConfig($data, $isAuthorizationAllowed, $expectedResults) $config = $this->wysiwygConfig->getConfig($data); $this->assertInstanceOf(DataObject::class, $config); $this->assertEquals($expectedResults[0], $config->getData('someData')); - $this->assertEquals('localhost/pub/static/', $config->getData('baseStaticUrl')); - $this->assertEquals('localhost/pub/static/', $config->getData('baseStaticDefaultUrl')); + $this->assertEquals('localhost/static/', $config->getData('baseStaticUrl')); + $this->assertEquals('localhost/static/', $config->getData('baseStaticDefaultUrl')); } /** 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 e6acd431be3d5..1763a6d1800a1 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php @@ -10,6 +10,7 @@ namespace Magento\Config\Model\Config\Backend\Admin; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; /** @@ -36,7 +37,6 @@ class Robots extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param DocumentRoot $documentRoot */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,13 +46,11 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [], - \Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot $documentRoot = null + array $data = [] ) { parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); - $documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); - $this->_directory = $filesystem->getDirectoryWrite($documentRoot->getPath()); + $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::PUB); $this->_file = 'robots.txt'; } diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index c16faea284296..c596f8c313ab3 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -233,7 +233,8 @@ public function moveTemporaryFile($fileName) ); } catch (\Exception $e) { throw new \Magento\Framework\Exception\LocalizedException( - __('Something went wrong while saving the file.') + __('Something went wrong while saving the file.'), + $e ); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php index 62964a311af42..e1c771d79694e 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -11,10 +11,13 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Model\FileProcessor; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Url\EncoderInterface; use Magento\Framework\UrlInterface; use Magento\MediaStorage\Model\File\Uploader; @@ -363,17 +366,73 @@ public function testMoveTemporaryFile() $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; $newPath = $destinationPath . $filePath; + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method('get')->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn(false); + ObjectManager::setInstance($objectManagerMock); + $this->mediaDirectory->expects($this->once()) ->method('renameFile') ->with($path, $newPath) ->willReturn(true); + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); $this->assertEquals('/f/i' . $filePath, $model->moveTemporaryFile($filePath)); } + public function testMoveTemporaryFileNewFileName() + { + $filePath = '/filename.ext1'; + + $destinationPath = 'customer/f/i'; + + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('isWritable') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with($destinationPath) + ->willReturn('/' . $destinationPath); + + $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; + + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method('get')->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturnOnConsecutiveCalls(true, true, false); + ObjectManager::setInstance($objectManagerMock); + + $this->mediaDirectory->expects($this->once()) + ->method('renameFile') + ->with($path, 'customer/f/i/filename_2.ext1') + ->willReturn(true); + + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $this->assertEquals('/f/i/filename_2.ext1', $model->moveTemporaryFile($filePath)); + } + public function testMoveTemporaryFileWithException() { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn(false); + ObjectManager::setInstance($objectManagerMock); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Something went wrong while saving the file'); diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php index 2d0018ff81ee5..816565ff7a905 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php @@ -44,7 +44,7 @@ protected function setUp(): void public function testProcess() { - $url = 'http://magento.local/pub/static/'; + $url = 'http://magento.local/static/'; $locale = 'en_US'; $css = '@import url("{{base_url_path}}frontend/_view/{{locale}}/css/email.css");'; $expectedCss = '@import url("' . $url . 'frontend/_view/' . $locale . '/css/email.css");'; diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php index fc8a0756a7b55..4946cd1092ff7 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php @@ -129,7 +129,7 @@ public function executeDataProvider(): array [ 'targetFolder' => 'media/catalog', 'type' => 'image', - 'absolutePath' => 'root/pub/media/catalog/test-image.jpeg' + 'absolutePath' => 'root/media/catalog/test-image.jpeg' ] ]; } diff --git a/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php b/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php index 6a65fff7c5ebc..30d0573b62d87 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php @@ -94,7 +94,7 @@ public function testGetMediaGalleryDataJson() $data = [ [ 'media_type' => 'external-video', - 'video_url' => 'http://magento.ce/pub/media/catalog/product/9/b/9br6ujuthnc.jpg', + 'video_url' => 'http://magento.ce/media/catalog/product/9/b/9br6ujuthnc.jpg', 'is_base' => true, ], [ diff --git a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php index 519a8cba014f2..75e3efd6c599a 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php @@ -99,11 +99,20 @@ class RetrieveImageTest extends TestCase */ private $fileDriverMock; - /** - * Set up - */ + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $objectManager = new ObjectManager($this); $this->contextMock = $this->createMock(Context::class); $this->validatorMock = $this diff --git a/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php new file mode 100644 index 0000000000000..59e21a9c237d0 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\MediaStorage\Model\File\Storage\Synchronization; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\Framework\Filesystem\DriverPool as LocalDriverPool; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Filesystem; + +/** + * Modifies the base URL. + */ +class MediaStorage +{ + /** + * @var bool + */ + private $isEnabled; + + /** + * @var WriteInterface + */ + private $remoteDir; + + /** + * @var WriteInterface + */ + private $localDir; + + /** + * @param Config $config + * @param Filesystem $filesystem + * @throws FileSystemException + * @throws RuntimeException + */ + public function __construct(Config $config, Filesystem $filesystem) + { + $this->isEnabled = $config->isEnabled(); + $this->remoteDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, RemoteDriverPool::REMOTE); + $this->localDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, LocalDriverPool::FILE); + } + + /** + * Download remote file + * @param Synchronization $subject + * @param string $relativeFileName + * @return null + * @throws FileSystemException + * @throws ValidatorException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSynchronize(Synchronization $subject, string $relativeFileName) + { + if ($this->isEnabled) { + if ($this->remoteDir->isExist($relativeFileName)) { + $file = $this->localDir->openFile($relativeFileName, 'w'); + try { + $file->lock(); + $file->write($this->remoteDir->readFile($relativeFileName)); + $file->unlock(); + $file->close(); + } catch (FileSystemException $e) { + $file->close(); + } + } + } + return null; + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index d82c47a7caf5e..c55923f6e2109 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -9,7 +9,8 @@ "magento/module-backend": "*", "magento/module-sitemap": "*", "magento/module-cms": "*", - "magento/module-downloadable": "*" + "magento/module-downloadable": "*", + "magento/module-media-storage": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index 586f07fc9ca83..fe16d1d4afca5 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -74,6 +74,9 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> + <type name="Magento\MediaStorage\Model\File\Storage\Synchronization"> + <plugin name="remote_media" type="Magento\RemoteStorage\Plugin\MediaStorage" /> + </type> <type name="Magento\Framework\Data\Collection\Filesystem"> <arguments> <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> diff --git a/app/code/Magento/RemoteStorage/etc/module.xml b/app/code/Magento/RemoteStorage/etc/module.xml index 6c1b7f0b05a34..c06658c11ea90 100644 --- a/app/code/Magento/RemoteStorage/etc/module.xml +++ b/app/code/Magento/RemoteStorage/etc/module.xml @@ -11,6 +11,7 @@ <module name="Magento_Backend"/> <module name="Magento_Sitemap"/> <module name="Magento_Store"/> + <module name="Magento_MediaStorage"/> </sequence> </module> </config> diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php index 116a574b7c670..26f1f9cd6f56f 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php @@ -61,7 +61,7 @@ public function testGetItems(array $products) */ public function productProvider() { - $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; + $storeBaseMediaUrl = 'http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; return [ [ [ diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index bfd2c47164cf6..866b3afd322a0 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -533,7 +533,7 @@ protected function getModelMock($mockBeforeSave = false) $methods[] = 'beforeSave'; } - $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; + $storeBaseMediaUrl = 'http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; $this->itemProviderMock->expects($this->any()) ->method('getItems') diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml index ff8087a52e42f..03cfdaaead18a 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml @@ -13,18 +13,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml index 93b9e159d4b04..f9913d5070fbd 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml @@ -31,18 +31,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php index c17e2846e22df..0622869c0b963 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php @@ -136,10 +136,10 @@ public function testGetStoreConfigs() $secureBaseUrl = 'https://magento/base_url'; $baseLinkUrl = 'http://magento/base_url/links'; $secureBaseLinkUrl = 'https://magento/base_url/links'; - $baseStaticUrl = 'http://magento/base_url/pub/static'; + $baseStaticUrl = 'http://magento/base_url/static'; $secureBaseStaticUrl = 'https://magento/base_url/static'; - $baseMediaUrl = 'http://magento/base_url/pub/media'; - $secureBaseMediaUrl = 'https://magento/base_url/pub/media'; + $baseMediaUrl = 'http://magento/base_url/media'; + $secureBaseMediaUrl = 'https://magento/base_url/media'; $locale = 'en_US'; $timeZone = 'America/Los_Angeles'; $baseCurrencyCode = 'USD'; diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index d2cd1baca894b..dd257de331b91 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -310,7 +310,7 @@ private function addFilterByParent(ProductCollection $productCollection, $parent * Method getting full media gallery for current Product * * Array structure: [ - * ['image'] => 'http://url/pub/media/catalog/product/2/0/blabla.jpg', + * ['image'] => 'http://url/media/catalog/product/2/0/blabla.jpg', * ['mediaGallery'] => [ * galleryImageId1 => simpleProductImage1.jpg, * galleryImageId2 => simpleProductImage2.jpg, diff --git a/app/code/Magento/Swatches/Helper/Media.php b/app/code/Magento/Swatches/Helper/Media.php index f3694515ecb26..bfcb354b41dfb 100644 --- a/app/code/Magento/Swatches/Helper/Media.php +++ b/app/code/Magento/Swatches/Helper/Media.php @@ -6,8 +6,9 @@ namespace Magento\Swatches\Helper; use Magento\Catalog\Helper\Image; -use Magento\Framework\App\Area; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; /** * Helper to move images from tmp to catalog directory @@ -72,6 +73,11 @@ class Media extends \Magento\Framework\App\Helper\AbstractHelper */ private $imageConfig; + /** + * @var string + */ + private $mediaUrlFormat; + /** * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param \Magento\Framework\Filesystem $filesystem @@ -80,6 +86,8 @@ class Media extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Image\Factory $imageFactory * @param \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection * @param \Magento\Framework\View\ConfigInterface $configInterface + * @param CatalogMediaConfig $catalogMediaConfig + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Catalog\Model\Product\Media\Config $mediaConfig, @@ -88,7 +96,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Image\Factory $imageFactory, \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection, - \Magento\Framework\View\ConfigInterface $configInterface + \Magento\Framework\View\ConfigInterface $configInterface, + CatalogMediaConfig $catalogMediaConfig = null ) { $this->mediaConfig = $mediaConfig; $this->fileStorageDb = $fileStorageDb; @@ -97,6 +106,9 @@ public function __construct( $this->imageFactory = $imageFactory; $this->themeCollection = $themeCollection; $this->viewConfig = $configInterface; + + $catalogMediaConfig = $catalogMediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaUrlFormat = $catalogMediaConfig->getMediaUrlFormat(); } /** @@ -106,17 +118,35 @@ public function __construct( */ public function getSwatchAttributeImage($swatchType, $file) { - $generationPath = $swatchType . '/' . $this->getFolderNameSize($swatchType) . $file; - $absoluteImagePath = $this->mediaDirectory - ->getAbsolutePath($this->getSwatchMediaPath() . '/' . $generationPath); - if (!file_exists($absoluteImagePath)) { - try { - $this->generateSwatchVariations($file); - } catch (\Exception $e) { - return ''; + $basePath = $this->getSwatchMediaUrl(); + + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $generationPath = $swatchType . '/' . $this->getFolderNameSize($swatchType) . $file; + $absoluteImagePath = $this->mediaDirectory + ->getAbsolutePath($this->getSwatchMediaPath() . '/' . $generationPath); + if (!$this->mediaDirectory->isExist(($absoluteImagePath))) { + try { + $this->generateSwatchVariations($file); + } catch (\Exception $e) { + return ''; + } } + + return $basePath . '/' . $generationPath; } - return $this->getSwatchMediaUrl() . '/' . $generationPath; + + return $basePath . '/' . $this->getRelativeTransformationParametersPath($swatchType, $file); + } + + private function getRelativeTransformationParametersPath($swatchType, $file) + { + $imageConfig = $this->getImageConfig(); + return $this->prepareFile($file) . '?' . http_build_query([ + 'width' => $imageConfig[$swatchType]['width'], + 'height' => $imageConfig[$swatchType]['height'], + 'store' => $this->storeManager->getStore()->getCode(), + 'image-type' => $swatchType + ]); } /** @@ -156,7 +186,7 @@ public function moveImageFromTmp($file) /** * Check whether file to move exists. Getting unique name * - * @param <type> $file + * @param string $file * @return string */ protected function getUniqueFileName($file) @@ -168,13 +198,18 @@ protected function getUniqueFileName($file) ); } else { $destFile = dirname($file) . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( - $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($file)) + $this->getOriginalFilePath($file) ); } return $destFile; } + private function getOriginalFilePath($file) + { + return $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($file)); + } + /** * Generate swatch thumb and small swatch image * @@ -183,16 +218,19 @@ protected function getUniqueFileName($file) */ public function generateSwatchVariations($imageUrl) { - $absoluteImagePath = $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($imageUrl)); - foreach ($this->swatchImageTypes as $swatchType) { - $imageConfig = $this->getImageConfig(); - $swatchNamePath = $this->generateNamePath($imageConfig, $imageUrl, $swatchType); - $image = $this->imageFactory->create($absoluteImagePath); - $this->setupImageProperties($image); - $image->resize($imageConfig[$swatchType]['width'], $imageConfig[$swatchType]['height']); - $this->setupImageProperties($image, true); - $image->save($swatchNamePath['path_for_save'], $swatchNamePath['name']); + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $absoluteImagePath = $this->getOriginalFilePath($imageUrl); + foreach ($this->swatchImageTypes as $swatchType) { + $imageConfig = $this->getImageConfig(); + $swatchNamePath = $this->generateNamePath($imageConfig, $imageUrl, $swatchType); + $image = $this->imageFactory->create($absoluteImagePath); + $this->setupImageProperties($image); + $image->resize($imageConfig[$swatchType]['width'], $imageConfig[$swatchType]['height']); + $this->setupImageProperties($image, true); + $image->save($swatchNamePath['path_for_save'], $swatchNamePath['name']); + } } + return $this; } @@ -281,7 +319,7 @@ protected function prepareFileName($imageUrl) } /** - * Url type http://url/pub/media/attribute/swatch/ + * Url type http://url/media/attribute/swatch/ * * @return string */ diff --git a/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php b/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php index e4988bdf9308c..9e9978b499150 100644 --- a/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php @@ -7,13 +7,16 @@ namespace Magento\Swatches\Test\Unit\Helper; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product\Media\Config; use Magento\Framework\Config\View; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Image; use Magento\Framework\Image\Factory; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Store\Model\Store; @@ -59,8 +62,23 @@ class MediaTest extends TestCase /** @var Media|ObjectManager */ protected $mediaHelperObject; + /** @var CatalogMediaConfig|MockObject */ + private $catalogMediaConfigMock; + + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $objectManager = new ObjectManager($this); $this->mediaConfigMock = $this->createMock(Config::class); @@ -78,6 +96,9 @@ protected function setUp(): void $this->storeMock = $this->createPartialMock(Store::class, ['getBaseUrl']); + $this->catalogMediaConfigMock = $this->createPartialMock(CatalogMediaConfig::class, ['getMediaUrlFormat']); + $this->catalogMediaConfigMock->method('getMediaUrlFormat')->willReturn(CatalogMediaConfig::HASH); + $this->mediaDirectoryMock = $this->createMock(Write::class); $this->fileSystemMock = $this->createPartialMock(Filesystem::class, ['getDirectoryWrite']); $this->fileSystemMock @@ -94,6 +115,7 @@ protected function setUp(): void 'storeManager' => $this->storeManagerMock, 'imageFactory' => $this->imageFactoryMock, 'configInterface' => $this->viewConfigMock, + 'catalogMediaConfig' => $this->catalogMediaConfigMock, ] ); } @@ -112,7 +134,7 @@ public function testGetSwatchAttributeImage($swatchType, $expectedResult) ->expects($this->once()) ->method('getBaseUrl') ->with('media') - ->willReturn('http://url/pub/media/'); + ->willReturn('http://url/media/'); $this->generateImageConfig(); @@ -120,7 +142,7 @@ public function testGetSwatchAttributeImage($swatchType, $expectedResult) $result = $this->mediaHelperObject->getSwatchAttributeImage($swatchType, '/f/i/file.png'); - $this->assertEquals($result, $expectedResult); + $this->assertEquals($expectedResult, $result); } /** @@ -131,11 +153,11 @@ public function dataForFullPath() return [ [ 'swatch_image', - 'http://url/pub/media/attribute/swatch/swatch_image/30x20/f/i/file.png', + 'http://url/media/attribute/swatch/swatch_image/30x20/f/i/file.png', ], [ 'swatch_thumb', - 'http://url/pub/media/attribute/swatch/swatch_thumb/110x90/f/i/file.png', + 'http://url/media/attribute/swatch/swatch_thumb/110x90/f/i/file.png', ], ]; } @@ -153,6 +175,10 @@ public function testMoveImageFromTmpNoDb() { $this->fileStorageDbMock->method('checkDbUsage')->willReturn(false); $this->fileStorageDbMock->method('renameFile')->willReturnSelf(); + $this->mediaDirectoryMock + ->expects($this->atLeastOnce()) + ->method('getAbsolutePath') + ->willReturn('attribute/swatch/f/i/file.tmp'); $result = $this->mediaHelperObject->moveImageFromTmp('file.tmp'); $this->assertNotNull($result); } @@ -177,7 +203,7 @@ public function testGenerateSwatchVariations() $this->imageFactoryMock->expects($this->any())->method('create')->willReturn($image); $this->generateImageConfig(); - $image->expects($this->any())->method('resize')->willReturnSelf(); + $image->method('resize')->willReturnSelf(); $image->expects($this->atLeastOnce())->method('backgroundColor')->with([255, 255, 255])->willReturnSelf(); $this->mediaHelperObject->generateSwatchVariations('/e/a/earth.png'); } @@ -195,11 +221,11 @@ public function testGetSwatchMediaUrl() ->expects($this->once()) ->method('getBaseUrl') ->with('media') - ->willReturn('http://url/pub/media/'); + ->willReturn('http://url/media/'); $result = $this->mediaHelperObject->getSwatchMediaUrl(); - $this->assertEquals($result, 'http://url/pub/media/attribute/swatch'); + $this->assertEquals($result, 'http://url/media/attribute/swatch'); } /** @@ -282,7 +308,7 @@ protected function generateImageConfig() ], ]; - $configMock->expects($this->any())->method('getMediaEntities')->willReturn($imageConfig); + $configMock->method('getMediaEntities')->willReturn($imageConfig); } public function testGetAttributeSwatchPath() diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php index 0bbf35e244241..1978362810763 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php @@ -35,7 +35,7 @@ public function testGetLogoSrc() )->method( 'getBaseUrl' )->willReturn( - 'http://localhost/pub/media/' + 'http://localhost/media/' ); $mediaDirectory->expects($this->any())->method('isFile')->willReturn(true); @@ -53,7 +53,7 @@ public function testGetLogoSrc() ]; $block = $objectManager->getObject(Logo::class, $arguments); - $this->assertEquals('http://localhost/pub/media/logo/default/image.gif', $block->getLogoSrc()); + $this->assertEquals('http://localhost/media/logo/default/image.gif', $block->getLogoSrc()); } /** 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 78a56013ae042..691a94e37e932 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 @@ -195,7 +195,7 @@ public function testAfterLoad() $this->urlBuilder->expects($this->once()) ->method('getBaseUrl') ->with(['_type' => UrlInterface::URL_TYPE_MEDIA]) - ->willReturn('http://magento2.com/pub/media/'); + ->willReturn('http://magento2.com/media/'); $this->mediaDirectory->expects($this->once()) ->method('getRelativePath') ->with('value') @@ -212,7 +212,7 @@ public function testAfterLoad() $this->assertEquals( [ [ - 'url' => 'http://magento2.com/pub/media/design/file/' . $value, + 'url' => 'http://magento2.com/media/design/file/' . $value, 'file' => $value, 'size' => 234234, 'exists' => true, @@ -241,7 +241,7 @@ public function testBeforeSave(string $fileName) 'scope_id' => 1, 'value' => [ [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $fileName, + 'url' => 'http://magento2.com/media/tmp/image/' . $fileName, 'file' => $fileName, 'size' => 234234, ] @@ -314,7 +314,7 @@ public function testBeforeSaveWithExistingFile() [ 'value' => [ [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $value, + 'url' => 'http://magento2.com/media/tmp/image/' . $value, 'file' => $value, 'size' => 234234, 'exists' => true @@ -358,7 +358,7 @@ public function getRelativeMediaPathDataProvider(): array { return [ 'Normal path' => ['pub/media/', 'filename.jpg'], - 'Complex path' => ['some_path/pub/media/', 'filename.jpg'], + 'Complex path' => ['some_path/media/', 'filename.jpg'], ]; } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php index ab7d622801f63..c16d7a49a7e6f 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php @@ -111,7 +111,7 @@ public function testSaveToTmp() $this->store->expects($this->once()) ->method('getBaseUrl') ->with(UrlInterface::URL_TYPE_MEDIA) - ->willReturn('http://magento2.com/pub/media/'); + ->willReturn('http://magento2.com/media/'); $this->directoryWrite->expects($this->once()) ->method('getAbsolutePath') ->with('tmp/' . FileProcessor::FILE_DIR) @@ -160,7 +160,7 @@ public function testSaveToTmp() 'name' => 'file.jpg', 'size' => '234234', 'type' => 'image/jpg', - 'url' => 'http://magento2.com/pub/media/tmp/' . FileProcessor::FILE_DIR . '/file.jpg' + 'url' => 'http://magento2.com/media/tmp/' . FileProcessor::FILE_DIR . '/file.jpg' ], $this->fileProcessor->saveToTmp($fieldCode) ); diff --git a/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php b/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php index 77cf71f75ac28..0ccaf9e65b675 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php @@ -105,7 +105,7 @@ public function testGetFaviconFileNegative() public function testGetFaviconFile() { $scopeConfigValue = 'path'; - $urlToMediaDir = 'http://magento.url/pub/media/'; + $urlToMediaDir = 'http://magento.url/media/'; $expectedFile = ImageFavicon::UPLOAD_DIR . '/' . $scopeConfigValue; $expectedUrl = $urlToMediaDir . $expectedFile; diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html index 1bff60064b983..cbb00f379a655 100644 --- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html +++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<img class = 'admin__control-thumbnail' data-bind="attr: {src: $data.value}"> +<img class = 'admin__control-thumbnail' style="max-height: 75px; max-width: 75px;" data-bind="attr: {src: $data.value}"> diff --git a/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php index 7afc9dc93f46e..6a23b5c66e5ba 100644 --- a/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php @@ -267,7 +267,7 @@ public function testMediaDirective() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; $this->storeMock->expects($this->once()) ->method('getBaseUrl') @@ -285,7 +285,7 @@ public function testMediaDirectiveWithEncodedQuotes() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; $this->storeMock->expects($this->once()) ->method('getBaseUrl') diff --git a/app/code/Magento/Wishlist/CustomerData/Wishlist.php b/app/code/Magento/Wishlist/CustomerData/Wishlist.php index ae54289d4b1c9..2f6b57a8650c4 100644 --- a/app/code/Magento/Wishlist/CustomerData/Wishlist.php +++ b/app/code/Magento/Wishlist/CustomerData/Wishlist.php @@ -68,7 +68,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -80,6 +80,8 @@ public function getSectionData() } /** + * Get counter + * * @return string */ protected function getCounter() @@ -156,7 +158,6 @@ protected function getItemData(\Magento\Wishlist\Model\Item $wishlistItem) * * @param \Magento\Catalog\Model\Product $product * @return array - * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function getImageData($product) { @@ -164,27 +165,11 @@ protected function getImageData($product) $helper = $this->imageHelperFactory->create() ->init($product, 'wishlist_sidebar_block'); - $template = 'Magento_Catalog/product/image_with_borders'; - - try { - $imagesize = $helper->getResizedImageInfo(); - } catch (NotLoadInfoImageException $exception) { - $imagesize = [$helper->getWidth(), $helper->getHeight()]; - } - - $width = $helper->getFrame() - ? $helper->getWidth() - : $imagesize[0]; - - $height = $helper->getFrame() - ? $helper->getHeight() - : $imagesize[1]; - return [ - 'template' => $template, + 'template' => 'Magento_Catalog/product/image_with_borders', 'src' => $helper->getUrl(), - 'width' => $width, - 'height' => $height, + 'width' => $helper->getWidth(), + 'height' => $helper->getHeight(), 'alt' => $helper->getLabel(), ]; } diff --git a/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php index 79ab3c9ba2082..0a1e40253b71c 100644 --- a/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php @@ -199,9 +199,6 @@ public function testGetSectionData() $this->catalogImageHelperMock->expects($this->any()) ->method('getFrame') ->willReturn(true); - $this->catalogImageHelperMock->expects($this->once()) - ->method('getResizedImageInfo') - ->willReturn([]); $this->wishlistHelperMock->expects($this->once()) ->method('getProductUrl') @@ -400,9 +397,6 @@ public function testGetSectionDataWithTwoItems() $this->catalogImageHelperMock->expects($this->any()) ->method('getFrame') ->willReturn(true); - $this->catalogImageHelperMock->expects($this->exactly(2)) - ->method('getResizedImageInfo') - ->willReturn([]); $this->wishlistHelperMock->expects($this->exactly(2)) ->method('getProductUrl') diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php index 641253cc34c2c..dbbeaebc15936 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php @@ -649,7 +649,7 @@ public function categoryImageDataProvider(): array 'image_prefix' => '' ], 'with_pub_media_strategy' => [ - 'image_prefix' => '/pub/media/catalog/category/' + 'image_prefix' => '/media/catalog/category/' ], 'catalog_category_strategy' => [ 'image_prefix' => 'catalog/category/' diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index f086a2211b51d..b747e78651955 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -673,7 +673,7 @@ public function categoryImageDataProvider(): array 'image_prefix' => '' ], 'with_pub_media_strategy' => [ - 'image_prefix' => '/pub/media/catalog/category/' + 'image_prefix' => '/media/catalog/category/' ], 'catalog_category_strategy' => [ 'image_prefix' => 'catalog/category/' diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index 7a999f1d205f2..7e94484961f9e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -73,11 +73,11 @@ public function testGetImagesJson(bool $isProductNew) $imagesJson = $this->block->getImagesJson(); $images = json_decode($imagesJson); $image = array_shift($images); - $this->assertMatchesRegularExpression('/\/m\/a\/magento_image/', $image->file); + $this->assertMatchesRegularExpression('~/m/a/magento_image~', $image->file); $this->assertSame('image', $image->media_type); $this->assertSame('Image Alt Text', $image->label); $this->assertSame('Image Alt Text', $image->label_default); - $this->assertMatchesRegularExpression('/\/pub\/media\/catalog\/product\/m\/a\/magento_image/', $image->url); + $this->assertMatchesRegularExpression('~/media/catalog/product/m/a/magento_image~', $image->url); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php index e5c6b1f8c1dd6..b57969280cdf3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php @@ -120,9 +120,23 @@ public function testGetGalleryImagesJsonWithoutImages(): void $this->assertImages(reset($result), $this->placeholderExpectation); } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @magentoDbIsolation enabled + * @return void + */ + public function testGetGalleryImagesJsonWithoutImagesWithImageOptimizationParametersInUrl(): void + { + $this->block->setData('product', $this->getProduct()); + $result = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + $this->assertImages(reset($result), $this->placeholderExpectation); + } + /** * @dataProvider galleryDisabledImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation enabled * @param array $images * @param array $expectation @@ -141,6 +155,7 @@ public function testGetGalleryImagesJsonWithDisabledImage(array $images, array $ * @dataProvider galleryDisabledImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation disabled * @param array $images * @param array $expectation @@ -173,6 +188,8 @@ public function galleryDisabledImagesDataProvider(): array } /** + * Test default image generation format. + * * @dataProvider galleryImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDbIsolation enabled @@ -230,10 +247,95 @@ public function galleryImagesDataProvider(): array ]; } + /** + * @dataProvider galleryImagesWithImageOptimizationParametersInUrlDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @magentoDbIsolation enabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testGetGalleryImagesJsonWithImageOptimizationParametersInUrl( + array $images, + array $expectation + ): void { + $product = $this->getProduct(); + $this->setGalleryImages($product, $images); + $this->block->setData('product', $this->getProduct()); + [$firstImage, $secondImage] = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + [$firstExpectedImage, $secondExpectedImage] = $expectation; + $this->assertImages($firstImage, $firstExpectedImage); + $this->assertImages($secondImage, $secondExpectedImage); + } + + /** + * @return array + */ + public function galleryImagesWithImageOptimizationParametersInUrlDataProvider(): array + { + + $imageExpectation = [ + 'thumb' => '/m/a/magento_image.jpg?width=88&height=110&store=default&image-type=thumbnail', + 'img' => '/m/a/magento_image.jpg?width=700&height=700&store=default&image-type=image', + 'full' => '/m/a/magento_image.jpg?store=default&image-type=image', + 'caption' => 'Image Alt Text', + 'position' => '1', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + $thumbnailExpectation = [ + 'thumb' => '/m/a/magento_thumbnail.jpg?width=88&height=110&store=default&image-type=thumbnail', + 'img' => '/m/a/magento_thumbnail.jpg?width=700&height=700&store=default&image-type=image', + 'full' => '/m/a/magento_thumbnail.jpg?store=default&image-type=image', + 'caption' => 'Thumbnail Image', + 'position' => '2', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + return [ + 'with_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => ['main' => true], + ], + 'expectation' => [ + $imageExpectation, + array_merge($thumbnailExpectation, ['isMain' => true]), + ], + ], + 'without_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => [ + array_merge($imageExpectation, ['isMain' => true]), + $thumbnailExpectation, + ], + ], + 'with_changed_position' => [ + 'images' => [ + '/m/a/magento_image.jpg' => ['position' => '2'], + '/m/a/magento_thumbnail.jpg' => ['position' => '1'], + ], + 'expectation' => [ + array_merge($thumbnailExpectation, ['position' => '1']), + array_merge($imageExpectation, ['position' => '2', 'isMain' => true]), + ], + ], + ]; + } + /** * @dataProvider galleryImagesOnStoreViewDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation disabled * @param array $images * @param array $expectation diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php index b88980181fb63..283a3834eab59 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php @@ -108,7 +108,7 @@ public function uploadActionDataProvider(): array 'name' => 'magento_image.jpg', 'type' => 'image/jpeg', 'file' => '/m/a/magento_image.jpg.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.jpg', + 'url' => 'http://localhost/media/tmp/catalog/product/m/a/magento_image.jpg', 'tmp_media_path' => '/m/a/magento_image.jpg', ], ], @@ -122,7 +122,7 @@ public function uploadActionDataProvider(): array 'name' => 'product_image.png', 'type' => 'image/png', 'file' => '/p/r/product_image.png.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/p/r/product_image.png', + 'url' => 'http://localhost/media/tmp/catalog/product/p/r/product_image.png', 'tmp_media_path' => '/p/r/product_image.png', ], ], @@ -136,7 +136,7 @@ public function uploadActionDataProvider(): array 'name' => 'magento_image.gif', 'type' => 'image/gif', 'file' => '/m/a/magento_image.gif.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.gif', + 'url' => 'http://localhost/media/tmp/catalog/product/m/a/magento_image.gif', 'tmp_media_path' => '/m/a/magento_image.gif', ], ], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php index 3f9f788dc28c7..a02a2b7aeef92 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php @@ -170,7 +170,7 @@ public function testGalleryAction(): void $this->dispatch(sprintf('catalog/product/gallery/id/%s', $product->getEntityId())); $this->assertStringContainsString( - 'http://localhost/pub/media/catalog/product/', + 'http://localhost/media/catalog/product/', $this->getResponse()->getBody() ); $this->assertStringContainsString($this->getProductImageFile(), $this->getResponse()->getBody()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php index 1c9b8f2ce1918..b741285ebb6f1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php @@ -52,7 +52,7 @@ public function testSaveFilePlaceholder($model) public function testGetUrlPlaceholder($model) { $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', + 'http://localhost/static/%s/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', $model->getUrl() ); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php b/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php index 46eb1e98ddc6a..7d3bf7ec1a1ea 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php @@ -82,7 +82,7 @@ public function testGetImageHtmlDeclaration( public function providerGetImageHtmlDeclaration() { return [ - [true, 'wysiwyg/hello.png', true, '<img src="http://example.com/pub/media/wysiwyg/hello.png" alt="" />'], + [true, 'wysiwyg/hello.png', true, '<img src="http://example.com/media/wysiwyg/hello.png" alt="" />'], [ false, 'wysiwyg/hello.png', @@ -96,7 +96,7 @@ function ($actualResult) { $this->assertStringContainsString($expectedResult, parse_url($actualResult, PHP_URL_PATH)); } ], - [true, 'wysiwyg/hello.png', false, 'http://example.com/pub/media/wysiwyg/hello.png'], + [true, 'wysiwyg/hello.png', false, 'http://example.com/media/wysiwyg/hello.png'], [false, 'wysiwyg/hello.png', true, '<img src="{{media url="wysiwyg/hello.png"}}" alt="" />'], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php index 3d6cbe98cf160..53b9dfee46aac 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php @@ -46,7 +46,7 @@ public function testGetConfig() public function testGetConfigCssUrls() { $config = $this->model->getConfig(); - $publicPathPattern = 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/%s'; + $publicPathPattern = 'http://localhost/static/%s/adminhtml/Magento/backend/en_US/%s'; $tinyMce4Config = $config->getData('tinymce4'); $contentCss = $tinyMce4Config['content_css']; if (is_array($contentCss)) { diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php index 076a669f3f8ad..7ce695cb476fe 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php @@ -105,7 +105,7 @@ public function imageDataProvider(): array true, false, 1, - '/pub/media/catalog/category/test-image.jpg' + '/media/catalog/category/test-image.jpg' ], [ 'test-image.jpg', 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 cb96ca2a14cac..96084981fe0b8 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 @@ -109,11 +109,12 @@ public function testGetFilesCollection(): void $collection = $this->storage->getFilesCollection(self::$_baseDir, 'image'); $this->assertInstanceOf(Collection::class, $collection); foreach ($collection as $item) { + $thumbUrl = parse_url($item->getThumbUrl(), PHP_URL_PATH); $this->assertInstanceOf(DataObject::class, $item); $this->assertStringEndsWith('/' . $fileName, $item->getUrl()); $this->assertEquals( - '/pub/media/.thumbsMagentoCmsModelWysiwygImagesStorageTest/magento_image.jpg', - parse_url($item->getThumbUrl(), PHP_URL_PATH), + '/media/.thumbsMagentoCmsModelWysiwygImagesStorageTest/magento_image.jpg', + $thumbUrl, "Check if Thumbnail URL is equal to the generated URL" ); $this->assertEquals( @@ -387,17 +388,17 @@ public function getThumbnailUrlDataProvider(): array [ '/', 'image1.png', - '/pub/media/.thumbs/image1.png' + '/media/.thumbs/image1.png' ], [ '/cms', 'image2.png', - '/pub/media/.thumbscms/image2.png' + '/media/.thumbscms/image2.png' ], [ '/cms/pages', 'image3.png', - '/pub/media/.thumbscms/pages/image3.png' + '/media/.thumbscms/pages/image3.png' ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php index 8458a26e44659..1fd45ba1c87ba 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php @@ -6,6 +6,8 @@ namespace Magento\Config\Model\Config\Backend\Admin; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; /** * @magentoAppArea adminhtml @@ -34,10 +36,7 @@ protected function setUp(): void $this->model->setPath('design/search_engine_robots/custom_instructions'); $this->model->afterLoad(); - $documentRootPath = $objectManager->get(DocumentRoot::class)->getPath(); - $this->rootDirectory = $objectManager->get( - \Magento\Framework\Filesystem::class - )->getDirectoryRead($documentRootPath); + $this->rootDirectory = $objectManager->get(Filesystem::class)->getDirectoryRead(DirectoryList::PUB); } /** @@ -57,7 +56,8 @@ public function testAfterLoadRobotsTxtNotExists() */ public function testAfterLoadRobotsTxtExists() { - $this->assertEquals('Sitemap: http://store.com/sitemap.xml', $this->model->getValue()); + $value = $this->model->getValue(); + $this->assertEquals('Sitemap: http://store.com/sitemap.xml', $value); } /** @@ -92,7 +92,8 @@ protected function _modifyConfig() { $robotsTxt = "User-Agent: *\nDisallow: /checkout"; $this->model->setValue($robotsTxt)->save(); - $this->assertStringEqualsFile($this->rootDirectory->getAbsolutePath('robots.txt'), $robotsTxt); + $file = $this->rootDirectory->getAbsolutePath('robots.txt'); + $this->assertStringEqualsFile($file, $robotsTxt); } /** diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php b/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php index bbb229221bac3..d840261669992 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php @@ -9,7 +9,7 @@ $rootDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class )->getDirectoryWrite( - DirectoryList::ROOT + DirectoryList::PUB ); if ($rootDirectory->isExist('robots.txt')) { $rootDirectory->delete('robots.txt'); diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php b/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php index c4fb2c92c45a5..3097132b74c2c 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\TestFramework\Helper\Bootstrap; -/** @var \Magento\Framework\Filesystem\Directory\Write $rootDirectory */ -$rootDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Filesystem::class -)->getDirectoryWrite( - DirectoryList::ROOT -); -$rootDirectory->copyFile($rootDirectory->getRelativePath(__DIR__ . '/robots.txt'), 'robots.txt'); +/** @var $fileSystem Filesystem */ +$fileSystem = Bootstrap::getObjectManager()->get(Filesystem::class); +$pubDirectory = $fileSystem->getDirectoryWrite(DirectoryList::PUB); +$rootDirectory = $fileSystem->getDirectoryRead(DirectoryList::ROOT); +$source = $rootDirectory->getAbsolutePath(__DIR__ . '/robots.txt'); +$content = $rootDirectory->readFile(__DIR__ . '/robots.txt'); +$pubDirectory->writeFile('robots.txt', $content); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php b/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php deleted file mode 100644 index c6aeaf9e0f927..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Framework\Console; - -use Magento\Framework\App\DeploymentConfig; -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\Filesystem; -use Magento\Framework\ObjectManagerInterface; -use Magento\TestFramework\Helper\Bootstrap; - -class CliTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ObjectManagerInterface - */ - private $objectManager; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var ConfigFilePool - */ - private $configFilePool; - - /** - * @var FileReader - */ - private $reader; - - /** - * @var Writer - */ - private $writer; - - /** - * @var array - */ - private $envConfig; - - /** - * @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); - - $this->envConfig = $this->reader->load(ConfigFilePool::APP_ENV); - } - - /** - * @inheritdoc - */ - protected function tearDown(): void - { - $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( - $this->configFilePool->getPath(ConfigFilePool::APP_ENV), - "<?php\n return array();\n" - ); - - $this->writer->saveConfig([ConfigFilePool::APP_ENV => $this->envConfig], true); - } - - /** - * Checks that settings from env.php config file are applied - * to created application instance. - * - * @magentoAppIsolation enabled - * @param bool $isPub - * @param array $params - * @dataProvider documentRootIsPubProvider - */ - public function testDocumentRootIsPublic($isPub, $params) - { - $config = include __DIR__ . '/_files/env.php'; - $config['directories']['document_root_is_pub'] = $isPub; - $this->writer->saveConfig([ConfigFilePool::APP_ENV => $config], true); - - $cli = new Cli(); - $cliReflection = new \ReflectionClass($cli); - - $serviceManagerProperty = $cliReflection->getProperty('serviceManager'); - $serviceManagerProperty->setAccessible(true); - $serviceManager = $serviceManagerProperty->getValue($cli); - $deploymentConfig = $this->objectManager->get(DeploymentConfig::class); - $serviceManager->setAllowOverride(true); - $serviceManager->setService(DeploymentConfig::class, $deploymentConfig); - $serviceManagerProperty->setAccessible(false); - - $documentRootResolver = $cliReflection->getMethod('documentRootResolver'); - $documentRootResolver->setAccessible(true); - - self::assertEquals($params, $documentRootResolver->invoke($cli)); - } - - /** - * Provides document root setting and expecting - * properties for object manager creation. - * - * @return array - */ - public function documentRootIsPubProvider(): array - { - return [ - [true, [ - 'MAGE_DIRS' => [ - 'pub' => ['uri' => ''], - 'media' => ['uri' => 'media'], - 'static' => ['uri' => 'static'], - 'upload' => ['uri' => 'media/upload'] - ] - ]], - [false, []] - ]; - } -} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php b/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php deleted file mode 100644 index e314e7638c22c..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -return [ - 'backend' => [ - 'frontName' => 'admin', - ], - 'crypt' => [ - 'key' => 'some_key', - ], - 'session' => [ - 'save' => 'files', - ], - 'db' => [ - 'table_prefix' => '', - 'connection' => [], - ], - 'resource' => [], - 'x-frame-options' => 'SAMEORIGIN', - 'MAGE_MODE' => 'default', - 'cache_types' => [ - 'config' => 1, - 'layout' => 1, - 'block_html' => 1, - 'collections' => 1, - 'reflection' => 1, - 'db_ddl' => 1, - 'eav' => 1, - 'customer_notification' => 1, - 'config_integration' => 1, - 'config_integration_api' => 1, - 'full_page' => 1, - 'translate' => 1, - 'config_webservice' => 1, - ], - 'install' => [ - 'date' => 'Thu, 09 Feb 2017 14:28:00 +0000', - ], - 'directories' => [ - 'document_root_is_pub' => true - ] -]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html index d5b6f35421ac6..ade4f52d5153f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html @@ -30,7 +30,7 @@ height="52" - src="http://magento2.vagrant236/pub/static/version1502812784/frontend/Magento/blank/en_US/Magento_Email/logo_email.png" + src="http://magento2.vagrant236/static/version1502812784/frontend/Magento/blank/en_US/Magento_Email/logo_email.png" alt="Main Website Store" border="0" /> diff --git a/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html b/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html index 573f3b166db35..0afba67d3b031 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html @@ -14,12 +14,12 @@ <div data-translate="[{"shown":"shown_1","translated":"translated_1","original":"original_1","location":"Tag attribute (ALT, TITLE, etc.)","scope":"scope_1"}]"title="some_title_shown_1_in_div"> some_text_<span data-translate="[{"shown":"shown_2","translated":"translated_2","original":"original_2","location":"Text","scope":"scope_2"}]">shown_2</span>_in_div </div> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/prototype/window.js"></script> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/static/frontend/Magento/luma/en_US/prototype/windows/themes/default.css"/> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/prototype/magento.css"/> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/edit-trigger.js"></script> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/translate-inline.js"></script> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/translate-inline.css"/> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/prototype/window.js"></script> +<link rel="stylesheet" type="text/css" href="http://localhost/static/frontend/Magento/luma/en_US/prototype/windows/themes/default.css"/> +<link rel="stylesheet" type="text/css" href="http://localhost/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/prototype/magento.css"/> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/mage/edit-trigger.js"></script> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/mage/translate-inline.js"></script> +<link rel="stylesheet" type="text/css" href="http://localhost/static/frontend/Magento/luma/en_US/mage/translate-inline.css"/> <script type="text/javascript"> (function($){ @@ -27,7 +27,7 @@ $(this).translateInline({ ajaxUrl: 'http://localhost/index.php/translation/ajax/index/', area: 'frontend', - editTrigger: {img: 'http://localhost/pub/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/fam_book_open.png'} + editTrigger: {img: 'http://localhost/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/fam_book_open.png'} }); }); })(jQuery); diff --git a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php index 785637a9470cb..ad4491b166cfe 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php @@ -181,7 +181,7 @@ public function testGetBaseUrlWithTypeRestoring() * Get url with type specified in params */ $mediaUrl = $this->model->getBaseUrl(['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA]); - $this->assertEquals('http://localhost/pub/media/', $mediaUrl, 'Incorrect media url'); + $this->assertEquals('http://localhost/media/', $mediaUrl, 'Incorrect media url'); $this->assertEquals('http://localhost/index.php/', $this->model->getBaseUrl(), 'Incorrect link url'); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php index fa664756d65f1..f584b8f7cfcd3 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php @@ -490,7 +490,7 @@ public function testGetViewFileUrl() { $actualResult = $this->_block->getViewFileUrl('css/styles.css'); $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/frontend/%s/en_US/css/styles.css', + 'http://localhost/static/%s/frontend/%s/en_US/css/styles.css', $actualResult ); } diff --git a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php index 4dfe01eed2d01..3120cf399d96c 100644 --- a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php @@ -17,8 +17,8 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * Base product image path */ - const BASE_IMAGE_PATH = '#http\:\/\/localhost\/pub\/media\/catalog\/product\/cache\/[a-z0-9]{32}:path:#'; - + const BASE_IMAGE_PATH = '#http://localhost/media/catalog/product/cache/[a-z0-9]{32}:path:#'; + /** * Test getCollection None images * 1) Check that image attributes were not loaded @@ -52,6 +52,7 @@ public function testGetCollectionNone() * 3) Check thumbnails when no thumbnail selected * * @magentoConfigFixture default_store sitemap/product/image_include all + * @magentoConfigFixture default/web/url/catalog_media_url_format hash */ public function testGetCollectionAll() { @@ -120,6 +121,7 @@ public function testGetCollectionAll() * 3) Check thumbnails when no thumbnail selected * * @magentoConfigFixture default_store sitemap/product/image_include base + * @magentoConfigFixture default/web/url/catalog_media_url_format hash */ public function testGetCollectionBase() { diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php index 3f7c3c5a9a452..d81a6fa52ea48 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php @@ -169,14 +169,14 @@ public function getBaseUrlDataProvider() [UrlInterface::URL_TYPE_DIRECT_LINK, false, true, 'http://localhost/index.php/'], [UrlInterface::URL_TYPE_DIRECT_LINK, true, false, 'http://localhost/'], [UrlInterface::URL_TYPE_DIRECT_LINK, true, true, 'http://localhost/'], - [UrlInterface::URL_TYPE_STATIC, false, false, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, false, true, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, true, false, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, true, true, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_MEDIA, false, false, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, false, true, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, true, false, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, true, true, 'http://localhost/pub/media/'] + [UrlInterface::URL_TYPE_STATIC, false, false, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, false, true, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, true, false, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, true, true, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_MEDIA, false, false, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, false, true, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, true, false, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, true, true, 'http://localhost/media/'] ]; } @@ -196,8 +196,8 @@ public function testGetBaseUrlInPub() $this->model = $this->_getStoreModel(); $this->model->load('default'); - $this->assertEquals('http://localhost/pub/static/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_STATIC)); - $this->assertEquals('http://localhost/pub/media/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_MEDIA)); + $this->assertEquals('http://localhost/static/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_STATIC)); + $this->assertEquals('http://localhost/media/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_MEDIA)); } /** diff --git a/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php index fc3b0399d0497..d4a14420c4ae6 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php @@ -11,7 +11,7 @@ public function testMediaDirective() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; /** @var \Magento\Widget\Model\Template\Filter $filter */ $filter = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -25,7 +25,7 @@ public function testMediaDirectiveWithEncodedQuotes() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; /** @var \Magento\Widget\Model\Template\Filter $filter */ $filter = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( diff --git a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php index a9d21ec84e32b..fb13ea57475ad 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php @@ -46,7 +46,7 @@ public function testGetPluginSettings() $jsFilename = $plugins['src']; $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/%s/editor_plugin.js', + 'http://localhost/static/%s/adminhtml/Magento/backend/en_US/%s/editor_plugin.js', $jsFilename ); 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 9507e50d71638..43aacecb6982e 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php @@ -125,7 +125,7 @@ public function splitQuote() ); $command = $this->getCliScriptCommand() . ' setup:db-schema:split-quote ' . implode(" ", array_keys($installParams)) . - ' -vvv --magento-init-params="' . + ' -vvv --no-interaction --magento-init-params="' . $initParams['magento-init-params'] . '"'; $this->shell->execute($command, array_values($installParams)); diff --git a/index.php b/index.php deleted file mode 100644 index 9ac7f6ffa71b2..0000000000000 --- a/index.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Application entry point - * - * Example - run a particular store or website: - * -------------------------------------------- - * require __DIR__ . '/app/bootstrap.php'; - * $params = $_SERVER; - * $params[\Magento\Store\Model\StoreManager::PARAM_RUN_CODE] = 'website2'; - * $params[\Magento\Store\Model\StoreManager::PARAM_RUN_TYPE] = 'website'; - * $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $params); - * \/** @var \Magento\Framework\App\Http $app *\/ - * $app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); - * $bootstrap->run($app); - * -------------------------------------------- - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -try { - require __DIR__ . '/app/bootstrap.php'; -} catch (\Exception $e) { - echo <<<HTML -<div style="font:12px/1.35em arial, helvetica, sans-serif;"> - <div style="margin:0 0 25px 0; border-bottom:1px solid #ccc;"> - <h3 style="margin:0;font-size:1.7em;font-weight:normal;text-transform:none;text-align:left;color:#2f2f2f;"> - Autoload error</h3> - </div> - <p>{$e->getMessage()}</p> -</div> -HTML; - exit(1); -} - -$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); -/** @var \Magento\Framework\App\Http $app */ -$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); -$bootstrap->run($app); diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php index 6caf2c0f88dfa..295ac50cf5687 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php @@ -157,12 +157,12 @@ public static function getDefaultConfig() self::DI => [parent::PATH => 'generated/metadata'], self::GENERATION => [parent::PATH => Io::DEFAULT_DIRECTORY], self::SESSION => [parent::PATH => 'var/session'], - self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'pub/media'], - self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'pub/static'], - self::PUB => [parent::PATH => 'pub', parent::URL_PATH => 'pub'], + self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'media'], + self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'static'], + self::PUB => [parent::PATH => 'pub', parent::URL_PATH => ''], self::LIB_WEB => [parent::PATH => 'lib/web'], self::TMP => [parent::PATH => 'var/tmp'], - self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'pub/media/upload'], + self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'media/upload'], self::TMP_MATERIALIZATION_DIR => [parent::PATH => 'var/view_preprocessed/pub/static'], self::TEMPLATE_MINIFICATION_DIR => [parent::PATH => 'var/view_preprocessed'], self::SETUP => [parent::PATH => 'setup/src'], diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php deleted file mode 100644 index 90c32b54f17c5..0000000000000 --- a/lib/internal/Magento/Framework/App/Test/Unit/Config/DocumentRootTest.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Framework\App\Test\Unit\Config; - -use Magento\Framework\App\Config; -use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Config\ConfigOptionsListConstants; -use Magento\Framework\Config\DocumentRoot; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Test class for checking settings that defined in config file - */ -class DocumentRootTest extends TestCase -{ - /** - * @var Config|MockObject - */ - private $configMock; - - /** - * @var DocumentRoot - */ - private $documentRoot; - - protected function setUp(): void - { - $this->configMock = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->documentRoot = new DocumentRoot($this->configMock); - } - - /** - * Ensures that the path returned matches the pub/ path. - */ - public function testGetPath() - { - $this->configMockSetForDocumentRootIsPub(); - - $this->assertSame(DirectoryList::PUB, $this->documentRoot->getPath()); - } - - /** - * Ensures that the deployment configuration returns the mocked value for - * the pub/ folder. - */ - public function testIsPub() - { - $this->configMockSetForDocumentRootIsPub(); - - $this->assertTrue($this->documentRoot->isPub()); - } - - private function configMockSetForDocumentRootIsPub() - { - $this->configMock->expects($this->any()) - ->method('get') - ->willReturnMap([ - [ - ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, - null, - true - ], - ]); - } -} diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php index 45ccc34f0ce5b..363a48d822ace 100644 --- a/lib/internal/Magento/Framework/Config/DocumentRoot.php +++ b/lib/internal/Magento/Framework/Config/DocumentRoot.php @@ -10,7 +10,7 @@ /** * Document root detector. - * + * @deprecared Magento always uses the pub directory * @api */ class DocumentRoot @@ -35,7 +35,7 @@ public function __construct(DeploymentConfig $config) */ public function getPath(): string { - return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; + return DirectoryList::PUB; } /** @@ -45,6 +45,6 @@ public function getPath(): string */ public function isPub(): bool { - return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); + return true; } } diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index f22c452549a78..c7192e7dfbb33 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -174,7 +174,6 @@ private function initObjectManager() { $params = (new ComplexParameter(self::INPUT_KEY_BOOTSTRAP))->mergeFromArgv($_SERVER, $_SERVER); $params[Bootstrap::PARAM_REQUIRE_MAINTENANCE] = null; - $params = $this->documentRootResolver($params); $requestParams = $this->serviceManager->get('magento-init-params'); $appBootstrapKey = Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS; @@ -230,26 +229,4 @@ protected function getVendorCommands($objectManager) return array_merge([], ...$commands); } - - /** - * Provides updated configuration in accordance to document root settings. - * - * @param array $config - * @return array - */ - private function documentRootResolver(array $config = []): array - { - $params = []; - $deploymentConfig = $this->serviceManager->get(DeploymentConfig::class); - if ((bool)$deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB)) { - $params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = [ - DirectoryList::PUB => [DirectoryList::URL_PATH => ''], - DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], - DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], - DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], - ]; - } - - return array_merge_recursive($config, $params); - } } diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 767cda60c0d35..be00cb9f64c18 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -7,10 +7,12 @@ namespace Magento\Framework\Data\Collection; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Data\Collection; -use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Phrase; /** * Filesystem items collection @@ -130,28 +132,26 @@ class Filesystem extends \Magento\Framework\Data\Collection protected $_collectedFiles = []; /** - * @var TargetDirectory|null + * @var \Magento\Framework\Filesystem */ - private $targetDirectory; + private $filesystem; /** - * @var DocumentRoot|null + * @var WriteInterface */ - private $documentRoot; + private $rootDirectory; /** * @param EntityFactoryInterface|null $_entityFactory - * @param TargetDirectory|null $targetDirectory - * @param DocumentRoot|null $documentRoot + * @param \Magento\Framework\Filesystem $filesystem */ public function __construct( EntityFactoryInterface $_entityFactory = null, - TargetDirectory $targetDirectory = null, - DocumentRoot $documentRoot = null + \Magento\Framework\Filesystem $filesystem = null ) { $this->_entityFactory = $_entityFactory ?? ObjectManager::getInstance()->get(EntityFactoryInterface::class); - $this->targetDirectory = $targetDirectory ?? ObjectManager::getInstance()->get(TargetDirectory::class); - $this->documentRoot = $documentRoot ?? ObjectManager::getInstance()->get(DocumentRoot::class); + $this->filesystem = $filesystem ?? ObjectManager::getInstance()->get(\Magento\Framework\Filesystem::class); + $this->rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); parent::__construct($this->_entityFactory); } @@ -237,11 +237,8 @@ public function setCollectRecursively($value) public function addTargetDir($value) { $value = (string)$value; - $directory = $this->targetDirectory->getDirectoryWrite($this->documentRoot->getPath()); - - if (!$directory->isDirectory($value)) { - // phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception('Unable to set target directory.'); + if (!$this->rootDirectory->isDirectory($value)) { + throw new FileSystemException(__('Unable to set target directory.')); } $this->_targetDirs[$value] = $value; return $this; @@ -266,19 +263,18 @@ public function setDirsFirst($value) * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @throws \Magento\Framework\Exception\FileSystemException + * @throws FileSystemException */ protected function _collectRecursive($dir) { - $directory = $this->targetDirectory->getDirectoryRead($this->documentRoot->getPath()); $collectedResult = []; if (!is_array($dir)) { $dir = [$dir]; } foreach ($dir as $folder) { - if ($nodes = $directory->search('/*', $folder)) { + if ($nodes = $this->rootDirectory->search('/*', $folder)) { foreach ($nodes as $node) { - $collectedResult[] = $directory->getAbsolutePath($node); + $collectedResult[] = $this->rootDirectory->getAbsolutePath($node); } } } @@ -287,7 +283,7 @@ protected function _collectRecursive($dir) } foreach ($collectedResult as $item) { - if ($directory->isDirectory($item) + if ($this->rootDirectory->isDirectory($item) && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item))) ) { if ($this->_collectDirs) { @@ -300,7 +296,7 @@ protected function _collectRecursive($dir) if ($this->_collectRecursively) { $this->_collectRecursive($item); } - } elseif ($this->_collectFiles && $directory->isFile( + } elseif ($this->_collectFiles && $this->rootDirectory->isFile( $item ) && (!$this->_allowedFilesMask || preg_match( $this->_allowedFilesMask, diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index b944ceb94628b..706d6efef44b9 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; @@ -46,8 +47,7 @@ class Uploader /** * Upload type. Used to right handle $_FILES array. - * - * @var \Magento\Framework\File\Uploader::SINGLE_STYLE|\Magento\Framework\File\Uploader::MULTIPLE_STYLE + * @var Uploader::SINGLE_STYLE|\Magento\Framework\File\Uploader::MULTIPLE_STYLE * @access protected */ protected $_uploadType; @@ -212,7 +212,7 @@ public function __construct( TargetDirectory $targetDirectory = null, DocumentRoot $documentRoot = null ) { - $this->directoryList= $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); + $this->directoryList = $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); $this->_setUploadFileId($fileId); if (!file_exists($this->_file['tmp_name'])) { @@ -741,7 +741,7 @@ private function validateFileId(array $fileId): void * Create destination folder * * @param string $destinationFolder - * @return \Magento\Framework\File\Uploader + * @return Uploader * @throws FileSystemException */ private function createDestinationFolder(string $destinationFolder) @@ -774,20 +774,24 @@ private function createDestinationFolder(string $destinationFolder) */ public static function getNewFileName($destinationFile) { + /** @var Filesystem $fileSystem */ + $fileSystem = ObjectManager::getInstance()->get(Filesystem::class); + $local = $fileSystem->getDirectoryRead(DirectoryList::ROOT); + /** @var TargetDirectory $targetDirectory */ + $targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); + $remote = $targetDirectory->getDirectoryRead(DirectoryList::ROOT); + + $fileExists = function ($path) use ($local, $remote) { + return $local->isExist($path) || $remote->isExist($path); + }; + $fileInfo = pathinfo($destinationFile); - if (file_exists($destinationFile)) { - $index = 1; - $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension']; - while (file_exists($fileInfo['dirname'] . '/' . $baseName)) { - $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension']; - $index++; - } - $destFileName = $baseName; - } else { - return $fileInfo['basename']; + $index = 1; + while ($fileExists($fileInfo['dirname'] . '/' . $fileInfo['basename'])) { + $fileInfo['basename'] = $fileInfo['filename'] . '_' . $index++ . '.' . $fileInfo['extension']; } - return $destFileName; + return $fileInfo['basename']; } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 1d60b7ce879bf..0ff25f868d7af 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -116,7 +116,7 @@ public function renameFile($path, $newPath, WriteInterface $targetDirectory = nu } $absolutePath = $this->driver->getAbsolutePath($this->path, $path); $absoluteNewPath = $targetDirectory->getAbsolutePath($newPath); - return $this->driver->rename($absolutePath, $absoluteNewPath, $targetDirectory->driver); + return $this->driver->rename($absolutePath, $absoluteNewPath, $targetDirectory->getDriver()); } /** diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php index 13d6e7b72d89f..fab7eb93aabf8 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php @@ -5,6 +5,7 @@ */ namespace Magento\Framework\HTTP\PhpEnvironment; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; use Magento\Framework\Stdlib\StringUtils; use Laminas\Http\Header\HeaderInterface; @@ -794,7 +795,7 @@ public function setRequestUri($requestUri = null) public function getBaseUrl() { $url = urldecode(parent::getBaseUrl()); - $url = str_replace('\\', '/', $url); + $url = str_replace(['\\', '/' . DirectoryList::PUB .'/'], '/', $url); return $url; } diff --git a/lib/internal/Magento/Framework/Image.php b/lib/internal/Magento/Framework/Image.php index b3867c0197b79..a14f94b8f2733 100644 --- a/lib/internal/Magento/Framework/Image.php +++ b/lib/internal/Magento/Framework/Image.php @@ -48,10 +48,6 @@ public function open() { $this->_adapter->checkDependencies(); - if (!file_exists($this->_fileName)) { - throw new \Exception("File '{$this->_fileName}' does not exist."); - } - $this->_adapter->open($this->_fileName); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php index 48935913e0561..76c659791308e 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php @@ -13,8 +13,10 @@ use Magento\Framework\App\Area; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Image\Factory; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Design\Theme\Image; use Magento\Framework\View\Design\Theme\Image\Uploader; @@ -70,8 +72,20 @@ class ImageTest extends TestCase */ protected $imagePathMock; + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $this->_mediaDirectoryMock = $this->createPartialMock( Write::class, ['isExist', 'copyFile', 'getRelativePath', 'delete'] diff --git a/nginx.conf.sample b/nginx.conf.sample index ead80ccb22ece..296f9fafd0a35 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -26,6 +26,9 @@ ## ## In production mode, you should uncomment the 'expires' directive in the /static/ location block +# Modules can be loaded only at the very beginning of the Nginx config file, please move the line below to the main config file +# load_module /etc/nginx/modules/ngx_http_image_filter_module.so; + root $MAGE_ROOT/pub; index index.php; @@ -134,6 +137,28 @@ location /static/ { } location /media/ { + +## The following section allows to offload image resizing from Magento instance to the Nginx. +## Catalog image URL format should be set accordingly. +## See https://docs.magento.com/m2/ee/user_guide/configuration/general/web.html#url-options +# location ~* ^/media/catalog/.* { +# +# # Replace placeholders and uncomment the line below to serve product images from public S3 +# # See examples of S3 authentication at https://github.com/anomalizer/ngx_aws_auth +# # proxy_pass https://<bucket-name>.<region-name>.amazonaws.com; +# +# set $width "-"; +# set $height "-"; +# if ($arg_width != '') { +# set $width $arg_width; +# } +# if ($arg_height != '') { +# set $height $arg_height; +# } +# image_filter resize $width $height; +# image_filter_jpeg_quality 90; +# } + try_files $uri $uri/ /get.php$is_args$args; location ~ ^/media/theme_customization/.*\.xml { diff --git a/pub/.htaccess b/pub/.htaccess index 6a97a6d14dc00..d30951ee22ca5 100644 --- a/pub/.htaccess +++ b/pub/.htaccess @@ -22,6 +22,11 @@ ## cgi.fix_pathinfo = 1 ## If it still doesn't work, rename php.ini to php5.ini +############################################ +## Enable usage of methods arguments in backtrace + + #SetEnv MAGE_DEBUG_SHOW_ARGS 1 + ############################################ ## This line is specific for 1and1 hosting @@ -33,24 +38,6 @@ DirectoryIndex index.php -<IfModule mod_php5.c> -############################################ -## Adjust memory limit - - php_value memory_limit 756M - php_value max_execution_time 18000 - -############################################ -## Disable automatic session start -## before autoload was initialized - - php_flag session.auto_start off - -############################################ -# Disable user agent verification to not break multiple image upload - - php_flag suhosin.session.cryptua off -</IfModule> <IfModule mod_php7.c> ############################################ ## Adjust memory limit @@ -75,7 +62,6 @@ php_flag suhosin.session.cryptua off </IfModule> - <IfModule mod_security.c> ########################################### # Disable POST processing to not break multiple image upload @@ -93,7 +79,7 @@ # Insert filter on all content ###SetOutputFilter DEFLATE # Insert filter on selected content types only - #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript + #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/x-javascript application/json image/svg+xml # Netscape 4.x has some problems... #BrowserMatch ^Mozilla/4 gzip-only-text/html @@ -121,6 +107,13 @@ </IfModule> +############################################ +## Workaround for Apache 2.4.6 CentOS build when working via ProxyPassMatch with HHVM (or any other) +## Please, set it on virtual host configuration level + +## SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 +############################################ + <IfModule mod_rewrite.c> ############################################ @@ -147,6 +140,13 @@ RewriteCond %{REQUEST_METHOD} ^TRAC[EK] RewriteRule .* - [L,R=405] +############################################ +## Redirect for mobile user agents + + #RewriteCond %{REQUEST_URI} !^/mobiledirectoryhere/.*$ + #RewriteCond %{HTTP_USER_AGENT} "android|blackberry|ipad|iphone|ipod|iemobile|opera mobile|palmos|webos|googlebot-mobile" [NC] + #RewriteRule ^(.*)$ /mobiledirectoryhere/ [L,R=302] + ############################################ ## Never rewrite for existing files, directories and links @@ -168,6 +168,7 @@ AddDefaultCharset Off #AddDefaultCharset UTF-8 + AddType 'text/html; charset=UTF-8' html <IfModule mod_expires.c> @@ -193,18 +194,15 @@ Require all denied </IfVersion> </Files> - -# For 404s and 403s that aren't handled by the application, show plain 404 response -ErrorDocument 404 /errors/404.php -ErrorDocument 403 /errors/404.php - -############################################ -## If running in cluster environment, uncomment this -## http://developer.yahoo.com/performance/rules.html#etags - - #FileETag none - -########################################### + <Files .htaccess> + <IfVersion < 2.4> + order allow,deny + deny from all + </IfVersion> + <IfVersion >= 2.4> + Require all denied + </IfVersion> + </Files> ## Deny access to cron.php <Files cron.php> <IfVersion < 2.4> @@ -226,8 +224,48 @@ ErrorDocument 403 /errors/404.php </IfVersion> </Files> +# For 404s and 403s that aren't handled by the application, show plain 404 response +ErrorDocument 404 /errors/404.php +ErrorDocument 403 /errors/404.php + +################################ +## If running in cluster environment, uncomment this +## http://developer.yahoo.com/performance/rules.html#etags + + #FileETag none + +# ###################################################################### +# # INTERNET EXPLORER # +# ###################################################################### + +# ---------------------------------------------------------------------- +# | Document modes | +# ---------------------------------------------------------------------- + +# Force Internet Explorer 8/9/10 to render pages in the highest mode +# available in the various cases when it may not. +# +# https://hsivonen.fi/doctype/#ie8 +# +# (!) Starting with Internet Explorer 11, document modes are deprecated. +# If your business still relies on older web apps and services that were +# designed for older versions of Internet Explorer, you might want to +# consider enabling `Enterprise Mode` throughout your company. +# +# https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode +# http://blogs.msdn.com/b/ie/archive/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11.aspx + <IfModule mod_headers.c> ############################################ + Header set X-UA-Compatible "IE=edge" + + # `mod_headers` cannot match based on the content-type, however, + # the `X-UA-Compatible` response header should be send only for + # HTML documents and not for the other resources. + <FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$"> + Header unset X-UA-Compatible + </FilesMatch> + ## Prevent clickjacking Header set X-Frame-Options SAMEORIGIN </IfModule> diff --git a/pub/index.php b/pub/index.php index 612e190719053..9e91f3bfa5488 100644 --- a/pub/index.php +++ b/pub/index.php @@ -7,7 +7,6 @@ */ use Magento\Framework\App\Bootstrap; -use Magento\Framework\App\Filesystem\DirectoryList; try { require __DIR__ . '/../app/bootstrap.php'; @@ -24,17 +23,7 @@ exit(1); } -$params = $_SERVER; -$params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = array_replace_recursive( - $params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] ?? [], - [ - DirectoryList::PUB => [DirectoryList::URL_PATH => ''], - DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], - DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], - DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], - ] -); -$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $params); +$bootstrap = Bootstrap::create(BP, $_SERVER); /** @var \Magento\Framework\App\Http $app */ $app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); $bootstrap->run($app); diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index cfcdebd4ac373..9b42548c4e105 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -37,7 +37,7 @@ public function __construct( /** * Generates image from $data and puts its to /tmp folder * - * @param string $config + * @param array $config * @return string $imagePath */ public function generate($config) diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php index e838dbee33603..0e9cc65f17bd9 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php @@ -15,6 +15,7 @@ /** * Deployment configuration options for the folders. + * @deprecared Magento always uses the pub directory */ class Directory implements ConfigOptionsListInterface { @@ -70,7 +71,7 @@ public function getOptions() $this->selectOptions, self::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, 'Flag to show is Pub is on root, can be true or false only', - false + true ), ]; } From 8baf70fd62ac58d95e33d1e00510504aa1a9abd4 Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Thu, 22 Oct 2020 17:27:15 -0500 Subject: [PATCH 0904/1013] MC-37726: Price filter uses base currency for ranges --- .../LayeredNavigation/Builder/Price.php | 2 +- .../Model/Resolver/Aggregations.php | 25 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php index 02b638edbdce8..1e2cc99663731 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Price.php @@ -72,7 +72,7 @@ public function build(AggregationInterface $aggregation, ?int $storeId): array ); } - return [$result]; + return [self::PRICE_BUCKET => $result]; } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php index 47a1d1f977f9b..7d10762d0f3b6 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php @@ -7,10 +7,12 @@ namespace Magento\CatalogGraphQl\Model\Resolver; +use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder; +use Magento\Directory\Model\PriceCurrency; +use Magento\Framework\App\ObjectManager; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation\LayerBuilder; use Magento\Store\Api\Data\StoreInterface; /** @@ -28,16 +30,24 @@ class Aggregations implements ResolverInterface */ private $layerBuilder; + /** + * @var PriceCurrency + */ + private $priceCurrency; + /** * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider + * @param PriceCurrency $priceCurrency * @param LayerBuilder $layerBuilder */ public function __construct( \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, - LayerBuilder $layerBuilder + LayerBuilder $layerBuilder, + PriceCurrency $priceCurrency = null ) { $this->filtersDataProvider = $filtersDataProvider; $this->layerBuilder = $layerBuilder; + $this->priceCurrency = $priceCurrency ?: ObjectManager::getInstance()->get(PriceCurrency::class); } /** @@ -60,7 +70,16 @@ public function resolve( /** @var StoreInterface $store */ $store = $context->getExtensionAttributes()->getStore(); $storeId = (int)$store->getId(); - return $this->layerBuilder->build($aggregations, $storeId); + $results = $this->layerBuilder->build($aggregations, $storeId); + if (isset($results['price_bucket'])) { + foreach ($results['price_bucket']['options'] as &$value) { + list($from, $to) = explode('-', $value['label']); + $newLabel = $this->priceCurrency->convertAndRound($from) . '-' . $this->priceCurrency->convertAndRound($to); + $value['label'] = $newLabel; + $value['value'] = str_replace('-', '_', $newLabel); + } + } + return $results; } else { return []; } From b7d5fa592063413e16251cc3743575d4baef5e5b Mon Sep 17 00:00:00 2001 From: "vadim.malesh" <engcom-vendorworker-charlie@adobe.com> Date: Fri, 23 Oct 2020 12:10:55 +0300 Subject: [PATCH 0905/1013] fix confirm customer by token --- .../ConfirmCustomerByToken.php | 40 ++++++++++++------- .../Customer/Model/ResourceModel/Customer.php | 16 -------- .../ConfirmCustomerByTokenTest.php | 38 ++++++++++-------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php index e8e9ac9764c3b..1000575805018 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php @@ -7,7 +7,9 @@ namespace Magento\Customer\Model\ForgotPasswordToken; -use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Exception\LocalizedException; /** * Confirm customer by reset password token @@ -20,37 +22,47 @@ class ConfirmCustomerByToken private $getByToken; /** - * @var CustomerResource + * @var CustomerRepositoryInterface */ - private $customerResource; + private $customerRepository; /** - * ConfirmByToken constructor. - * * @param GetCustomerByToken $getByToken - * @param CustomerResource $customerResource + * @param CustomerRepositoryInterface $customerRepository */ - public function __construct( - GetCustomerByToken $getByToken, - CustomerResource $customerResource - ) { + public function __construct(GetCustomerByToken $getByToken, CustomerRepositoryInterface $customerRepository) + { $this->getByToken = $getByToken; - $this->customerResource = $customerResource; + $this->customerRepository = $customerRepository; } /** * Confirm customer account my rp_token * * @param string $resetPasswordToken - * * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function execute(string $resetPasswordToken): void { $customer = $this->getByToken->execute($resetPasswordToken); if ($customer->getConfirmation()) { - $this->customerResource->updateColumn($customer->getId(), 'confirmation', null); + $this->resetConfirmation($customer); } } + + /** + * Reset customer confirmation + * + * @param CustomerInterface $customer + * @return void + */ + private function resetConfirmation(CustomerInterface $customer): void + { + // skip unnecessary address and customer validation + $customer->setData('ignore_validation_flag', true); + $customer->setConfirmation(null); + + $this->customerRepository->save($customer); + } } diff --git a/app/code/Magento/Customer/Model/ResourceModel/Customer.php b/app/code/Magento/Customer/Model/ResourceModel/Customer.php index e0a79822ebeb8..1477287f79f4b 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Customer.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Customer.php @@ -403,20 +403,4 @@ public function changeResetPasswordLinkToken(\Magento\Customer\Model\Customer $c } return $this; } - - /** - * @param int $customerId - * @param string $column - * @param string $value - */ - public function updateColumn($customerId, $column, $value) - { - $this->getConnection()->update( - $this->getTable('customer_entity'), - [$column => $value], - [$this->getEntityIdField() . ' = ?' => $customerId] - ); - - return $this; - } } diff --git a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php index 30aa70e89d2d0..4a6769e0653ad 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -7,11 +7,10 @@ namespace Magento\Customer\Test\Unit\Model\ForgotPasswordToken; +use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; -use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; -use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; -use Magento\Customer\Model\ResourceModel\Customer; use Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken; +use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -33,22 +32,26 @@ class ConfirmCustomerByTokenTest extends TestCase private $customerMock; /** - * @var CustomerResource|MockObject + * @var CustomerRepositoryInterface|MockObject */ - private $customerResourceMock; + private $customerRepositoryMock; /** * @inheritDoc */ protected function setUp(): void { - $this->customerMock = $this->getMockForAbstractClass(CustomerInterface::class); - $this->customerResourceMock = $this->createMock(CustomerResource::class); + $this->customerMock = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->addMethods(['setData']) + ->getMockForAbstractClass(); + + $this->customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class); $getCustomerByTokenMock = $this->createMock(GetCustomerByToken::class); $getCustomerByTokenMock->method('execute')->willReturn($this->customerMock); - $this->model = new ConfirmCustomerByToken($getCustomerByTokenMock, $this->customerResourceMock); + $this->model = new ConfirmCustomerByToken($getCustomerByTokenMock, $this->customerRepositoryMock); } /** @@ -58,17 +61,18 @@ protected function setUp(): void */ public function testExecuteWithConfirmation(): void { - $customerId = 777; - $this->customerMock->expects($this->once()) ->method('getConfirmation') ->willReturn('GWz2ik7Kts517MXAgrm4DzfcxKayGCm4'); $this->customerMock->expects($this->once()) - ->method('getId') - ->willReturn($customerId); - $this->customerResourceMock->expects($this->once()) - ->method('updateColumn') - ->with($customerId, 'confirmation', null); + ->method('setData') + ->with('ignore_validation_flag', true); + $this->customerMock->expects($this->once()) + ->method('setConfirmation') + ->with(null); + $this->customerRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->customerMock); $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); } @@ -83,8 +87,8 @@ public function testExecuteWithoutConfirmation(): void $this->customerMock->expects($this->once()) ->method('getConfirmation') ->willReturn(null); - $this->customerResourceMock->expects($this->never()) - ->method('updateColumn'); + $this->customerRepositoryMock->expects($this->never()) + ->method('save'); $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); } From c53bb9b6cb544514354852a482e137d62bbbf932 Mon Sep 17 00:00:00 2001 From: Viktor Petryk <victor.petryk@transoftgroup.com> Date: Fri, 23 Oct 2020 12:27:36 +0300 Subject: [PATCH 0906/1013] MC-37213: [MFTF] AdminMediaGalleryCatalogUiUsedInProductFilterTest is flaky --- .../AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml index 32a07d6f6273f..a48256a4c2d29 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterOnTest.xml @@ -29,7 +29,7 @@ <magentoCLI command="config:set {{WysiwygDisabledByDefault.path}} {{WysiwygDisabledByDefault.value}}" stepKey="disableWYSIWYG"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToAssertEmptyUsedIn"/> <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> @@ -41,7 +41,7 @@ <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clickDeleteSelectedButton"/> <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView2"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersAfterDeleteImages"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> From 08ba757ff90f3515fdaa74495ef4b31937e64b1c Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 23 Oct 2020 13:23:45 +0300 Subject: [PATCH 0907/1013] MC-37884: [GraphQL] Cart Price Rule for the Whole Cart with coupon does not apply --- .../QuoteGraphQl/Model/Resolver/CartPrices.php | 17 ++++++++++++++++- .../Quote/Guest/ApplyCouponsToCartTest.php | 13 +++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php index 6a57a7662af09..5eb4d090a0f7b 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -14,6 +14,7 @@ use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address\Total; use Magento\Quote\Model\Quote\TotalsCollector; +use Magento\SalesRule\Model\Spi\QuoteResetAppliedRulesInterface; /** * @inheritdoc @@ -25,13 +26,21 @@ class CartPrices implements ResolverInterface */ private $totalsCollector; + /** + * @var QuoteResetAppliedRulesInterface + */ + private $resetAppliedRules; + /** * @param TotalsCollector $totalsCollector + * @param QuoteResetAppliedRulesInterface $resetAppliedRules */ public function __construct( - TotalsCollector $totalsCollector + TotalsCollector $totalsCollector, + QuoteResetAppliedRulesInterface $resetAppliedRules ) { $this->totalsCollector = $totalsCollector; + $this->resetAppliedRules = $resetAppliedRules; } /** @@ -45,6 +54,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Quote $quote */ $quote = $value['model']; + /** + * To calculate a right discount value + * before calculate totals + * need to reset Cart Fixed Rules in the quote + */ + $this->resetAppliedRules->execute($quote); $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); $currency = $quote->getQuoteCurrencyCode(); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php index d33d0ee0569cd..a9dadccaa5373 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php @@ -21,6 +21,9 @@ class ApplyCouponsToCartTest extends GraphQlAbstract */ private $getMaskedQuoteIdByReservedOrderId; + /** + * @inheritdoc + */ protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); @@ -36,12 +39,17 @@ protected function setUp(): void public function testApplyCouponsToCart() { $couponCode = '2?ds5!2d'; + $expectedGrandTotal = 15.00; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId, $couponCode); $response = $this->graphQlMutation($query); self::assertArrayHasKey('applyCouponToCart', $response); self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupons'][0]['code']); + self::assertEquals( + $expectedGrandTotal, + $response['applyCouponToCart']['cart']['prices']['grand_total']['value'] + ); } /** @@ -146,6 +154,11 @@ private function getQuery(string $maskedQuoteId, string $couponCode): string applied_coupons { code } + prices { + grand_total { + value + } + } } } } From 32be7bb1080b72d619262c1d575886450981c1b4 Mon Sep 17 00:00:00 2001 From: mastiuhin-olexandr <mastiuhin.olexandr@transoftgroup.com> Date: Fri, 23 Oct 2020 13:54:01 +0300 Subject: [PATCH 0908/1013] MC-33288: [2.4][MSI][MFTF] StorefrontLoggedInCustomerCreateOrderAllOptionQuantityConfigurableProductCustomStockTest fails because of bad design --- .../Model/ResourceModel/Product/Relation.php | 49 ++++++++++- .../ResourceModel/Product/RelationTest.php | 86 +++++++++++++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php index c855fc5371b46..deba2b555d5cc 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php @@ -5,13 +5,37 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\Model\ResourceModel\Db\Context; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Catalog\Api\Data\ProductInterface; + /** * Catalog Product Relations Resource model * * @author Magento Core Team <core@magentocommerce.com> */ -class Relation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Relation extends AbstractDb { + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param Context $context + * @param null $connectionName + * @param MetadataPool $metadataPool + */ + public function __construct( + Context $context, + $connectionName = null, + MetadataPool $metadataPool + ) { + parent::__construct($context, $connectionName); + $this->metadataPool = $metadataPool; + } + /** * Initialize resource model and define main table * @@ -109,4 +133,27 @@ public function removeRelations($parentId, $childIds) } return $this; } + + /** + * Finds parent relations by given children ids. + * + * @param array $childrenIds Child products entity ids. + * @return array Parent products entity ids. + */ + public function getRelationsByChildren(array $childrenIds): array + { + $connection = $this->getConnection(); + $linkField = $this->metadataPool->getMetadata(ProductInterface::class) + ->getLinkField(); + $select = $connection->select() + ->from( + ['cpe' => $this->getTable('catalog_product_entity')], + 'entity_id' + )->join( + ['relation' => $this->getTable('catalog_product_relation')], + 'relation.parent_id = cpe.' . $linkField + )->where('child_id IN(?)', $childrenIds); + + return $connection->fetchCol($select); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php new file mode 100644 index 0000000000000..e9fa6d5bf96b7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/RelationTest.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\ConfigurableProduct\Model\ResourceModel\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Relation; +use Magento\Framework\ObjectManagerInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests Catalog Product Relation resource model. + * + * @see Relation + */ +class RelationTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Relation + */ + private $model; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(Relation::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + } + + /** + * Tests that getRelationsByChildren will return parent products entity ids of child products entity ids. + * + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + */ + public function testGetRelationsByChildren(): void + { + // Find configurable products options + $productOptionSkus = ['simple_10', 'simple_20', 'simple_30', 'simple_40']; + $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', $productOptionSkus, 'in') + ->create(); + $productOptions = $this->productRepository->getList($searchCriteria) + ->getItems(); + + $productOptionsIds = []; + + foreach ($productOptions as $productOption) { + $productOptionsIds[] = $productOption->getId(); + } + + // Find configurable products + $searchCriteria = $this->searchCriteriaBuilder->addFilter('sku', ['configurable', 'configurable_12345'], 'in') + ->create(); + $configurableProducts = $this->productRepository->getList($searchCriteria) + ->getItems(); + + // Assert there are configurable products ids in result of getRelationsByChildren method. + $result = $this->model->getRelationsByChildren($productOptionsIds); + + foreach ($configurableProducts as $configurableProduct) { + $this->assertContains($configurableProduct->getId(), $result); + } + } +} From bee2147b02fe43df6e1333ef1ae5a8bf52676c04 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 23 Oct 2020 14:57:53 +0300 Subject: [PATCH 0909/1013] MC-37884: [GraphQL] Cart Price Rule for the Whole Cart with coupon does not apply --- .../QuoteGraphQl/Model/Resolver/CartPrices.php | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php index 5eb4d090a0f7b..66cc9ed11ed9f 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -14,7 +14,6 @@ use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address\Total; use Magento\Quote\Model\Quote\TotalsCollector; -use Magento\SalesRule\Model\Spi\QuoteResetAppliedRulesInterface; /** * @inheritdoc @@ -26,21 +25,13 @@ class CartPrices implements ResolverInterface */ private $totalsCollector; - /** - * @var QuoteResetAppliedRulesInterface - */ - private $resetAppliedRules; - /** * @param TotalsCollector $totalsCollector - * @param QuoteResetAppliedRulesInterface $resetAppliedRules */ public function __construct( - TotalsCollector $totalsCollector, - QuoteResetAppliedRulesInterface $resetAppliedRules + TotalsCollector $totalsCollector ) { $this->totalsCollector = $totalsCollector; - $this->resetAppliedRules = $resetAppliedRules; } /** @@ -59,7 +50,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value * before calculate totals * need to reset Cart Fixed Rules in the quote */ - $this->resetAppliedRules->execute($quote); + $quote->setCartFixedRules([]); $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); $currency = $quote->getQuoteCurrencyCode(); From 5bd9efb6711346b646247c2eef3afd462b89e4ab Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 23 Oct 2020 15:12:09 +0300 Subject: [PATCH 0910/1013] MC-37663: Cannot invoice orders which contain bundle products comprised of physical and virtual products --- ...eOrderWithVirtualAndSimpleChildrenTest.xml | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml new file mode 100644 index 0000000000000..8cc26fd92b94c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.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="StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product placing order"/> + <title value="Admin should be able to invoice order for the bundle product with virtual and simple products in options"/> + <description value="Place order for bundle product and create invoice"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38683"/> + <useCaseId value="MC-37663"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <!--Create bundle product with fixed price with simple and virtual products in options--> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem1"> + <field key="price">100.00</field> + </createData> + <createData entity="VirtualProduct" stepKey="createProductForBundleItem2"> + <field key="price">50.00</field> + </createData> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createSecondBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="createProductForBundleItem1"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="createProductForBundleItem2"/> + </createData> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$$createFixedBundleProduct.id$$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <!--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> + </before> + <after> + <deleteData createDataKey="createProductForBundleItem1" stepKey="deleteProductForBundleItem1"/> + <deleteData createDataKey="createProductForBundleItem2" stepKey="deleteProductForBundleItem2"/> + <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!--Open Product Page--> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage"> + <argument name="product" value="$createFixedBundleProduct$"/> + </actionGroup> + <!-- Add bundle to cart --> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> + <argument name="productUrl" value="$$createFixedBundleProduct.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + <!--Navigate to checkout--> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> + <!--Click next button to open payment section--> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!--Click place order--> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!--Order review page has address that was created during checkout--> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <!--Create Invoice for this Order--> + <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoiceActionGroup" stepKey="submitInvoice"/> + </test> +</tests> From 414621cc8517448529d9412958ae290e7650202c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 23 Oct 2020 15:56:57 +0300 Subject: [PATCH 0911/1013] MC-38620: Merge release branch into 2.4-develop - Revert "Updating root composer files for publication service for 2.4.1" This reverts commit 445b0f1a --- composer.json | 513 +++++++++++++++++++++++++------------------------- 1 file changed, 256 insertions(+), 257 deletions(-) diff --git a/composer.json b/composer.json index b42794e0ba557..57fbfaaa35c2b 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,6 @@ "preferred-install": "dist", "sort-packages": true }, - "version": "2.4.1", "require": { "php": "~7.3.0||~7.4.0", "ext-bcmath": "*", @@ -83,31 +82,6 @@ "webonyx/graphql-php": "^0.13.8", "wikimedia/less.php": "~1.8.0" }, - "suggest": { - "ext-pcntl": "Need for run processes in parallel mode" - }, - "autoload": { - "exclude-from-classmap": [ - "**/dev/**", - "**/update/**", - "**/Test/**" - ], - "files": [ - "app/etc/NonComposerComponentRegistration.php" - ], - "psr-0": { - "": [ - "app/code/", - "generated/code/" - ] - }, - "psr-4": { - "Magento\\": "app/code/Magento/", - "Magento\\Framework\\": "lib/internal/Magento/Framework/", - "Magento\\Setup\\": "setup/src/Magento/Setup/", - "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" - } - }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", @@ -123,261 +97,286 @@ "sebastian/phpcpd": "~5.0.0", "squizlabs/php_codesniffer": "~3.5.4" }, - "conflict": { - "gene/bluefoot": "*" + "suggest": { + "ext-pcntl": "Need for run processes in parallel mode" }, "replace": { - "magento/module-marketplace": "100.4.0", - "magento/module-admin-analytics": "100.4.1", - "magento/module-admin-notification": "100.4.0", - "magento/module-advanced-pricing-import-export": "100.4.1", - "magento/module-amqp": "100.4.0", - "magento/module-amqp-store": "100.4.0", - "magento/module-analytics": "100.4.1", - "magento/module-asynchronous-operations": "100.4.1", - "magento/module-authorization": "100.4.1", - "magento/module-advanced-search": "100.4.0", - "magento/module-backend": "102.0.1", - "magento/module-backup": "100.4.1", - "magento/module-bundle": "101.0.1", - "magento/module-bundle-graph-ql": "100.4.1", - "magento/module-bundle-import-export": "100.4.0", - "magento/module-cache-invalidate": "100.4.0", - "magento/module-captcha": "100.4.1", - "magento/module-cardinal-commerce": "100.4.0", - "magento/module-catalog": "104.0.1", - "magento/module-catalog-customer-graph-ql": "100.4.1", - "magento/module-catalog-analytics": "100.4.0", - "magento/module-catalog-import-export": "101.1.1", - "magento/module-catalog-inventory": "100.4.1", - "magento/module-catalog-inventory-graph-ql": "100.4.0", - "magento/module-catalog-rule": "101.2.1", - "magento/module-catalog-rule-graph-ql": "100.4.0", - "magento/module-catalog-rule-configurable": "100.4.0", - "magento/module-catalog-search": "102.0.1", - "magento/module-catalog-url-rewrite": "100.4.1", - "magento/module-catalog-widget": "100.4.1", - "magento/module-checkout": "100.4.1", - "magento/module-checkout-agreements": "100.4.0", - "magento/module-checkout-agreements-graph-ql": "100.4.0", - "magento/module-cms": "104.0.1", - "magento/module-cms-url-rewrite": "100.4.1", - "magento/module-config": "101.2.1", - "magento/module-configurable-import-export": "100.4.0", - "magento/module-configurable-product": "100.4.1", - "magento/module-configurable-product-sales": "100.4.0", - "magento/module-contact": "100.4.1", - "magento/module-cookie": "100.4.1", - "magento/module-cron": "100.4.1", - "magento/module-currency-symbol": "100.4.0", - "magento/module-customer": "103.0.1", - "magento/module-customer-analytics": "100.4.0", - "magento/module-customer-downloadable-graph-ql": "100.4.0", - "magento/module-customer-import-export": "100.4.1", - "magento/module-deploy": "100.4.1", - "magento/module-developer": "100.4.1", - "magento/module-dhl": "100.4.0", - "magento/module-directory": "100.4.1", - "magento/module-directory-graph-ql": "100.4.0", - "magento/module-downloadable": "100.4.1", - "magento/module-downloadable-graph-ql": "100.4.1", - "magento/module-downloadable-import-export": "100.4.0", - "magento/module-eav": "102.1.1", - "magento/module-elasticsearch": "101.0.1", - "magento/module-elasticsearch-6": "100.4.1", - "magento/module-elasticsearch-7": "100.4.1", - "magento/module-email": "101.1.1", - "magento/module-encryption-key": "100.4.0", - "magento/module-fedex": "100.4.1", - "magento/module-gift-message": "100.4.0", - "magento/module-gift-message-graph-ql": "100.4.0", - "magento/module-google-adwords": "100.4.0", - "magento/module-google-analytics": "100.4.0", - "magento/module-google-optimizer": "100.4.1", - "magento/module-graph-ql": "100.4.1", - "magento/module-graph-ql-cache": "100.4.0", - "magento/module-catalog-graph-ql": "100.4.1", - "magento/module-catalog-cms-graph-ql": "100.4.0", - "magento/module-catalog-url-rewrite-graph-ql": "100.4.0", - "magento/module-configurable-product-graph-ql": "100.4.1", - "magento/module-customer-graph-ql": "100.4.1", - "magento/module-eav-graph-ql": "100.4.0", - "magento/module-swatches-graph-ql": "100.4.1", - "magento/module-tax-graph-ql": "100.4.0", - "magento/module-url-rewrite-graph-ql": "100.4.0", - "magento/module-cms-url-rewrite-graph-ql": "100.4.0", - "magento/module-weee-graph-ql": "100.4.0", - "magento/module-cms-graph-ql": "100.4.0", - "magento/module-grouped-import-export": "100.4.0", - "magento/module-grouped-product": "100.4.1", - "magento/module-grouped-catalog-inventory": "100.4.0", - "magento/module-grouped-product-graph-ql": "100.4.1", - "magento/module-import-export": "101.0.1", - "magento/module-indexer": "100.4.1", - "magento/module-instant-purchase": "100.4.0", - "magento/module-integration": "100.4.1", - "magento/module-layered-navigation": "100.4.1", - "magento/module-login-as-customer": "100.4.1", - "magento/module-login-as-customer-admin-ui": "100.4.1", - "magento/module-login-as-customer-api": "100.4.1", - "magento/module-login-as-customer-assistance": "100.4.0", - "magento/module-login-as-customer-frontend-ui": "100.4.1", - "magento/module-login-as-customer-log": "100.4.0", - "magento/module-login-as-customer-quote": "100.4.0", - "magento/module-login-as-customer-page-cache": "100.4.1", - "magento/module-login-as-customer-sales": "100.4.1", - "magento/module-media-content": "100.4.1", - "magento/module-media-content-api": "100.4.1", - "magento/module-media-content-catalog": "100.4.1", - "magento/module-media-content-cms": "100.4.1", - "magento/module-media-gallery": "100.4.1", - "magento/module-media-gallery-api": "101.0.1", - "magento/module-media-gallery-ui": "100.4.0", - "magento/module-media-gallery-ui-api": "100.4.0", - "magento/module-media-gallery-integration": "100.4.0", - "magento/module-media-gallery-synchronization": "100.4.0", - "magento/module-media-gallery-synchronization-api": "100.4.0", - "magento/module-media-content-synchronization": "100.4.0", - "magento/module-media-content-synchronization-api": "100.4.0", - "magento/module-media-content-synchronization-catalog": "100.4.0", - "magento/module-media-content-synchronization-cms": "100.4.0", - "magento/module-media-gallery-synchronization-metadata": "100.4.0", - "magento/module-media-gallery-metadata": "100.4.0", - "magento/module-media-gallery-metadata-api": "100.4.0", - "magento/module-media-gallery-catalog-ui": "100.4.0", - "magento/module-media-gallery-cms-ui": "100.4.0", - "magento/module-media-gallery-catalog-integration": "100.4.0", - "magento/module-media-gallery-catalog": "100.4.0", + "magento/module-marketplace": "*", + "magento/module-admin-analytics": "*", + "magento/module-admin-notification": "*", + "magento/module-advanced-pricing-import-export": "*", + "magento/module-amqp": "*", + "magento/module-amqp-store": "*", + "magento/module-analytics": "*", + "magento/module-asynchronous-operations": "*", + "magento/module-authorization": "*", + "magento/module-advanced-search": "*", + "magento/module-backend": "*", + "magento/module-backup": "*", + "magento/module-bundle": "*", + "magento/module-bundle-graph-ql": "*", + "magento/module-bundle-import-export": "*", + "magento/module-cache-invalidate": "*", + "magento/module-captcha": "*", + "magento/module-cardinal-commerce": "*", + "magento/module-catalog": "*", + "magento/module-catalog-customer-graph-ql": "*", + "magento/module-catalog-analytics": "*", + "magento/module-catalog-import-export": "*", + "magento/module-catalog-inventory": "*", + "magento/module-catalog-inventory-graph-ql": "*", + "magento/module-catalog-rule": "*", + "magento/module-catalog-rule-graph-ql": "*", + "magento/module-catalog-rule-configurable": "*", + "magento/module-catalog-search": "*", + "magento/module-catalog-url-rewrite": "*", + "magento/module-catalog-widget": "*", + "magento/module-checkout": "*", + "magento/module-checkout-agreements": "*", + "magento/module-checkout-agreements-graph-ql": "*", + "magento/module-cms": "*", + "magento/module-cms-url-rewrite": "*", + "magento/module-config": "*", + "magento/module-configurable-import-export": "*", + "magento/module-configurable-product": "*", + "magento/module-configurable-product-sales": "*", + "magento/module-contact": "*", + "magento/module-cookie": "*", + "magento/module-cron": "*", + "magento/module-currency-symbol": "*", + "magento/module-customer": "*", + "magento/module-customer-analytics": "*", + "magento/module-customer-downloadable-graph-ql": "*", + "magento/module-customer-import-export": "*", + "magento/module-deploy": "*", + "magento/module-developer": "*", + "magento/module-dhl": "*", + "magento/module-directory": "*", + "magento/module-directory-graph-ql": "*", + "magento/module-downloadable": "*", + "magento/module-downloadable-graph-ql": "*", + "magento/module-downloadable-import-export": "*", + "magento/module-eav": "*", + "magento/module-elasticsearch": "*", + "magento/module-elasticsearch-6": "*", + "magento/module-elasticsearch-7": "*", + "magento/module-email": "*", + "magento/module-encryption-key": "*", + "magento/module-fedex": "*", + "magento/module-gift-message": "*", + "magento/module-gift-message-graph-ql": "*", + "magento/module-google-adwords": "*", + "magento/module-google-analytics": "*", + "magento/module-google-optimizer": "*", + "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*", + "magento/module-catalog-graph-ql": "*", + "magento/module-catalog-cms-graph-ql": "*", + "magento/module-catalog-url-rewrite-graph-ql": "*", + "magento/module-configurable-product-graph-ql": "*", + "magento/module-customer-graph-ql": "*", + "magento/module-eav-graph-ql": "*", + "magento/module-swatches-graph-ql": "*", + "magento/module-tax-graph-ql": "*", + "magento/module-url-rewrite-graph-ql": "*", + "magento/module-cms-url-rewrite-graph-ql": "*", + "magento/module-weee-graph-ql": "*", + "magento/module-cms-graph-ql": "*", + "magento/module-grouped-import-export": "*", + "magento/module-grouped-product": "*", + "magento/module-grouped-catalog-inventory": "*", + "magento/module-grouped-product-graph-ql": "*", + "magento/module-import-export": "*", + "magento/module-indexer": "*", + "magento/module-instant-purchase": "*", + "magento/module-integration": "*", + "magento/module-layered-navigation": "*", + "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": "*", + "magento/module-login-as-customer-page-cache": "*", + "magento/module-login-as-customer-sales": "*", + "magento/module-media-content": "*", + "magento/module-media-content-api": "*", + "magento/module-media-content-catalog": "*", + "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-synchronization-metadata": "*", + "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-gallery-renditions": "*", "magento/module-media-gallery-renditions-api": "*", - "magento/module-media-storage": "100.4.0", - "magento/module-message-queue": "100.4.1", - "magento/module-msrp": "100.4.0", - "magento/module-msrp-configurable-product": "100.4.0", - "magento/module-msrp-grouped-product": "100.4.0", - "magento/module-multishipping": "100.4.1", - "magento/module-mysql-mq": "100.4.0", - "magento/module-new-relic-reporting": "100.4.0", - "magento/module-newsletter": "100.4.1", - "magento/module-newsletter-graph-ql": "100.4.0", - "magento/module-offline-payments": "100.4.0", - "magento/module-offline-shipping": "100.4.0", - "magento/module-page-cache": "100.4.1", - "magento/module-payment": "100.4.1", - "magento/module-paypal": "101.0.1", - "magento/module-paypal-captcha": "100.4.0", - "magento/module-paypal-graph-ql": "100.4.1", - "magento/module-persistent": "100.4.1", - "magento/module-product-alert": "100.4.1", - "magento/module-product-video": "100.4.1", - "magento/module-quote": "101.2.1", - "magento/module-quote-analytics": "100.4.1", - "magento/module-quote-bundle-options": "100.4.0", - "magento/module-quote-configurable-options": "100.4.0", - "magento/module-quote-downloadable-links": "100.4.0", - "magento/module-quote-graph-ql": "100.4.1", - "magento/module-related-product-graph-ql": "100.4.0", - "magento/module-release-notification": "100.4.0", - "magento/module-reports": "100.4.1", - "magento/module-require-js": "100.4.0", - "magento/module-review": "100.4.1", - "magento/module-review-graph-ql": "100.4.0", - "magento/module-review-analytics": "100.4.0", - "magento/module-robots": "101.1.0", - "magento/module-rss": "100.4.0", - "magento/module-rule": "100.4.0", - "magento/module-sales": "103.0.1", - "magento/module-sales-analytics": "100.4.0", - "magento/module-sales-graph-ql": "100.4.1", - "magento/module-sales-inventory": "100.4.0", - "magento/module-sales-rule": "101.2.1", - "magento/module-sales-sequence": "100.4.1", - "magento/module-sample-data": "100.4.1", - "magento/module-search": "101.1.1", - "magento/module-security": "100.4.1", - "magento/module-send-friend": "100.4.0", - "magento/module-send-friend-graph-ql": "100.4.0", - "magento/module-shipping": "100.4.1", - "magento/module-sitemap": "100.4.1", - "magento/module-store": "101.1.1", - "magento/module-store-graph-ql": "100.4.1", - "magento/module-swagger": "100.4.0", - "magento/module-swagger-webapi": "100.4.0", - "magento/module-swagger-webapi-async": "100.4.0", - "magento/module-swatches": "100.4.1", - "magento/module-swatches-layered-navigation": "100.4.0", - "magento/module-tax": "100.4.1", - "magento/module-tax-import-export": "100.4.1", - "magento/module-theme": "101.1.1", - "magento/module-theme-graph-ql": "100.4.0", - "magento/module-translation": "100.4.1", - "magento/module-ui": "101.2.1", - "magento/module-ups": "100.4.1", - "magento/module-url-rewrite": "102.0.1", - "magento/module-user": "101.2.1", - "magento/module-usps": "100.4.0", - "magento/module-variable": "100.4.0", - "magento/module-vault": "101.2.1", - "magento/module-vault-graph-ql": "100.4.0", - "magento/module-version": "100.4.0", - "magento/module-webapi": "100.4.0", - "magento/module-webapi-async": "100.4.0", - "magento/module-webapi-security": "100.4.0", - "magento/module-weee": "100.4.1", - "magento/module-widget": "101.2.1", - "magento/module-wishlist": "101.2.1", - "magento/module-wishlist-graph-ql": "100.4.1", - "magento/module-wishlist-analytics": "100.4.0", - "magento/theme-adminhtml-backend": "100.4.1", - "magento/theme-frontend-blank": "100.4.1", - "magento/theme-frontend-luma": "100.4.1", - "magento/language-de_de": "100.4.0", - "magento/language-en_us": "100.4.0", - "magento/language-es_es": "100.4.0", - "magento/language-fr_fr": "100.4.0", - "magento/language-nl_nl": "100.4.0", - "magento/language-pt_br": "100.4.0", - "magento/language-zh_hans_cn": "100.4.0", - "magento/framework": "103.0.1", - "magento/framework-amqp": "100.4.0", - "magento/framework-bulk": "101.0.0", - "magento/framework-message-queue": "100.4.1", + "magento/module-media-storage": "*", + "magento/module-message-queue": "*", + "magento/module-msrp": "*", + "magento/module-msrp-configurable-product": "*", + "magento/module-msrp-grouped-product": "*", + "magento/module-multishipping": "*", + "magento/module-mysql-mq": "*", + "magento/module-new-relic-reporting": "*", + "magento/module-newsletter": "*", + "magento/module-newsletter-graph-ql": "*", + "magento/module-offline-payments": "*", + "magento/module-offline-shipping": "*", + "magento/module-page-cache": "*", + "magento/module-payment": "*", + "magento/module-paypal": "*", + "magento/module-paypal-captcha": "*", + "magento/module-paypal-graph-ql": "*", + "magento/module-persistent": "*", + "magento/module-product-alert": "*", + "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": "*", + "magento/module-rule": "*", + "magento/module-sales": "*", + "magento/module-sales-analytics": "*", + "magento/module-sales-graph-ql": "*", + "magento/module-sales-inventory": "*", + "magento/module-sales-rule": "*", + "magento/module-sales-sequence": "*", + "magento/module-sample-data": "*", + "magento/module-search": "*", + "magento/module-security": "*", + "magento/module-send-friend": "*", + "magento/module-send-friend-graph-ql": "*", + "magento/module-shipping": "*", + "magento/module-sitemap": "*", + "magento/module-store": "*", + "magento/module-store-graph-ql": "*", + "magento/module-swagger": "*", + "magento/module-swagger-webapi": "*", + "magento/module-swagger-webapi-async": "*", + "magento/module-swatches": "*", + "magento/module-swatches-layered-navigation": "*", + "magento/module-tax": "*", + "magento/module-tax-import-export": "*", + "magento/module-theme": "*", + "magento/module-theme-graph-ql": "*", + "magento/module-translation": "*", + "magento/module-ui": "*", + "magento/module-ups": "*", + "magento/module-url-rewrite": "*", + "magento/module-user": "*", + "magento/module-usps": "*", + "magento/module-variable": "*", + "magento/module-vault": "*", + "magento/module-vault-graph-ql": "*", + "magento/module-version": "*", + "magento/module-webapi": "*", + "magento/module-webapi-async": "*", + "magento/module-webapi-security": "*", + "magento/module-weee": "*", + "magento/module-widget": "*", + "magento/module-wishlist": "*", + "magento/module-wishlist-graph-ql": "*", + "magento/module-wishlist-analytics": "*", + "magento/theme-adminhtml-backend": "*", + "magento/theme-frontend-blank": "*", + "magento/theme-frontend-luma": "*", + "magento/language-de_de": "*", + "magento/language-en_us": "*", + "magento/language-es_es": "*", + "magento/language-fr_fr": "*", + "magento/language-nl_nl": "*", + "magento/language-pt_br": "*", + "magento/language-zh_hans_cn": "*", + "magento/framework": "*", + "magento/framework-amqp": "*", + "magento/framework-bulk": "*", + "magento/framework-message-queue": "*", "trentrichardson/jquery-timepicker-addon": "1.4.3", "components/jquery": "1.11.0", "blueimp/jquery-file-upload": "5.6.14", "components/jqueryui": "1.10.4", "twbs/bootstrap": "3.1.0", "tinymce/tinymce": "3.4.7", - "magento/module-tinymce-3": "100.4.1", - "magento/module-csp": "100.4.0" + "magento/module-tinymce-3": "*", + "magento/module-csp": "*" }, - "autoload-dev": { - "psr-4": { - "Magento\\PhpStan\\": "dev/tests/static/framework/Magento/PhpStan/", - "Magento\\Sniffs\\": "dev/tests/static/framework/Magento/Sniffs/", - "Magento\\TestFramework\\Inspection\\": "dev/tests/static/framework/Magento/TestFramework/Inspection/", - "Magento\\TestFramework\\Utility\\": "dev/tests/static/framework/Magento/TestFramework/Utility/", - "Magento\\Tools\\": "dev/tools/Magento/Tools/", - "Magento\\Tools\\Sanity\\": "dev/build/publication/sanity/Magento/Tools/Sanity/" - } + "conflict": { + "gene/bluefoot": "*" }, - "prefer-stable": true, "extra": { "component_paths": { - "blueimp/jquery-file-upload": "lib/web/jquery/fileUploader", + "trentrichardson/jquery-timepicker-addon": "lib/web/jquery/jquery-ui-timepicker-addon.js", "components/jquery": [ "lib/web/jquery.js", "lib/web/jquery/jquery.min.js", "lib/web/jquery/jquery-migrate.js" ], + "blueimp/jquery-file-upload": "lib/web/jquery/fileUploader", "components/jqueryui": [ "lib/web/jquery/jquery-ui.js" ], - "tinymce/tinymce": "lib/web/tiny_mce_4", - "trentrichardson/jquery-timepicker-addon": "lib/web/jquery/jquery-ui-timepicker-addon.js", "twbs/bootstrap": [ "lib/web/jquery/jquery.tabs.js" + ], + "tinymce/tinymce": "lib/web/tiny_mce_4" + } + }, + "autoload": { + "psr-4": { + "Magento\\Framework\\": "lib/internal/Magento/Framework/", + "Magento\\Setup\\": "setup/src/Magento/Setup/", + "Magento\\": "app/code/Magento/", + "Zend\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" + }, + "psr-0": { + "": [ + "app/code/", + "generated/code/" ] + }, + "files": [ + "app/etc/NonComposerComponentRegistration.php" + ], + "exclude-from-classmap": [ + "**/dev/**", + "**/update/**", + "**/Test/**" + ] + }, + "autoload-dev": { + "psr-4": { + "Magento\\Sniffs\\": "dev/tests/static/framework/Magento/Sniffs/", + "Magento\\Tools\\": "dev/tools/Magento/Tools/", + "Magento\\Tools\\Sanity\\": "dev/build/publication/sanity/Magento/Tools/Sanity/", + "Magento\\TestFramework\\Inspection\\": "dev/tests/static/framework/Magento/TestFramework/Inspection/", + "Magento\\TestFramework\\Utility\\": "dev/tests/static/framework/Magento/TestFramework/Utility/", + "Magento\\PhpStan\\": "dev/tests/static/framework/Magento/PhpStan/" } - } + }, + "prefer-stable": true } From af21c7c05107489f1f0c5ecfbb00eb80070760b9 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 23 Oct 2020 16:04:36 +0300 Subject: [PATCH 0912/1013] MC-38620: Merge release branch into 2.4-develop - Revert "Updating composer versions for version-setter for 2.4.1" This reverts commit 7037dec9 --- app/code/Magento/AdminAnalytics/composer.json | 22 +++--- .../Magento/AdminNotification/composer.json | 24 +++---- .../AdvancedPricingImportExport/composer.json | 28 ++++---- app/code/Magento/AdvancedSearch/composer.json | 28 ++++---- app/code/Magento/Amqp/composer.json | 18 +++-- app/code/Magento/AmqpStore/composer.json | 22 +++--- app/code/Magento/Analytics/composer.json | 18 +++-- .../AsynchronousOperations/composer.json | 26 ++++--- app/code/Magento/Authorization/composer.json | 16 ++--- app/code/Magento/Backend/composer.json | 48 ++++++------- app/code/Magento/Backup/composer.json | 20 +++--- app/code/Magento/Bundle/composer.json | 50 +++++++------ app/code/Magento/BundleGraphQl/composer.json | 26 ++++--- .../Magento/BundleImportExport/composer.json | 26 ++++--- .../Magento/CacheInvalidate/composer.json | 16 ++--- app/code/Magento/Captcha/composer.json | 24 +++---- .../Magento/CardinalCommerce/composer.json | 20 +++--- app/code/Magento/Catalog/composer.json | 70 +++++++++---------- .../Magento/CatalogAnalytics/composer.json | 14 ++-- .../Magento/CatalogCmsGraphQl/composer.json | 22 +++--- .../CatalogCustomerGraphQl/composer.json | 18 +++-- app/code/Magento/CatalogGraphQl/composer.json | 32 ++++----- .../Magento/CatalogImportExport/composer.json | 34 +++++---- .../Magento/CatalogInventory/composer.json | 28 ++++---- .../CatalogInventoryGraphQl/composer.json | 16 ++--- app/code/Magento/CatalogRule/composer.json | 32 ++++----- .../CatalogRuleConfigurable/composer.json | 22 +++--- .../Magento/CatalogRuleGraphQl/composer.json | 14 ++-- app/code/Magento/CatalogSearch/composer.json | 38 +++++----- .../Magento/CatalogUrlRewrite/composer.json | 32 ++++----- .../CatalogUrlRewriteGraphQl/composer.json | 22 +++--- app/code/Magento/CatalogWidget/composer.json | 32 ++++----- app/code/Magento/Checkout/composer.json | 54 +++++++------- .../Magento/CheckoutAgreements/composer.json | 22 +++--- .../CheckoutAgreementsGraphQl/composer.json | 18 +++-- app/code/Magento/Cms/composer.json | 34 +++++---- app/code/Magento/CmsGraphQl/composer.json | 24 +++---- app/code/Magento/CmsUrlRewrite/composer.json | 20 +++--- .../CmsUrlRewriteGraphQl/composer.json | 22 +++--- app/code/Magento/Config/composer.json | 28 ++++---- .../ConfigurableImportExport/composer.json | 26 ++++--- .../Magento/ConfigurableProduct/composer.json | 50 +++++++------ .../ConfigurableProductGraphQl/composer.json | 22 +++--- .../ConfigurableProductSales/composer.json | 22 +++--- app/code/Magento/Contact/composer.json | 22 +++--- app/code/Magento/Cookie/composer.json | 18 +++-- app/code/Magento/Cron/composer.json | 18 +++-- app/code/Magento/Csp/composer.json | 16 ++--- app/code/Magento/CurrencySymbol/composer.json | 24 +++---- app/code/Magento/Customer/composer.json | 54 +++++++------- .../Magento/CustomerAnalytics/composer.json | 14 ++-- .../CustomerDownloadableGraphQl/composer.json | 18 +++-- .../Magento/CustomerGraphQl/composer.json | 26 ++++--- .../CustomerImportExport/composer.json | 26 ++++--- app/code/Magento/Deploy/composer.json | 22 +++--- app/code/Magento/Developer/composer.json | 18 +++-- app/code/Magento/Dhl/composer.json | 34 +++++---- app/code/Magento/Directory/composer.json | 20 +++--- .../Magento/DirectoryGraphQl/composer.json | 16 ++--- app/code/Magento/Downloadable/composer.json | 48 ++++++------- .../Magento/DownloadableGraphQl/composer.json | 28 ++++---- .../DownloadableImportExport/composer.json | 26 ++++--- app/code/Magento/Eav/composer.json | 24 +++---- app/code/Magento/EavGraphQl/composer.json | 16 ++--- app/code/Magento/Elasticsearch/composer.json | 32 ++++----- app/code/Magento/Elasticsearch6/composer.json | 24 +++---- app/code/Magento/Elasticsearch7/composer.json | 24 +++---- app/code/Magento/Email/composer.json | 34 +++++---- app/code/Magento/EncryptionKey/composer.json | 18 +++-- app/code/Magento/Fedex/composer.json | 30 ++++---- app/code/Magento/GiftMessage/composer.json | 34 +++++---- .../Magento/GiftMessageGraphQl/composer.json | 16 ++--- app/code/Magento/GoogleAdwords/composer.json | 18 +++-- .../Magento/GoogleAnalytics/composer.json | 22 +++--- .../Magento/GoogleOptimizer/composer.json | 26 ++++--- app/code/Magento/GraphQl/composer.json | 18 +++-- app/code/Magento/GraphQlCache/composer.json | 14 ++-- .../GroupedCatalogInventory/composer.json | 20 +++--- .../Magento/GroupedImportExport/composer.json | 24 +++---- app/code/Magento/GroupedProduct/composer.json | 42 ++++++----- .../GroupedProductGraphQl/composer.json | 16 ++--- app/code/Magento/ImportExport/composer.json | 26 ++++--- app/code/Magento/Indexer/composer.json | 16 ++--- .../Magento/InstantPurchase/composer.json | 18 +++-- app/code/Magento/Integration/composer.json | 28 ++++---- .../Magento/LayeredNavigation/composer.json | 18 +++-- .../Magento/LoginAsCustomer/composer.json | 26 +++---- .../LoginAsCustomerAdminUi/composer.json | 33 ++++----- .../Magento/LoginAsCustomerApi/composer.json | 14 ++-- .../LoginAsCustomerAssistance/composer.json | 31 ++++---- .../LoginAsCustomerFrontendUi/composer.json | 21 +++--- .../Magento/LoginAsCustomerLog/composer.json | 31 ++++---- .../LoginAsCustomerPageCache/composer.json | 25 +++---- .../LoginAsCustomerQuote/composer.json | 23 +++--- .../LoginAsCustomerSales/composer.json | 23 +++--- app/code/Magento/Marketplace/composer.json | 16 ++--- app/code/Magento/MediaContent/composer.json | 14 ++-- .../Magento/MediaContentApi/composer.json | 12 ++-- .../Magento/MediaContentCatalog/composer.json | 18 +++-- .../Magento/MediaContentCms/composer.json | 14 ++-- .../MediaContentSynchronization/composer.json | 21 +++--- .../composer.json | 11 ++- .../composer.json | 16 ++--- .../composer.json | 16 ++--- app/code/Magento/MediaGallery/composer.json | 14 ++-- .../Magento/MediaGalleryApi/composer.json | 10 ++- .../Magento/MediaGalleryCatalog/composer.json | 14 ++-- .../composer.json | 24 +++---- .../MediaGalleryCatalogUi/composer.json | 20 +++--- .../Magento/MediaGalleryCmsUi/composer.json | 14 ++-- .../MediaGalleryIntegration/composer.json | 32 ++++----- .../MediaGalleryMetadata/composer.json | 12 ++-- .../MediaGalleryMetadataApi/composer.json | 10 ++- .../MediaGallerySynchronization/composer.json | 16 ++--- .../composer.json | 12 ++-- .../composer.json | 16 ++--- app/code/Magento/MediaGalleryUi/composer.json | 31 ++++---- .../Magento/MediaGalleryUiApi/composer.json | 13 ++-- app/code/Magento/MediaStorage/composer.json | 30 ++++---- app/code/Magento/MessageQueue/composer.json | 16 ++--- app/code/Magento/Msrp/composer.json | 28 ++++---- .../MsrpConfigurableProduct/composer.json | 20 +++--- .../Magento/MsrpGroupedProduct/composer.json | 20 +++--- app/code/Magento/Multishipping/composer.json | 34 +++++---- app/code/Magento/MysqlMq/composer.json | 18 +++-- .../Magento/NewRelicReporting/composer.json | 26 ++++--- app/code/Magento/Newsletter/composer.json | 32 ++++----- .../Magento/NewsletterGraphQl/composer.json | 22 +++--- .../Magento/OfflinePayments/composer.json | 20 +++--- .../Magento/OfflineShipping/composer.json | 36 +++++----- app/code/Magento/PageCache/composer.json | 20 +++--- app/code/Magento/Payment/composer.json | 28 ++++---- app/code/Magento/Paypal/composer.json | 48 ++++++------- app/code/Magento/PaypalCaptcha/composer.json | 22 +++--- app/code/Magento/PaypalGraphQl/composer.json | 34 +++++---- app/code/Magento/Persistent/composer.json | 26 ++++--- app/code/Magento/ProductAlert/composer.json | 26 ++++--- app/code/Magento/ProductVideo/composer.json | 28 ++++---- app/code/Magento/Quote/composer.json | 44 ++++++------ app/code/Magento/QuoteAnalytics/composer.json | 14 ++-- .../Magento/QuoteBundleOptions/composer.json | 12 ++-- .../QuoteConfigurableOptions/composer.json | 12 ++-- .../QuoteDownloadableLinks/composer.json | 12 ++-- app/code/Magento/QuoteGraphQl/composer.json | 34 +++++---- .../RelatedProductGraphQl/composer.json | 18 +++-- .../Magento/ReleaseNotification/composer.json | 22 +++--- app/code/Magento/Reports/composer.json | 46 ++++++------ app/code/Magento/RequireJs/composer.json | 14 ++-- app/code/Magento/Review/composer.json | 34 +++++---- .../Magento/ReviewAnalytics/composer.json | 14 ++-- app/code/Magento/ReviewGraphQl/composer.json | 22 +++--- app/code/Magento/Robots/composer.json | 18 +++-- app/code/Magento/Rss/composer.json | 20 +++--- app/code/Magento/Rule/composer.json | 22 +++--- app/code/Magento/Sales/composer.json | 64 ++++++++--------- app/code/Magento/SalesAnalytics/composer.json | 14 ++-- app/code/Magento/SalesGraphQl/composer.json | 24 +++---- app/code/Magento/SalesInventory/composer.json | 22 +++--- app/code/Magento/SalesRule/composer.json | 54 +++++++------- app/code/Magento/SalesSequence/composer.json | 14 ++-- app/code/Magento/SampleData/composer.json | 16 ++--- app/code/Magento/Search/composer.json | 24 +++---- app/code/Magento/Security/composer.json | 22 +++--- app/code/Magento/SendFriend/composer.json | 26 ++++--- .../Magento/SendFriendGraphQl/composer.json | 16 ++--- app/code/Magento/Shipping/composer.json | 46 ++++++------ app/code/Magento/Sitemap/composer.json | 34 +++++---- app/code/Magento/Store/composer.json | 32 ++++----- app/code/Magento/StoreGraphQl/composer.json | 14 ++-- app/code/Magento/Swagger/composer.json | 14 ++-- app/code/Magento/SwaggerWebapi/composer.json | 16 ++--- .../Magento/SwaggerWebapiAsync/composer.json | 18 +++-- app/code/Magento/Swatches/composer.json | 38 +++++----- .../Magento/SwatchesGraphQl/composer.json | 16 ++--- .../SwatchesLayeredNavigation/composer.json | 14 ++-- app/code/Magento/Tax/composer.json | 44 ++++++------ app/code/Magento/TaxGraphQl/composer.json | 16 ++--- .../Magento/TaxImportExport/composer.json | 24 +++---- app/code/Magento/Theme/composer.json | 40 +++++------ app/code/Magento/ThemeGraphQl/composer.json | 14 ++-- app/code/Magento/Tinymce3/composer.json | 24 +++---- app/code/Magento/Translation/composer.json | 24 +++---- app/code/Magento/Ui/composer.json | 26 ++++--- app/code/Magento/Ups/composer.json | 30 ++++---- app/code/Magento/UrlRewrite/composer.json | 28 ++++---- .../Magento/UrlRewriteGraphQl/composer.json | 16 ++--- app/code/Magento/User/composer.json | 28 ++++---- app/code/Magento/Usps/composer.json | 30 ++++---- app/code/Magento/Variable/composer.json | 22 +++--- app/code/Magento/Vault/composer.json | 29 ++++---- app/code/Magento/VaultGraphQl/composer.json | 14 ++-- app/code/Magento/Version/composer.json | 14 ++-- app/code/Magento/Webapi/composer.json | 26 ++++--- app/code/Magento/WebapiAsync/composer.json | 24 +++---- app/code/Magento/WebapiSecurity/composer.json | 16 ++--- app/code/Magento/Weee/composer.json | 40 +++++------ app/code/Magento/WeeeGraphQl/composer.json | 20 +++--- app/code/Magento/Widget/composer.json | 30 ++++---- app/code/Magento/Wishlist/composer.json | 48 ++++++------- .../Magento/WishlistAnalytics/composer.json | 14 ++-- .../Magento/WishlistGraphQl/composer.json | 17 +++-- .../adminhtml/Magento/backend/composer.json | 14 ++-- .../frontend/Magento/blank/composer.json | 14 ++-- .../frontend/Magento/luma/composer.json | 16 ++--- app/i18n/Magento/de_DE/composer.json | 6 +- app/i18n/Magento/en_US/composer.json | 6 +- app/i18n/Magento/es_ES/composer.json | 6 +- app/i18n/Magento/fr_FR/composer.json | 6 +- app/i18n/Magento/nl_NL/composer.json | 6 +- app/i18n/Magento/pt_BR/composer.json | 6 +- app/i18n/Magento/zh_Hans_CN/composer.json | 6 +- .../Magento/Framework/Amqp/composer.json | 18 +++-- .../Magento/Framework/Bulk/composer.json | 18 +++-- .../Framework/MessageQueue/composer.json | 18 +++-- lib/internal/Magento/Framework/composer.json | 10 ++- 215 files changed, 2308 insertions(+), 2739 deletions(-) diff --git a/app/code/Magento/AdminAnalytics/composer.json b/app/code/Magento/AdminAnalytics/composer.json index 69a1a44212a2c..cf60b1d88ae55 100644 --- a/app/code/Magento/AdminAnalytics/composer.json +++ b/app/code/Magento/AdminAnalytics/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-admin-analytics", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-config": "101.2.*", - "magento/module-ui": "101.2.*", - "magento/module-release-notification": "100.4.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-ui": "*", + "magento/module-release-notification": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/AdminNotification/composer.json b/app/code/Magento/AdminNotification/composer.json index 3b3251458449e..d421fc869621b 100644 --- a/app/code/Magento/AdminNotification/composer.json +++ b/app/code/Magento/AdminNotification/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-admin-notification", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-config": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*", + "magento/module-ui": "*", + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/AdvancedPricingImportExport/composer.json b/app/code/Magento/AdvancedPricingImportExport/composer.json index d5124bdd0953d..ea6a39fba2c3d 100644 --- a/app/code/Magento/AdvancedPricingImportExport/composer.json +++ b/app/code/Magento/AdvancedPricingImportExport/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-advanced-pricing-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-import-export": "101.1.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-import-export": "101.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-import-export": "*", + "magento/module-catalog-inventory": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/AdvancedSearch/composer.json b/app/code/Magento/AdvancedSearch/composer.json index 9da312cc176de..720309b619e43 100644 --- a/app/code/Magento/AdvancedSearch/composer.json +++ b/app/code/Magento/AdvancedSearch/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-advanced-search", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-search": "102.0.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-search": "101.1.*", - "magento/module-store": "101.1.*", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-search": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-search": "*", + "magento/module-store": "*", "php": "~7.3.0||~7.4.0" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/Amqp/composer.json b/app/code/Magento/Amqp/composer.json index 8faeaa24be85c..9e7a035112b04 100644 --- a/app/code/Magento/Amqp/composer.json +++ b/app/code/Magento/Amqp/composer.json @@ -1,21 +1,20 @@ { "name": "magento/module-amqp", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*", - "magento/framework-amqp": "100.4.*", - "magento/framework-message-queue": "100.4.*", + "magento/framework": "*", + "magento/framework-amqp": "*", + "magento/framework-message-queue": "*", "php": "~7.3.0||~7.4.0" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/AmqpStore/composer.json b/app/code/Magento/AmqpStore/composer.json index f886ad8cd9217..70a10810ece21 100644 --- a/app/code/Magento/AmqpStore/composer.json +++ b/app/code/Magento/AmqpStore/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-amqp-store", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*", - "magento/framework-amqp": "100.4.*", - "magento/module-store": "101.1.*", + "magento/framework": "*", + "magento/framework-amqp": "*", + "magento/module-store": "*", "php": "~7.3.0||~7.4.0" }, "suggest": { - "magento/module-asynchronous-operations": "100.4.*", - "magento/framework-message-queue": "100.4.*" + "magento/module-asynchronous-operations": "*", + "magento/framework-message-queue": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/Analytics/composer.json b/app/code/Magento/Analytics/composer.json index 5f635c21979ed..84f8af066bf11 100644 --- a/app/code/Magento/Analytics/composer.json +++ b/app/code/Magento/Analytics/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-analytics", "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-integration": "*", + "magento/module-store": "*", + "magento/framework": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-backend": "102.0.*", - "magento/module-config": "101.2.*", - "magento/module-integration": "100.4.*", - "magento/module-store": "101.1.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/AsynchronousOperations/composer.json b/app/code/Magento/AsynchronousOperations/composer.json index a70ead26df02c..b5de631418e72 100644 --- a/app/code/Magento/AsynchronousOperations/composer.json +++ b/app/code/Magento/AsynchronousOperations/composer.json @@ -1,28 +1,27 @@ { "name": "magento/module-asynchronous-operations", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { - "magento/framework": "103.0.*", - "magento/framework-message-queue": "100.4.*", - "magento/framework-bulk": "101.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-ui": "101.2.*", + "magento/framework": "*", + "magento/framework-message-queue": "*", + "magento/framework-bulk": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", "php": "~7.3.0||~7.4.0" }, "suggest": { - "magento/module-admin-notification": "100.4.*", + "magento/module-admin-notification": "*", "magento/module-logging": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/Authorization/composer.json b/app/code/Magento/Authorization/composer.json index 3da4582ee5071..401444404ca3e 100644 --- a/app/code/Magento/Authorization/composer.json +++ b/app/code/Magento/Authorization/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-authorization", "description": "Authorization module provides access to Magento ACL functionality.", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*" + "magento/framework": "*", + "magento/module-backend": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index fc3b62d645bfc..ee5491057d861 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -1,38 +1,37 @@ { "name": "magento/module-backend", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "102.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backup": "100.4.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-developer": "100.4.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-quote": "101.2.*", - "magento/module-reports": "100.4.*", - "magento/module-require-js": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-security": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-translation": "100.4.*", - "magento/module-ui": "101.2.*", - "magento/module-user": "101.2.*" + "magento/framework": "*", + "magento/module-backup": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-developer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-quote": "*", + "magento/module-reports": "*", + "magento/module-require-js": "*", + "magento/module-sales": "*", + "magento/module-security": "*", + "magento/module-store": "*", + "magento/module-translation": "*", + "magento/module-ui": "*", + "magento/module-user": "*" }, "suggest": { - "magento/module-theme": "101.1.*" + "magento/module-theme": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -42,4 +41,3 @@ } } } - diff --git a/app/code/Magento/Backup/composer.json b/app/code/Magento/Backup/composer.json index 18949cdb04456..9a5904beda550 100644 --- a/app/code/Magento/Backup/composer.json +++ b/app/code/Magento/Backup/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-backup", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-cron": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-cron": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Bundle/composer.json b/app/code/Magento/Bundle/composer.json index 1d584dc896dec..1b5ca24ee098c 100644 --- a/app/code/Magento/Bundle/composer.json +++ b/app/code/Magento/Bundle/composer.json @@ -1,39 +1,38 @@ { "name": "magento/module-bundle", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-catalog-rule": "101.2.*", - "magento/module-checkout": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-gift-message": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-catalog-rule": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-gift-message": "*", + "magento/module-media-storage": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-webapi": "100.4.*", - "magento/module-bundle-sample-data": "Sample Data version: 100.4.*", - "magento/module-sales-rule": "101.2.*" + "magento/module-webapi": "*", + "magento/module-bundle-sample-data": "*", + "magento/module-sales-rule": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -43,4 +42,3 @@ } } } - diff --git a/app/code/Magento/BundleGraphQl/composer.json b/app/code/Magento/BundleGraphQl/composer.json index 1d0cca1b02786..e3c54719f4d0e 100644 --- a/app/code/Magento/BundleGraphQl/composer.json +++ b/app/code/Magento/BundleGraphQl/composer.json @@ -2,23 +2,22 @@ "name": "magento/module-bundle-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-catalog": "*", + "magento/module-bundle": "*", + "magento/module-catalog-graph-ql": "*", + "magento/module-quote": "*", + "magento/module-quote-graph-ql": "*", + "magento/module-store": "*", + "magento/module-sales": "*", + "magento/module-sales-graph-ql": "*", + "magento/framework": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-catalog": "104.0.*", - "magento/module-bundle": "101.0.*", - "magento/module-catalog-graph-ql": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-quote-graph-ql": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-sales": "103.0.*", - "magento/module-sales-graph-ql": "100.4.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/BundleImportExport/composer.json b/app/code/Magento/BundleImportExport/composer.json index a1134382212ab..faca3eac9a721 100644 --- a/app/code/Magento/BundleImportExport/composer.json +++ b/app/code/Magento/BundleImportExport/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-bundle-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-bundle": "101.0.*", - "magento/module-store": "101.1.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-import-export": "101.1.*", - "magento/module-eav": "102.1.*", - "magento/module-import-export": "101.0.*" + "magento/framework": "*", + "magento/module-bundle": "*", + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-catalog-import-export": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/CacheInvalidate/composer.json b/app/code/Magento/CacheInvalidate/composer.json index 385683d208b10..7801554c890e1 100644 --- a/app/code/Magento/CacheInvalidate/composer.json +++ b/app/code/Magento/CacheInvalidate/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-cache-invalidate", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-page-cache": "100.4.*" + "magento/framework": "*", + "magento/module-page-cache": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/Captcha/composer.json b/app/code/Magento/Captcha/composer.json index 5f9c0d31d7fbf..3c3aa58c3fe2f 100644 --- a/app/code/Magento/Captcha/composer.json +++ b/app/code/Magento/Captcha/composer.json @@ -1,27 +1,26 @@ { "name": "magento/module-captcha", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-authorization": "100.4.*", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-store": "*", + "magento/module-authorization": "*", "laminas/laminas-captcha": "^2.7.1", "laminas/laminas-db": "^2.8.2", "laminas/laminas-session": "^2.7.3" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -31,4 +30,3 @@ } } } - diff --git a/app/code/Magento/CardinalCommerce/composer.json b/app/code/Magento/CardinalCommerce/composer.json index 0c560942989fa..8b2989ef915e1 100644 --- a/app/code/Magento/CardinalCommerce/composer.json +++ b/app/code/Magento/CardinalCommerce/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-cardinal-commerce", "description": "Provides a possibility to enable 3-D Secure 2.0 support for payment methods.", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-payment": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-payment": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Catalog/composer.json b/app/code/Magento/Catalog/composer.json index 42e7ad2c1d5e8..6dde1d76e5e81 100644 --- a/app/code/Magento/Catalog/composer.json +++ b/app/code/Magento/Catalog/composer.json @@ -1,49 +1,48 @@ { "name": "magento/module-catalog", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "104.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-asynchronous-operations": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-catalog-rule": "101.2.*", - "magento/module-catalog-url-rewrite": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-cms": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-indexer": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-msrp": "100.4.*", - "magento/module-page-cache": "100.4.*", - "magento/module-product-alert": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-url-rewrite": "102.0.*", - "magento/module-widget": "101.2.*", - "magento/module-wishlist": "101.2.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-asynchronous-operations": "*", + "magento/module-backend": "*", + "magento/module-catalog-inventory": "*", + "magento/module-catalog-rule": "*", + "magento/module-catalog-url-rewrite": "*", + "magento/module-checkout": "*", + "magento/module-cms": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-indexer": "*", + "magento/module-media-storage": "*", + "magento/module-msrp": "*", + "magento/module-page-cache": "*", + "magento/module-product-alert": "*", + "magento/module-quote": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-url-rewrite": "*", + "magento/module-widget": "*", + "magento/module-wishlist": "*" }, "suggest": { - "magento/module-cookie": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-catalog-sample-data": "Sample Data version: 100.4.*" + "magento/module-cookie": "*", + "magento/module-sales": "*", + "magento/module-catalog-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -53,4 +52,3 @@ } } } - diff --git a/app/code/Magento/CatalogAnalytics/composer.json b/app/code/Magento/CatalogAnalytics/composer.json index 0efaa58bede19..43fb4c8a6f433 100644 --- a/app/code/Magento/CatalogAnalytics/composer.json +++ b/app/code/Magento/CatalogAnalytics/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-catalog-analytics", "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-analytics": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-analytics": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/CatalogCmsGraphQl/composer.json b/app/code/Magento/CatalogCmsGraphQl/composer.json index 38c79b614b63e..aa7a742f2f315 100644 --- a/app/code/Magento/CatalogCmsGraphQl/composer.json +++ b/app/code/Magento/CatalogCmsGraphQl/composer.json @@ -2,22 +2,21 @@ "name": "magento/module-catalog-cms-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-cms-graph-ql": "100.4.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-cms-graph-ql": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*", - "magento/module-cms": "104.0.*", - "magento/module-catalog-graph-ql": "100.4.*" + "magento/module-graph-ql": "*", + "magento/module-cms": "*", + "magento/module-catalog-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/CatalogCustomerGraphQl/composer.json b/app/code/Magento/CatalogCustomerGraphQl/composer.json index ebb5040875c2a..a7c887af0379b 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/composer.json +++ b/app/code/Magento/CatalogCustomerGraphQl/composer.json @@ -2,19 +2,18 @@ "name": "magento/module-catalog-customer-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-catalog-graph-ql": "*", + "magento/module-store": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-catalog-graph-ql": "100.4.*", - "magento/module-store": "101.1.*" - }, "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index 50757cd357a86..46d7454a6d7e2 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -2,27 +2,26 @@ "name": "magento/module-catalog-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-eav": "102.1.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-search": "101.1.*", - "magento/module-store": "101.1.*", - "magento/module-eav-graph-ql": "100.4.*", - "magento/module-catalog-search": "102.0.*", - "magento/framework": "103.0.*", - "magento/module-graph-ql": "100.4.*" + "magento/module-eav": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-search": "*", + "magento/module-store": "*", + "magento/module-eav-graph-ql": "*", + "magento/module-catalog-search": "*", + "magento/framework": "*", + "magento/module-graph-ql": "*" }, "suggest": { - "magento/module-graph-ql-cache": "100.4.*", - "magento/module-store-graph-ql": "100.4.*" + "magento/module-graph-ql-cache": "*", + "magento/module-store-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/CatalogImportExport/composer.json b/app/code/Magento/CatalogImportExport/composer.json index 9a8ab5adde9fd..92a6620827990 100644 --- a/app/code/Magento/CatalogImportExport/composer.json +++ b/app/code/Magento/CatalogImportExport/composer.json @@ -1,30 +1,29 @@ { "name": "magento/module-catalog-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", "ext-ctype": "*", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-catalog-url-rewrite": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-import-export": "101.0.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-authorization": "100.4.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-catalog-url-rewrite": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-authorization": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -34,4 +33,3 @@ } } } - diff --git a/app/code/Magento/CatalogInventory/composer.json b/app/code/Magento/CatalogInventory/composer.json index e55114501b28a..b810e6613aebb 100644 --- a/app/code/Magento/CatalogInventory/composer.json +++ b/app/code/Magento/CatalogInventory/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-catalog-inventory", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-quote": "101.2.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-quote": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -31,4 +30,3 @@ }, "abandoned": "magento/inventory-composer-metapackage" } - diff --git a/app/code/Magento/CatalogInventoryGraphQl/composer.json b/app/code/Magento/CatalogInventoryGraphQl/composer.json index 9138144e82c8f..d6d5b01091341 100644 --- a/app/code/Magento/CatalogInventoryGraphQl/composer.json +++ b/app/code/Magento/CatalogInventoryGraphQl/composer.json @@ -2,18 +2,17 @@ "name": "magento/module-catalog-inventory-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/CatalogRule/composer.json b/app/code/Magento/CatalogRule/composer.json index e1d4f361ef731..7c40ca8a9a33a 100644 --- a/app/code/Magento/CatalogRule/composer.json +++ b/app/code/Magento/CatalogRule/composer.json @@ -1,30 +1,29 @@ { "name": "magento/module-catalog-rule", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-rule": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-rule": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-import-export": "101.0.*", - "magento/module-catalog-rule-sample-data": "Sample Data version: 100.4.*" + "magento/module-import-export": "*", + "magento/module-catalog-rule-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -34,4 +33,3 @@ } } } - diff --git a/app/code/Magento/CatalogRuleConfigurable/composer.json b/app/code/Magento/CatalogRuleConfigurable/composer.json index 95bd798a6f507..19274fbae146f 100644 --- a/app/code/Magento/CatalogRuleConfigurable/composer.json +++ b/app/code/Magento/CatalogRuleConfigurable/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-catalog-rule-configurable", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", + "magento/framework": "*", "magento/magento-composer-installer": "*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-rule": "101.2.*", - "magento/module-configurable-product": "100.4.*" + "magento/module-catalog": "*", + "magento/module-catalog-rule": "*", + "magento/module-configurable-product": "*" }, "suggest": { - "magento/module-catalog-rule": "101.2.*" + "magento/module-catalog-rule": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/CatalogRuleGraphQl/composer.json b/app/code/Magento/CatalogRuleGraphQl/composer.json index aed9eef07394e..c82d9bb20ddab 100644 --- a/app/code/Magento/CatalogRuleGraphQl/composer.json +++ b/app/code/Magento/CatalogRuleGraphQl/composer.json @@ -2,18 +2,17 @@ "name": "magento/module-catalog-rule-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, "suggest": { - "magento/module-catalog-rule": "101.2.*" + "magento/module-catalog-rule": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/CatalogSearch/composer.json b/app/code/Magento/CatalogSearch/composer.json index 201447c73a031..1efece402fd84 100644 --- a/app/code/Magento/CatalogSearch/composer.json +++ b/app/code/Magento/CatalogSearch/composer.json @@ -1,33 +1,32 @@ { "name": "magento/module-catalog-search", "description": "Catalog search", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "102.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-indexer": "100.4.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-search": "101.1.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-indexer": "*", + "magento/module-catalog-inventory": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-search": "*", + "magento/module-store": "*", + "magento/module-theme": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -37,4 +36,3 @@ } } } - diff --git a/app/code/Magento/CatalogUrlRewrite/composer.json b/app/code/Magento/CatalogUrlRewrite/composer.json index 16f804631b29b..fe489bcf0a3a0 100644 --- a/app/code/Magento/CatalogUrlRewrite/composer.json +++ b/app/code/Magento/CatalogUrlRewrite/composer.json @@ -1,30 +1,29 @@ { "name": "magento/module-catalog-url-rewrite", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-import-export": "101.1.*", - "magento/module-eav": "102.1.*", - "magento/module-import-export": "101.0.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-url-rewrite": "102.0.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-import-export": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*", + "magento/module-store": "*", + "magento/module-ui": "*", + "magento/module-url-rewrite": "*" }, "suggest": { - "magento/module-webapi": "100.4.*" + "magento/module-webapi": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -34,4 +33,3 @@ } } } - diff --git a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json index 20012a583e2ef..3b64d51b85568 100644 --- a/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CatalogUrlRewriteGraphQl/composer.json @@ -2,22 +2,21 @@ "name": "magento/module-catalog-url-rewrite-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-store": "101.1.*", - "magento/module-catalog": "104.0.*", - "magento/framework": "103.0.*" + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/framework": "*" }, "suggest": { - "magento/module-catalog-url-rewrite": "100.4.*", - "magento/module-catalog-graph-ql": "100.4.*", - "magento/module-url-rewrite-graph-ql": "100.4.*" + "magento/module-catalog-url-rewrite": "*", + "magento/module-catalog-graph-ql": "*", + "magento/module-url-rewrite-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/CatalogWidget/composer.json b/app/code/Magento/CatalogWidget/composer.json index 0ddbd5d7201dc..305fb3ec47ad6 100644 --- a/app/code/Magento/CatalogWidget/composer.json +++ b/app/code/Magento/CatalogWidget/composer.json @@ -1,28 +1,27 @@ { "name": "magento/module-catalog-widget", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-rule": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-widget": "101.2.*", - "magento/module-wishlist": "101.2.*", - "magento/module-theme": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-rule": "*", + "magento/module-store": "*", + "magento/module-widget": "*", + "magento/module-wishlist": "*", + "magento/module-theme": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/Checkout/composer.json b/app/code/Magento/Checkout/composer.json index 9474d0942726a..5f7b5425667e5 100644 --- a/app/code/Magento/Checkout/composer.json +++ b/app/code/Magento/Checkout/composer.json @@ -1,41 +1,40 @@ { "name": "magento/module-checkout", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-msrp": "100.4.*", - "magento/module-page-cache": "100.4.*", - "magento/module-payment": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-sales-rule": "101.2.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-captcha": "100.4.*", - "magento/module-authorization": "100.4.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-msrp": "*", + "magento/module-page-cache": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-sales-rule": "*", + "magento/module-shipping": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-captcha": "*", + "magento/module-authorization": "*" }, "suggest": { - "magento/module-cookie": "100.4.*" + "magento/module-cookie": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -45,4 +44,3 @@ } } } - diff --git a/app/code/Magento/CheckoutAgreements/composer.json b/app/code/Magento/CheckoutAgreements/composer.json index b9e1011bc7172..1741de53e8637 100644 --- a/app/code/Magento/CheckoutAgreements/composer.json +++ b/app/code/Magento/CheckoutAgreements/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-checkout-agreements", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-checkout": "*", + "magento/module-quote": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/CheckoutAgreementsGraphQl/composer.json b/app/code/Magento/CheckoutAgreementsGraphQl/composer.json index ab59af7c0746f..26b80a4457b4a 100644 --- a/app/code/Magento/CheckoutAgreementsGraphQl/composer.json +++ b/app/code/Magento/CheckoutAgreementsGraphQl/composer.json @@ -2,20 +2,19 @@ "name": "magento/module-checkout-agreements-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-checkout-agreements": "100.4.*" + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-checkout-agreements": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*" + "magento/module-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/Cms/composer.json b/app/code/Magento/Cms/composer.json index 600ea98cceb16..8d69320102b5e 100644 --- a/app/code/Magento/Cms/composer.json +++ b/app/code/Magento/Cms/composer.json @@ -1,31 +1,30 @@ { "name": "magento/module-cms", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "104.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-email": "101.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-variable": "100.4.*", - "magento/module-widget": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-email": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*", + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-variable": "*", + "magento/module-widget": "*" }, "suggest": { - "magento/module-cms-sample-data": "Sample Data version: 100.4.*" + "magento/module-cms-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -35,4 +34,3 @@ } } } - diff --git a/app/code/Magento/CmsGraphQl/composer.json b/app/code/Magento/CmsGraphQl/composer.json index e98d8501711c6..0e4c849fe8344 100644 --- a/app/code/Magento/CmsGraphQl/composer.json +++ b/app/code/Magento/CmsGraphQl/composer.json @@ -2,23 +2,22 @@ "name": "magento/module-cms-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cms": "104.0.*", - "magento/module-widget": "101.2.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-widget": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*", - "magento/module-graph-ql-cache": "100.4.*", - "magento/module-store-graph-ql": "100.4.*" + "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*", + "magento/module-store-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/CmsUrlRewrite/composer.json b/app/code/Magento/CmsUrlRewrite/composer.json index e4c1baef35c36..80e150771975f 100644 --- a/app/code/Magento/CmsUrlRewrite/composer.json +++ b/app/code/Magento/CmsUrlRewrite/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-cms-url-rewrite", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cms": "104.0.*", - "magento/module-store": "101.1.*", - "magento/module-url-rewrite": "102.0.*" + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-store": "*", + "magento/module-url-rewrite": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json index 5a0f1c6d393ab..d8fbbb4c2e6fd 100644 --- a/app/code/Magento/CmsUrlRewriteGraphQl/composer.json +++ b/app/code/Magento/CmsUrlRewriteGraphQl/composer.json @@ -2,22 +2,21 @@ "name": "magento/module-cms-url-rewrite-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cms": "104.0.*", - "magento/module-store": "101.1.*", - "magento/module-url-rewrite-graph-ql": "100.4.*" + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-store": "*", + "magento/module-url-rewrite-graph-ql": "*" }, "suggest": { - "magento/module-cms-url-rewrite": "100.4.*", - "magento/module-catalog-graph-ql": "100.4.*" + "magento/module-cms-url-rewrite": "*", + "magento/module-catalog-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Config/composer.json b/app/code/Magento/Config/composer.json index 95611a0acecfb..63eca42a6ac48 100644 --- a/app/code/Magento/Config/composer.json +++ b/app/code/Magento/Config/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-config", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-cron": "100.4.*", - "magento/module-deploy": "100.4.*", - "magento/module-directory": "100.4.*", - "magento/module-email": "101.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-cron": "*", + "magento/module-deploy": "*", + "magento/module-directory": "*", + "magento/module-email": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/ConfigurableImportExport/composer.json b/app/code/Magento/ConfigurableImportExport/composer.json index 7da2c2c3b886c..e27510166a421 100644 --- a/app/code/Magento/ConfigurableImportExport/composer.json +++ b/app/code/Magento/ConfigurableImportExport/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-configurable-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-import-export": "101.1.*", - "magento/module-configurable-product": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-import-export": "101.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-import-export": "*", + "magento/module-configurable-product": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/ConfigurableProduct/composer.json b/app/code/Magento/ConfigurableProduct/composer.json index 0e8afa3bbb2f8..7b1b1a18416f5 100644 --- a/app/code/Magento/ConfigurableProduct/composer.json +++ b/app/code/Magento/ConfigurableProduct/composer.json @@ -1,39 +1,38 @@ { "name": "magento/module-configurable-product", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-media-storage": "*", + "magento/module-quote": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-msrp": "100.4.*", - "magento/module-webapi": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-sales-rule": "101.2.*", - "magento/module-product-video": "100.4.*", - "magento/module-configurable-sample-data": "Sample Data version: 100.4.*", - "magento/module-product-links-sample-data": "Sample Data version: 100.4.*", - "magento/module-tax": "100.4.*" + "magento/module-msrp": "*", + "magento/module-webapi": "*", + "magento/module-sales": "*", + "magento/module-sales-rule": "*", + "magento/module-product-video": "*", + "magento/module-configurable-sample-data": "*", + "magento/module-product-links-sample-data": "*", + "magento/module-tax": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -43,4 +42,3 @@ } } } - diff --git a/app/code/Magento/ConfigurableProductGraphQl/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index c2b06b3a8f9e0..295efb65b1978 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -2,21 +2,20 @@ "name": "magento/module-configurable-product-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "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": "*", + "magento/framework": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-catalog": "104.0.*", - "magento/module-configurable-product": "100.4.*", - "magento/module-graph-ql": "100.4.*", - "magento/module-catalog-graph-ql": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-quote-graph-ql": "100.4.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/ConfigurableProductSales/composer.json b/app/code/Magento/ConfigurableProductSales/composer.json index 8b8d2467c58cf..edac2b7782dcc 100644 --- a/app/code/Magento/ConfigurableProductSales/composer.json +++ b/app/code/Magento/ConfigurableProductSales/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-configurable-product-sales", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-sales": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-configurable-product": "100.4.*" + "magento/module-configurable-product": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/Contact/composer.json b/app/code/Magento/Contact/composer.json index 1eb955166e5e8..1600c1e0c2543 100644 --- a/app/code/Magento/Contact/composer.json +++ b/app/code/Magento/Contact/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-contact", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cms": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Cookie/composer.json b/app/code/Magento/Cookie/composer.json index 2a47b375d35d9..5a47a5c7993bf 100644 --- a/app/code/Magento/Cookie/composer.json +++ b/app/code/Magento/Cookie/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-cookie", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-backend": "102.0.*" + "magento/module-backend": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Cron/composer.json b/app/code/Magento/Cron/composer.json index a40fd986bee27..00da35140744b 100644 --- a/app/code/Magento/Cron/composer.json +++ b/app/code/Magento/Cron/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-cron", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Csp/composer.json b/app/code/Magento/Csp/composer.json index af4759029168b..352735712b1b0 100644 --- a/app/code/Magento/Csp/composer.json +++ b/app/code/Magento/Csp/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-csp", "description": "CSP module enables Content Security Policies for Magento", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/CurrencySymbol/composer.json b/app/code/Magento/CurrencySymbol/composer.json index 8a5536844630f..746cfa0ed033d 100644 --- a/app/code/Magento/CurrencySymbol/composer.json +++ b/app/code/Magento/CurrencySymbol/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-currency-symbol", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-page-cache": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-page-cache": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/Customer/composer.json b/app/code/Magento/Customer/composer.json index 51a533c1eda19..db3108a78e9aa 100644 --- a/app/code/Magento/Customer/composer.json +++ b/app/code/Magento/Customer/composer.json @@ -1,41 +1,40 @@ { "name": "magento/module-customer", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "103.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-integration": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-newsletter": "100.4.*", - "magento/module-page-cache": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-wishlist": "101.2.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-integration": "*", + "magento/module-media-storage": "*", + "magento/module-newsletter": "*", + "magento/module-page-cache": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-wishlist": "*" }, "suggest": { - "magento/module-cookie": "100.4.*", - "magento/module-customer-sample-data": "Sample Data version: 100.4.*" + "magento/module-cookie": "*", + "magento/module-customer-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -45,4 +44,3 @@ } } } - diff --git a/app/code/Magento/CustomerAnalytics/composer.json b/app/code/Magento/CustomerAnalytics/composer.json index 56bca0fb02456..abd9e93d89583 100644 --- a/app/code/Magento/CustomerAnalytics/composer.json +++ b/app/code/Magento/CustomerAnalytics/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-customer-analytics", "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-analytics": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-customer": "103.0.*", - "magento/module-analytics": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/CustomerDownloadableGraphQl/composer.json b/app/code/Magento/CustomerDownloadableGraphQl/composer.json index 5518f0cb68f6b..f7cdbb0dc86d6 100644 --- a/app/code/Magento/CustomerDownloadableGraphQl/composer.json +++ b/app/code/Magento/CustomerDownloadableGraphQl/composer.json @@ -2,20 +2,19 @@ "name": "magento/module-customer-downloadable-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-downloadable-graph-ql": "100.4.*", - "magento/module-graph-ql": "100.4.*", - "magento/framework": "103.0.*" + "magento/module-downloadable-graph-ql": "*", + "magento/module-graph-ql": "*", + "magento/framework": "*" }, "suggest": { - "magento/module-catalog-graph-ql": "100.4.*" + "magento/module-catalog-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/CustomerGraphQl/composer.json b/app/code/Magento/CustomerGraphQl/composer.json index 82c85db4ce6fa..2ec396ca8ee92 100644 --- a/app/code/Magento/CustomerGraphQl/composer.json +++ b/app/code/Magento/CustomerGraphQl/composer.json @@ -2,23 +2,22 @@ "name": "magento/module-customer-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-authorization": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-graph-ql": "*", + "magento/module-newsletter": "*", + "magento/module-integration": "*", + "magento/module-store": "*", + "magento/framework": "*", + "magento/module-directory": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-authorization": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-graph-ql": "100.4.*", - "magento/module-newsletter": "100.4.*", - "magento/module-integration": "100.4.*", - "magento/module-store": "101.1.*", - "magento/framework": "103.0.*", - "magento/module-directory": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/CustomerImportExport/composer.json b/app/code/Magento/CustomerImportExport/composer.json index 2773a9295f239..8104ea01875a6 100644 --- a/app/code/Magento/CustomerImportExport/composer.json +++ b/app/code/Magento/CustomerImportExport/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-customer-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-import-export": "101.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/Deploy/composer.json b/app/code/Magento/Deploy/composer.json index 752b7551777f2..d8668dbb84874 100644 --- a/app/code/Magento/Deploy/composer.json +++ b/app/code/Magento/Deploy/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-deploy", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-config": "101.2.*", - "magento/module-require-js": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-user": "101.2.*" + "magento/framework": "*", + "magento/module-config": "*", + "magento/module-require-js": "*", + "magento/module-store": "*", + "magento/module-user": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "cli_commands.php", @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/Developer/composer.json b/app/code/Magento/Developer/composer.json index 14c1fb91706a9..c5c949ec45f62 100644 --- a/app/code/Magento/Developer/composer.json +++ b/app/code/Magento/Developer/composer.json @@ -1,21 +1,20 @@ { "name": "magento/module-developer", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-config": "101.2.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-config": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/Dhl/composer.json b/app/code/Magento/Dhl/composer.json index 4f359ba46f25d..d81ae0d7b4969 100644 --- a/app/code/Magento/Dhl/composer.json +++ b/app/code/Magento/Dhl/composer.json @@ -1,32 +1,31 @@ { "name": "magento/module-dhl", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-shipping": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-checkout": "100.4.*" + "magento/module-checkout": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -36,4 +35,3 @@ } } } - diff --git a/app/code/Magento/Directory/composer.json b/app/code/Magento/Directory/composer.json index d9c54914a790a..e3646d38fe64d 100644 --- a/app/code/Magento/Directory/composer.json +++ b/app/code/Magento/Directory/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-directory", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-config": "101.2.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/DirectoryGraphQl/composer.json b/app/code/Magento/DirectoryGraphQl/composer.json index 3c74e1d497969..ef473e1c43b94 100644 --- a/app/code/Magento/DirectoryGraphQl/composer.json +++ b/app/code/Magento/DirectoryGraphQl/composer.json @@ -2,18 +2,17 @@ "name": "magento/module-directory-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-directory": "*", + "magento/module-store": "*", + "magento/module-graph-ql": "*", + "magento/framework": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-directory": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-graph-ql": "100.4.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/Downloadable/composer.json b/app/code/Magento/Downloadable/composer.json index 045bfdd234ffa..992bdbd1e263c 100644 --- a/app/code/Magento/Downloadable/composer.json +++ b/app/code/Magento/Downloadable/composer.json @@ -1,38 +1,37 @@ { "name": "magento/module-downloadable", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-gift-message": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-gift-message": "*", + "magento/module-media-storage": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-theme": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-downloadable-sample-data": "Sample Data version: 100.4.*" + "magento/module-downloadable-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -42,4 +41,3 @@ } } } - diff --git a/app/code/Magento/DownloadableGraphQl/composer.json b/app/code/Magento/DownloadableGraphQl/composer.json index 8eba317d8b413..d03a5953506e5 100644 --- a/app/code/Magento/DownloadableGraphQl/composer.json +++ b/app/code/Magento/DownloadableGraphQl/composer.json @@ -2,25 +2,24 @@ "name": "magento/module-downloadable-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-store": "101.1.*", - "magento/module-catalog": "104.0.*", - "magento/module-downloadable": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-quote-graph-ql": "100.4.*", - "magento/framework": "103.0.*" + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-downloadable": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-quote-graph-ql": "*", + "magento/framework": "*" }, "suggest": { - "magento/module-catalog-graph-ql": "100.4.*", - "magento/module-sales-graph-ql": "100.4.*" + "magento/module-catalog-graph-ql": "*", + "magento/module-sales-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/DownloadableImportExport/composer.json b/app/code/Magento/DownloadableImportExport/composer.json index 230c4a04b17ad..6dd7043fc02a9 100644 --- a/app/code/Magento/DownloadableImportExport/composer.json +++ b/app/code/Magento/DownloadableImportExport/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-downloadable-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-import-export": "101.1.*", - "magento/module-downloadable": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-import-export": "101.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-import-export": "*", + "magento/module-downloadable": "*", + "magento/module-eav": "*", + "magento/module-import-export": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/Eav/composer.json b/app/code/Magento/Eav/composer.json index b94035e5b518f..5636b0d05841c 100644 --- a/app/code/Magento/Eav/composer.json +++ b/app/code/Magento/Eav/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-eav", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "102.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/EavGraphQl/composer.json b/app/code/Magento/EavGraphQl/composer.json index 962768a55d178..ba4138f67cf62 100644 --- a/app/code/Magento/EavGraphQl/composer.json +++ b/app/code/Magento/EavGraphQl/composer.json @@ -2,19 +2,18 @@ "name": "magento/module-eav-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-eav": "102.1.*" + "magento/framework": "*", + "magento/module-eav": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*" + "magento/module-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index ade8dbeb3429f..b79ae7bc5cc47 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -1,28 +1,27 @@ { "name": "magento/module-elasticsearch", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-advanced-search": "100.4.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-search": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-search": "101.1.*", - "magento/module-store": "101.1.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/framework": "103.0.*", + "magento/module-advanced-search": "*", + "magento/module-catalog": "*", + "magento/module-catalog-search": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-search": "*", + "magento/module-store": "*", + "magento/module-catalog-inventory": "*", + "magento/framework": "*", "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/Elasticsearch6/composer.json b/app/code/Magento/Elasticsearch6/composer.json index f68744be48139..1ee92c0b0a3b3 100644 --- a/app/code/Magento/Elasticsearch6/composer.json +++ b/app/code/Magento/Elasticsearch6/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-elasticsearch-6", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-advanced-search": "100.4.*", - "magento/module-catalog-search": "102.0.*", - "magento/module-search": "101.1.*", - "magento/module-elasticsearch": "101.0.*", + "magento/framework": "*", + "magento/module-advanced-search": "*", + "magento/module-catalog-search": "*", + "magento/module-search": "*", + "magento/module-elasticsearch": "*", "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/Elasticsearch7/composer.json b/app/code/Magento/Elasticsearch7/composer.json index ab2c52a2b9b32..1e59ceaebaf84 100644 --- a/app/code/Magento/Elasticsearch7/composer.json +++ b/app/code/Magento/Elasticsearch7/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-elasticsearch-7", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-elasticsearch": "101.0.*", + "magento/framework": "*", + "magento/module-elasticsearch": "*", "elasticsearch/elasticsearch": "~7.7.0", - "magento/module-advanced-search": "100.4.*", - "magento/module-catalog-search": "102.0.*" + "magento/module-advanced-search": "*", + "magento/module-catalog-search": "*" }, "suggest": { - "magento/module-config": "101.2.*", - "magento/module-search": "101.1.*" + "magento/module-config": "*", + "magento/module-search": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/Email/composer.json b/app/code/Magento/Email/composer.json index c5cee1c6610fd..334bbcf9d4617 100644 --- a/app/code/Magento/Email/composer.json +++ b/app/code/Magento/Email/composer.json @@ -1,31 +1,30 @@ { "name": "magento/module-email", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-cms": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*", - "magento/module-require-js": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-variable": "100.4.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-cms": "*", + "magento/module-config": "*", + "magento/module-store": "*", + "magento/module-theme": "*", + "magento/module-require-js": "*", + "magento/module-media-storage": "*", + "magento/module-variable": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-theme": "101.1.*" + "magento/module-theme": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -35,4 +34,3 @@ } } } - diff --git a/app/code/Magento/EncryptionKey/composer.json b/app/code/Magento/EncryptionKey/composer.json index 9efe0d770e7c3..6677a5b181f83 100644 --- a/app/code/Magento/EncryptionKey/composer.json +++ b/app/code/Magento/EncryptionKey/composer.json @@ -1,21 +1,20 @@ { "name": "magento/module-encryption-key", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-config": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/Fedex/composer.json b/app/code/Magento/Fedex/composer.json index 46835867dde94..575311e148457 100644 --- a/app/code/Magento/Fedex/composer.json +++ b/app/code/Magento/Fedex/composer.json @@ -1,28 +1,27 @@ { "name": "magento/module-fedex", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-shipping": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/GiftMessage/composer.json b/app/code/Magento/GiftMessage/composer.json index 389964813fa57..cdf0533c3270d 100644 --- a/app/code/Magento/GiftMessage/composer.json +++ b/app/code/Magento/GiftMessage/composer.json @@ -1,31 +1,30 @@ { "name": "magento/module-gift-message", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-eav": "102.1.*", - "magento/module-multishipping": "100.4.*" + "magento/module-eav": "*", + "magento/module-multishipping": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -35,4 +34,3 @@ } } } - diff --git a/app/code/Magento/GiftMessageGraphQl/composer.json b/app/code/Magento/GiftMessageGraphQl/composer.json index ba8f9ebd37db1..48088f2a48a32 100644 --- a/app/code/Magento/GiftMessageGraphQl/composer.json +++ b/app/code/Magento/GiftMessageGraphQl/composer.json @@ -2,19 +2,18 @@ "name": "magento/module-gift-message-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-gift-message": "100.4.*" + "magento/framework": "*", + "magento/module-gift-message": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*" + "magento/module-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/GoogleAdwords/composer.json b/app/code/Magento/GoogleAdwords/composer.json index 8043d7eba9488..a37470115584f 100644 --- a/app/code/Magento/GoogleAdwords/composer.json +++ b/app/code/Magento/GoogleAdwords/composer.json @@ -1,21 +1,20 @@ { "name": "magento/module-google-adwords", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-sales": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/GoogleAnalytics/composer.json b/app/code/Magento/GoogleAnalytics/composer.json index 3f068a0f8361d..64d210c4f4811 100644 --- a/app/code/Magento/GoogleAnalytics/composer.json +++ b/app/code/Magento/GoogleAnalytics/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-google-analytics", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cookie": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-cookie": "*", + "magento/module-sales": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/GoogleOptimizer/composer.json b/app/code/Magento/GoogleOptimizer/composer.json index 2f496d75f9042..426526a922ec8 100644 --- a/app/code/Magento/GoogleOptimizer/composer.json +++ b/app/code/Magento/GoogleOptimizer/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-google-optimizer", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-cms": "104.0.*", - "magento/module-google-analytics": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-cms": "*", + "magento/module-google-analytics": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index ffb6c0fbc7d94..401e77a787acf 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -2,20 +2,19 @@ "name": "magento/module-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-eav": "102.1.*", - "magento/framework": "103.0.*", - "magento/module-webapi": "100.4.*" + "magento/module-eav": "*", + "magento/framework": "*", + "magento/module-webapi": "*" }, "suggest": { - "magento/module-graph-ql-cache": "100.4.*" + "magento/module-graph-ql-cache": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/GraphQlCache/composer.json b/app/code/Magento/GraphQlCache/composer.json index 57b470cb512b9..4cfdd0c4f660a 100644 --- a/app/code/Magento/GraphQlCache/composer.json +++ b/app/code/Magento/GraphQlCache/composer.json @@ -2,17 +2,16 @@ "name": "magento/module-graph-ql-cache", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-page-cache": "*", + "magento/module-graph-ql": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-page-cache": "100.4.*", - "magento/module-graph-ql": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/GroupedCatalogInventory/composer.json b/app/code/Magento/GroupedCatalogInventory/composer.json index 668bf4428e4ff..0d91d939494a8 100644 --- a/app/code/Magento/GroupedCatalogInventory/composer.json +++ b/app/code/Magento/GroupedCatalogInventory/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-grouped-catalog-inventory", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-grouped-product": "100.4.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-grouped-product": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/GroupedImportExport/composer.json b/app/code/Magento/GroupedImportExport/composer.json index 90c595a9e531e..8806058c2bfc8 100644 --- a/app/code/Magento/GroupedImportExport/composer.json +++ b/app/code/Magento/GroupedImportExport/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-grouped-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-import-export": "101.1.*", - "magento/module-eav": "102.1.*", - "magento/module-grouped-product": "100.4.*", - "magento/module-import-export": "101.0.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-import-export": "*", + "magento/module-eav": "*", + "magento/module-grouped-product": "*", + "magento/module-import-export": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/GroupedProduct/composer.json b/app/code/Magento/GroupedProduct/composer.json index 2d0958c694875..554b0c239c8fb 100644 --- a/app/code/Magento/GroupedProduct/composer.json +++ b/app/code/Magento/GroupedProduct/composer.json @@ -1,35 +1,34 @@ { "name": "magento/module-grouped-product", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-msrp": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-wishlist": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-media-storage": "*", + "magento/module-msrp": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-ui": "*", + "magento/module-wishlist": "*" }, "suggest": { - "magento/module-grouped-product-sample-data": "Sample Data version: 100.4.*" + "magento/module-grouped-product-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -39,4 +38,3 @@ } } } - diff --git a/app/code/Magento/GroupedProductGraphQl/composer.json b/app/code/Magento/GroupedProductGraphQl/composer.json index e3dd433f606e5..5784acb5f5d04 100644 --- a/app/code/Magento/GroupedProductGraphQl/composer.json +++ b/app/code/Magento/GroupedProductGraphQl/composer.json @@ -2,18 +2,17 @@ "name": "magento/module-grouped-product-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-grouped-product": "*", + "magento/module-catalog": "*", + "magento/module-catalog-graph-ql": "*", + "magento/framework": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-grouped-product": "100.4.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-graph-ql": "100.4.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/ImportExport/composer.json b/app/code/Magento/ImportExport/composer.json index 251a477cab76f..3be5c03dc2828 100644 --- a/app/code/Magento/ImportExport/composer.json +++ b/app/code/Magento/ImportExport/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", "ext-ctype": "*", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-eav": "102.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-eav": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/Indexer/composer.json b/app/code/Magento/Indexer/composer.json index 9451b257e8a01..07d652e9fa2b5 100644 --- a/app/code/Magento/Indexer/composer.json +++ b/app/code/Magento/Indexer/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-indexer", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*" + "magento/framework": "*", + "magento/module-backend": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/InstantPurchase/composer.json b/app/code/Magento/InstantPurchase/composer.json index 89623cc92d2fe..0807926b755a0 100644 --- a/app/code/Magento/InstantPurchase/composer.json +++ b/app/code/Magento/InstantPurchase/composer.json @@ -6,17 +6,16 @@ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-store": "101.1.*", - "magento/module-catalog": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-sales": "103.0.*", - "magento/module-shipping": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-vault": "101.2.*", - "magento/framework": "103.0.*" + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-sales": "*", + "magento/module-shipping": "*", + "magento/module-quote": "*", + "magento/module-vault": "*", + "magento/framework": "*" }, "autoload": { "files": [ @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Integration/composer.json b/app/code/Magento/Integration/composer.json index 34a2e17f4f087..c85e84284b43f 100644 --- a/app/code/Magento/Integration/composer.json +++ b/app/code/Magento/Integration/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-integration", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-security": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-user": "101.2.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-security": "*", + "magento/module-store": "*", + "magento/module-user": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/LayeredNavigation/composer.json b/app/code/Magento/LayeredNavigation/composer.json index 1c8cd342d071e..fa3c90dbbd774 100644 --- a/app/code/Magento/LayeredNavigation/composer.json +++ b/app/code/Magento/LayeredNavigation/composer.json @@ -1,21 +1,20 @@ { "name": "magento/module-layered-navigation", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/LoginAsCustomer/composer.json b/app/code/Magento/LoginAsCustomer/composer.json index 09f2957e8ab29..e58ec90e8f8bb 100755 --- a/app/code/Magento/LoginAsCustomer/composer.json +++ b/app/code/Magento/LoginAsCustomer/composer.json @@ -1,29 +1,25 @@ { "name": "magento/module-login-as-customer", "description": "Allow for admin to enter a customer account", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-login-as-customer-api": "100.4.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { - "magento/module-backend": "102.0.*" + "magento/module-backend": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { - "files": [ - "registration.php" - ], + "files": [ "registration.php" ], "psr-4": { "Magento\\LoginAsCustomer\\": "" } } } - diff --git a/app/code/Magento/LoginAsCustomerAdminUi/composer.json b/app/code/Magento/LoginAsCustomerAdminUi/composer.json index 90694eeb1d1d0..b6291226827a8 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/composer.json +++ b/app/code/Magento/LoginAsCustomerAdminUi/composer.json @@ -1,31 +1,28 @@ { "name": "magento/module-login-as-customer-admin-ui", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", + "description": "", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-login-as-customer-api": "100.4.*", - "magento/module-login-as-customer-frontend-ui": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-login-as-customer-frontend-ui": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-sales": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-login-as-customer": "100.4.*" + "magento/module-login-as-customer": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { - "files": [ - "registration.php" - ], + "files": [ "registration.php" ], "psr-4": { "Magento\\LoginAsCustomerAdminUi\\": "" } } } - diff --git a/app/code/Magento/LoginAsCustomerApi/composer.json b/app/code/Magento/LoginAsCustomerApi/composer.json index ba88a33748821..b48319b61398f 100644 --- a/app/code/Magento/LoginAsCustomerApi/composer.json +++ b/app/code/Magento/LoginAsCustomerApi/composer.json @@ -1,23 +1,19 @@ { "name": "magento/module-login-as-customer-api", "description": "Allow for admin to enter a customer account", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" - }, "autoload": { - "files": [ - "registration.php" - ], + "files": [ "registration.php" ], "psr-4": { "Magento\\LoginAsCustomerApi\\": "" } } } - diff --git a/app/code/Magento/LoginAsCustomerAssistance/composer.json b/app/code/Magento/LoginAsCustomerAssistance/composer.json index 9c6ac755ed809..a02852533b950 100644 --- a/app/code/Magento/LoginAsCustomerAssistance/composer.json +++ b/app/code/Magento/LoginAsCustomerAssistance/composer.json @@ -1,30 +1,27 @@ { "name": "magento/module-login-as-customer-assistance", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", + "description": "", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-login-as-customer": "100.4.*", - "magento/module-login-as-customer-api": "100.4.*" + "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": "100.4.*" + "magento/module-login-as-customer-admin-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { - "files": [ - "registration.php" - ], + "files": [ "registration.php" ], "psr-4": { "Magento\\LoginAsCustomerAssistance\\": "" } } } - diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/composer.json b/app/code/Magento/LoginAsCustomerFrontendUi/composer.json index a4d772dd2f0e2..279d8ae3ec79e 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/composer.json +++ b/app/code/Magento/LoginAsCustomerFrontendUi/composer.json @@ -1,25 +1,22 @@ { "name": "magento/module-login-as-customer-frontend-ui", + "description": "", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-customer": "*", + "magento/module-store": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-login-as-customer-api": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*" - }, "autoload": { - "files": [ - "registration.php" - ], + "files": [ "registration.php" ], "psr-4": { "Magento\\LoginAsCustomerFrontendUi\\": "" } } } - diff --git a/app/code/Magento/LoginAsCustomerLog/composer.json b/app/code/Magento/LoginAsCustomerLog/composer.json index 85cd7de469078..cf888f8cb1a59 100644 --- a/app/code/Magento/LoginAsCustomerLog/composer.json +++ b/app/code/Magento/LoginAsCustomerLog/composer.json @@ -1,30 +1,27 @@ { "name": "magento/module-login-as-customer-log", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", + "description": "", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-login-as-customer-api": "100.4.*", - "magento/module-ui": "101.2.*", - "magento/module-user": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-login-as-customer-api": "*", + "magento/module-ui": "*", + "magento/module-user": "*" }, "suggest": { - "magento/module-login-as-customer": "100.4.*" + "magento/module-login-as-customer": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { - "files": [ - "registration.php" - ], + "files": [ "registration.php" ], "psr-4": { "Magento\\LoginAsCustomerLog\\": "" } } } - diff --git a/app/code/Magento/LoginAsCustomerPageCache/composer.json b/app/code/Magento/LoginAsCustomerPageCache/composer.json index d16bb45538b09..84d7f2e2a6730 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/composer.json +++ b/app/code/Magento/LoginAsCustomerPageCache/composer.json @@ -1,27 +1,24 @@ { "name": "magento/module-login-as-customer-page-cache", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", + "description": "", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-login-as-customer-api": "100.4.*" + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { - "magento/module-page-cache": "100.4.*" + "magento/module-page-cache": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { - "files": [ - "registration.php" - ], + "files": [ "registration.php" ], "psr-4": { "Magento\\LoginAsCustomerPageCache\\": "" } } } - diff --git a/app/code/Magento/LoginAsCustomerQuote/composer.json b/app/code/Magento/LoginAsCustomerQuote/composer.json index 089cc8d2f2eb0..556ffc0d3be43 100644 --- a/app/code/Magento/LoginAsCustomerQuote/composer.json +++ b/app/code/Magento/LoginAsCustomerQuote/composer.json @@ -1,21 +1,21 @@ { "name": "magento/module-login-as-customer-quote", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", + "description": "", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-quote": "101.2.*" + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-quote": "*" }, "suggest": { - "magento/module-login-as-customer-api": "100.4.*" + "magento/module-login-as-customer-api": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +25,3 @@ } } } - diff --git a/app/code/Magento/LoginAsCustomerSales/composer.json b/app/code/Magento/LoginAsCustomerSales/composer.json index 63295a7cebb8f..3891504e54092 100644 --- a/app/code/Magento/LoginAsCustomerSales/composer.json +++ b/app/code/Magento/LoginAsCustomerSales/composer.json @@ -1,21 +1,21 @@ { "name": "magento/module-login-as-customer-sales", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", + "description": "", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-user": "101.2.*", - "magento/module-login-as-customer-api": "100.4.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-user": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { - "magento/module-sales": "103.0.*" + "magento/module-sales": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Marketplace/composer.json b/app/code/Magento/Marketplace/composer.json index 442825c378948..42bbcf151a17b 100644 --- a/app/code/Magento/Marketplace/composer.json +++ b/app/code/Magento/Marketplace/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-marketplace", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*" + "magento/framework": "*", + "magento/module-backend": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/MediaContent/composer.json b/app/code/Magento/MediaContent/composer.json index 22655d67ab269..4dc2b3eba0f68 100644 --- a/app/code/Magento/MediaContent/composer.json +++ b/app/code/Magento/MediaContent/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-media-content", "description": "Magento module provides the implementation for managing relations between content and media files used in that content", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-api": "*", + "magento/module-media-gallery-api": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-content-api": "100.4.*", - "magento/module-media-gallery-api": "101.0.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/MediaContentApi/composer.json b/app/code/Magento/MediaContentApi/composer.json index a89c7e1a56c58..fd1f2f9a0f265 100644 --- a/app/code/Magento/MediaContentApi/composer.json +++ b/app/code/Magento/MediaContentApi/composer.json @@ -1,17 +1,16 @@ { "name": "magento/module-media-content-api", "description": "Magento module provides the API interfaces for managing relations between content and media files used in that content", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-media-gallery-api": "*", + "magento/framework": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-media-gallery-api": "101.0.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -21,4 +20,3 @@ } } } - diff --git a/app/code/Magento/MediaContentCatalog/composer.json b/app/code/Magento/MediaContentCatalog/composer.json index c8c82ce54e609..2b19bc95f6ed3 100644 --- a/app/code/Magento/MediaContentCatalog/composer.json +++ b/app/code/Magento/MediaContentCatalog/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-media-content-catalog", "description": "Magento module provides the implementation of MediaContent functionality for Magento_Catalog module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-media-content-api": "*", + "magento/module-catalog": "*", + "magento/module-eav": "*", + "magento/module-store": "*", + "magento/framework": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-media-content-api": "100.4.*", - "magento/module-catalog": "104.0.*", - "magento/module-eav": "102.1.*", - "magento/module-store": "101.1.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/MediaContentCms/composer.json b/app/code/Magento/MediaContentCms/composer.json index 5ab23bff4aa6e..ea32fdd7a49fa 100644 --- a/app/code/Magento/MediaContentCms/composer.json +++ b/app/code/Magento/MediaContentCms/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-media-content-cms", "description": "Magento module provides the implementation of MediaContent functionality for Magento_Cms module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-media-content-api": "*", + "magento/module-cms": "*", + "magento/framework": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/module-media-content-api": "100.4.*", - "magento/module-cms": "104.0.*", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index c69fad7fc8ceb..c4ee6ca1b0cf3 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-media-content-synchronization", "description": "Magento module provides implementation of the media content data synchronization.", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", + "magento/framework": "*", "magento/framework-bulk": "*", - "magento/module-media-content-synchronization-api": "100.4.*", - "magento/module-media-content-api": "100.4.*", - "magento/module-asynchronous-operations": "*" + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-content-api": "*", + "magento/framework-message-queue": "*" }, "suggest": { - "magento/module-media-gallery-synchronization": "100.4.*" + "magento/module-media-gallery-synchronization": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json index bc31a91df288b..398aaf1de8071 100644 --- a/app/code/Magento/MediaContentSynchronizationApi/composer.json +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -1,17 +1,16 @@ { "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": "*", + "magento/module-media-content-api": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-content-api": "*" - }, "autoload": { "files": [ "registration.php" diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/composer.json b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json index b4746f335a654..733f29d3a42c2 100644 --- a/app/code/Magento/MediaContentSynchronizationCatalog/composer.json +++ b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json @@ -1,19 +1,18 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-content-synchronization-api": "100.4.*", - "magento/module-media-gallery-synchronization-api": "100.4.*", - "magento/module-media-content-api": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/MediaContentSynchronizationCms/composer.json b/app/code/Magento/MediaContentSynchronizationCms/composer.json index 1e83d61ae1b8c..9028b9dacd0a2 100644 --- a/app/code/Magento/MediaContentSynchronizationCms/composer.json +++ b/app/code/Magento/MediaContentSynchronizationCms/composer.json @@ -1,19 +1,18 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-content-synchronization-api": "100.4.*", - "magento/module-media-gallery-synchronization-api": "100.4.*", - "magento/module-media-content-api": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/MediaGallery/composer.json b/app/code/Magento/MediaGallery/composer.json index ff098785c27c6..d430a174a9738 100644 --- a/app/code/Magento/MediaGallery/composer.json +++ b/app/code/Magento/MediaGallery/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-media-gallery", "description": "Magento module responsible for media handling", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-cms": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-gallery-api": "101.0.*", - "magento/module-cms": "104.0.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryApi/composer.json b/app/code/Magento/MediaGalleryApi/composer.json index d2b6985699c72..8bea8ee95b55a 100644 --- a/app/code/Magento/MediaGalleryApi/composer.json +++ b/app/code/Magento/MediaGalleryApi/composer.json @@ -1,16 +1,15 @@ { "name": "magento/module-media-gallery-api", "description": "Magento module responsible for media gallery asset attributes storage and management", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "101.0.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -20,4 +19,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryCatalog/composer.json b/app/code/Magento/MediaGalleryCatalog/composer.json index c77e76985cc89..192d86684aa76 100644 --- a/app/code/Magento/MediaGalleryCatalog/composer.json +++ b/app/code/Magento/MediaGalleryCatalog/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-media-gallery-catalog", "description": "Magento module responsible for catalog gallery processor delete operation handling", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-catalog": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-gallery-api": "101.0.*", - "magento/module-catalog": "104.0.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/composer.json b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json index 80e8d398a0086..efabb70da9f39 100644 --- a/app/code/Magento/MediaGalleryCatalogIntegration/composer.json +++ b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-media-gallery-catalog-integration", "description": "Magento module responsible for extending catalog image uploader functionality", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cms": "104.0.*", - "magento/module-media-gallery-api": "101.0.*", - "magento/module-media-gallery-synchronization-api": "100.4.*", - "magento/module-media-gallery-ui-api": "100.4.*" + "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": "104.0.*" + "magento/module-catalog": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json index 2299ec3079b35..985d581beff25 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/composer.json +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -1,21 +1,20 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cms": "104.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" - }, "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json index f45a802a86e00..1ecfb9a3c8855 100644 --- a/app/code/Magento/MediaGalleryCmsUi/composer.json +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -1,18 +1,17 @@ { "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": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-cms": "104.0.*", - "magento/module-backend": "102.0.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryIntegration/composer.json b/app/code/Magento/MediaGalleryIntegration/composer.json index 61db48751bd94..a9709da81222e 100644 --- a/app/code/Magento/MediaGalleryIntegration/composer.json +++ b/app/code/Magento/MediaGalleryIntegration/composer.json @@ -1,24 +1,26 @@ { "name": "magento/module-media-gallery-integration", "description": "Magento module responsible for integration of enhanced media gallery", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-gallery-ui-api": "100.4.*", - "magento/module-media-gallery-api": "101.0.*", - "magento/module-media-gallery-synchronization-api": "100.4.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-ui": "*" + }, + "require-dev": { + "magento/module-cms": "*" }, "suggest": { - "magento/module-catalog": "104.0.*", - "magento/module-cms": "104.0.*" + "magento/module-catalog": "*", + "magento/module-cms": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,9 +28,5 @@ "psr-4": { "Magento\\MediaGalleryIntegration\\": "" } - }, - "require-dev": { - "magento/module-cms": "*" } } - diff --git a/app/code/Magento/MediaGalleryMetadata/composer.json b/app/code/Magento/MediaGalleryMetadata/composer.json index 0c085040ed450..c2ce66ce64c36 100644 --- a/app/code/Magento/MediaGalleryMetadata/composer.json +++ b/app/code/Magento/MediaGalleryMetadata/composer.json @@ -1,17 +1,16 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-gallery-metadata-api": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -21,4 +20,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryMetadataApi/composer.json b/app/code/Magento/MediaGalleryMetadataApi/composer.json index 9ad2057bf30d9..f8673884b050c 100644 --- a/app/code/Magento/MediaGalleryMetadataApi/composer.json +++ b/app/code/Magento/MediaGalleryMetadataApi/composer.json @@ -1,16 +1,15 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" - }, "autoload": { "files": [ "registration.php" @@ -20,4 +19,3 @@ } } } - diff --git a/app/code/Magento/MediaGallerySynchronization/composer.json b/app/code/Magento/MediaGallerySynchronization/composer.json index cd07f2b07e396..f9d642dd02568 100644 --- a/app/code/Magento/MediaGallerySynchronization/composer.json +++ b/app/code/Magento/MediaGallerySynchronization/composer.json @@ -1,19 +1,18 @@ { "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": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-gallery-api": "101.0.*", - "magento/module-media-gallery-synchronization-api": "100.4.*", - "magento/framework-message-queue": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/MediaGallerySynchronizationApi/composer.json b/app/code/Magento/MediaGallerySynchronizationApi/composer.json index 7c07b904301df..19bab75dd5f42 100644 --- a/app/code/Magento/MediaGallerySynchronizationApi/composer.json +++ b/app/code/Magento/MediaGallerySynchronizationApi/composer.json @@ -1,17 +1,16 @@ { "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": "*", + "magento/module-media-gallery-api": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-gallery-api": "101.0.*" - }, "autoload": { "files": [ "registration.php" @@ -21,4 +20,3 @@ } } } - diff --git a/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json index c037034b9f216..0674014026b24 100644 --- a/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json +++ b/app/code/Magento/MediaGallerySynchronizationMetadata/composer.json @@ -1,19 +1,18 @@ { "name": "magento/module-media-gallery-synchronization-metadata", "description": "Magento module responsible for images metadata synchronization", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-synchronization-api": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-media-gallery-api": "101.0.*", - "magento/module-media-gallery-metadata-api": "100.4.*", - "magento/module-media-gallery-synchronization-api": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json index 938c6e3f097bf..204e0b37c3bf8 100644 --- a/app/code/Magento/MediaGalleryUi/composer.json +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -1,27 +1,26 @@ { "name": "magento/module-media-gallery-ui", "description": "Magento module responsible for the media gallery UI implementation", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-ui": "101.2.*", - "magento/module-store": "101.1.*", - "magento/module-media-gallery-ui-api": "100.4.*", - "magento/module-media-gallery-api": "101.0.*", - "magento/module-media-gallery-metadata-api": "100.4.*", - "magento/module-media-gallery-synchronization-api": "100.4.*", - "magento/module-media-content-api": "100.4.*", - "magento/module-cms": "104.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": "*", "magento/module-directory": "*", "magento/module-authorization": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json index cb31d2a42dc67..d577f50523f13 100644 --- a/app/code/Magento/MediaGalleryUiApi/composer.json +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -1,19 +1,18 @@ { "name": "magento/module-media-gallery-ui-api", "description": "Magento module responsible for the media gallery UI implementation API", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, "suggest": { "magento/module-cms": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" diff --git a/app/code/Magento/MediaStorage/composer.json b/app/code/Magento/MediaStorage/composer.json index f82969d6a90d3..cb1057febb23e 100644 --- a/app/code/Magento/MediaStorage/composer.json +++ b/app/code/Magento/MediaStorage/composer.json @@ -1,27 +1,26 @@ { "name": "magento/module-media-storage", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/framework-bulk": "101.0.*", - "magento/module-backend": "102.0.*", - "magento/module-config": "101.2.*", - "magento/module-store": "101.1.*", - "magento/module-catalog": "104.0.*", - "magento/module-theme": "101.1.*", - "magento/module-asynchronous-operations": "100.4.*", - "magento/module-authorization": "100.4.*" + "magento/framework": "*", + "magento/framework-bulk": "*", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-theme": "*", + "magento/module-asynchronous-operations": "*", + "magento/module-authorization": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -31,4 +30,3 @@ } } } - diff --git a/app/code/Magento/MessageQueue/composer.json b/app/code/Magento/MessageQueue/composer.json index 994288c14f5c9..57603f0a73acc 100644 --- a/app/code/Magento/MessageQueue/composer.json +++ b/app/code/Magento/MessageQueue/composer.json @@ -1,21 +1,20 @@ { "name": "magento/module-message-queue", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { - "magento/framework": "103.0.*", - "magento/framework-message-queue": "100.4.*", + "magento/framework": "*", + "magento/framework-message-queue": "*", "magento/magento-composer-installer": "*", "php": "~7.3.0||~7.4.0" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/Msrp/composer.json b/app/code/Magento/Msrp/composer.json index 29f328a219ac1..5c9d2e4cf58fa 100644 --- a/app/code/Magento/Msrp/composer.json +++ b/app/code/Magento/Msrp/composer.json @@ -1,28 +1,27 @@ { "name": "magento/module-msrp", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-downloadable": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-downloadable": "*", + "magento/module-eav": "*", + "magento/module-store": "*", + "magento/module-tax": "*" }, "suggest": { - "magento/module-bundle": "101.0.*", - "magento/module-msrp-sample-data": "Sample Data version: 100.4.*" + "magento/module-bundle": "*", + "magento/module-msrp-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/MsrpConfigurableProduct/composer.json b/app/code/Magento/MsrpConfigurableProduct/composer.json index 352971f7772dc..53d274a3c4006 100644 --- a/app/code/Magento/MsrpConfigurableProduct/composer.json +++ b/app/code/Magento/MsrpConfigurableProduct/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-msrp-configurable-product", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-msrp": "100.4.*", - "magento/module-configurable-product": "100.4.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-msrp": "*", + "magento/module-configurable-product": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/MsrpGroupedProduct/composer.json b/app/code/Magento/MsrpGroupedProduct/composer.json index b77c21b583595..5c426b5910ad7 100644 --- a/app/code/Magento/MsrpGroupedProduct/composer.json +++ b/app/code/Magento/MsrpGroupedProduct/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-msrp-grouped-product", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-msrp": "100.4.*", - "magento/module-grouped-product": "100.4.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-msrp": "*", + "magento/module-grouped-product": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Multishipping/composer.json b/app/code/Magento/Multishipping/composer.json index 77ff7f8fdec33..8834603562332 100644 --- a/app/code/Magento/Multishipping/composer.json +++ b/app/code/Magento/Multishipping/composer.json @@ -1,29 +1,28 @@ { "name": "magento/module-multishipping", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-payment": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-theme": "101.1.*", - "magento/module-captcha": "100.4.*" + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-theme": "*", + "magento/module-captcha": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -33,4 +32,3 @@ } } } - diff --git a/app/code/Magento/MysqlMq/composer.json b/app/code/Magento/MysqlMq/composer.json index 508adbcf69acc..225b3a091a462 100644 --- a/app/code/Magento/MysqlMq/composer.json +++ b/app/code/Magento/MysqlMq/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-mysql-mq", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*", - "magento/framework-message-queue": "100.4.*", + "magento/framework": "*", + "magento/framework-message-queue": "*", "magento/magento-composer-installer": "*", - "magento/module-store": "101.1.*", + "magento/module-store": "*", "php": "~7.3.0||~7.4.0" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/NewRelicReporting/composer.json b/app/code/Magento/NewRelicReporting/composer.json index c098476a0af20..ca4c72d5a3aad 100644 --- a/app/code/Magento/NewRelicReporting/composer.json +++ b/app/code/Magento/NewRelicReporting/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-new-relic-reporting", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", + "magento/framework": "*", "magento/magento-composer-installer": "*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-configurable-product": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*" + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-configurable-product": "*", + "magento/module-customer": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/Newsletter/composer.json b/app/code/Magento/Newsletter/composer.json index 3ef7c63be0459..790370c328644 100644 --- a/app/code/Magento/Newsletter/composer.json +++ b/app/code/Magento/Newsletter/composer.json @@ -1,28 +1,27 @@ { "name": "magento/module-newsletter", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-cms": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-email": "101.1.*", - "magento/module-require-js": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-widget": "101.2.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-cms": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-email": "*", + "magento/module-require-js": "*", + "magento/module-store": "*", + "magento/module-widget": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/NewsletterGraphQl/composer.json b/app/code/Magento/NewsletterGraphQl/composer.json index e55f7915448bb..92352a8a9adfe 100644 --- a/app/code/Magento/NewsletterGraphQl/composer.json +++ b/app/code/Magento/NewsletterGraphQl/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-newsletter-graph-ql", "description": "Provides GraphQl functionality for the newsletter subscriptions.", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", + "type": "magento2-module", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-customer": "103.0.*", - "magento/module-newsletter": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-customer": "*", + "magento/module-newsletter": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*" + "magento/module-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/OfflinePayments/composer.json b/app/code/Magento/OfflinePayments/composer.json index 3b88c1161e932..56c7eb2778c48 100644 --- a/app/code/Magento/OfflinePayments/composer.json +++ b/app/code/Magento/OfflinePayments/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-offline-payments", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-payment": "100.4.*" + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-payment": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/OfflineShipping/composer.json b/app/code/Magento/OfflineShipping/composer.json index 00e5e2deedc31..7cd6f05f8ad1c 100644 --- a/app/code/Magento/OfflineShipping/composer.json +++ b/app/code/Magento/OfflineShipping/composer.json @@ -1,32 +1,31 @@ { "name": "magento/module-offline-shipping", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-sales-rule": "101.2.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-sales-rule": "*", + "magento/module-shipping": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-checkout": "100.4.*", - "magento/module-offline-shipping-sample-data": "Sample Data version: 100.4.*" + "magento/module-checkout": "*", + "magento/module-offline-shipping-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -36,4 +35,3 @@ } } } - diff --git a/app/code/Magento/PageCache/composer.json b/app/code/Magento/PageCache/composer.json index 991f2967e0fc9..506fd54886d92 100644 --- a/app/code/Magento/PageCache/composer.json +++ b/app/code/Magento/PageCache/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-page-cache", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-config": "101.2.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-config": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json index 37df1e5c84606..72246c5698f80 100644 --- a/app/code/Magento/Payment/composer.json +++ b/app/code/Magento/Payment/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-payment", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/Paypal/composer.json b/app/code/Magento/Paypal/composer.json index e71f4eee3450c..1b35fae2de1bc 100644 --- a/app/code/Magento/Paypal/composer.json +++ b/app/code/Magento/Paypal/composer.json @@ -1,39 +1,38 @@ { "name": "magento/module-paypal", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.0.1", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-instant-purchase": "100.4.*", - "magento/module-payment": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-vault": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-instant-purchase": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-vault": "*" }, "suggest": { - "magento/module-checkout-agreements": "100.4.*" + "magento/module-checkout-agreements": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -43,4 +42,3 @@ } } } - diff --git a/app/code/Magento/PaypalCaptcha/composer.json b/app/code/Magento/PaypalCaptcha/composer.json index bedfad57a4090..b88eb2f1a552e 100644 --- a/app/code/Magento/PaypalCaptcha/composer.json +++ b/app/code/Magento/PaypalCaptcha/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-paypal-captcha", "description": "Provides CAPTCHA validation for PayPal Payflow Pro", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-captcha": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-captcha": "*", + "magento/module-checkout": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-paypal": "101.0.*" + "magento/module-paypal": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/PaypalGraphQl/composer.json b/app/code/Magento/PaypalGraphQl/composer.json index 00d549a1f86ee..285217da64d72 100644 --- a/app/code/Magento/PaypalGraphQl/composer.json +++ b/app/code/Magento/PaypalGraphQl/composer.json @@ -1,31 +1,30 @@ { "name": "magento/module-paypal-graph-ql", "description": "GraphQl support for Paypal", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-quote": "101.2.*", - "magento/module-checkout": "100.4.*", - "magento/module-paypal": "101.0.*", - "magento/module-quote-graph-ql": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-payment": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-vault": "101.2.*" + "magento/framework": "*", + "magento/module-quote": "*", + "magento/module-checkout": "*", + "magento/module-paypal": "*", + "magento/module-quote-graph-ql": "*", + "magento/module-sales": "*", + "magento/module-payment": "*", + "magento/module-store": "*", + "magento/module-vault": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*", - "magento/module-store-graph-ql": "100.4.*" + "magento/module-graph-ql": "*", + "magento/module-store-graph-ql": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -35,4 +34,3 @@ } } } - diff --git a/app/code/Magento/Persistent/composer.json b/app/code/Magento/Persistent/composer.json index 5a260afc1947e..68fe5cb47c00e 100644 --- a/app/code/Magento/Persistent/composer.json +++ b/app/code/Magento/Persistent/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-persistent", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-cron": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-page-cache": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-cron": "*", + "magento/module-customer": "*", + "magento/module-page-cache": "*", + "magento/module-quote": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/ProductAlert/composer.json b/app/code/Magento/ProductAlert/composer.json index 69753d3edaf70..bfe2a43b373ce 100644 --- a/app/code/Magento/ProductAlert/composer.json +++ b/app/code/Magento/ProductAlert/composer.json @@ -1,27 +1,26 @@ { "name": "magento/module-product-alert", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-store": "*", + "magento/module-theme": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -31,4 +30,3 @@ } } } - diff --git a/app/code/Magento/ProductVideo/composer.json b/app/code/Magento/ProductVideo/composer.json index 38c1e2aa09fc0..b7268338398a7 100644 --- a/app/code/Magento/ProductVideo/composer.json +++ b/app/code/Magento/ProductVideo/composer.json @@ -1,29 +1,28 @@ { "name": "magento/module-product-video", "description": "Add Video to Products", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", + "magento/framework": "*", "magento/magento-composer-installer": "*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-eav": "102.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*" + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-eav": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-customer": "103.0.*", - "magento/module-config": "101.2.*" + "magento/module-customer": "*", + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -33,4 +32,3 @@ } } } - diff --git a/app/code/Magento/Quote/composer.json b/app/code/Magento/Quote/composer.json index fa7f2430f1c2e..31312fae26e78 100644 --- a/app/code/Magento/Quote/composer.json +++ b/app/code/Magento/Quote/composer.json @@ -1,36 +1,35 @@ { "name": "magento/module-quote", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-payment": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-sales-sequence": "100.4.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-payment": "*", + "magento/module-sales": "*", + "magento/module-sales-sequence": "*", + "magento/module-shipping": "*", + "magento/module-store": "*", + "magento/module-tax": "*" }, "suggest": { - "magento/module-webapi": "100.4.*" + "magento/module-webapi": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -40,4 +39,3 @@ } } } - diff --git a/app/code/Magento/QuoteAnalytics/composer.json b/app/code/Magento/QuoteAnalytics/composer.json index 19e70e0326d57..4bfb7172c4c83 100644 --- a/app/code/Magento/QuoteAnalytics/composer.json +++ b/app/code/Magento/QuoteAnalytics/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-quote-analytics", "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-quote": "*", + "magento/module-analytics": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-quote": "101.2.*", - "magento/module-analytics": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/QuoteBundleOptions/composer.json b/app/code/Magento/QuoteBundleOptions/composer.json index 2d6cd95f82761..a2651272018a8 100644 --- a/app/code/Magento/QuoteBundleOptions/composer.json +++ b/app/code/Magento/QuoteBundleOptions/composer.json @@ -1,17 +1,16 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-quote": "101.2.*" - }, "autoload": { "files": [ "registration.php" @@ -21,4 +20,3 @@ } } } - diff --git a/app/code/Magento/QuoteConfigurableOptions/composer.json b/app/code/Magento/QuoteConfigurableOptions/composer.json index 0411ce38850c3..51d6933d5c6d6 100644 --- a/app/code/Magento/QuoteConfigurableOptions/composer.json +++ b/app/code/Magento/QuoteConfigurableOptions/composer.json @@ -1,17 +1,16 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-quote": "101.2.*" - }, "autoload": { "files": [ "registration.php" @@ -21,4 +20,3 @@ } } } - diff --git a/app/code/Magento/QuoteDownloadableLinks/composer.json b/app/code/Magento/QuoteDownloadableLinks/composer.json index a098f1c196fd1..ad120dea96263 100644 --- a/app/code/Magento/QuoteDownloadableLinks/composer.json +++ b/app/code/Magento/QuoteDownloadableLinks/composer.json @@ -1,17 +1,16 @@ { "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" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-quote": "101.2.*" - }, "autoload": { "files": [ "registration.php" @@ -21,4 +20,3 @@ } } } - diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 9327504d78941..25f089cf75a62 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -2,28 +2,27 @@ "name": "magento/module-quote-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-quote": "101.2.*", - "magento/module-checkout": "100.4.*", - "magento/module-catalog": "104.0.*", - "magento/module-store": "101.1.*", - "magento/module-customer": "103.0.*", - "magento/module-customer-graph-ql": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-graph-ql": "100.4.*", - "magento/module-gift-message": "100.4.*" + "magento/framework": "*", + "magento/module-quote": "*", + "magento/module-checkout": "*", + "magento/module-catalog": "*", + "magento/module-store": "*", + "magento/module-customer": "*", + "magento/module-customer-graph-ql": "*", + "magento/module-sales": "*", + "magento/module-directory": "*", + "magento/module-graph-ql": "*", + "magento/module-gift-message": "*" }, "suggest": { - "magento/module-graph-ql-cache": "100.4.*" + "magento/module-graph-ql-cache": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -33,4 +32,3 @@ } } } - diff --git a/app/code/Magento/RelatedProductGraphQl/composer.json b/app/code/Magento/RelatedProductGraphQl/composer.json index 5b2fa809ef789..2cb851d56e58e 100644 --- a/app/code/Magento/RelatedProductGraphQl/composer.json +++ b/app/code/Magento/RelatedProductGraphQl/composer.json @@ -2,20 +2,19 @@ "name": "magento/module-related-product-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-graph-ql": "100.4.*", - "magento/framework": "103.0.*" + "magento/module-catalog": "*", + "magento/module-catalog-graph-ql": "*", + "magento/framework": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*" + "magento/module-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -25,4 +24,3 @@ } } } - diff --git a/app/code/Magento/ReleaseNotification/composer.json b/app/code/Magento/ReleaseNotification/composer.json index d59f25a07b456..c2e347bc66ef0 100644 --- a/app/code/Magento/ReleaseNotification/composer.json +++ b/app/code/Magento/ReleaseNotification/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-release-notification", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-user": "101.2.*", - "magento/module-backend": "102.0.*", - "magento/module-ui": "101.2.*", - "magento/framework": "103.0.*" + "magento/module-user": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", + "magento/framework": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Reports/composer.json b/app/code/Magento/Reports/composer.json index 1560ed99b6971..f1fe6c1e2c83a 100644 --- a/app/code/Magento/Reports/composer.json +++ b/app/code/Magento/Reports/composer.json @@ -1,35 +1,34 @@ { "name": "magento/module-reports", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-cms": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-downloadable": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-quote": "101.2.*", - "magento/module-review": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-sales-rule": "101.2.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-widget": "101.2.*", - "magento/module-wishlist": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-cms": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-downloadable": "*", + "magento/module-eav": "*", + "magento/module-quote": "*", + "magento/module-review": "*", + "magento/module-sales": "*", + "magento/module-sales-rule": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-widget": "*", + "magento/module-wishlist": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -39,4 +38,3 @@ } } } - diff --git a/app/code/Magento/RequireJs/composer.json b/app/code/Magento/RequireJs/composer.json index 9835ed7a6d9d9..9c3b84e88df53 100644 --- a/app/code/Magento/RequireJs/composer.json +++ b/app/code/Magento/RequireJs/composer.json @@ -1,19 +1,18 @@ { "name": "magento/module-require-js", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/Review/composer.json b/app/code/Magento/Review/composer.json index 530828348fe3d..5a428ae15fd67 100644 --- a/app/code/Magento/Review/composer.json +++ b/app/code/Magento/Review/composer.json @@ -1,31 +1,30 @@ { "name": "magento/module-review", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-newsletter": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-newsletter": "*", + "magento/module-store": "*", + "magento/module-theme": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-cookie": "100.4.*", - "magento/module-review-sample-data": "Sample Data version: 100.4.*" + "magento/module-cookie": "*", + "magento/module-review-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -35,4 +34,3 @@ } } } - diff --git a/app/code/Magento/ReviewAnalytics/composer.json b/app/code/Magento/ReviewAnalytics/composer.json index 04f8f7b96deb4..d18ec43a93ac1 100644 --- a/app/code/Magento/ReviewAnalytics/composer.json +++ b/app/code/Magento/ReviewAnalytics/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-review-analytics", "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-review": "*", + "magento/module-analytics": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-review": "100.4.*", - "magento/module-analytics": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/ReviewGraphQl/composer.json b/app/code/Magento/ReviewGraphQl/composer.json index 9bb3d32aa62b5..819ddefd76213 100644 --- a/app/code/Magento/ReviewGraphQl/composer.json +++ b/app/code/Magento/ReviewGraphQl/composer.json @@ -2,22 +2,21 @@ "name": "magento/module-review-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/module-catalog": "104.0.*", - "magento/module-review": "100.4.*", - "magento/module-store": "101.1.*", - "magento/framework": "103.0.*" + "magento/module-catalog": "*", + "magento/module-review": "*", + "magento/module-store": "*", + "magento/framework": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*", - "magento/module-graph-ql-cache": "100.4.*" + "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Robots/composer.json b/app/code/Magento/Robots/composer.json index 486a817d02118..2035010b0ce8b 100644 --- a/app/code/Magento/Robots/composer.json +++ b/app/code/Magento/Robots/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-robots", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.1.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-theme": "101.1.*" + "magento/module-theme": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Rss/composer.json b/app/code/Magento/Rss/composer.json index 26493fdcd774d..bd845acc12f9a 100644 --- a/app/code/Magento/Rss/composer.json +++ b/app/code/Magento/Rss/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-rss", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Rule/composer.json b/app/code/Magento/Rule/composer.json index 882c7a7660bda..0ab2b6780dcad 100644 --- a/app/code/Magento/Rule/composer.json +++ b/app/code/Magento/Rule/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-rule", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-eav": "102.1.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-eav": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/Sales/composer.json b/app/code/Magento/Sales/composer.json index 12f4188747357..411ad3739d560 100644 --- a/app/code/Magento/Sales/composer.json +++ b/app/code/Magento/Sales/composer.json @@ -1,46 +1,45 @@ { "name": "magento/module-sales", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "103.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-bundle": "101.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-gift-message": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-payment": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-reports": "100.4.*", - "magento/module-sales-rule": "101.2.*", - "magento/module-sales-sequence": "100.4.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-widget": "101.2.*", - "magento/module-wishlist": "101.2.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-bundle": "*", + "magento/module-catalog-inventory": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-gift-message": "*", + "magento/module-media-storage": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-reports": "*", + "magento/module-sales-rule": "*", + "magento/module-sales-sequence": "*", + "magento/module-shipping": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-widget": "*", + "magento/module-wishlist": "*" }, "suggest": { - "magento/module-sales-sample-data": "Sample Data version: 100.4.*" + "magento/module-sales-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -50,4 +49,3 @@ } } } - diff --git a/app/code/Magento/SalesAnalytics/composer.json b/app/code/Magento/SalesAnalytics/composer.json index 5e57e330c1c5f..ca7926f2d8b5a 100644 --- a/app/code/Magento/SalesAnalytics/composer.json +++ b/app/code/Magento/SalesAnalytics/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-sales-analytics", "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-sales": "*", + "magento/module-analytics": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-sales": "103.0.*", - "magento/module-analytics": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/SalesGraphQl/composer.json b/app/code/Magento/SalesGraphQl/composer.json index 1d7cf3da05685..b85d8c0f852da 100644 --- a/app/code/Magento/SalesGraphQl/composer.json +++ b/app/code/Magento/SalesGraphQl/composer.json @@ -2,22 +2,21 @@ "name": "magento/module-sales-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-tax": "*", + "magento/module-quote": "*", + "magento/module-graph-ql": "*", + "magento/module-shipping": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-catalog": "104.0.*", - "magento/module-tax": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-graph-ql": "100.4.*", - "magento/module-shipping": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/SalesInventory/composer.json b/app/code/Magento/SalesInventory/composer.json index fbf4820f4d110..6a91b04a7c0d9 100644 --- a/app/code/Magento/SalesInventory/composer.json +++ b/app/code/Magento/SalesInventory/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-sales-inventory", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-sales": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/SalesRule/composer.json b/app/code/Magento/SalesRule/composer.json index 65fd55a2b3806..572e191093275 100644 --- a/app/code/Magento/SalesRule/composer.json +++ b/app/code/Magento/SalesRule/composer.json @@ -1,41 +1,40 @@ { "name": "magento/module-sales-rule", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-rule": "101.2.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-payment": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-reports": "100.4.*", - "magento/module-rule": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-widget": "101.2.*", - "magento/module-captcha": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-authorization": "100.4.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-rule": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-reports": "*", + "magento/module-rule": "*", + "magento/module-sales": "*", + "magento/module-shipping": "*", + "magento/module-store": "*", + "magento/module-ui": "*", + "magento/module-widget": "*", + "magento/module-captcha": "*", + "magento/module-checkout": "*", + "magento/module-authorization": "*" }, "suggest": { - "magento/module-sales-rule-sample-data": "Sample Data version: 100.4.*" + "magento/module-sales-rule-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -45,4 +44,3 @@ } } } - diff --git a/app/code/Magento/SalesSequence/composer.json b/app/code/Magento/SalesSequence/composer.json index a39c350628c8a..a0f9cb45cafc8 100644 --- a/app/code/Magento/SalesSequence/composer.json +++ b/app/code/Magento/SalesSequence/composer.json @@ -1,19 +1,18 @@ { "name": "magento/module-sales-sequence", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/SampleData/composer.json b/app/code/Magento/SampleData/composer.json index baa802aa8753c..30efc94bc9274 100644 --- a/app/code/Magento/SampleData/composer.json +++ b/app/code/Magento/SampleData/composer.json @@ -1,22 +1,21 @@ { "name": "magento/module-sample-data", "description": "Sample Data fixtures", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, "suggest": { - "magento/sample-data-media": "Sample Data version: 100.4.*" + "magento/sample-data-media": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "cli_commands.php", @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Search/composer.json b/app/code/Magento/Search/composer.json index 945a016262d2b..3df1dc5935ad8 100644 --- a/app/code/Magento/Search/composer.json +++ b/app/code/Magento/Search/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-search", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog-search": "102.0.*", - "magento/module-reports": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog-search": "*", + "magento/module-reports": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/Security/composer.json b/app/code/Magento/Security/composer.json index 6b3ac2c37ef29..4978f0c628f96 100644 --- a/app/code/Magento/Security/composer.json +++ b/app/code/Magento/Security/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-security", "description": "Security management module", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-store": "101.1.*", - "magento/module-user": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-store": "*", + "magento/module-user": "*" }, "suggest": { - "magento/module-customer": "103.0.*" + "magento/module-customer": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/SendFriend/composer.json b/app/code/Magento/SendFriend/composer.json index cf8ef2f923eb3..17c908ab33e3e 100644 --- a/app/code/Magento/SendFriend/composer.json +++ b/app/code/Magento/SendFriend/composer.json @@ -1,25 +1,24 @@ { "name": "magento/module-send-friend", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-customer": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-captcha": "100.4.*", - "magento/module-authorization": "100.4.*", - "magento/module-theme": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-customer": "*", + "magento/module-store": "*", + "magento/module-captcha": "*", + "magento/module-authorization": "*", + "magento/module-theme": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +28,3 @@ } } } - diff --git a/app/code/Magento/SendFriendGraphQl/composer.json b/app/code/Magento/SendFriendGraphQl/composer.json index b0ce37458b1c5..456780c1c1841 100644 --- a/app/code/Magento/SendFriendGraphQl/composer.json +++ b/app/code/Magento/SendFriendGraphQl/composer.json @@ -2,18 +2,17 @@ "name": "magento/module-send-friend-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-send-friend": "*", + "magento/module-graph-ql": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-send-friend": "100.4.*", - "magento/module-graph-ql": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/Shipping/composer.json b/app/code/Magento/Shipping/composer.json index ca9c0486b25df..5ea8430226ad8 100644 --- a/app/code/Magento/Shipping/composer.json +++ b/app/code/Magento/Shipping/composer.json @@ -1,38 +1,37 @@ { "name": "magento/module-shipping", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", "ext-gd": "*", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-contact": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-payment": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-ui": "101.2.*", - "magento/module-user": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-contact": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-ui": "*", + "magento/module-user": "*" }, "suggest": { - "magento/module-fedex": "100.4.*", - "magento/module-ups": "100.4.*", - "magento/module-config": "101.2.*" + "magento/module-fedex": "*", + "magento/module-ups": "*", + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -42,4 +41,3 @@ } } } - diff --git a/app/code/Magento/Sitemap/composer.json b/app/code/Magento/Sitemap/composer.json index 64ab7b60e17ab..6a9f20ac8bddf 100644 --- a/app/code/Magento/Sitemap/composer.json +++ b/app/code/Magento/Sitemap/composer.json @@ -1,31 +1,30 @@ { "name": "magento/module-sitemap", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-url-rewrite": "100.4.*", - "magento/module-cms": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-eav": "102.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-robots": "101.1.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-url-rewrite": "*", + "magento/module-cms": "*", + "magento/module-config": "*", + "magento/module-eav": "*", + "magento/module-media-storage": "*", + "magento/module-robots": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -35,4 +34,3 @@ } } } - diff --git a/app/code/Magento/Store/composer.json b/app/code/Magento/Store/composer.json index 870a77578672b..e6f7f0d5ac274 100644 --- a/app/code/Magento/Store/composer.json +++ b/app/code/Magento/Store/composer.json @@ -1,30 +1,29 @@ { "name": "magento/module-store", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-ui": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-media-storage": "*", + "magento/module-ui": "*", + "magento/module-customer": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*" }, "suggest": { - "magento/module-deploy": "100.4.*" + "magento/module-deploy": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -34,4 +33,3 @@ } } } - diff --git a/app/code/Magento/StoreGraphQl/composer.json b/app/code/Magento/StoreGraphQl/composer.json index 42023b2403e91..a7cab5851a9ee 100644 --- a/app/code/Magento/StoreGraphQl/composer.json +++ b/app/code/Magento/StoreGraphQl/composer.json @@ -2,17 +2,16 @@ "name": "magento/module-store-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-graph-ql": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-graph-ql": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/Swagger/composer.json b/app/code/Magento/Swagger/composer.json index 2cffb5d716d54..759e72350b0a6 100644 --- a/app/code/Magento/Swagger/composer.json +++ b/app/code/Magento/Swagger/composer.json @@ -1,19 +1,18 @@ { "name": "magento/module-swagger", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/SwaggerWebapi/composer.json b/app/code/Magento/SwaggerWebapi/composer.json index 09c078448232d..78021f7cb4ec5 100644 --- a/app/code/Magento/SwaggerWebapi/composer.json +++ b/app/code/Magento/SwaggerWebapi/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-swagger-webapi", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-swagger": "100.4.*" + "magento/framework": "*", + "magento/module-swagger": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/SwaggerWebapiAsync/composer.json b/app/code/Magento/SwaggerWebapiAsync/composer.json index d42e387499e8a..283b2fe1f1758 100644 --- a/app/code/Magento/SwaggerWebapiAsync/composer.json +++ b/app/code/Magento/SwaggerWebapiAsync/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-swagger-webapi-async", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-swagger": "100.4.*" + "magento/framework": "*", + "magento/module-swagger": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Swatches/composer.json b/app/code/Magento/Swatches/composer.json index 4198e66b8f06a..2c9b7a03ba011 100644 --- a/app/code/Magento/Swatches/composer.json +++ b/app/code/Magento/Swatches/composer.json @@ -1,33 +1,32 @@ { "name": "magento/module-swatches", "description": "Add Swatches to Products", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-configurable-product": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-page-cache": "100.4.*", - "magento/module-media-storage": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-config": "*", + "magento/module-configurable-product": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-page-cache": "*", + "magento/module-media-storage": "*", + "magento/module-store": "*", + "magento/module-theme": "*" }, "suggest": { - "magento/module-layered-navigation": "100.4.*", - "magento/module-swatches-sample-data": "Sample Data version: 100.4.*" + "magento/module-layered-navigation": "*", + "magento/module-swatches-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -37,4 +36,3 @@ } } } - diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 87d6ff61e5fe2..1b98b4044a2ff 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -2,18 +2,17 @@ "name": "magento/module-swatches-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-swatches": "*", + "magento/module-catalog": "*", + "magento/module-catalog-graph-ql": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.1", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-swatches": "100.4.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-graph-ql": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/SwatchesLayeredNavigation/composer.json b/app/code/Magento/SwatchesLayeredNavigation/composer.json index f0f794db094f2..3b987f8096f18 100644 --- a/app/code/Magento/SwatchesLayeredNavigation/composer.json +++ b/app/code/Magento/SwatchesLayeredNavigation/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-swatches-layered-navigation", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", + "magento/framework": "*", "magento/magento-composer-installer": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/Tax/composer.json b/app/code/Magento/Tax/composer.json index 1b43e4c5ac6fd..2fe0597c85a63 100644 --- a/app/code/Magento/Tax/composer.json +++ b/app/code/Magento/Tax/composer.json @@ -1,36 +1,35 @@ { "name": "magento/module-tax", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-page-cache": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-reports": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-page-cache": "*", + "magento/module-quote": "*", + "magento/module-reports": "*", + "magento/module-sales": "*", + "magento/module-shipping": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-tax-sample-data": "Sample Data version: 100.4.*" + "magento/module-tax-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -40,4 +39,3 @@ } } } - diff --git a/app/code/Magento/TaxGraphQl/composer.json b/app/code/Magento/TaxGraphQl/composer.json index 0b4805fe24493..b97e414cacb67 100644 --- a/app/code/Magento/TaxGraphQl/composer.json +++ b/app/code/Magento/TaxGraphQl/composer.json @@ -2,19 +2,18 @@ "name": "magento/module-tax-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, "suggest": { - "magento/module-tax": "100.4.*", - "magento/module-catalog-graph-ql": "100.4.*" + "magento/module-tax": "*", + "magento/module-catalog-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/TaxImportExport/composer.json b/app/code/Magento/TaxImportExport/composer.json index 0d4ea14db838f..01c069b4299c1 100644 --- a/app/code/Magento/TaxImportExport/composer.json +++ b/app/code/Magento/TaxImportExport/composer.json @@ -1,24 +1,23 @@ { "name": "magento/module-tax-import-export", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-directory": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-directory": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -28,4 +27,3 @@ } } } - diff --git a/app/code/Magento/Theme/composer.json b/app/code/Magento/Theme/composer.json index 97e1664ed563e..63779c6f9bf5d 100644 --- a/app/code/Magento/Theme/composer.json +++ b/app/code/Magento/Theme/composer.json @@ -1,34 +1,33 @@ { "name": "magento/module-theme", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.1.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-cms": "104.0.*", - "magento/module-config": "101.2.*", - "magento/module-customer": "103.0.*", - "magento/module-eav": "102.1.*", - "magento/module-media-storage": "100.4.*", - "magento/module-require-js": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-widget": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-cms": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-eav": "*", + "magento/module-media-storage": "*", + "magento/module-require-js": "*", + "magento/module-store": "*", + "magento/module-ui": "*", + "magento/module-widget": "*" }, "suggest": { - "magento/module-theme-sample-data": "Sample Data version: 100.4.*", - "magento/module-deploy": "100.4.*", - "magento/module-directory": "100.4.*" + "magento/module-theme-sample-data": "*", + "magento/module-deploy": "*", + "magento/module-directory": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -38,4 +37,3 @@ } } } - diff --git a/app/code/Magento/ThemeGraphQl/composer.json b/app/code/Magento/ThemeGraphQl/composer.json index d2308d43f1670..cee740d449b37 100644 --- a/app/code/Magento/ThemeGraphQl/composer.json +++ b/app/code/Magento/ThemeGraphQl/composer.json @@ -2,18 +2,17 @@ "name": "magento/module-theme-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, "suggest": { - "magento/module-store-graph-ql": "100.4.*" + "magento/module-store-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/Tinymce3/composer.json b/app/code/Magento/Tinymce3/composer.json index b5ab9f5d6b680..0b8cf6824295e 100644 --- a/app/code/Magento/Tinymce3/composer.json +++ b/app/code/Magento/Tinymce3/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-tinymce-3", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-ui": "101.2.*", - "magento/module-variable": "100.4.*", - "magento/module-widget": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", + "magento/module-variable": "*", + "magento/module-widget": "*" }, "suggest": { - "magento/module-cms": "104.0.*" + "magento/module-cms": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Translation/composer.json b/app/code/Magento/Translation/composer.json index 7813065f14141..7f67749fa88f4 100644 --- a/app/code/Magento/Translation/composer.json +++ b/app/code/Magento/Translation/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-translation", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-developer": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-developer": "*", + "magento/module-store": "*", + "magento/module-theme": "*" }, "suggest": { - "magento/module-deploy": "100.4.*" + "magento/module-deploy": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/Ui/composer.json b/app/code/Magento/Ui/composer.json index b856a805b1010..b4aeda0fc1e6a 100644 --- a/app/code/Magento/Ui/composer.json +++ b/app/code/Magento/Ui/composer.json @@ -1,27 +1,26 @@ { "name": "magento/module-ui", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-eav": "102.1.*", - "magento/module-store": "101.1.*", - "magento/module-user": "101.2.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-eav": "*", + "magento/module-store": "*", + "magento/module-user": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -31,4 +30,3 @@ } } } - diff --git a/app/code/Magento/Ups/composer.json b/app/code/Magento/Ups/composer.json index 1115802f21fa6..fa8962f0af592 100644 --- a/app/code/Magento/Ups/composer.json +++ b/app/code/Magento/Ups/composer.json @@ -1,29 +1,28 @@ { "name": "magento/module-ups", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-directory": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog-inventory": "*", + "magento/module-directory": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-shipping": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-config": "101.2.*" + "magento/module-config": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -33,4 +32,3 @@ } } } - diff --git a/app/code/Magento/UrlRewrite/composer.json b/app/code/Magento/UrlRewrite/composer.json index 9e1146fa7fe44..44ca51e8bcbe2 100644 --- a/app/code/Magento/UrlRewrite/composer.json +++ b/app/code/Magento/UrlRewrite/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-url-rewrite", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "102.0.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-url-rewrite": "100.4.*", - "magento/module-cms": "104.0.*", - "magento/module-cms-url-rewrite": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-url-rewrite": "*", + "magento/module-cms": "*", + "magento/module-cms-url-rewrite": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/UrlRewriteGraphQl/composer.json b/app/code/Magento/UrlRewriteGraphQl/composer.json index 15b97a44941cb..766ad3ab46ebd 100644 --- a/app/code/Magento/UrlRewriteGraphQl/composer.json +++ b/app/code/Magento/UrlRewriteGraphQl/composer.json @@ -2,19 +2,18 @@ "name": "magento/module-url-rewrite-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-url-rewrite": "102.0.*" + "magento/framework": "*", + "magento/module-url-rewrite": "*" }, "suggest": { - "magento/module-graph-ql": "100.4.*" + "magento/module-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/User/composer.json b/app/code/Magento/User/composer.json index 9242b5461d5df..6ba4be749cc7c 100644 --- a/app/code/Magento/User/composer.json +++ b/app/code/Magento/User/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-user", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-email": "101.1.*", - "magento/module-integration": "100.4.*", - "magento/module-security": "100.4.*", - "magento/module-store": "101.1.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-email": "*", + "magento/module-integration": "*", + "magento/module-security": "*", + "magento/module-store": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/Usps/composer.json b/app/code/Magento/Usps/composer.json index 8648000e4b08e..3d5c0669c679d 100644 --- a/app/code/Magento/Usps/composer.json +++ b/app/code/Magento/Usps/composer.json @@ -1,28 +1,27 @@ { "name": "magento/module-usps", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", "lib-libxml": "*", - "magento/framework": "103.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-config": "101.2.*", - "magento/module-directory": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-shipping": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-config": "*", + "magento/module-directory": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-shipping": "*", + "magento/module-store": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -32,4 +31,3 @@ } } } - diff --git a/app/code/Magento/Variable/composer.json b/app/code/Magento/Variable/composer.json index 52231c86a49ca..e6eed40a814db 100644 --- a/app/code/Magento/Variable/composer.json +++ b/app/code/Magento/Variable/composer.json @@ -1,23 +1,22 @@ { "name": "magento/module-variable", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-store": "101.1.*", - "magento/module-config": "101.2.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-store": "*", + "magento/module-config": "*", + "magento/module-ui": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -27,4 +26,3 @@ } } } - diff --git a/app/code/Magento/Vault/composer.json b/app/code/Magento/Vault/composer.json index 7dacdfe0d6e26..31d5ceb906246 100644 --- a/app/code/Magento/Vault/composer.json +++ b/app/code/Magento/Vault/composer.json @@ -1,25 +1,25 @@ { "name": "magento/module-vault", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], + "description": "", "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-payment": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*" + "magento/framework": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-payment": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-theme": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -29,4 +29,3 @@ } } } - diff --git a/app/code/Magento/VaultGraphQl/composer.json b/app/code/Magento/VaultGraphQl/composer.json index 9e7832c8ee184..aff9a700fbcad 100644 --- a/app/code/Magento/VaultGraphQl/composer.json +++ b/app/code/Magento/VaultGraphQl/composer.json @@ -2,17 +2,16 @@ "name": "magento/module-vault-graph-ql", "description": "N/A", "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-vault": "*", + "magento/module-graph-ql": "*" + }, "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-vault": "101.2.*", - "magento/module-graph-ql": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/Version/composer.json b/app/code/Magento/Version/composer.json index d1a9f535b7c2d..d2b2127446c21 100644 --- a/app/code/Magento/Version/composer.json +++ b/app/code/Magento/Version/composer.json @@ -1,19 +1,18 @@ { "name": "magento/module-version", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -23,4 +22,3 @@ } } } - diff --git a/app/code/Magento/Webapi/composer.json b/app/code/Magento/Webapi/composer.json index 0313c903d367a..11382cc318554 100644 --- a/app/code/Magento/Webapi/composer.json +++ b/app/code/Magento/Webapi/composer.json @@ -1,27 +1,26 @@ { "name": "magento/module-webapi", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-authorization": "100.4.*", - "magento/module-backend": "102.0.*", - "magento/module-integration": "100.4.*", - "magento/module-store": "101.1.*" + "magento/framework": "*", + "magento/module-authorization": "*", + "magento/module-backend": "*", + "magento/module-integration": "*", + "magento/module-store": "*" }, "suggest": { - "magento/module-user": "101.2.*", - "magento/module-customer": "103.0.*" + "magento/module-user": "*", + "magento/module-customer": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -31,4 +30,3 @@ } } } - diff --git a/app/code/Magento/WebapiAsync/composer.json b/app/code/Magento/WebapiAsync/composer.json index 715dd1e0be707..e0c6a96f1ffe6 100644 --- a/app/code/Magento/WebapiAsync/composer.json +++ b/app/code/Magento/WebapiAsync/composer.json @@ -1,26 +1,25 @@ { "name": "magento/module-webapi-async", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/framework-message-queue": "100.4.*", - "magento/module-webapi": "100.4.*", - "magento/module-asynchronous-operations": "100.4.*" + "magento/framework": "*", + "magento/framework-message-queue": "*", + "magento/module-webapi": "*", + "magento/module-asynchronous-operations": "*" }, "suggest": { - "magento/module-user": "101.2.*", - "magento/module-customer": "103.0.*" + "magento/module-user": "*", + "magento/module-customer": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -30,4 +29,3 @@ } } } - diff --git a/app/code/Magento/WebapiSecurity/composer.json b/app/code/Magento/WebapiSecurity/composer.json index adcbf01fe212b..5b48ed8644709 100644 --- a/app/code/Magento/WebapiSecurity/composer.json +++ b/app/code/Magento/WebapiSecurity/composer.json @@ -1,20 +1,19 @@ { "name": "magento/module-webapi-security", "description": "WebapiSecurity module provides option to loosen security on some webapi resources.", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-webapi": "100.4.*" + "magento/framework": "*", + "magento/module-webapi": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -24,4 +23,3 @@ } } } - diff --git a/app/code/Magento/Weee/composer.json b/app/code/Magento/Weee/composer.json index f7da07b15fe74..7024de0f595c7 100644 --- a/app/code/Magento/Weee/composer.json +++ b/app/code/Magento/Weee/composer.json @@ -1,34 +1,33 @@ { "name": "magento/module-weee", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-directory": "100.4.*", - "magento/module-eav": "102.1.*", - "magento/module-page-cache": "100.4.*", - "magento/module-quote": "101.2.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-directory": "*", + "magento/module-eav": "*", + "magento/module-page-cache": "*", + "magento/module-quote": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-bundle": "101.0.*" + "magento/module-bundle": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -38,4 +37,3 @@ } } } - diff --git a/app/code/Magento/WeeeGraphQl/composer.json b/app/code/Magento/WeeeGraphQl/composer.json index 9355b5a196ae7..be7e50ab2fca1 100644 --- a/app/code/Magento/WeeeGraphQl/composer.json +++ b/app/code/Magento/WeeeGraphQl/composer.json @@ -2,21 +2,20 @@ "name": "magento/module-weee-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.0", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-tax": "100.4.*", - "magento/module-weee": "100.4.*" + "magento/framework": "*", + "magento/module-store": "*", + "magento/module-tax": "*", + "magento/module-weee": "*" }, "suggest": { - "magento/module-catalog-graph-ql": "100.4.*" + "magento/module-catalog-graph-ql": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -26,4 +25,3 @@ } } } - diff --git a/app/code/Magento/Widget/composer.json b/app/code/Magento/Widget/composer.json index 9785dcb9099f8..2cf8429095ce7 100644 --- a/app/code/Magento/Widget/composer.json +++ b/app/code/Magento/Widget/composer.json @@ -1,29 +1,28 @@ { "name": "magento/module-widget", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-cms": "104.0.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*", - "magento/module-variable": "100.4.*", - "magento/module-ui": "101.2.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-cms": "*", + "magento/module-store": "*", + "magento/module-theme": "*", + "magento/module-variable": "*", + "magento/module-ui": "*" }, "suggest": { - "magento/module-widget-sample-data": "Sample Data version: 100.4.*" + "magento/module-widget-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -33,4 +32,3 @@ } } } - diff --git a/app/code/Magento/Wishlist/composer.json b/app/code/Magento/Wishlist/composer.json index c08de372b366f..b426ffe01cecc 100644 --- a/app/code/Magento/Wishlist/composer.json +++ b/app/code/Magento/Wishlist/composer.json @@ -1,38 +1,37 @@ { "name": "magento/module-wishlist", "description": "N/A", - "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "101.2.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-backend": "102.0.*", - "magento/module-catalog": "104.0.*", - "magento/module-catalog-inventory": "100.4.*", - "magento/module-checkout": "100.4.*", - "magento/module-customer": "103.0.*", - "magento/module-rss": "100.4.*", - "magento/module-sales": "103.0.*", - "magento/module-store": "101.1.*", - "magento/module-theme": "101.1.*", - "magento/module-ui": "101.2.*", - "magento/module-captcha": "100.4.*" + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-catalog-inventory": "*", + "magento/module-checkout": "*", + "magento/module-customer": "*", + "magento/module-rss": "*", + "magento/module-sales": "*", + "magento/module-store": "*", + "magento/module-theme": "*", + "magento/module-ui": "*", + "magento/module-captcha": "*" }, "suggest": { - "magento/module-configurable-product": "100.4.*", - "magento/module-downloadable": "100.4.*", - "magento/module-bundle": "101.0.*", - "magento/module-cookie": "100.4.*", - "magento/module-grouped-product": "100.4.*", - "magento/module-wishlist-sample-data": "Sample Data version: 100.4.*" + "magento/module-configurable-product": "*", + "magento/module-downloadable": "*", + "magento/module-bundle": "*", + "magento/module-cookie": "*", + "magento/module-grouped-product": "*", + "magento/module-wishlist-sample-data": "*" }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" @@ -42,4 +41,3 @@ } } } - diff --git a/app/code/Magento/WishlistAnalytics/composer.json b/app/code/Magento/WishlistAnalytics/composer.json index 91ad9baeecb37..309257f857ed2 100644 --- a/app/code/Magento/WishlistAnalytics/composer.json +++ b/app/code/Magento/WishlistAnalytics/composer.json @@ -1,18 +1,17 @@ { "name": "magento/module-wishlist-analytics", "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-wishlist": "*", + "magento/module-analytics": "*" + }, "type": "magento2-module", "license": [ "OSL-3.0", "AFL-3.0" ], - "version": "100.4.0", - "require": { - "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/module-wishlist": "101.2.*", - "magento/module-analytics": "100.4.*" - }, "autoload": { "files": [ "registration.php" @@ -22,4 +21,3 @@ } } } - diff --git a/app/code/Magento/WishlistGraphQl/composer.json b/app/code/Magento/WishlistGraphQl/composer.json index 84e0cde975b77..58bc738bd24d6 100644 --- a/app/code/Magento/WishlistGraphQl/composer.json +++ b/app/code/Magento/WishlistGraphQl/composer.json @@ -2,19 +2,18 @@ "name": "magento/module-wishlist-graph-ql", "description": "N/A", "type": "magento2-module", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", + "magento/framework": "*", "magento/module-catalog": "*", - "magento/module-catalog-graph-ql": "100.4.*", - "magento/module-wishlist": "101.2.*", - "magento/module-store": "101.1.*" + "magento/module-catalog-graph-ql": "*", + "magento/module-wishlist": "*", + "magento/module-store": "*" }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" diff --git a/app/design/adminhtml/Magento/backend/composer.json b/app/design/adminhtml/Magento/backend/composer.json index daeffa56c2a62..249441be1753e 100644 --- a/app/design/adminhtml/Magento/backend/composer.json +++ b/app/design/adminhtml/Magento/backend/composer.json @@ -1,23 +1,21 @@ { "name": "magento/theme-adminhtml-backend", "description": "N/A", - "type": "magento2-theme", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-theme", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/design/frontend/Magento/blank/composer.json b/app/design/frontend/Magento/blank/composer.json index 2e39bcf46a49c..066d0cd1cc9f2 100644 --- a/app/design/frontend/Magento/blank/composer.json +++ b/app/design/frontend/Magento/blank/composer.json @@ -1,23 +1,21 @@ { "name": "magento/theme-frontend-blank", "description": "N/A", - "type": "magento2-theme", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-theme", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/design/frontend/Magento/luma/composer.json b/app/design/frontend/Magento/luma/composer.json index e3c39dd579604..16bed43fe8cbf 100644 --- a/app/design/frontend/Magento/luma/composer.json +++ b/app/design/frontend/Magento/luma/composer.json @@ -1,24 +1,22 @@ { "name": "magento/theme-frontend-luma", "description": "N/A", - "type": "magento2-theme", - "license": [ - "OSL-3.0", - "AFL-3.0" - ], "config": { "sort-packages": true }, - "version": "100.4.1", "require": { "php": "~7.3.0||~7.4.0", - "magento/framework": "103.0.*", - "magento/theme-frontend-blank": "100.4.*" + "magento/framework": "*", + "magento/theme-frontend-blank": "*" }, + "type": "magento2-theme", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/i18n/Magento/de_DE/composer.json b/app/i18n/Magento/de_DE/composer.json index fd23d037ba459..5a488a3e32c2b 100644 --- a/app/i18n/Magento/de_DE/composer.json +++ b/app/i18n/Magento/de_DE/composer.json @@ -1,7 +1,6 @@ { "name": "magento/language-de_de", "description": "German (Germany) language", - "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -9,14 +8,13 @@ "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/i18n/Magento/en_US/composer.json b/app/i18n/Magento/en_US/composer.json index 194854d58bbe2..1108c70de28a6 100644 --- a/app/i18n/Magento/en_US/composer.json +++ b/app/i18n/Magento/en_US/composer.json @@ -1,7 +1,6 @@ { "name": "magento/language-en_us", "description": "English (United States) language", - "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -9,14 +8,13 @@ "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/i18n/Magento/es_ES/composer.json b/app/i18n/Magento/es_ES/composer.json index 0b49475587d54..5bc3cb5730adf 100644 --- a/app/i18n/Magento/es_ES/composer.json +++ b/app/i18n/Magento/es_ES/composer.json @@ -1,7 +1,6 @@ { "name": "magento/language-es_es", "description": "Spanish (Spain) language", - "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -9,14 +8,13 @@ "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/i18n/Magento/fr_FR/composer.json b/app/i18n/Magento/fr_FR/composer.json index ada414e6a7a32..50c541308673b 100644 --- a/app/i18n/Magento/fr_FR/composer.json +++ b/app/i18n/Magento/fr_FR/composer.json @@ -1,7 +1,6 @@ { "name": "magento/language-fr_fr", "description": "French (France) language", - "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -9,14 +8,13 @@ "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/i18n/Magento/nl_NL/composer.json b/app/i18n/Magento/nl_NL/composer.json index a881eed112ea0..a182e179d4103 100644 --- a/app/i18n/Magento/nl_NL/composer.json +++ b/app/i18n/Magento/nl_NL/composer.json @@ -1,7 +1,6 @@ { "name": "magento/language-nl_nl", "description": "Dutch (Netherlands) language", - "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -9,14 +8,13 @@ "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/i18n/Magento/pt_BR/composer.json b/app/i18n/Magento/pt_BR/composer.json index 6e10bc16f6a79..46734cc09b363 100644 --- a/app/i18n/Magento/pt_BR/composer.json +++ b/app/i18n/Magento/pt_BR/composer.json @@ -1,7 +1,6 @@ { "name": "magento/language-pt_br", "description": "Portuguese (Brazil) language", - "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -9,14 +8,13 @@ "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } - diff --git a/app/i18n/Magento/zh_Hans_CN/composer.json b/app/i18n/Magento/zh_Hans_CN/composer.json index 8491eced1389f..ce214ce649f56 100644 --- a/app/i18n/Magento/zh_Hans_CN/composer.json +++ b/app/i18n/Magento/zh_Hans_CN/composer.json @@ -1,7 +1,6 @@ { "name": "magento/language-zh_hans_cn", "description": "Chinese (China) language", - "type": "magento2-language", "license": [ "OSL-3.0", "AFL-3.0" @@ -9,14 +8,13 @@ "config": { "sort-packages": true }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*" + "magento/framework": "*" }, + "type": "magento2-language", "autoload": { "files": [ "registration.php" ] } } - diff --git a/lib/internal/Magento/Framework/Amqp/composer.json b/lib/internal/Magento/Framework/Amqp/composer.json index c1a34426fe87e..fc65e37d12ecf 100644 --- a/lib/internal/Magento/Framework/Amqp/composer.json +++ b/lib/internal/Magento/Framework/Amqp/composer.json @@ -1,27 +1,25 @@ { "name": "magento/framework-amqp", "description": "N/A", + "config": { + "sort-packages": true + }, "type": "magento2-library", "license": [ "OSL-3.0", "AFL-3.0" ], - "config": { - "sort-packages": true - }, - "version": "100.4.0", "require": { - "magento/framework": "103.0.*", + "magento/framework": "*", "php": "~7.3.0||~7.4.0", "php-amqplib/php-amqplib": "~2.7.0||~2.10.0" }, "autoload": { - "files": [ - "registration.php" - ], "psr-4": { "Magento\\Framework\\Amqp\\": "" - } + }, + "files": [ + "registration.php" + ] } } - diff --git a/lib/internal/Magento/Framework/Bulk/composer.json b/lib/internal/Magento/Framework/Bulk/composer.json index aa9dad9589657..b8e0992182169 100644 --- a/lib/internal/Magento/Framework/Bulk/composer.json +++ b/lib/internal/Magento/Framework/Bulk/composer.json @@ -1,26 +1,24 @@ { "name": "magento/framework-bulk", "description": "N/A", + "config": { + "sort-packages": true + }, "type": "magento2-library", "license": [ "OSL-3.0", "AFL-3.0" ], - "config": { - "sort-packages": true - }, - "version": "101.0.0", "require": { - "magento/framework": "103.0.*", + "magento/framework": "*", "php": "~7.3.0||~7.4.0" }, "autoload": { - "files": [ - "registration.php" - ], "psr-4": { "Magento\\Framework\\Bulk\\": "" - } + }, + "files": [ + "registration.php" + ] } } - diff --git a/lib/internal/Magento/Framework/MessageQueue/composer.json b/lib/internal/Magento/Framework/MessageQueue/composer.json index 86119b13adb37..056f1d40c39cf 100644 --- a/lib/internal/Magento/Framework/MessageQueue/composer.json +++ b/lib/internal/Magento/Framework/MessageQueue/composer.json @@ -1,26 +1,24 @@ { "name": "magento/framework-message-queue", "description": "N/A", + "config": { + "sort-packages": true + }, "type": "magento2-library", "license": [ "OSL-3.0", "AFL-3.0" ], - "config": { - "sort-packages": true - }, - "version": "100.4.1", "require": { - "magento/framework": "103.0.*", + "magento/framework": "*", "php": "~7.3.0||~7.4.0" }, "autoload": { - "files": [ - "registration.php" - ], "psr-4": { "Magento\\Framework\\MessageQueue\\": "" - } + }, + "files": [ + "registration.php" + ] } } - diff --git a/lib/internal/Magento/Framework/composer.json b/lib/internal/Magento/Framework/composer.json index e33ba5fdfd6b1..dfc81189bf544 100644 --- a/lib/internal/Magento/Framework/composer.json +++ b/lib/internal/Magento/Framework/composer.json @@ -9,7 +9,6 @@ "config": { "sort-packages": true }, - "version": "103.0.1", "require": { "php": "~7.3.0||~7.4.0", "ext-bcmath": "*", @@ -54,12 +53,11 @@ "ext-imagick": "Use Image Magick >=3.0.0 as an optional alternative image processing library" }, "autoload": { - "files": [ - "registration.php" - ], "psr-4": { "Magento\\Framework\\": "" - } + }, + "files": [ + "registration.php" + ] } } - From 51b24f430911f2dfacb47cd77b37c8102c34c614 Mon Sep 17 00:00:00 2001 From: eduard13 <e.chitoraga@atwix.com> Date: Fri, 23 Oct 2020 16:05:16 +0300 Subject: [PATCH 0913/1013] Excluding the disabled parent category from the category breadcrumbs --- .../Category/DataProvider/Breadcrumbs.php | 2 + .../Magento/GraphQl/Catalog/CategoryTest.php | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php index 863e621bd8df3..dcd6f816088dd 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/DataProvider/Breadcrumbs.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category\DataProvider; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; /** @@ -46,6 +47,7 @@ public function getData(string $categoryPath): array $collection = $this->collectionFactory->create(); $collection->addAttributeToSelect(['name', 'url_key', 'url_path']); $collection->addAttributeToFilter('entity_id', $parentCategoryIds); + $collection->addAttributeToFilter(CategoryInterface::KEY_IS_ACTIVE, 1); foreach ($collection as $category) { $breadcrumbsData[] = [ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index f086a2211b51d..d31a0aa88efc1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -660,6 +660,45 @@ public function testCategoryImage(?string $imagePrefix) $this->assertEquals($expectedImageUrl, $childCategory['image']); } + /** + * Testing breadcrumbs that shouldn't include disabled parent categories + * + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testBreadCrumbsWithDisabledParentCategory() + { + $parentCategoryId = 4; + $childCategoryId = 5; + $category = $this->categoryRepository->get($parentCategoryId); + $category->setIsActive(false); + $this->categoryRepository->save($category); + + $query = <<<QUERY +{ + category(id: {$childCategoryId}) { + name + breadcrumbs { + category_id + category_name + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + $expectedResponse = [ + 'category' => [ + 'name' => 'Category 1.1.1', + 'breadcrumbs' => [ + [ + 'category_id' => 3, + 'category_name' => "Category 1", + ] + ] + ] + ]; + $this->assertEquals($expectedResponse, $response); + } + /** * @return array */ From 18b61947a375c86deb67e490e3a89c612ecbcdd3 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Fri, 23 Oct 2020 16:24:07 +0300 Subject: [PATCH 0914/1013] MC-37954: PLP sort by name is case-sensitive with ElasticSearch --- .../BatchDataMapper/ProductDataMapper.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 9fa001097df87..53472710671a4 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -90,6 +90,13 @@ class ProductDataMapper implements BatchDataMapperInterface */ private $filterableAttributeTypes; + /** + * @var string[] + */ + private $sortableCaseSensitiveAttributes = [ + 'name', + ]; + /** * @param Builder $builder * @param FieldMapperInterface $fieldMapper @@ -99,6 +106,7 @@ class ProductDataMapper implements BatchDataMapperInterface * @param array $excludedAttributes * @param array $sortableAttributesValuesToImplode * @param array $filterableAttributeTypes + * @param array $sortableCaseSensitiveAttributes */ public function __construct( Builder $builder, @@ -108,7 +116,8 @@ public function __construct( DataProvider $dataProvider, array $excludedAttributes = [], array $sortableAttributesValuesToImplode = [], - array $filterableAttributeTypes = [] + array $filterableAttributeTypes = [], + array $sortableCaseSensitiveAttributes = [] ) { $this->builder = $builder; $this->fieldMapper = $fieldMapper; @@ -122,6 +131,10 @@ public function __construct( $this->dataProvider = $dataProvider; $this->attributeOptionsCache = []; $this->filterableAttributeTypes = $filterableAttributeTypes; + $this->sortableCaseSensitiveAttributes = array_merge( + $this->sortableCaseSensitiveAttributes, + $sortableCaseSensitiveAttributes + ); } /** @@ -298,6 +311,12 @@ function (string $valueId) { $attributeValues = [$productId => implode(' ', $attributeValues)]; } + if (in_array($attribute->getAttributeCode(), $this->sortableCaseSensitiveAttributes)) { + foreach ($attributeValues as $key => $attributeValue) { + $attributeValues[$key] = strtolower($attributeValue); + } + } + return $attributeValues; } From 1b9e373af576d2303833c9719b591eb86bed7ceb Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 23 Oct 2020 16:33:41 +0300 Subject: [PATCH 0915/1013] MC-38620: Merge release branch into 2.4-develop - fix one composer.json file --- app/code/Magento/MediaContentSynchronization/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json index c4ee6ca1b0cf3..9f0f4f9588ad6 100644 --- a/app/code/Magento/MediaContentSynchronization/composer.json +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -7,7 +7,7 @@ "magento/framework-bulk": "*", "magento/module-media-content-synchronization-api": "*", "magento/module-media-content-api": "*", - "magento/framework-message-queue": "*" + "magento/module-asynchronous-operations": "*" }, "suggest": { "magento/module-media-gallery-synchronization": "*" From a851e7bd14f4e59cc23a0b669d464dfac705f673 Mon Sep 17 00:00:00 2001 From: Lukasz Bajsarowicz <lukasz.bajsarowicz@gmail.com> Date: Fri, 23 Oct 2020 15:34:30 +0200 Subject: [PATCH 0916/1013] Update app/code/Magento/Cms/Block/BlockByIdentifier.php Co-authored-by: Ihor Sviziev <ihor-sviziev@users.noreply.github.com> --- app/code/Magento/Cms/Block/BlockByIdentifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php index 464a002563323..eb8bf3d5fe352 100644 --- a/app/code/Magento/Cms/Block/BlockByIdentifier.php +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -85,7 +85,7 @@ protected function _toHtml(): string */ private function getIdentifier(): ?string { - return $this->getdata('identifier') ?: null; + return $this->getData('identifier') ?: null; } /** From ce38ff69fa46176c4af875d34de26d3d2b9aab94 Mon Sep 17 00:00:00 2001 From: Nikita Shcherbatykh <nikita.shcherbatykh@transoftgroup.com> Date: Fri, 23 Oct 2020 16:38:54 +0300 Subject: [PATCH 0917/1013] MC-37663: Cannot invoice orders which contain bundle products comprised of physical and virtual products --- ...eOrderWithVirtualAndSimpleChildrenTest.xml | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml index 8cc26fd92b94c..fe4faed29d144 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml @@ -20,10 +20,10 @@ <before> <createData entity="CustomerEntityOne" stepKey="createCustomer"/> <!--Create bundle product with fixed price with simple and virtual products in options--> - <createData entity="SimpleProduct2" stepKey="createProductForBundleItem1"> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> <field key="price">100.00</field> </createData> - <createData entity="VirtualProduct" stepKey="createProductForBundleItem2"> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> <field key="price">50.00</field> </createData> <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> @@ -36,34 +36,28 @@ <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> <requiredEntity createDataKey="createFixedBundleProduct"/> <requiredEntity createDataKey="createFirstBundleOption"/> - <requiredEntity createDataKey="createProductForBundleItem1"/> + <requiredEntity createDataKey="createSimpleProduct"/> </createData> <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> <requiredEntity createDataKey="createFixedBundleProduct"/> <requiredEntity createDataKey="createSecondBundleOption"/> - <requiredEntity createDataKey="createProductForBundleItem2"/> + <requiredEntity createDataKey="createVirtualProduct"/> </createData> <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> - <argument name="productId" value="$$createFixedBundleProduct.id$$"/> + <argument name="productId" value="$createFixedBundleProduct.id$"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--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="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> </before> <after> - <deleteData createDataKey="createProductForBundleItem1" stepKey="deleteProductForBundleItem1"/> - <deleteData createDataKey="createProductForBundleItem2" stepKey="deleteProductForBundleItem2"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProductForBundleItem"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProductForBundleItem"/> <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Login customer on storefront--> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> @@ -73,9 +67,9 @@ <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage"> <argument name="product" value="$createFixedBundleProduct$"/> </actionGroup> - <!-- Add bundle to cart --> + <!--Add bundle to cart--> <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> - <argument name="productUrl" value="$$createFixedBundleProduct.name$$"/> + <argument name="productUrl" value="$createFixedBundleProduct.name$"/> </actionGroup> <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> <argument name="quantity" value="1"/> From 0ff2f042dfb2405389e6eaf442b38712fb201b9e Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 23 Oct 2020 16:58:21 +0300 Subject: [PATCH 0918/1013] MC-38620: Merge release branch into 2.4-develop - composer update --- composer.lock | 1665 +++++++++++++++++-------------------------------- 1 file changed, 570 insertions(+), 1095 deletions(-) diff --git a/composer.lock b/composer.lock index 551167152be4d..b06e0e9fa9e5c 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": "aadcf8a265dd7ecbb86dd3dd4e49bc28", + "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -117,21 +117,21 @@ }, { "name": "colinmollenhour/php-redis-session-abstract", - "version": "v1.4.2", + "version": "v1.4.3", "source": { "type": "git", "url": "https://github.com/colinmollenhour/php-redis-session-abstract.git", - "reference": "669521218794f125c7b668252f4f576eda65e1e4" + "reference": "39ca38da5e0a981bc1a7e39a86693c128784a513" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/669521218794f125c7b668252f4f576eda65e1e4", - "reference": "669521218794f125c7b668252f4f576eda65e1e4", + "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/39ca38da5e0a981bc1a7e39a86693c128784a513", + "reference": "39ca38da5e0a981bc1a7e39a86693c128784a513", "shasum": "" }, "require": { "colinmollenhour/credis": "~1.6", - "php": "^5.5 || ^7.0" + "php": "^5.5 || ^7.0|| ^7.1 || ^7.2" }, "type": "library", "autoload": { @@ -150,20 +150,20 @@ ], "description": "A Redis-based session handler with optimistic locking", "homepage": "https://github.com/colinmollenhour/php-redis-session-abstract", - "time": "2020-01-08T17:41:01+00:00" + "time": "2020-10-07T09:47:22+00:00" }, { "name": "composer/ca-bundle", - "version": "1.2.7", + "version": "1.2.8", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd" + "reference": "8a7ecad675253e4654ea05505233285377405215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd", - "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215", + "reference": "8a7ecad675253e4654ea05505233285377405215", "shasum": "" }, "require": { @@ -206,30 +206,20 @@ "ssl", "tls" ], - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2020-04-08T08:27:21+00:00" + "time": "2020-08-23T12:54:47+00:00" }, { "name": "composer/composer", - "version": "1.10.9", + "version": "1.10.15", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "83c3250093d5491600a822e176b107a945baf95a" + "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/83c3250093d5491600a822e176b107a945baf95a", - "reference": "83c3250093d5491600a822e176b107a945baf95a", + "url": "https://api.github.com/repos/composer/composer/zipball/547c9ee73fe26c77af09a0ea16419176b1cdbd12", + "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12", "shasum": "" }, "require": { @@ -296,34 +286,20 @@ "dependency", "package" ], - "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" + "time": "2020-10-13T13:59:09+00:00" }, { "name": "composer/semver", - "version": "1.5.1", + "version": "1.7.1", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de" + "reference": "38276325bd896f90dfcfe30029aa5db40df387a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de", - "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de", + "url": "https://api.github.com/repos/composer/semver/zipball/38276325bd896f90dfcfe30029aa5db40df387a7", + "reference": "38276325bd896f90dfcfe30029aa5db40df387a7", "shasum": "" }, "require": { @@ -371,7 +347,7 @@ "validation", "versioning" ], - "time": "2020-01-13T12:06:48+00:00" + "time": "2020-09-27T13:13:07+00:00" }, { "name": "composer/spdx-licenses", @@ -431,34 +407,20 @@ "spdx", "validator" ], - "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.2", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", - "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", + "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", "shasum": "" }, "require": { @@ -489,21 +451,7 @@ "Xdebug", "performance" ], - "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" + "time": "2020-08-19T10:27:58+00:00" }, { "name": "container-interop/container-interop", @@ -770,23 +718,23 @@ }, { "name": "guzzlehttp/promises", - "version": "v1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + "reference": "60d379c243457e073cff02bc323a2a86cb355631" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", + "reference": "60d379c243457e073cff02bc323a2a86cb355631", "shasum": "" }, "require": { - "php": ">=5.5.0" + "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.0" + "symfony/phpunit-bridge": "^4.4 || ^5.1" }, "type": "library", "extra": { @@ -817,20 +765,20 @@ "keywords": [ "promise" ], - "time": "2016-12-20T10:07:11+00:00" + "time": "2020-09-30T07:37:28+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.6.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", + "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", "shasum": "" }, "require": { @@ -843,15 +791,15 @@ }, "require-dev": { "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" }, "suggest": { - "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-master": "1.7-dev" } }, "autoload": { @@ -888,7 +836,7 @@ "uri", "url" ], - "time": "2019-07-01T23:21:34+00:00" + "time": "2020-09-30T07:37:11+00:00" }, { "name": "justinrainbow/json-schema", @@ -1535,41 +1483,41 @@ }, { "name": "laminas/laminas-eventmanager", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-eventmanager.git", - "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748" + "reference": "1940ccf30e058b2fd66f5a9d696f1b5e0027b082" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", - "reference": "ce4dc0bdf3b14b7f9815775af9dfee80a63b4748", + "url": "https://api.github.com/repos/laminas/laminas-eventmanager/zipball/1940ccf30e058b2fd66f5a9d696f1b5e0027b082", + "reference": "1940ccf30e058b2fd66f5a9d696f1b5e0027b082", "shasum": "" }, "require": { "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ^8.0" }, "replace": { - "zendframework/zend-eventmanager": "self.version" + "zendframework/zend-eventmanager": "^3.2.1" }, "require-dev": { - "athletic/athletic": "^0.1", - "container-interop/container-interop": "^1.1.0", + "container-interop/container-interop": "^1.1", "laminas/laminas-coding-standard": "~1.0.0", "laminas/laminas-stdlib": "^2.7.3 || ^3.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "^8.5.8" }, "suggest": { - "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature", + "container-interop/container-interop": "^1.1, to use the lazy listeners feature", "laminas/laminas-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev", - "dev-develop": "3.3-dev" + "dev-master": "3.3.x-dev", + "dev-develop": "3.4.x-dev" } }, "autoload": { @@ -1589,20 +1537,20 @@ "events", "laminas" ], - "time": "2019-12-31T16:44:52+00:00" + "time": "2020-08-25T11:10:44+00:00" }, { "name": "laminas/laminas-feed", - "version": "2.12.2", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-feed.git", - "reference": "8a193ac96ebcb3e16b6ee754ac2a889eefacb654" + "reference": "3c91415633cb1be6f9d78683d69b7dcbfe6b4012" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/8a193ac96ebcb3e16b6ee754ac2a889eefacb654", - "reference": "8a193ac96ebcb3e16b6ee754ac2a889eefacb654", + "url": "https://api.github.com/repos/laminas/laminas-feed/zipball/3c91415633cb1be6f9d78683d69b7dcbfe6b4012", + "reference": "3c91415633cb1be6f9d78683d69b7dcbfe6b4012", "shasum": "" }, "require": { @@ -1656,7 +1604,7 @@ "feed", "laminas" ], - "time": "2020-03-29T12:36:29+00:00" + "time": "2020-08-18T13:45:04+00:00" }, { "name": "laminas/laminas-filter", @@ -1807,26 +1755,20 @@ "form", "laminas" ], - "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.12.0", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-http.git", - "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575" + "reference": "33b7942f51ce905ce9bfc8bf28badc501d3904b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-http/zipball/48bd06ffa3a6875e2b77d6852405eb7b1589d575", - "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575", + "url": "https://api.github.com/repos/laminas/laminas-http/zipball/33b7942f51ce905ce9bfc8bf28badc501d3904b5", + "reference": "33b7942f51ce905ce9bfc8bf28badc501d3904b5", "shasum": "" }, "require": { @@ -1849,12 +1791,6 @@ "paragonie/certainty": "For automated management of cacert.pem" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.12.x-dev", - "dev-develop": "2.13.x-dev" - } - }, "autoload": { "psr-4": { "Laminas\\Http\\": "src/" @@ -1871,13 +1807,7 @@ "http client", "laminas" ], - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2020-06-23T15:14:37+00:00" + "time": "2020-08-18T17:11:58+00:00" }, { "name": "laminas/laminas-hydrator", @@ -2263,16 +2193,16 @@ }, { "name": "laminas/laminas-mail", - "version": "2.11.0", + "version": "2.12.3", "source": { "type": "git", "url": "https://github.com/laminas/laminas-mail.git", - "reference": "4c5545637eea3dc745668ddff1028692ed004c4b" + "reference": "c154a733b122539ac2c894561996c770db289f70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/4c5545637eea3dc745668ddff1028692ed004c4b", - "reference": "4c5545637eea3dc745668ddff1028692ed004c4b", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/c154a733b122539ac2c894561996c770db289f70", + "reference": "c154a733b122539ac2c894561996c770db289f70", "shasum": "" }, "require": { @@ -2282,7 +2212,7 @@ "laminas/laminas-stdlib": "^2.7 || ^3.0", "laminas/laminas-validator": "^2.10.2", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0", + "php": "^7.1", "true/punycode": "^2.1" }, "replace": { @@ -2292,8 +2222,8 @@ "laminas/laminas-coding-standard": "~1.0.0", "laminas/laminas-config": "^2.6", "laminas/laminas-crypt": "^2.6 || ^3.0", - "laminas/laminas-servicemanager": "^2.7.10 || ^3.3.1", - "phpunit/phpunit": "^5.7.25 || ^6.4.4 || ^7.1.4" + "laminas/laminas-servicemanager": "^3.2.1", + "phpunit/phpunit": "^7.5.20" }, "suggest": { "laminas/laminas-crypt": "Crammd5 support in SMTP Auth", @@ -2301,10 +2231,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.11.x-dev", - "dev-develop": "2.12.x-dev" - }, "laminas": { "component": "Laminas\\Mail", "config-provider": "Laminas\\Mail\\ConfigProvider" @@ -2325,13 +2251,7 @@ "laminas", "mail" ], - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2020-06-30T20:17:23+00:00" + "time": "2020-08-12T14:51:33+00:00" }, { "name": "laminas/laminas-math", @@ -2443,16 +2363,16 @@ }, { "name": "laminas/laminas-modulemanager", - "version": "2.8.4", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-modulemanager.git", - "reference": "92b1cde1aab5aef687b863face6dd5d9c6751c78" + "reference": "789bbd4ab391da9221f265f6bb2d594f8f11855b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-modulemanager/zipball/92b1cde1aab5aef687b863face6dd5d9c6751c78", - "reference": "92b1cde1aab5aef687b863face6dd5d9c6751c78", + "url": "https://api.github.com/repos/laminas/laminas-modulemanager/zipball/789bbd4ab391da9221f265f6bb2d594f8f11855b", + "reference": "789bbd4ab391da9221f265f6bb2d594f8f11855b", "shasum": "" }, "require": { @@ -2460,10 +2380,11 @@ "laminas/laminas-eventmanager": "^3.2 || ^2.6.3", "laminas/laminas-stdlib": "^3.1 || ^2.7", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0", + "webimpress/safe-writer": "^1.0.2 || ^2.1" }, "replace": { - "zendframework/zend-modulemanager": "self.version" + "zendframework/zend-modulemanager": "^2.8.4" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", @@ -2483,8 +2404,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8.x-dev", - "dev-develop": "2.9.x-dev" + "dev-master": "2.9.x-dev", + "dev-develop": "2.10.x-dev" } }, "autoload": { @@ -2502,7 +2423,7 @@ "laminas", "modulemanager" ], - "time": "2019-12-31T17:26:56+00:00" + "time": "2020-08-25T09:29:22+00:00" }, { "name": "laminas/laminas-mvc", @@ -2952,35 +2873,35 @@ }, { "name": "laminas/laminas-stdlib", - "version": "3.2.1", + "version": "3.3.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-stdlib.git", - "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6" + "reference": "b9d84eaa39fde733356ea948cdef36c631f202b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/2b18347625a2f06a1a485acfbc870f699dbe51c6", - "reference": "2b18347625a2f06a1a485acfbc870f699dbe51c6", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/b9d84eaa39fde733356ea948cdef36c631f202b6", + "reference": "b9d84eaa39fde733356ea948cdef36c631f202b6", "shasum": "" }, "require": { "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ^8.0" }, "replace": { - "zendframework/zend-stdlib": "self.version" + "zendframework/zend-stdlib": "^3.2.1" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", - "phpbench/phpbench": "^0.13", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2" + "phpbench/phpbench": "^0.17.1", + "phpunit/phpunit": "^9.3.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev", - "dev-develop": "3.3.x-dev" + "dev-master": "3.3.x-dev", + "dev-develop": "3.4.x-dev" } }, "autoload": { @@ -2998,7 +2919,7 @@ "laminas", "stdlib" ], - "time": "2019-12-31T17:51:15+00:00" + "time": "2020-08-25T09:08:16+00:00" }, { "name": "laminas/laminas-text", @@ -3275,31 +3196,27 @@ }, { "name": "laminas/laminas-zendframework-bridge", - "version": "1.0.4", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/laminas/laminas-zendframework-bridge.git", - "reference": "fcd87520e4943d968557803919523772475e8ea3" + "reference": "6ede70583e101030bcace4dcddd648f760ddf642" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/fcd87520e4943d968557803919523772475e8ea3", - "reference": "fcd87520e4943d968557803919523772475e8ea3", + "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/6ede70583e101030bcace4dcddd648f760ddf642", + "reference": "6ede70583e101030bcace4dcddd648f760ddf642", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3", "squizlabs/php_codesniffer": "^3.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev", - "dev-develop": "1.1.x-dev" - }, "laminas": { "module": "Laminas\\ZendFrameworkBridge" } @@ -3323,7 +3240,7 @@ "laminas", "zf" ], - "time": "2020-05-20T16:45:56+00:00" + "time": "2020-09-14T14:23:00+00:00" }, { "name": "magento/composer", @@ -3489,16 +3406,16 @@ }, { "name": "monolog/monolog", - "version": "1.25.4", + "version": "1.25.5", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "3022efff205e2448b560c833c6fbbf91c3139168" + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/3022efff205e2448b560c833c6fbbf91c3139168", - "reference": "3022efff205e2448b560c833c6fbbf91c3139168", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1817faadd1846cd08be9a49e905dc68823bc38c0", + "reference": "1817faadd1846cd08be9a49e905dc68823bc38c0", "shasum": "" }, "require": { @@ -3562,7 +3479,7 @@ "logging", "psr-3" ], - "time": "2020-05-22T07:31:27+00:00" + "time": "2020-07-23T08:35:51+00:00" }, { "name": "paragonie/random_compat", @@ -3889,16 +3806,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.28", + "version": "2.0.29", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260" + "reference": "497856a8d997f640b4a516062f84228a772a48a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", - "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/497856a8d997f640b4a516062f84228a772a48a8", + "reference": "497856a8d997f640b4a516062f84228a772a48a8", "shasum": "" }, "require": { @@ -3907,7 +3824,6 @@ "require-dev": { "phing/phing": "~2.7", "phpunit/phpunit": "^4.8.35|^5.7|^6.0", - "sami/sami": "~2.0", "squizlabs/php_codesniffer": "~2.0" }, "suggest": { @@ -3977,21 +3893,7 @@ "x.509", "x509" ], - "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" + "time": "2020-09-08T04:24:43+00:00" }, { "name": "psr/container", @@ -4309,16 +4211,16 @@ }, { "name": "seld/jsonlint", - "version": "1.8.0", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1" + "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", - "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/590cfec960b77fd55e39b7d9246659e95dd6d337", + "reference": "590cfec960b77fd55e39b7d9246659e95dd6d337", "shasum": "" }, "require": { @@ -4354,7 +4256,7 @@ "parser", "validator" ], - "time": "2020-04-30T19:05:18+00:00" + "time": "2020-08-25T06:56:57+00:00" }, { "name": "seld/phar-utils", @@ -4402,16 +4304,16 @@ }, { "name": "symfony/console", - "version": "v4.4.10", + "version": "v4.4.15", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "326b064d804043005526f5a0494cfb49edb59bb0" + "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/326b064d804043005526f5a0494cfb49edb59bb0", - "reference": "326b064d804043005526f5a0494cfb49edb59bb0", + "url": "https://api.github.com/repos/symfony/console/zipball/90933b39c7b312fc3ceaa1ddeac7eb48cb953124", + "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124", "shasum": "" }, "require": { @@ -4475,25 +4377,11 @@ ], "description": "Symfony Console 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" + "time": "2020-09-15T07:58:55+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -4542,34 +4430,20 @@ ], "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.10", + "version": "v4.4.15", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866" + "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5370aaa7807c7a439b21386661ffccf3dff2866", - "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e17bb5e0663dc725f7cdcafc932132735b4725cd", + "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd", "shasum": "" }, "require": { @@ -4587,6 +4461,7 @@ "psr/log": "~1.0", "symfony/config": "^3.4|^4.0|^5.0", "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "~3.4|~4.4", "symfony/expression-language": "^3.4|^4.0|^5.0", "symfony/http-foundation": "^3.4|^4.0|^5.0", "symfony/service-contracts": "^1.1|^2", @@ -4626,21 +4501,7 @@ ], "description": "Symfony EventDispatcher 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-20T08:37:50+00:00" + "time": "2020-09-18T14:07:46+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4702,34 +4563,20 @@ "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:19:58+00:00" }, { "name": "symfony/filesystem", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "6e4320f06d5f2cce0d96530162491f4465179157" + "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", - "reference": "6e4320f06d5f2cce0d96530162491f4465179157", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/1a8697545a8d87b9f2f6b1d32414199cc5e20aae", + "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae", "shasum": "" }, "require": { @@ -4766,34 +4613,20 @@ ], "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" + "time": "2020-09-27T14:02:37+00:00" }, { "name": "symfony/finder", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" + "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", - "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", + "url": "https://api.github.com/repos/symfony/finder/zipball/2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", + "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", "shasum": "" }, "require": { @@ -4829,34 +4662,20 @@ ], "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" + "time": "2020-09-02T16:23:27+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/aed596913b70fae57be53d86faa2e9ef85a2297b", + "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b", "shasum": "" }, "require": { @@ -4868,7 +4687,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4905,34 +4724,20 @@ "polyfill", "portable" ], - "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" + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe" + "reference": "4ad5115c0f5d5172a9fe8147675ec6de266d8826" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", - "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/4ad5115c0f5d5172a9fe8147675ec6de266d8826", + "reference": "4ad5115c0f5d5172a9fe8147675ec6de266d8826", "shasum": "" }, "require": { @@ -4947,7 +4752,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4990,34 +4795,20 @@ "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" + "time": "2020-10-21T09:57:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + "reference": "8db0ae7936b42feb370840cf24de1a144fb0ef27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8db0ae7936b42feb370840cf24de1a144fb0ef27", + "reference": "8db0ae7936b42feb370840cf24de1a144fb0ef27", "shasum": "" }, "require": { @@ -5029,7 +4820,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5071,34 +4862,20 @@ "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" + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + "reference": "b5f7b932ee6fa802fc792eabd77c4c88084517ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b5f7b932ee6fa802fc792eabd77c4c88084517ce", + "reference": "b5f7b932ee6fa802fc792eabd77c4c88084517ce", "shasum": "" }, "require": { @@ -5110,7 +4887,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5148,34 +4925,20 @@ "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" + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" + "reference": "3fe414077251a81a1b15b1c709faf5c2fbae3d4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/3fe414077251a81a1b15b1c709faf5c2fbae3d4e", + "reference": "3fe414077251a81a1b15b1c709faf5c2fbae3d4e", "shasum": "" }, "require": { @@ -5185,7 +4948,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5225,34 +4988,20 @@ "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" + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "639447d008615574653fb3bc60d1986d7172eaae" + "reference": "beecef6b463b06954638f02378f52496cb84bacc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", - "reference": "639447d008615574653fb3bc60d1986d7172eaae", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/beecef6b463b06954638f02378f52496cb84bacc", + "reference": "beecef6b463b06954638f02378f52496cb84bacc", "shasum": "" }, "require": { @@ -5261,7 +5010,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5298,34 +5047,20 @@ "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" + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" + "reference": "9d920e3218205554171b2503bb3e4a1366824a16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9d920e3218205554171b2503bb3e4a1366824a16", + "reference": "9d920e3218205554171b2503bb3e4a1366824a16", "shasum": "" }, "require": { @@ -5334,7 +5069,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5374,34 +5109,20 @@ "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" + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.18.0", + "version": "v1.19.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + "reference": "f54ef00f4678f348f133097fa8c3701d197ff44d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/f54ef00f4678f348f133097fa8c3701d197ff44d", + "reference": "f54ef00f4678f348f133097fa8c3701d197ff44d", "shasum": "" }, "require": { @@ -5410,7 +5131,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.19-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5454,38 +5175,24 @@ "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" + "time": "2020-10-23T09:01:57+00:00" }, { "name": "symfony/process", - "version": "v4.4.10", + "version": "v4.4.15", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5" + "reference": "9b887acc522935f77555ae8813495958c7771ba7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c714958428a85c86ab97e3a0c96db4c4f381b7f5", - "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5", + "url": "https://api.github.com/repos/symfony/process/zipball/9b887acc522935f77555ae8813495958c7771ba7", + "reference": "9b887acc522935f77555ae8813495958c7771ba7", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "type": "library", "extra": { @@ -5517,34 +5224,20 @@ ], "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" + "time": "2020-09-02T16:08:58+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.1.3", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442" + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442", - "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", "shasum": "" }, "require": { @@ -5557,7 +5250,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -5593,21 +5286,7 @@ "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" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "tedivm/jshrink", @@ -5754,6 +5433,55 @@ ], "time": "2018-01-15T15:26:51+00:00" }, + { + "name": "webimpress/safe-writer", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/webimpress/safe-writer.git", + "reference": "5cfafdec5873c389036f14bf832a5efc9390dcdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webimpress/safe-writer/zipball/5cfafdec5873c389036f14bf832a5efc9390dcdd", + "reference": "5cfafdec5873c389036f14bf832a5efc9390dcdd", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.8 || ^9.3.7", + "vimeo/psalm": "^3.14.2", + "webimpress/coding-standard": "^1.1.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-1.0": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Webimpress\\SafeWriter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Tool to write files safely, to avoid race conditions", + "keywords": [ + "concurrent write", + "file writer", + "race condition", + "safe writer", + "webimpress" + ], + "time": "2020-08-25T07:21:11+00:00" + }, { "name": "webonyx/graphql-php", "version": "v0.13.9", @@ -5804,12 +5532,6 @@ "api", "graphql" ], - "funding": [ - { - "url": "https://opencollective.com/webonyx-graphql-php", - "type": "open_collective" - } - ], "time": "2020-07-02T05:49:25+00:00" }, { @@ -5877,16 +5599,16 @@ "packages-dev": [ { "name": "allure-framework/allure-codeception", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/allure-framework/allure-codeception.git", - "reference": "9e0e25f8960fa5ac17c65c932ea8153ce6700713" + "reference": "a69800eeef83007ced9502a3349ff72f5fb6b4e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/9e0e25f8960fa5ac17c65c932ea8153ce6700713", - "reference": "9e0e25f8960fa5ac17c65c932ea8153ce6700713", + "url": "https://api.github.com/repos/allure-framework/allure-codeception/zipball/a69800eeef83007ced9502a3349ff72f5fb6b4e2", + "reference": "a69800eeef83007ced9502a3349ff72f5fb6b4e2", "shasum": "" }, "require": { @@ -5924,7 +5646,7 @@ "steps", "testing" ], - "time": "2020-03-13T11:07:13+00:00" + "time": "2020-09-09T10:51:33+00:00" }, { "name": "allure-framework/allure-php-api", @@ -6031,16 +5753,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.147.1", + "version": "3.158.12", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc" + "reference": "ba2956c3cb5ff0d7b808683b1c57ebc3f5cc9633" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8a561a4a1645ccdd06413a4f2defe55d35e0eecc", - "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ba2956c3cb5ff0d7b808683b1c57ebc3f5cc9633", + "reference": "ba2956c3cb5ff0d7b808683b1c57ebc3f5cc9633", "shasum": "" }, "require": { @@ -6112,7 +5834,7 @@ "s3", "sdk" ], - "time": "2020-07-20T18:18:31+00:00" + "time": "2020-10-22T18:12:00+00:00" }, { "name": "beberlei/assert", @@ -6330,16 +6052,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.6", + "version": "4.1.8", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9" + "reference": "41036e8af66e727c4587012f0366b7f0576a99da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", - "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/41036e8af66e727c4587012f0366b7f0576a99da", + "reference": "41036e8af66e727c4587012f0366b7f0576a99da", "shasum": "" }, "require": { @@ -6411,31 +6133,26 @@ "functional testing", "unit testing" ], - "funding": [ - { - "url": "https://opencollective.com/codeception", - "type": "open_collective" - } - ], - "time": "2020-06-07T16:31:51+00:00" + "time": "2020-10-11T17:54:58+00:00" }, { "name": "codeception/lib-asserts", - "version": "1.12.0", + "version": "1.13.2", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "acd0dc8b394595a74b58dcc889f72569ff7d8e71" + "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/acd0dc8b394595a74b58dcc889f72569ff7d8e71", - "reference": "acd0dc8b394595a74b58dcc889f72569ff7d8e71", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/184231d5eab66bc69afd6b9429344d80c67a33b6", + "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6", "shasum": "" }, "require": { "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", - "php": ">=5.6.0 <8.0" + "ext-dom": "*", + "php": ">=5.6.0 <9.0" }, "type": "library", "autoload": { @@ -6455,40 +6172,41 @@ }, { "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } ], "description": "Assertion methods used by Codeception core and Asserts module", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "codeception" ], - "time": "2020-04-17T18:20:46+00:00" + "time": "2020-10-21T16:26:20+00:00" }, { "name": "codeception/module-asserts", - "version": "1.2.1", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-asserts.git", - "reference": "79f13d05b63f2fceba4d0e78044bab668c9b2a6b" + "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/79f13d05b63f2fceba4d0e78044bab668c9b2a6b", - "reference": "79f13d05b63f2fceba4d0e78044bab668c9b2a6b", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/59374f2fef0cabb9e8ddb53277e85cdca74328de", + "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de", "shasum": "" }, "require": { "codeception/codeception": "*@dev", - "codeception/lib-asserts": "^1.12.0", - "php": ">=5.6.0 <8.0" + "codeception/lib-asserts": "^1.13.1", + "php": ">=5.6.0 <9.0" }, "conflict": { "codeception/codeception": "<4.0" }, - "require-dev": { - "codeception/util-robohelpers": "dev-master" - }, "type": "library", "autoload": { "classmap": [ @@ -6505,16 +6223,20 @@ }, { "name": "Gintautas Miselis" + }, + { + "name": "Gustavo Nieves", + "homepage": "https://medium.com/@ganieves" } ], "description": "Codeception module containing various assertions", - "homepage": "http://codeception.com/", + "homepage": "https://codeception.com/", "keywords": [ "assertions", "asserts", "codeception" ], - "time": "2020-04-20T07:26:11+00:00" + "time": "2020-10-21T16:48:15+00:00" }, { "name": "codeception/module-sequence", @@ -6561,16 +6283,16 @@ }, { "name": "codeception/module-webdriver", - "version": "1.1.0", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "09c167817393090ce3dbce96027d94656b1963ce" + "reference": "d055c645f600e991e33d1f289a9645eee46c384e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/09c167817393090ce3dbce96027d94656b1963ce", - "reference": "09c167817393090ce3dbce96027d94656b1963ce", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/d055c645f600e991e33d1f289a9645eee46c384e", + "reference": "d055c645f600e991e33d1f289a9645eee46c384e", "shasum": "" }, "require": { @@ -6612,20 +6334,20 @@ "browser-testing", "codeception" ], - "time": "2020-05-31T08:47:24+00:00" + "time": "2020-10-11T18:54:47+00:00" }, { "name": "codeception/phpunit-wrapper", - "version": "9.0.2", + "version": "9.0.5", "source": { "type": "git", "url": "https://github.com/Codeception/phpunit-wrapper.git", - "reference": "eb27243d8edde68593bf8d9ef5e9074734777931" + "reference": "72bac7770866799e23a7dda1ac6bec2f8baccf45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/eb27243d8edde68593bf8d9ef5e9074734777931", - "reference": "eb27243d8edde68593bf8d9ef5e9074734777931", + "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/72bac7770866799e23a7dda1ac6bec2f8baccf45", + "reference": "72bac7770866799e23a7dda1ac6bec2f8baccf45", "shasum": "" }, "require": { @@ -6656,7 +6378,7 @@ } ], "description": "PHPUnit classes used by Codeception", - "time": "2020-04-17T18:16:31+00:00" + "time": "2020-10-11T18:14:42+00:00" }, { "name": "codeception/stub", @@ -6842,16 +6564,16 @@ }, { "name": "doctrine/annotations", - "version": "1.10.3", + "version": "1.10.4", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" + "reference": "bfe91e31984e2ba76df1c1339681770401ec262f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", - "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/bfe91e31984e2ba76df1c1339681770401ec262f", + "reference": "bfe91e31984e2ba76df1c1339681770401ec262f", "shasum": "" }, "require": { @@ -6861,7 +6583,8 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^7.5" + "phpstan/phpstan": "^0.12.20", + "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", "extra": { @@ -6907,7 +6630,7 @@ "docblock", "parser" ], - "time": "2020-05-25T17:24:27+00:00" + "time": "2020-08-10T19:35:50+00:00" }, { "name": "doctrine/cache", @@ -6989,20 +6712,6 @@ "redis", "xcache" ], - "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" }, { @@ -7126,20 +6835,6 @@ "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" }, { @@ -7202,20 +6897,6 @@ "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" }, { @@ -7307,12 +6988,6 @@ } ], "description": "A tool to automatically fix PHP code style", - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], "time": "2020-06-27T23:57:46+00:00" }, { @@ -8040,28 +7715,29 @@ }, { "name": "league/flysystem", - "version": "1.0.69", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "7106f78428a344bc4f643c233a94e48795f10967" + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7106f78428a344bc4f643c233a94e48795f10967", - "reference": "7106f78428a344bc4f643c233a94e48795f10967", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a", "shasum": "" }, "require": { "ext-fileinfo": "*", - "php": ">=5.5.9" + "league/mime-type-detection": "^1.3", + "php": "^7.2.5 || ^8.0" }, "conflict": { "league/flysystem-sftp": "<1.0.6" }, "require-dev": { - "phpspec/phpspec": "^3.4", - "phpunit/phpunit": "^5.7.26" + "phpspec/prophecy": "^1.11.1", + "phpunit/phpunit": "^8.5.8" }, "suggest": { "ext-fileinfo": "Required for MimeType", @@ -8120,7 +7796,48 @@ "sftp", "storage" ], - "time": "2020-05-18T15:13:39+00:00" + "time": "2020-08-23T07:39:11+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa", + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.36", + "phpunit/phpunit": "^8.5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "time": "2020-10-18T11:50:25+00:00" }, { "name": "lusitanian/oauth", @@ -8230,16 +7947,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.1.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "8a106ea029f222f4354854636861273c7577bee9" + "reference": "c6760313811f2c04545a261c706d2a73dd727b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8a106ea029f222f4354854636861273c7577bee9", - "reference": "8a106ea029f222f4354854636861273c7577bee9", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/c6760313811f2c04545a261c706d2a73dd727b9a", + "reference": "c6760313811f2c04545a261c706d2a73dd727b9a", "shasum": "" }, "require": { @@ -8317,7 +8034,7 @@ "magento", "testing" ], - "time": "2020-08-19T19:57:27+00:00" + "time": "2020-09-28T18:26:59+00:00" }, { "name": "mikey179/vfsstream", @@ -8367,25 +8084,25 @@ }, { "name": "mtdowling/jmespath.php", - "version": "2.5.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "52168cb9472de06979613d365c7f1ab8798be895" + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/52168cb9472de06979613d365c7f1ab8798be895", - "reference": "52168cb9472de06979613d365c7f1ab8798be895", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb", "shasum": "" }, "require": { - "php": ">=5.4.0", - "symfony/polyfill-mbstring": "^1.4" + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" }, "require-dev": { - "composer/xdebug-handler": "^1.2", - "phpunit/phpunit": "^4.8.36|^7.5.15" + "composer/xdebug-handler": "^1.4", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" }, "bin": [ "bin/jp.php" @@ -8393,7 +8110,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } }, "autoload": { @@ -8420,7 +8137,7 @@ "json", "jsonpath" ], - "time": "2019-12-30T18:03:34+00:00" + "time": "2020-07-31T21:01:56+00:00" }, { "name": "mustache/mustache", @@ -8514,12 +8231,6 @@ "object", "object graph" ], - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], "time": "2020-06-29T13:22:24+00:00" }, { @@ -8735,23 +8446,23 @@ }, { "name": "php-cs-fixer/diff", - "version": "v1.3.0", + "version": "v1.3.1", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756" + "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/78bb099e9c16361126c86ce82ec4405ebab8e756", - "reference": "78bb099e9c16361126c86ce82ec4405ebab8e756", + "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759", + "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^5.6 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", "symfony/process": "^3.3" }, "type": "library", @@ -8765,14 +8476,14 @@ "BSD-3-Clause" ], "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, { "name": "SpacePossum" } @@ -8782,7 +8493,7 @@ "keywords": [ "diff" ], - "time": "2018-02-15T16:58:55+00:00" + "time": "2020-10-14T08:39:05+00:00" }, { "name": "php-webdriver/webdriver", @@ -9006,16 +8717,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.2.0", + "version": "5.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df" + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df", - "reference": "3170448f5769fe19f456173d833734e0ff1b84df", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", "shasum": "" }, "require": { @@ -9054,20 +8765,20 @@ } ], "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-07-20T20:05:34+00:00" + "time": "2020-09-03T19:13:55+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", - "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", "shasum": "" }, "require": { @@ -9099,20 +8810,20 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-06-27T10:12:23+00:00" + "time": "2020-09-17T18:55:26+00:00" }, { "name": "phpmd/phpmd", - "version": "2.8.2", + "version": "2.9.1", "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "714629ed782537f638fe23c4346637659b779a77" + "reference": "ce10831d4ddc2686c1348a98069771dd314534a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/714629ed782537f638fe23c4346637659b779a77", - "reference": "714629ed782537f638fe23c4346637659b779a77", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/ce10831d4ddc2686c1348a98069771dd314534a8", + "reference": "ce10831d4ddc2686c1348a98069771dd314534a8", "shasum": "" }, "require": { @@ -9123,6 +8834,8 @@ }, "require-dev": { "easy-doc/easy-doc": "0.0.0 || ^1.3.2", + "ext-json": "*", + "ext-simplexml": "*", "gregwar/rst": "^1.0", "mikey179/vfsstream": "^1.6.4", "phpunit/phpunit": "^4.8.36 || ^5.7.27", @@ -9169,7 +8882,7 @@ "phpmd", "pmd" ], - "time": "2020-02-16T20:15:50+00:00" + "time": "2020-09-23T22:06:32+00:00" }, { "name": "phpoption/phpoption", @@ -9224,42 +8937,32 @@ "php", "type" ], - "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": "1.11.1", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2", - "phpdocumentor/reflection-docblock": "^5.0", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^8.0 || ^9.0 <9.3" }, "type": "library", "extra": { @@ -9297,7 +9000,7 @@ "spy", "stub" ], - "time": "2020-07-08T12:44:21+00:00" + "time": "2020-09-29T09:10:42+00:00" }, { "name": "phpstan/phpstan", @@ -9403,33 +9106,27 @@ "testing", "xunit" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "time": "2020-05-23T08:02:54+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", - "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", + "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -9459,34 +9156,28 @@ "filesystem", "iterator" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-07-11T05:18:21+00:00" + "time": "2020-09-28T05:57:25+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.0.2", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66" + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f6eedfed1085dd1f4c599629459a0277d25f9a66", - "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-pcntl": "*" @@ -9494,7 +9185,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -9518,33 +9209,27 @@ "keywords": [ "process" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T11:53:53+00:00" + "time": "2020-09-28T05:58:55+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" + "reference": "18c887016e60e52477e54534956d7b47bc52cd84" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", - "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/18c887016e60e52477e54534956d7b47bc52cd84", + "reference": "18c887016e60e52477e54534956d7b47bc52cd84", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -9573,13 +9258,7 @@ "keywords": [ "template" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T11:55:37+00:00" + "time": "2020-09-28T06:03:05+00:00" }, { "name": "phpunit/php-timer", @@ -9632,16 +9311,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374" + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5672711b6b07b14d5ab694e700c62eeb82fcf374", - "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", + "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", "shasum": "" }, "require": { @@ -9677,14 +9356,8 @@ "keywords": [ "tokenizer" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], "abandoned": true, - "time": "2020-06-27T06:36:25+00:00" + "time": "2020-08-04T08:28:15+00:00" }, { "name": "phpunit/phpunit", @@ -9870,23 +9543,23 @@ }, { "name": "sebastian/code-unit", - "version": "1.0.5", + "version": "1.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "c1e2df332c905079980b119c4db103117e5e5c90" + "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", - "reference": "c1e2df332c905079980b119c4db103117e5e5c90", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/59236be62b1bb9919e6d7f60b0b832dc05cef9ab", + "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -9912,33 +9585,27 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:50:45+00:00" + "time": "2020-10-02T14:47:54+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", - "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -9963,35 +9630,29 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:04:00+00:00" + "time": "2020-09-28T05:30:19+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.3", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" + "reference": "7a8ff306445707539c1a6397372a982a1ec55120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", - "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7a8ff306445707539c1a6397372a982a1ec55120", + "reference": "7a8ff306445707539c1a6397372a982a1ec55120", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", + "php": ">=7.3", "sebastian/diff": "^4.0", "sebastian/exporter": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -10033,33 +9694,27 @@ "compare", "equality" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:05:46+00:00" + "time": "2020-09-30T06:47:25+00:00" }, { "name": "sebastian/diff", - "version": "4.0.2", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" + "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", - "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ffc949a1a2aae270ea064453d7535b82e4c32092", + "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": "^9.3", "symfony/process": "^4.2 || ^5" }, "type": "library", @@ -10095,33 +9750,27 @@ "unidiff", "unified diff" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-30T04:46:02+00:00" + "time": "2020-09-28T05:32:55+00:00" }, { "name": "sebastian/environment", - "version": "5.1.2", + "version": "5.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" + "reference": "388b6ced16caa751030f6a69e588299fa09200ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", - "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", + "reference": "388b6ced16caa751030f6a69e588299fa09200ac", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "suggest": { "ext-posix": "*" @@ -10129,7 +9778,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10154,35 +9803,29 @@ "environment", "hhvm" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:07:24+00:00" + "time": "2020-09-28T05:52:38+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.2", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "571d721db4aec847a0e59690b954af33ebf9f023" + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", - "reference": "571d721db4aec847a0e59690b954af33ebf9f023", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", + "php": ">=7.3", "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^9.2" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -10227,13 +9870,7 @@ "export", "exporter" ], - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:08:55+00:00" + "time": "2020-09-28T05:24:23+00:00" }, { "name": "sebastian/finder-facade", @@ -10338,25 +9975,25 @@ }, { "name": "sebastian/object-enumerator", - "version": "4.0.2", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" + "reference": "f6f5957013d84725427d361507e13513702888a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", - "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f6f5957013d84725427d361507e13513702888a4", + "reference": "f6f5957013d84725427d361507e13513702888a4", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0", + "php": ">=7.3", "sebastian/object-reflector": "^2.0", "sebastian/recursion-context": "^4.0" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -10381,33 +10018,27 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:11:32+00:00" + "time": "2020-09-28T05:55:06+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.2", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "127a46f6b057441b201253526f81d5406d6c7840" + "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", - "reference": "127a46f6b057441b201253526f81d5406d6c7840", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", + "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -10432,13 +10063,7 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:12:55+00:00" + "time": "2020-09-28T05:56:16+00:00" }, { "name": "sebastian/phpcpd", @@ -10493,23 +10118,23 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.2", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" + "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", - "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", + "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { @@ -10542,30 +10167,24 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:14:17+00:00" + "time": "2020-09-28T05:17:32+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0653718a5a629b065e91f774595267f8dc32e213" + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", - "reference": "0653718a5a629b065e91f774595267f8dc32e213", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -10593,38 +10212,32 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:16:22+00:00" + "time": "2020-09-28T06:45:17+00:00" }, { "name": "sebastian/type", - "version": "2.2.1", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" + "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", - "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fa592377f3923946cb90bf1f6a71ba2e5f229909", + "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.2" + "phpunit/phpunit": "^9.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-master": "2.3-dev" } }, "autoload": { @@ -10645,30 +10258,24 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-07-05T08:31:53+00:00" + "time": "2020-10-06T08:41:03+00:00" }, { "name": "sebastian/version", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", - "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": "^7.3 || ^8.0" + "php": ">=7.3" }, "type": "library", "extra": { @@ -10694,13 +10301,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-06-26T12:18:43+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { "name": "spomky-labs/otphp", @@ -10775,16 +10376,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.5", + "version": "3.5.8", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6" + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6", - "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", "shasum": "" }, "require": { @@ -10822,20 +10423,20 @@ "phpcs", "standards" ], - "time": "2020-04-17T01:09:41+00:00" + "time": "2020-10-23T02:01:07+00:00" }, { "name": "symfony/config", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "b8623ef3d99fe62a34baf7a111b576216965f880" + "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/b8623ef3d99fe62a34baf7a111b576216965f880", - "reference": "b8623ef3d99fe62a34baf7a111b576216965f880", + "url": "https://api.github.com/repos/symfony/config/zipball/6ad8be6e1280f6734150d8a04a9160dd34ceb191", + "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191", "shasum": "" }, "require": { @@ -10888,34 +10489,20 @@ ], "description": "Symfony Config 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-23T13:08:13+00:00" + "time": "2020-09-02T16:23:27+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "6508423eded583fc07e88a0172803e1a62f0310c" + "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6508423eded583fc07e88a0172803e1a62f0310c", - "reference": "6508423eded583fc07e88a0172803e1a62f0310c", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2dea4a3ef2eb79138354c1d49e9372cc921af20b", + "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b", "shasum": "" }, "require": { @@ -10977,34 +10564,20 @@ ], "description": "Symfony DependencyInjection 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-06-12T08:11:32+00:00" + "time": "2020-10-01T12:14:45+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.1.3", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", - "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", "shasum": "" }, "require": { @@ -11013,7 +10586,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.2-dev" }, "thanks": { "name": "symfony/contracts", @@ -11041,34 +10614,20 @@ ], "description": "A generic function and convention to trigger deprecation notices", "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-06-06T08:49:21+00:00" + "time": "2020-09-07T11:33:47+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f93055171b847915225bd5b0a5792888419d8d75" + "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f93055171b847915225bd5b0a5792888419d8d75", - "reference": "f93055171b847915225bd5b0a5792888419d8d75", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/353b42e7b4fd1c898aab09a059466c9cea74039b", + "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b", "shasum": "" }, "require": { @@ -11116,34 +10675,20 @@ ], "description": "Symfony HttpFoundation 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-06-15T06:52:54+00:00" + "time": "2020-09-27T14:14:57+00:00" }, { "name": "symfony/mime", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "c0c418f05e727606e85b482a8591519c4712cf45" + "reference": "4404d6545125863561721514ad9388db2661eec5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/c0c418f05e727606e85b482a8591519c4712cf45", - "reference": "c0c418f05e727606e85b482a8591519c4712cf45", + "url": "https://api.github.com/repos/symfony/mime/zipball/4404d6545125863561721514ad9388db2661eec5", + "reference": "4404d6545125863561721514ad9388db2661eec5", "shasum": "" }, "require": { @@ -11193,34 +10738,20 @@ "mime", "mime-type" ], - "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" + "time": "2020-09-02T16:23:27+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" + "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", - "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4c7e155bf7d93ea4ba3824d5a14476694a5278dd", + "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd", "shasum": "" }, "require": { @@ -11263,25 +10794,11 @@ "configuration", "options" ], - "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" + "time": "2020-09-27T03:44:28+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -11327,34 +10844,20 @@ ], "description": "Symfony Stopwatch 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/yaml", - "version": "v5.1.2", + "version": "v5.1.7", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23" + "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ea342353a3ef4f453809acc4ebc55382231d4d23", - "reference": "ea342353a3ef4f453809acc4ebc55382231d4d23", + "url": "https://api.github.com/repos/symfony/yaml/zipball/e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", + "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", "shasum": "" }, "require": { @@ -11404,34 +10907,20 @@ ], "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" + "time": "2020-09-27T03:44:28+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.1.3", + "version": "v1.3.2", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb" + "reference": "72d9fee55e14e07a6283c9b3e28c09e85923a148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/9f277171e296a3c8629c04ac93ec95ff0f208ccb", - "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/72d9fee55e14e07a6283c9b3e28c09e85923a148", + "reference": "72d9fee55e14e07a6283c9b3e28c09e85923a148", "shasum": "" }, "require": { @@ -11452,15 +10941,21 @@ "psr-4": { "Safe\\": [ "lib/", + "deprecated/", "generated/" ] }, "files": [ + "deprecated/apc.php", + "deprecated/libevent.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "lib/special_cases.php", "generated/apache.php", - "generated/apc.php", "generated/apcu.php", "generated/array.php", "generated/bzip2.php", + "generated/calendar.php", "generated/classobj.php", "generated/com.php", "generated/cubrid.php", @@ -11489,14 +10984,12 @@ "generated/inotify.php", "generated/json.php", "generated/ldap.php", - "generated/libevent.php", "generated/libxml.php", "generated/lzf.php", "generated/mailparse.php", "generated/mbstring.php", "generated/misc.php", "generated/msql.php", - "generated/mssql.php", "generated/mysql.php", "generated/mysqli.php", "generated/mysqlndMs.php", @@ -11528,7 +11021,6 @@ "generated/sqlsrv.php", "generated/ssdeep.php", "generated/ssh2.php", - "generated/stats.php", "generated/stream.php", "generated/strings.php", "generated/swoole.php", @@ -11542,8 +11034,7 @@ "generated/yaml.php", "generated/yaz.php", "generated/zip.php", - "generated/zlib.php", - "lib/special_cases.php" + "generated/zlib.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -11551,7 +11042,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-07-10T09:34:29+00:00" + "time": "2020-10-22T09:17:04+00:00" }, { "name": "theseer/fdomdocument", @@ -11631,12 +11122,6 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], "time": "2020-07-12T23:59:07+00:00" }, { @@ -11699,16 +11184,6 @@ "env", "environment" ], - "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" }, { From 0d002e3fb7530c98abc66349329ed6e3cae03cc9 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Fri, 23 Oct 2020 19:11:50 +0300 Subject: [PATCH 0919/1013] Unfinished test. Not sure how to check element's css visibility. --- .../Test/Mftf/Section/AdminHeaderSection.xml | 1 + .../Test/Mftf/Test/AdminSearchHotkeyTest.xml | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml index 186bb183d68d6..f1e2ad911dfbc 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -11,6 +11,7 @@ <section name="AdminHeaderSection"> <element name="pageTitle" type="text" selector=".page-header h1.page-title"/> <element name="adminUserAccountText" type="text" selector=".page-header .admin-user-account-text" /> + <element name="globalSearchInput" type="text" selector="#search-global" /> <!-- Legacy heading section. Mostly used for admin 404 and 403 pages --> <element name="pageHeading" type="text" selector=".page-content .page-heading"/> <!-- Used for page not found error --> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml new file mode 100644 index 0000000000000..ae700332b948d --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AdminSearchHotkeyTest"> + <annotations> + <features value="Backend"/> + <stories value="Search form hotkey in backend"/> + <title value="Admin should be able focus on the search field with a hotkey"/> + <description value="Admin should be able focus on the search field with a hotkey - forwardslash"/> + <severity value="MINOR"/> + <group value="backend"/> + <group value="search"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <pressKey selector="body" parameterArray="[/]" stepKey="pressForwardslashKey"/> + <waitForElementVisible selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="waitForGlobalSearchInput"/> + <seeElement selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeGlobalSearchInput"/> + <seeInField userInput="" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeEmptyGlobalSearchInput"/> + <pressKey selector="{{AdminHeaderSection.globalSearchInput}}" parameterArray="[/]" stepKey="pressForwardslashKeyAgain"/> + <seeInField userInput="/" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeForwardSlashInGlobalSearchInput"/> + </test> +</tests> From 45ff6359d82a761562aa4a56782e1c262ab8bb33 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 23 Oct 2020 12:56:17 -0500 Subject: [PATCH 0920/1013] 29251 fix static --- .../BuyRequest/SuperAttributeDataProvider.php | 60 +++++++++++++------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php index d9c8ade39f621..0fa4b8da50817 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Cart/BuyRequest/SuperAttributeDataProvider.php @@ -8,17 +8,17 @@ namespace Magento\ConfigurableProductGraphQl\Model\Cart\BuyRequest; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\CatalogInventory\Api\StockStateInterface; +use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\Stdlib\ArrayManager; -use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\ConfigurableProductGraphQl\Model\Options\Collection as OptionCollection; -use Magento\Framework\EntityManager\MetadataPool; use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; /** * DataProvider for building super attribute options in buy requests @@ -76,7 +76,6 @@ public function __construct( */ public function execute(array $cartItemData): array { - $parentSku = $this->arrayManager->get('parent_sku', $cartItemData); if ($parentSku === null) { return []; @@ -94,17 +93,7 @@ public function execute(array $cartItemData): array throw new GraphQlNoSuchEntityException(__('Could not find specified product.')); } - - // Child stock check has to be performed a catalog by default would not show/check it - $childProduct = $this->productRepository->get($sku, false, null, true); - - $result = $this->stockState->checkQuoteItemQty($childProduct->getId(), $qty, $qty, $qty, $cart->getStoreId()); - - if ($result->getHasError() ) { - throw new LocalizedException( - __($result->getMessage()) - ); - } + $this->checkProductStock($sku, (float) $qty, (int) $cart->getStoreId()); $configurableProductLinks = $parentProduct->getExtensionAttributes()->getConfigurableProductLinks(); if (!in_array($product->getId(), $configurableProductLinks)) { @@ -124,12 +113,47 @@ public function execute(array $cartItemData): array } } } - // Some options might be disabled and/or available when parent and child sku are provided + $this->checkSuperAttributeData($parentSku, $superAttributesData); + + return ['super_attribute' => $superAttributesData]; + } + + /** + * Stock check for a product + * + * @param string $sku + * @param float $qty + * @param int $scopeId + */ + private function checkProductStock(string $sku, float $qty, int $scopeId): void + { + // Child stock check has to be performed a catalog by default would not show/check it + $childProduct = $this->productRepository->get($sku, false, null, true); + + $result = $this->stockState->checkQuoteItemQty($childProduct->getId(), $qty, $qty, $qty, $scopeId); + + if ($result->getHasError()) { + throw new LocalizedException( + __($result->getMessage()) + ); + } + } + + /** + * Check super attribute data. + * + * Some options might be disabled and/or available when parent and child sku are provided. + * + * @param string $parentSku + * @param array $superAttributesData + * @throws LocalizedException + */ + private function checkSuperAttributeData(string $parentSku, array $superAttributesData): void + { if (empty($superAttributesData)) { throw new LocalizedException( __('The product with SKU %sku is out of stock.', ['sku' => $parentSku]) ); } - return ['super_attribute' => $superAttributesData]; } } From badf7a17b2f76e1cd2ddb31aa26415668ef4640f Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Fri, 23 Oct 2020 14:01:57 -0500 Subject: [PATCH 0921/1013] 29251 fix static --- .../Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php index 12eebe9b926e8..f2dd6389d2c4a 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AddSimpleProductToCart.php @@ -16,7 +16,7 @@ use Magento\QuoteGraphQl\Model\Cart\BuyRequest\BuyRequestBuilder; /** - * Add simple product to cart + * Add simple product to cart mutation */ class AddSimpleProductToCart { From 97c350bbbb60aee37293a6e5afd8306c9ce4bf6e Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Fri, 23 Oct 2020 15:41:06 -0500 Subject: [PATCH 0922/1013] MC-37726: Price filter uses base currency for ranges --- .../Model/Resolver/Aggregations.php | 4 +- .../Catalog/ProductSearchAggregationsTest.php | 133 +++++++++++++++--- 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php index 7d10762d0f3b6..cddb95d5ba765 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php @@ -74,7 +74,9 @@ public function resolve( if (isset($results['price_bucket'])) { foreach ($results['price_bucket']['options'] as &$value) { list($from, $to) = explode('-', $value['label']); - $newLabel = $this->priceCurrency->convertAndRound($from) . '-' . $this->priceCurrency->convertAndRound($to); + $newLabel = $this->priceCurrency->convertAndRound($from) + . '-' + . $this->priceCurrency->convertAndRound($to); $value['label'] = $newLabel; $value['value'] = str_replace('-', '_', $newLabel); } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index 9dbd902f1714e..075388b0f023c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -7,6 +7,11 @@ namespace Magento\GraphQl\Catalog; +use Magento\Config\App\Config\Type\System; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Directory\Model\Currency; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -22,28 +27,9 @@ public function testAggregationBooleanAttribute() . 'MC-36768: Custom attribute not appears in elasticsearch' ); - $skus= '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"'; - $query = <<<QUERY -{ - products(filter: {sku: {in: [{$skus}]}}){ - items{ - id - sku - name - } - aggregations{ - label - attribute_code - count - options{ - label - value - count - } - } - } -} -QUERY; + $query = $this->getGraphQlQuery( + '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"' + ); $result = $this->graphQlQuery($query); @@ -69,4 +55,107 @@ function ($a) { $this->assertCount(2, $booleanAggregation['options']); $this->assertContainsEquals(['label' => '0', 'value'=> '0', 'count' => '2'], $booleanAggregation['options']); } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/products_for_search.php + */ + public function testAggregationPriceRanges() + { + $query = $this->getGraphQlQuery( + '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"' + ); + $result = $this->graphQlQuery($query); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('aggregations', $result['products']); + + $priceAggregation = array_filter( + $result['products']['aggregations'], + function ($a) { + return $a['attribute_code'] == 'price'; + } + ); + $this->assertNotEmpty($priceAggregation); + $priceAggregation = reset($priceAggregation); + $this->assertEquals('Price', $priceAggregation['label']); + $this->assertEquals(4, $priceAggregation['count']); + $expectedOptions = [ + ['label' => '10-20', 'value'=> '10_20', 'count' => '2'], + ['label' => '20-30', 'value'=> '20_30', 'count' => '1'], + ['label' => '30-40', 'value'=> '30_40', 'count' => '1'], + ['label' => '40-50', 'value'=> '40_50', 'count' => '1'] + ]; + $this->assertEquals($expectedOptions, $priceAggregation['options']); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php + * @magentoApiDataFixture Magento/Catalog/_files/products_for_search.php + */ + public function testAggregationPriceRangesWithCurrencyHeader() + { + // add USD as allowed (not default) currency + $objectManager = Bootstrap::getObjectManager(); + /* @var Store $store */ + $store = $objectManager->create(Store::class); + $store->load('fixture_second_store'); + /** @var Config $configResource */ + $configResource = $objectManager->get(Config::class); + $configResource->saveConfig( + Currency::XML_PATH_CURRENCY_ALLOW, + 'USD', + ScopeInterface::SCOPE_STORES, + $store->getId() + ); + // Configuration cache clean is required to reload currency setting + /** @var System $config */ + $config = $objectManager->get(System::class); + $config->clean(); + + $headerMap['Store'] = 'fixture_second_store'; + $headerMap['Content-Currency'] = 'USD'; + $query = $this->getGraphQlQuery( + '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"' + ); + $result = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertArrayNotHasKey('errors', $result); + $this->assertArrayHasKey('aggregations', $result['products']); + $priceAggregation = array_filter( + $result['products']['aggregations'], + function ($a) { + return $a['attribute_code'] == 'price'; + } + ); + $this->assertNotEmpty($priceAggregation); + $priceAggregation = reset($priceAggregation); + $this->assertEquals('Price', $priceAggregation['label']); + $this->assertEquals(4, $priceAggregation['count']); + $expectedOptions = [ + ['label' => '10-20', 'value'=> '10_20', 'count' => '2'], + ['label' => '20-30', 'value'=> '20_30', 'count' => '1'], + ['label' => '30-40', 'value'=> '30_40', 'count' => '1'], + ['label' => '40-50', 'value'=> '40_50', 'count' => '1'] + ]; + $this->assertEquals($expectedOptions, $priceAggregation['options']); + } + + private function getGraphQlQuery(string $skus) + { + return <<<QUERY +{ + products(filter: {sku: {in: [{$skus}]}}){ + aggregations{ + label + attribute_code + count + options{ + label + value + count + } + } + } +} +QUERY; + } } From 63a5bd20dadcffea89703bc8605b4974bb0a0850 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Fri, 23 Oct 2020 16:19:47 -0500 Subject: [PATCH 0923/1013] [AWS S3] Cover Downloadable and CMS functionality by functional tests (#6240) --- .../AwsS3AdminAddImageToWYSIWYGBlockTest.xml | 80 ++++++++ .../AwsS3AdminAddImageToWYSIWYGCMSTest.xml | 75 +++++++ ...nCreateDownloadableProductWithLinkTest.xml | 186 ++++++++++++++++++ .../StorefrontDownloadableLinkSection.xml | 15 ++ 4 files changed, 356 insertions(+) create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml create mode 100644 app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml new file mode 100644 index 0000000000000..54b7795a84cd3 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.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="AwsS3AdminAddImageToWYSIWYGBlockTest"> + <annotations> + <features value="Cms"/> + <stories value="MC-37460: Support by Magento CMS"/> + <group value="Cms"/> + <title value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <description value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38302"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> + <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> + <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + </before> + <actionGroup ref="AssignBlockToCMSPage" stepKey="assignBlockToCMSPage"> + <argument name="Block" value="$$createPreReqBlock$$"/> + <argument name="CmsPage" value="$$createCMSPage$$"/> + </actionGroup> + <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> + </actionGroup> + <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="All Store View" stepKey="selectAllStoreView" /> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <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> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> + <argument name="Image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> + <argument name="Image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> + <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="clickSaveBlock"/> + <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="waitForPageLoad11" /> + <!--see image on Storefront--> + <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"/> + <waitForPageLoad stepKey="waitForGridReload"/> + <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml new file mode 100644 index 0000000000000..d361fb60f31c7 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AwsS3AdminAddImageToWYSIWYGCMSTest"> + <annotations> + <features value="Cms"/> + <stories value="MC-37460: Support by Magento CMS"/> + <group value="Cms"/> + <title value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <description value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38295"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> + <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> + <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + </before> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + </after> + + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <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> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> + <argument name="Image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> + <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="$$createCMSPage.identifier$$" stepKey="fillFieldUrlKey"/> + <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> + <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> + <see userInput="You saved the page." stepKey="seeSuccessMessage"/> + <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> + <waitForPageLoad stepKey="wait4"/> + <seeElement selector="{{StorefrontCMSPageSection.mediaDescription}}" stepKey="assertMediaDescription"/> + <seeElementInDOM selector="{{StorefrontCMSPageSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml new file mode 100644 index 0000000000000..449f00281c425 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml @@ -0,0 +1,186 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AwsS3AdminCreateDownloadableProductWithLinkTest"> + <annotations> + <features value="Catalog"/> + <stories value="Support remote file storage by downloadable products"/> + <title value="Create, view and check out downloadable product with remote filesystem configured. "/> + <description value="Admin should be able to create downloadable product with remote filesystem enabled"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38036"/> + <testCaseId value="MC-38037"/> + <testCaseId value="MC-38039"/> + <group value="Downloadable"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Create downloadable product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Add downloadable links --> + <click selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="openDownloadableSection"/> + <checkOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable"/> + <fillField userInput="{{downloadableData.link_title}}" selector="{{AdminProductDownloadableSection.linksTitleInput}}" stepKey="fillDownloadableLinkTitle"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkLinksPurchasedSeparately"/> + <fillField userInput="{{downloadableData.sample_title}}" selector="{{AdminProductDownloadableSection.samplesTitleInput}}" stepKey="fillDownloadableSampleTitle"/> + <actionGroup ref="AddDownloadableProductLinkWithMaxDownloadsActionGroup" stepKey="addDownloadableLinkWithMaxDownloads"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + <actionGroup ref="AddDownloadableProductLinkActionGroup" stepKey="addDownloadableLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + <!-- Add downloadable sample--> + <actionGroup ref="AddDownloadableSampleFileActionGroup" stepKey="addDownloadableProductSample"/> + + <!-- Save product --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> + + <!-- Login to frontend --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signIn"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Assert product in storefront category page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckProductPriceInCategoryActionGroup" stepKey="StorefrontCheckCategorySimpleProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Assert product in storefront product page --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageActionGroup" stepKey="AssertProductInStorefrontProductPage"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Assert product price in storefront product page --> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{DownloadableProduct.price}}" stepKey="assertProductPrice"/> + + <!-- Assert link sample urls are accessible --> + <!-- Click on the link sample --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableLinkSampleWithMaxDownloads"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLinkWithMaxDownloads.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLinkWithMaxDownloads.title)}}" stepKey="clickDownloadableLinkSampleWithMaxDownloads"/> + <waitForPageLoad stepKey="waitForLinkSampleWithMaxDownloadsPage"/> + <!-- Grab Link Sample id --> + <switchToNextTab stepKey="switchToLinkSampleWithMaxDownloadsTab"/> + <grabFromCurrentUrl regex="~/link_id/(\d+)/~" stepKey="grabDownloadableLinkWithMaxDownloadsId"/> + <!-- Check is svg --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedSvg('Logo')}}" stepKey="assertDownloadableLinkWithMaxDownloadsIsSvg"/> + <closeTab stepKey="closeLinkSampleWithMaxDownloadsTab"/> + <!-- Click on the link sample --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableLinkSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLink.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLink.title)}}" stepKey="clickDownloadableLinkSample"/> + <waitForPageLoad stepKey="waitForLinkSamplePage"/> + <!-- Grab Link Sample id --> + <switchToNextTab stepKey="switchToLinkSampleTab"/> + <grabFromCurrentUrl regex="~/link_id/(\d+)/~" stepKey="grabDownloadableLinkSampleId"/> + <!-- Check is image --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedImage}}" stepKey="assertDownloadableLinkSampleIsImage"/> + <closeTab stepKey="closeLinkSampleTab"/> + + <!-- Assert sample file is accessible --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableSampleLabel(downloadableSampleFile.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableSampleLabel(downloadableSampleFile.title)}}" stepKey="clickDownloadableSample"/> + <waitForPageLoad stepKey="waitForSamplePage"/> + <!-- Grab Sample id --> + <switchToNextTab stepKey="switchToSampleTab"/> + <grabFromCurrentUrl regex="~/sample_id/(\d+)/~" stepKey="grabDownloadableSampleId"/> + <!-- Check is image --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedImage}}" stepKey="assertDownloadableSampleIsImage"/> + <closeTab stepKey="closeSampleTab"/> + + <!-- Select product link in storefront product page--> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkBlock}}" stepKey="scrollToLinks"/> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="selectProductLink"/> + + <!-- Add product with selected link to the cart --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="DownloadableProduct"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Assert product price in cart --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$52.99" stepKey="assertProductPriceInCart"/> + + <!-- Perform checkout --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> + <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Open created order --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchOrder"> + <argument name="keyword" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + + <!-- Open Create invoice --> + <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createCreditMemo"/> + + <!-- Check downloadable product link on frontend --> + <actionGroup ref="StorefrontAssertDownloadableProductIsPresentInCustomerAccount" stepKey="seeStorefrontMyAccountDownloadableProductsLink"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + <click selector="{{StorefrontCustomerDownloadableProductsSection.downloadableLink}}" stepKey="clickDownloadLink" /> + <waitForPageLoad stepKey="waitForDownloadedLinkPage"/> + <!-- Grab downloadable URL --> + <switchToNextTab stepKey="switchToDownloadedLinkTab"/> + <grabFromCurrentUrl regex="~/link/id/(.+)/~" stepKey="grabDownloadLinkUrl"/> + <!-- Check is svg --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedSvg('Logo')}}" stepKey="assertDownloadedLinkIsSvg"/> + <closeTab stepKey="closeDownloadedLinkTab"/> + </test> +</tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml new file mode 100644 index 0000000000000..6364600faee30 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.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="StorefrontDownloadableLinkSection"> + <element name="downloadedImage" type="text" selector="//img[contains(@style, '-webkit-user-select')]"/> + <element name="downloadedSvg" type="text" selector="//*[@id='{{id}}']" parameterized="true"/> + </section> +</sections> From 3009cdc6772ec1e8b0fae0eaad37865fb88c70dc Mon Sep 17 00:00:00 2001 From: Andrii Beziazychnyi <a.beziazychnyi@atwix.com> Date: Sat, 24 Oct 2020 02:16:44 +0300 Subject: [PATCH 0924/1013] M2CE-30469: [GRAPHQL] Made the attribute `destination_cart_id` for mergeCarts mutation not required. --- .../Model/Resolver/MergeCarts.php | 81 +++++++++++++++---- .../Magento/QuoteGraphQl/etc/schema.graphqls | 2 +- .../GraphQl/Quote/Customer/MergeCartsTest.php | 3 - 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php index d77d19df55603..50682413aea1e 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php @@ -7,17 +7,23 @@ namespace Magento\QuoteGraphQl\Model\Resolver; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\Quote\Api\CartRepositoryInterface; use Magento\GraphQl\Model\Query\ContextInterface; -use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Cart\CustomerCartResolver; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; /** * Merge Carts Resolver + * + * @SuppressWarnings(PHPMD.LongVariable) */ class MergeCarts implements ResolverInterface { @@ -31,44 +37,87 @@ class MergeCarts implements ResolverInterface */ private $cartRepository; + /** + * @var CustomerCartResolver + */ + private $customerCartResolver; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedQuoteId; + /** * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository + * @param CustomerCartResolver $customerCartResolver + * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId */ public function __construct( GetCartForUser $getCartForUser, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + CustomerCartResolver $customerCartResolver, + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId ) { $this->getCartForUser = $getCartForUser; $this->cartRepository = $cartRepository; + $this->customerCartResolver = $customerCartResolver; + $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; } /** * @inheritdoc */ - public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) - { + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { if (empty($args['source_cart_id'])) { - throw new GraphQlInputException(__('Required parameter "source_cart_id" is missing')); - } - - if (empty($args['destination_cart_id'])) { - throw new GraphQlInputException(__('Required parameter "destination_cart_id" is missing')); + throw new GraphQlInputException(__( + 'Required parameter "source_cart_id" is missing' + )); } /** @var ContextInterface $context */ if (false === $context->getExtensionAttributes()->getIsCustomer()) { - throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + throw new GraphQlAuthorizationException(__( + 'The current customer isn\'t authorized.' + )); + } + $currentUserId = $context->getUserId(); + + if (empty($args['destination_cart_id'])) { + try { + $cart = $this->customerCartResolver->resolve($currentUserId); + } catch (CouldNotSaveException $exception) { + throw new GraphQlNoSuchEntityException( + __('Could not create empty cart for customer'), + $exception + ); + } + $customerMaskedCartId = $this->quoteIdToMaskedQuoteId->execute( + (int) $cart->getId() + ); } $guestMaskedCartId = $args['source_cart_id']; - $customerMaskedCartId = $args['destination_cart_id']; + $customerMaskedCartId = $customerMaskedCartId ?? $args['destination_cart_id']; - $currentUserId = $context->getUserId(); $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); // passing customerId as null enforces source cart should always be a guestcart - $guestCart = $this->getCartForUser->execute($guestMaskedCartId, null, $storeId); - $customerCart = $this->getCartForUser->execute($customerMaskedCartId, $currentUserId, $storeId); + $guestCart = $this->getCartForUser->execute( + $guestMaskedCartId, + null, + $storeId + ); + $customerCart = $this->getCartForUser->execute( + $customerMaskedCartId, + $currentUserId, + $storeId + ); $customerCart->merge($guestCart); $guestCart->setIsActive(false); $this->cartRepository->save($customerCart); diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 4e0e7ce5732be..cc9d1803b3e31 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -20,7 +20,7 @@ type Mutation { setPaymentMethodOnCart(input: SetPaymentMethodOnCartInput): SetPaymentMethodOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentMethodOnCart") setGuestEmailOnCart(input: SetGuestEmailOnCartInput): SetGuestEmailOnCartOutput @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\SetGuestEmailOnCart") 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") + 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") } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php index 65e91bf193020..b33d9982b8187 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php @@ -192,9 +192,6 @@ public function testMergeCartsWithEmptySourceCartId() */ public function testMergeCartsWithEmptyDestinationCartId() { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Required parameter "destination_cart_id" is missing'); - $guestQuote = $this->quoteFactory->create(); $this->quoteResource->load( $guestQuote, From bb7d50bcc838dcf66fc325e7e9ecbfdd8f1637aa Mon Sep 17 00:00:00 2001 From: Oleh Usik <o.usik@atwix.com> Date: Sat, 24 Oct 2020 17:13:17 +0300 Subject: [PATCH 0925/1013] add Backward Compatibility --- .../CaptchaWithDisabledGuestCheckoutTest.xml | 3 +++ .../StorefrontCatalogNavigationMenuUIDesktopTest.xml | 9 +++++++++ ...ateFieldForUKCustomerRemainOptionAfterRefreshTest.xml | 1 + ...igsChangesIsNotAffectedStartedCheckoutProcessTest.xml | 1 + ...refrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml | 2 ++ ...rsistentDataForGuestCustomerWithPhysicalQuoteTest.xml | 3 +++ ...rontUpdatePriceInShoppingCartAfterProductSaveTest.xml | 1 + .../Test/StorefrontVisibilityOfDuplicateProductTest.xml | 4 ++++ .../Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml | 2 ++ ...dminFrontendAreaSessionMustNotAffectAdminAreaTest.xml | 2 ++ .../CheckShoppingCartBehaviorAfterSessionExpiredTest.xml | 1 + .../Test/StorefrontGuestCheckoutDisabledProductTest.xml | 3 +++ .../AdminCreateCartPriceRuleForGeneratedCouponTest.xml | 1 + .../Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml | 1 + .../Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml | 1 + .../Test/StorefrontInlineTranslationOnCheckoutTest.xml | 3 +++ 16 files changed, 38 insertions(+) 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 39a774369c331..66183cb31aebc 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -46,7 +46,10 @@ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaField}}" stepKey="seeCaptchaField"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad2" /> + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickCart2"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout2"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml index 07c67f6f290f1..fb4bd4d1dcb74 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -40,7 +40,10 @@ <!-- Assert single row - no hover state --> <createData entity="ApiCategoryA" stepKey="createFirstCategoryBlank"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForBlankSingleRowAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryBlank.name$$)}}" stepKey="hoverFirstCategoryBlank"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}" stepKey="assertNoHoverState"/> @@ -87,6 +90,8 @@ <!-- Several rows. Hover on category without children --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForBlankSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithoutChildrenBlank.name$$)}}" stepKey="hoverCategoryWithoutChildren"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createCategoryWithoutChildrenBlank.name$$, 'level0')}}" stepKey="dontSeeChildrenInCategory"/> @@ -166,6 +171,8 @@ <!-- Single row. No hover state --> <actionGroup ref="ReloadPageActionGroup" stepKey="reload"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLumaSingleRowAppear"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFirstCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInFirstCategory"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createSecondCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInSecondCategory"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createThirdCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateThirdCategory"/> @@ -201,6 +208,8 @@ <!-- Several rows. Hover on Category without children --> <actionGroup ref="ReloadPageActionGroup" stepKey="refresh"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLumaSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFifthCategoryLuma.name$$)}}" stepKey="hoverOnCategoryWithoutChildren"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFifthCategoryLuma.name$$, 'level0')}}" stepKey="dontSeeSubcategoriesInCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index de4e64e3c5938..f578a9c02caca 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -43,6 +43,7 @@ <selectOption stepKey="selectCounty" selector="{{CheckoutShippingSection.country}}" userInput="{{UK_Address.country_id}}"/> <waitForPageLoad stepKey="waitFormToReload"/> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1" /> <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index 3128387e4155b..df229c4b6ed78 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -91,6 +91,7 @@ <!-- Back to the Checkout and refresh the page --> <switchToPreviousTab stepKey="switchToPreviousTab"/> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitPageReload"/> <!-- Payment step is opened after refreshing --> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSection"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index 67cf37f75c979..033898bb90557 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -173,6 +173,8 @@ <argument name="address" value="US_Address_NY_Default_Shipping"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadThePage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageToReload"/> + <waitForText selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.60" time="90" stepKey="waitForTaxAmount"/> <!--Select Free Shipping and proceed to checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index 6ac85e77766e1..a3c093d005371 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -50,6 +50,7 @@ <see selector="{{CheckoutCartSummarySection.total}}" userInput="15" stepKey="assertOrderTotalField"/> <!-- 5. Refresh browser page (F5) --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad"/> <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterPageReload"/> <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsChecked"> <argument name="carrierCode" value="flatrate"/> @@ -71,6 +72,8 @@ <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> <!-- 10. Refresh browser page(F5) --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadCheckoutPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageLoad"/> + <actionGroup ref="StorefrontAssertGuestShippingInfoActionGroup" stepKey="assertGuestShippingPersistedInfoAfterReloadingCheckoutShippingPage"/> <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsChecked"> <argument name="shippingMethod" value="Free Shipping"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index 299d33244f1fb..f014a7a5bd1ee 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -63,6 +63,7 @@ <!--Check price--> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload"/> <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock1"/> <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index a066d5077f713..79705e679fb78 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -63,6 +63,8 @@ <argument name="scope" value="Global"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForProductPageReload"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> <waitForPageLoad stepKey="waitForFilters"/> <actionGroup ref="CreateOptionsForAttributeActionGroup" stepKey="createOptions"> @@ -142,6 +144,8 @@ <argument name="scope" value="Global"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadDuplicatedProductPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForDuplicatedProductReload"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="createConfigurationsDuplicatedProduct"/> <waitForElementVisible selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="waitForCreateConfigurationsPageLoad"/> <click selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 710e4bba29e05..7442a32d58b2d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -41,6 +41,8 @@ <argument name="indices" value=""/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLoad2"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index 9eb0558bdfd2e..e026bee87dcd4 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -77,7 +77,9 @@ <!-- 5. Open admin tab with page with products. Reload this page twice. --> <switchToPreviousTab stepKey="switchToPreviousTab"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadAdminCatalogPageFirst"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReloadFirst"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadAdminCatalogPageSecond"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReloadSecond"/> <seeInTitle userInput="Products / Inventory / Catalog / Magento Admin" stepKey="seeAdminProductsPageTitle"/> <see userInput="Products" selector="{{AdminHeaderSection.pageTitle}}" stepKey="seeAdminProductsPageHeader"/> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml index 533a06986b70a..5b023e12bc55d 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml @@ -56,6 +56,7 @@ <!--Reset cookies and refresh the page--> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad"/> <!--Check product exists in cart--> <see userInput="$$createProduct.name$$" stepKey="ProductExistsInCart"/> </test> diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 92a1c1facd6a6..ee5f2fccfe203 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -125,6 +125,7 @@ <!-- Check cart --> <wait time="60" stepKey="waitForCartToBeUpdated"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload"/> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickMiniCart"/> <dontSeeElement selector="{{StorefrontMinicartSection.quantity}}" stepKey="dontSeeCartItem"/> <!-- Add simple product to shopping cart --> @@ -151,6 +152,8 @@ <!--Check cart--> <wait time="60" stepKey="waitForCartToBeUpdated2"/> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage2"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload2"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickMiniCart2"/> <dontSeeElement selector="{{StorefrontMinicartSection.quantity}}" stepKey="dontSeeCartItem2"/> </test> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 79672b5bdd559..b3d81cea7f97f 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -60,6 +60,7 @@ <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1"/> <click selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" stepKey="expandCouponSection2"/> <!-- Assert coupon codes grid header is correct --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 96b3990dfd063..74542be376c45 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -64,6 +64,7 @@ <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> </actionGroup> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1"/> <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" dependentSelector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" visible="true" stepKey="clickManageCouponCodes2"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index 8ad9578e9184a..85481f6fd4d5f 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -154,6 +154,7 @@ <!-- Verify swatch tooltips are not visible --> <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageReload"/> <moveMouseOver selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" stepKey="hoverDisabledSwatch"/> <wait time="1" stepKey="waitForTooltip2"/> <dontSeeElement selector="{{StorefrontProductInfoMainSection.swatchOptionTooltip}}" stepKey="swatchTooltipNotVisible"/> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index cfee0785ac1d1..4eff032ce160e 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -121,6 +121,7 @@ <!-- 3. Go to storefront and click on cart button on the top --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReload"/> <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> <!-- Check button "Proceed to Checkout". There must be red borders and "book" icons on labels that can be translated. --> @@ -490,6 +491,8 @@ <!-- Reload page after full clear --> <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPageAfterFullClean"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoadAfterFullClean"/> + <!-- Add product to cart and go through Checkout process like you did in steps ##3-6 and check translation you maid. --> <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProductPage1"> From a93afdef222b6fbadc68c0a3b452346d3bdf3016 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Sat, 24 Oct 2020 22:30:16 +0300 Subject: [PATCH 0926/1013] Fixed invalid test. Now it checks if parent element has active class. --- .../Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml | 1 + .../Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml index f1e2ad911dfbc..4ebb3316a0245 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -12,6 +12,7 @@ <element name="pageTitle" type="text" selector=".page-header h1.page-title"/> <element name="adminUserAccountText" type="text" selector=".page-header .admin-user-account-text" /> <element name="globalSearchInput" type="text" selector="#search-global" /> + <element name="globalSearchInputVisible" type="text" selector=".search-global-field._active #search-global" /> <!-- Legacy heading section. Mostly used for admin 404 and 403 pages --> <element name="pageHeading" type="text" selector=".page-content .page-heading"/> <!-- Used for page not found error --> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml index ae700332b948d..89e8668fa3c23 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml @@ -26,8 +26,7 @@ </after> <pressKey selector="body" parameterArray="[/]" stepKey="pressForwardslashKey"/> - <waitForElementVisible selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="waitForGlobalSearchInput"/> - <seeElement selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeGlobalSearchInput"/> + <seeElement selector="{{AdminHeaderSection.globalSearchInputVisible}}" stepKey="seeActiveGlobalSearchInput"/> <seeInField userInput="" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeEmptyGlobalSearchInput"/> <pressKey selector="{{AdminHeaderSection.globalSearchInput}}" parameterArray="[/]" stepKey="pressForwardslashKeyAgain"/> <seeInField userInput="/" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeForwardSlashInGlobalSearchInput"/> From d4e5894ae29b8088c8cc6db59f332bfc70dd9a4a Mon Sep 17 00:00:00 2001 From: Andrii Beziazychnyi <a.beziazychnyi@atwix.com> Date: Sun, 25 Oct 2020 22:28:53 +0200 Subject: [PATCH 0927/1013] M2CE-30469: The tests and logic fixes --- .../Model/Resolver/MergeCarts.php | 8 +- .../GraphQl/Quote/Customer/MergeCartsTest.php | 76 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php index 50682413aea1e..d53ab597eaba4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php @@ -89,7 +89,7 @@ public function resolve( } $currentUserId = $context->getUserId(); - if (empty($args['destination_cart_id'])) { + if (!isset($args['destination_cart_id'])) { try { $cart = $this->customerCartResolver->resolve($currentUserId); } catch (CouldNotSaveException $exception) { @@ -101,6 +101,12 @@ public function resolve( $customerMaskedCartId = $this->quoteIdToMaskedQuoteId->execute( (int) $cart->getId() ); + } else { + if (empty($args['destination_cart_id'])) { + throw new GraphQlInputException(__( + 'The parameter "destination_cart_id" cannot be empty' + )); + } } $guestMaskedCartId = $args['source_cart_id']; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php index b33d9982b8187..4f50b9df3098a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/MergeCartsTest.php @@ -192,6 +192,9 @@ public function testMergeCartsWithEmptySourceCartId() */ public function testMergeCartsWithEmptyDestinationCartId() { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The parameter "destination_cart_id" cannot be empty'); + $guestQuote = $this->quoteFactory->create(); $this->quoteResource->load( $guestQuote, @@ -206,6 +209,54 @@ public function testMergeCartsWithEmptyDestinationCartId() $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } + /** + * @magentoApiDataFixture Magento/Checkout/_files/quote_with_virtual_product_saved.php + * @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 testMergeCartsWithoutDestinationCartId() + { + $guestQuote = $this->quoteFactory->create(); + $this->quoteResource->load( + $guestQuote, + 'test_order_with_virtual_product_without_address', + 'reserved_order_id' + ); + $guestQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$guestQuote->getId()); + $query = $this->getCartMergeMutationWithoutDestinationCartId( + $guestQuoteMaskedId + ); + $mergeResponse = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('mergeCarts', $mergeResponse); + $cartResponse = $mergeResponse['mergeCarts']; + self::assertArrayHasKey('items', $cartResponse); + self::assertCount(2, $cartResponse['items']); + + $customerQuote = $this->quoteFactory->create(); + $this->quoteResource->load($customerQuote, 'test_quote', 'reserved_order_id'); + $customerQuoteMaskedId = $this->quoteIdToMaskedId->execute((int)$customerQuote->getId()); + + $cartResponse = $this->graphQlMutation( + $this->getCartQuery($customerQuoteMaskedId), + [], + '', + $this->getHeaderMap() + ); + + self::assertArrayHasKey('cart', $cartResponse); + self::assertArrayHasKey('items', $cartResponse['cart']); + self::assertCount(2, $cartResponse['cart']['items']); + $item1 = $cartResponse['cart']['items'][0]; + self::assertArrayHasKey('quantity', $item1); + self::assertEquals(2, $item1['quantity']); + $item2 = $cartResponse['cart']['items'][1]; + self::assertArrayHasKey('quantity', $item2); + self::assertEquals(1, $item2['quantity']); + } + /** * Add simple product to cart * @@ -253,6 +304,31 @@ private function getCartMergeMutation(string $guestQuoteMaskedId, string $custom QUERY; } + /** + * Create the mergeCart mutation + * + * @param string $guestQuoteMaskedId + * @return string + */ + private function getCartMergeMutationWithoutDestinationCartId( + string $guestQuoteMaskedId + ): string { + return <<<QUERY +mutation { + mergeCarts( + source_cart_id: "{$guestQuoteMaskedId}" + ){ + items { + quantity + product { + sku + } + } + } +} +QUERY; + } + /** * Get cart query * From dedf0950ae58a4dbeea5db0672897b540613a0c6 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Mon, 26 Oct 2020 10:45:58 +0200 Subject: [PATCH 0928/1013] MC-37128: Create automated test for "[Security] No XSS injections in order comments" --- .../_files/quote_with_address_and_shipping_method_saved.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.php index 0f8e92e252057..98953fe785695 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_address_and_shipping_method_saved.php @@ -18,7 +18,9 @@ $quoteRepository = $objectManager->get(CartRepositoryInterface::class); /** @var CartManagementInterface $quoteManagement */ $quoteManagement = $objectManager->get(CartManagementInterface::class); -$quote = $objectManager->get(GetQuoteByReservedOrderId::class)->execute('test_order_1'); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('test_order_1'); $quote->setIsActive(true); $quote->getShippingAddress()->setShippingMethod('flatrate_flatrate'); $quote->getShippingAddress()->setCollectShippingRates(true); From 3aa86ee47ac73aa12c5771bf8b4adaa278d15628 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 26 Oct 2020 11:01:06 +0200 Subject: [PATCH 0929/1013] MC-37954: PLP sort by name is case-sensitive with ElasticSearch --- .../Model/Adapter/BatchDataMapper/ProductDataMapper.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 53472710671a4..0edc63b10f9ab 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -272,6 +272,9 @@ private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool * @param array $attributeValues * @param int $storeId * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ private function prepareAttributeValues( int $productId, From 80c3184e9655f3a003119b30dbc744839dad5c63 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Mon, 26 Oct 2020 11:06:56 +0200 Subject: [PATCH 0930/1013] MC-38488: [MFTF] AdminMediaGalleryAssertUsedInLinkBlocksGridTest failed because of bad design --- ...eryAssertImageUsedInLinkBlocksGridTest.xml | 63 ++++++++------- ...ediaGalleryDeletedAllImagesActionGroup.xml | 30 ++++++++ .../Test/Mftf/Helper/MediaGalleryUiHelper.php | 77 +++++++++++++++++++ ...nhancedMediaGalleryImageActionsSection.xml | 2 +- ...nEnhancedMediaGalleryMassActionSection.xml | 1 + .../AdminMediaGalleryMessagesSection.xml | 13 ++++ .../web/js/grid/filters/elements/ui-select.js | 4 +- 7 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml index c25e55cd30461..63f6a483665c9 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml @@ -9,29 +9,44 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest"> <annotations> - <features value="AdminMediaGalleryUsedInBlocksFilter"/> - <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> - <title value="Used in blocks link"/> + <features value="MediaGalleryCmsUi"/> <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"/> + <title value="Used in blocks link"/> <description value="User filters assets used in blocks"/> <severity value="CRITICAL"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> <group value="media_gallery_ui"/> </annotations> <before> - <createData entity="_defaultBlock" stepKey="block" /> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <createData entity="_defaultBlock" stepKey="createBlock" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImages"/> </before> <after> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTest"> - <argument name="tags" value="block_html"/> + <deleteData createDataKey="createBlock" stepKey="deleteBlock"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultViewAgain"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCreatedFolderAgain"> + <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeImageDetails"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"> + <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> + + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImagesAfterTest"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage"> - <argument name="CMSBlockPage" value="$$block$$"/> + <argument name="CMSBlockPage" value="$createBlock$"/> </actionGroup> <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImageIcon" /> <waitForPageLoad stepKey="waitForInitialPageLoad" /> @@ -39,10 +54,10 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"> - <argument name="name" value="blockImage"/> + <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"> - <argument name="name" value="blockImage"/> + <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> <waitForPageLoad stepKey="waitForGridToLoadAfterNewFolderCreated"/> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> @@ -57,34 +72,16 @@ <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCreatedFolder"> - <argument name="name" value="blockImage"/> + <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInBlocks"> <argument name="entityName" value="Blocks"/> </actionGroup> - <wait time="5" stepKey="waitForAssertLoads"/> - <reloadPage stepKey="reloadBlocksGridPage"/> - <waitForPageLoad stepKey="waitForBlocksGridPageLoad"/> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> </actionGroup> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilterInBlocksGrid"/> - - <deleteData createDataKey="block" stepKey="deleteBlock"/> - - <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultViewAgain"/> - <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCreatedFolderAgain"> - <argument name="name" value="blockImage"/> - </actionGroup> - <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> - <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> - <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeImageDetails"/> - - <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> - <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"> - <argument name="name" value="blockImage"/> - </actionGroup> </test> </tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml new file mode 100644 index 0000000000000..f77521879c8ea --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.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="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup"> + <annotations> + <description>Open Media Gallery page and delete all images</description> + </annotations> + + <amOnPage url="{{AdminStandaloneMediaGalleryPage.url}}" stepKey="openMediaGalleryPage"/> + <!-- 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\MediaGalleryUi\Test\Mftf\Helper\MediaGalleryUiHelper" method="deleteAllImagesUsingMassAction" stepKey="deleteAllImagesUsingMassAction"> + <argument name="emptyRow">{{AdminMediaGalleryGridSection.noDataMessage}}</argument> + <argument name="deleteImagesButton">{{AdminEnhancedMediaGalleryMassActionSection.deleteImages}}</argument> + <argument name="checkImage">{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckboxAll}}</argument> + <argument name="deleteSelectedButton">{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}</argument> + <argument name="modalAcceptButton">{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}</argument> + <argument name="successMessageContainer">{{AdminMediaGalleryMessagesSection.success}}</argument> + <argument name="successMessage">been successfully deleted</argument> + </helper> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php b/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php new file mode 100644 index 0000000000000..4059a8460bb51 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\Remote\RemoteWebElement; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for MediaGalleryUi module. + */ +class MediaGalleryUiHelper extends Helper +{ + /** + * Delete all images using mass action. + * + * @param string $emptyRow + * @param string $deleteImagesButton + * @param string $checkImage + * @param string $deleteSelectedButton + * @param string $modalAcceptButton + * @param string $successMessageContainer + * @param string $successMessage + * + * @return void + */ + public function deleteAllImagesUsingMassAction( + string $emptyRow, + string $deleteImagesButton, + string $checkImage, + string $deleteSelectedButton, + string $modalAcceptButton, + 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($emptyRow)); + while (empty($rows)) { + $magentoWebDriver->click($deleteImagesButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($deleteSelectedButton, 10); + + // Check all images + /** @var RemoteWebElement[] $images */ + $imagesCheckboxes = $webDriver->findElements(WebDriverBy::cssSelector($checkImage)); + /** @var RemoteWebElement $image */ + foreach ($imagesCheckboxes as $imageCheckbox) { + $imageCheckbox->click(); + } + + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteSelectedButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForElementVisible($successMessageContainer, 10); + $magentoWebDriver->see($successMessage, $successMessageContainer); + + $rows = $webDriver->findElements(WebDriverBy::cssSelector($emptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index a8e9feaa2d623..bfb92f0cd8ee7 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -10,7 +10,7 @@ <section name="AdminEnhancedMediaGalleryImageActionsSection"> <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> - <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[@class='action-menu-item' and contains(text(), 'View Details')]" timeout="30" /> + <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[text()='View Details']" timeout="30" /> <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"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml index 07f2dc23530e1..9018ccb4ddd69 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml @@ -13,5 +13,6 @@ <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"/> + <element name="massActionCheckboxAll" type="checkbox" selector="[data-id='media-gallery-masonry-grid'] .mediagallery-massaction-checkbox input[type='checkbox']"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml new file mode 100644 index 0000000000000..42a936b6c0ebc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.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="AdminMediaGalleryMessagesSection"> + <element name="success" type="text" selector=".media-gallery-container ul.messages div.message.message-success span"/> + </section> +</sections> diff --git a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js index a913f3fa4a042..cddcc7d49ffe8 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/filters/elements/ui-select.js @@ -18,13 +18,15 @@ define([ loadedOption: [], validationLoading: true, imports: { + applied: '${ $.filterChipsProvider }:applied', activeIndex: '${ $.bookmarkProvider }:activeIndex' }, modules: { filterChips: '${ $.filterChipsProvider }' }, listens: { - activeIndex: 'validateInitialValue' + activeIndex: 'validateInitialValue', + applied: 'validateInitialValue' } }, From 76b9492952301f7f4a7b888bac9cdc16d2568d9c Mon Sep 17 00:00:00 2001 From: engcom-Kilo <mikola.malevanec@transoftgroup.com> Date: Mon, 19 Oct 2020 17:48:24 +0300 Subject: [PATCH 0931/1013] MC-38509: Submitting invalid create account form leaves the submit button disabled. --- ...frontCreateCustomerWithInvalidDataTest.xml | 39 +++++++++++++++++++ .../frontend/web/js/block-submit-on-send.js | 6 +++ 2 files changed, 45 insertions(+) create mode 100644 app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml new file mode 100644 index 0000000000000..ef610831a721d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCreateCustomerWithInvalidDataTest"> + <annotations> + <stories value="Create a Customer via the Storefront"/> + <features value="Customer"/> + <title value="Register customer on storefront after customer form validation failed."/> + <description value="Customer should be able to re-submit register form after correcting invalid form data on storefront."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38532"/> + <useCaseId value="MC-38509"/> + <group value="customer"/> + </annotations> + + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> + <!--Try to submit register form with wrong password.--> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountFormWithWrongData"> + <argument name="customer" value="Simple_Customer_With_Password_Length_Is_Below_Eight_Characters"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="tryToSubmitFormWithWrongPassword"/> + <actionGroup ref="AssertMessageCustomerCreateAccountPasswordComplexityActionGroup" stepKey="seeTheErrorPasswordLength"> + <argument name="message" value="Minimum length of this field must be equal or greater than 8 symbols. Leading and trailing spaces will be ignored."/> + </actionGroup> + <!--Re-submit customer register form with correct data.--> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountFormWithCorrectData"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="seeSuccessMessage"/> + </test> +</tests> 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 index b941ec7a254d8..75f4ee6097685 100644 --- 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 @@ -14,9 +14,15 @@ define([ dataForm.submit(function () { $(this).find(':submit').attr('disabled', 'disabled'); + + if (this.isValid === false) { + $(this).find(':submit').prop('disabled', false); + } + this.isValid = true; }); dataForm.bind('invalid-form.validate', function () { $(this).find(':submit').prop('disabled', false); + this.isValid = false; }); }; }); From 0e9238ea655adbc361d5d9077bdd7efea28b8c93 Mon Sep 17 00:00:00 2001 From: SmVladyslav <vlatame.tsg@gmail.com> Date: Mon, 26 Oct 2020 12:18:32 +0200 Subject: [PATCH 0932/1013] MC-37120: Shopping cart is empty after adding simple product if another product was removed from Multiple Addresses checkout before --- .../Model/DisableMultishipping.php | 38 ++++++ .../Observer/DisableMultishippingObserver.php | 46 +++++++ .../Plugin/DisableMultishippingMode.php | 25 ++-- .../Unit/Model/DisableMultishippingTest.php | 116 ++++++++++++++++++ .../Plugin/DisableMultishippingModeTest.php | 90 +++++++++----- .../Multishipping/etc/frontend/events.xml | 12 ++ .../DisableMultishippingObserverTest.php | 87 +++++++++++++ .../Plugin/DisableMultishippingModeTest.php | 70 +++++++++++ 8 files changed, 442 insertions(+), 42 deletions(-) create mode 100644 app/code/Magento/Multishipping/Model/DisableMultishipping.php create mode 100644 app/code/Magento/Multishipping/Observer/DisableMultishippingObserver.php create mode 100644 app/code/Magento/Multishipping/Test/Unit/Model/DisableMultishippingTest.php create mode 100644 app/code/Magento/Multishipping/etc/frontend/events.xml create mode 100644 dev/tests/integration/testsuite/Magento/Multishipping/Observer/DisableMultishippingObserverTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Multishipping/Plugin/DisableMultishippingModeTest.php diff --git a/app/code/Magento/Multishipping/Model/DisableMultishipping.php b/app/code/Magento/Multishipping/Model/DisableMultishipping.php new file mode 100644 index 0000000000000..a871cee715538 --- /dev/null +++ b/app/code/Magento/Multishipping/Model/DisableMultishipping.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Model; + +use Magento\Quote\Api\Data\CartInterface; + +/** + * Turn Off Multishipping mode if enabled. + */ +class DisableMultishipping +{ + /** + * Disable Multishipping mode for provided Quote and return TRUE if it was changed. + * + * @param CartInterface $quote + * @return bool + */ + public function execute(CartInterface $quote): bool + { + $modeChanged = false; + if ($quote->getIsMultiShipping()) { + $quote->setIsMultiShipping(0); + $extensionAttributes = $quote->getExtensionAttributes(); + if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { + $extensionAttributes->setShippingAssignments([]); + } + + $modeChanged = true; + } + + return $modeChanged; + } +} diff --git a/app/code/Magento/Multishipping/Observer/DisableMultishippingObserver.php b/app/code/Magento/Multishipping/Observer/DisableMultishippingObserver.php new file mode 100644 index 0000000000000..a72bce87965a4 --- /dev/null +++ b/app/code/Magento/Multishipping/Observer/DisableMultishippingObserver.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Observer; + +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Event\Observer as EventObserver; +use Magento\Multishipping\Model\DisableMultishipping; +use Magento\Quote\Api\Data\CartInterface; + +/** + * Observer for disabling Multishipping mode. + */ +class DisableMultishippingObserver implements ObserverInterface +{ + /** + * @var DisableMultishipping + */ + private $disableMultishipping; + + /** + * @param DisableMultishipping $disableMultishipping + */ + public function __construct( + DisableMultishipping $disableMultishipping + ) { + $this->disableMultishipping = $disableMultishipping; + } + + /** + * Disable Multishipping mode before saving Quote. + * + * @param EventObserver $observer + * @return void + */ + public function execute(EventObserver $observer): void + { + /** @var CartInterface $quote */ + $quote = $observer->getEvent()->getCart()->getQuote(); + $this->disableMultishipping->execute($quote); + } +} diff --git a/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php index fff2346d76240..f4e6928173f60 100644 --- a/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php +++ b/app/code/Magento/Multishipping/Plugin/DisableMultishippingMode.php @@ -9,6 +9,7 @@ use Magento\Checkout\Model\Cart; use Magento\Framework\App\Action\Action; +use Magento\Multishipping\Model\DisableMultishipping; /** * Turns Off Multishipping mode for Quote. @@ -20,13 +21,21 @@ class DisableMultishippingMode */ private $cart; + /** + * @var DisableMultishipping + */ + private $disableMultishipping; + /** * @param Cart $cart + * @param DisableMultishipping $disableMultishipping */ public function __construct( - Cart $cart + Cart $cart, + DisableMultishipping $disableMultishipping ) { $this->cart = $cart; + $this->disableMultishipping = $disableMultishipping; } /** @@ -36,16 +45,16 @@ public function __construct( * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeExecute(Action $subject) + public function beforeExecute(Action $subject): void { $quote = $this->cart->getQuote(); - if ($quote->getIsMultiShipping()) { - $quote->setIsMultiShipping(0); - $extensionAttributes = $quote->getExtensionAttributes(); - if ($extensionAttributes && $extensionAttributes->getShippingAssignments()) { - $extensionAttributes->setShippingAssignments([]); - } + $modChanged = $this->disableMultishipping->execute($quote); + if ($modChanged) { + $totalsCollectedBefore = $quote->getTotalsCollectedFlag(); $this->cart->saveQuote(); + if (!$totalsCollectedBefore) { + $quote->setTotalsCollectedFlag(false); + } } } } diff --git a/app/code/Magento/Multishipping/Test/Unit/Model/DisableMultishippingTest.php b/app/code/Magento/Multishipping/Test/Unit/Model/DisableMultishippingTest.php new file mode 100644 index 0000000000000..9882f8d1441aa --- /dev/null +++ b/app/code/Magento/Multishipping/Test/Unit/Model/DisableMultishippingTest.php @@ -0,0 +1,116 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Test\Unit\Model; + +use Magento\Multishipping\Model\DisableMultishipping; +use Magento\Quote\Api\Data\CartExtensionInterface; +use Magento\Quote\Api\Data\CartInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * 'Disable Multishipping' model unit tests. + */ +class DisableMultishippingTest extends TestCase +{ + /** + * @var CartInterface|MockObject + */ + private $quoteMock; + + /** + * @var DisableMultishipping + */ + private $disableMultishippingModel; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->quoteMock = $this->getMockBuilder(CartInterface::class) + ->addMethods(['getIsMultiShipping', 'setIsMultiShipping']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->disableMultishippingModel = new DisableMultishipping(); + } + + /** + * Test 'execute' method if 'MultiShipping' mode is enabled. + * + * @param bool $hasShippingAssignments + * @return void + * @dataProvider executeWithMultishippingModeEnabledDataProvider + */ + public function testExecuteWithMultishippingModeEnabled(bool $hasShippingAssignments): void + { + $shippingAssignments = $hasShippingAssignments ? ['example_shipping_assigment'] : null; + + $this->quoteMock->expects($this->once()) + ->method('getIsMultiShipping') + ->willReturn(true); + + $this->quoteMock->expects($this->once()) + ->method('setIsMultiShipping') + ->with(0); + + /** @var CartExtensionInterface|MockObject $extensionAttributesMock */ + $extensionAttributesMock = $this->getMockBuilder(CartExtensionInterface::class) + ->addMethods(['getShippingAssignments', 'setShippingAssignments']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $extensionAttributesMock->expects($this->once()) + ->method('getShippingAssignments') + ->willReturn($shippingAssignments); + + $extensionAttributesMock->expects($hasShippingAssignments ? $this->once() : $this->never()) + ->method('setShippingAssignments') + ->with([]) + ->willReturnSelf(); + + $this->quoteMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributesMock); + + $this->assertTrue($this->disableMultishippingModel->execute($this->quoteMock)); + } + + /** + * DataProvider for testExecuteWithMultishippingModeEnabled(). + * + * @return array + */ + public function executeWithMultishippingModeEnabledDataProvider(): array + { + return [ + 'check_with_shipping_assignments' => [true], + 'check_without_shipping_assignments' => [false] + ]; + } + + /** + * Test 'execute' method if 'Multishipping' mode is disabled. + * + * @return void + */ + public function testExecuteWithMultishippingModeDisabled(): void + { + $this->quoteMock->expects($this->once()) + ->method('getIsMultiShipping') + ->willReturn(false); + + $this->quoteMock->expects($this->never()) + ->method('setIsMultiShipping'); + + $this->quoteMock->expects($this->never()) + ->method('getExtensionAttributes'); + + $this->assertFalse($this->disableMultishippingModel->execute($this->quoteMock)); + } +} diff --git a/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php index fb16bd251706c..64cbcbf147d48 100644 --- a/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php +++ b/app/code/Magento/Multishipping/Test/Unit/Plugin/DisableMultishippingModeTest.php @@ -10,10 +10,9 @@ use Magento\Checkout\Controller\Index\Index; use Magento\Checkout\Model\Cart; +use Magento\Multishipping\Model\DisableMultishipping as DisableMultishippingModel; use Magento\Multishipping\Plugin\DisableMultishippingMode; -use Magento\Quote\Api\Data\CartExtensionInterface; -use Magento\Quote\Api\Data\ShippingAssignmentInterface; -use Magento\Quote\Model\Quote; +use Magento\Quote\Api\Data\CartInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -23,15 +22,20 @@ class DisableMultishippingModeTest extends TestCase { /** - * @var MockObject + * @var Cart|MockObject */ private $cartMock; /** - * @var MockObject + * @var CartInterface|MockObject */ private $quoteMock; + /** + * @var DisableMultishippingModel|MockObject + */ + private $disableMultishippingMock; + /** * @var DisableMultishippingMode */ @@ -43,43 +47,40 @@ class DisableMultishippingModeTest extends TestCase protected function setUp(): void { $this->cartMock = $this->createMock(Cart::class); - $this->quoteMock = $this->getMockBuilder(Quote::class) - ->addMethods(['setIsMultiShipping', 'getIsMultiShipping']) - ->onlyMethods(['__wakeUp', 'getExtensionAttributes']) + $this->quoteMock = $this->getMockBuilder(CartInterface::class) + ->addMethods(['setTotalsCollectedFlag', 'getTotalsCollectedFlag']) ->disableOriginalConstructor() - ->getMock(); + ->getMockForAbstractClass(); $this->cartMock->expects($this->once()) ->method('getQuote') ->willReturn($this->quoteMock); - $this->object = new DisableMultishippingMode($this->cartMock); + $this->disableMultishippingMock = $this->createMock(DisableMultishippingModel::class); + $this->object = new DisableMultishippingMode( + $this->cartMock, + $this->disableMultishippingMock + ); } /** - * Tests turn off multishipping on multishipping quote. + * Test 'Disable Multishipping' plugin if 'Multishipping' mode is changed. * + * @param bool $totalsCollectedBefore * @return void + * @dataProvider pluginWithChangedMultishippingModeDataProvider */ - public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void + public function testPluginWithChangedMultishippingMode(bool $totalsCollectedBefore): void { $subject = $this->createMock(Index::class); - $extensionAttributes = $this->getMockBuilder(CartExtensionInterface::class) - ->disableOriginalConstructor() - ->setMethods(['setShippingAssignments', 'getShippingAssignments']) - ->getMockForAbstractClass(); - $extensionAttributes->method('getShippingAssignments') - ->willReturn( - $this->getMockForAbstractClass(ShippingAssignmentInterface::class) - ); - $extensionAttributes->expects($this->once()) - ->method('setShippingAssignments') - ->with([]); - $this->quoteMock->method('getExtensionAttributes') - ->willReturn($extensionAttributes); + $this->disableMultishippingMock->expects($this->once()) + ->method('execute') + ->with($this->quoteMock) + ->willReturn(true); $this->quoteMock->expects($this->once()) - ->method('getIsMultiShipping')->willReturn(1); - $this->quoteMock->expects($this->once()) - ->method('setIsMultiShipping') - ->with(0); + ->method('getTotalsCollectedFlag') + ->willReturn($totalsCollectedBefore); + $this->quoteMock->expects($totalsCollectedBefore ? $this->never() : $this->once()) + ->method('setTotalsCollectedFlag') + ->with(false); $this->cartMock->expects($this->once()) ->method('saveQuote'); @@ -87,16 +88,37 @@ public function testExecuteTurnsOffMultishippingModeOnMultishippingQuote(): void } /** - * Tests turn off multishipping on non-multishipping quote. + * DataProvider for testPluginWithChangedMultishippingMode(). + * + * @return array + */ + public function pluginWithChangedMultishippingModeDataProvider(): array + { + return [ + 'check_when_totals_are_collected' => [true], + 'check_when_totals_are_not_collected' => [false] + ]; + } + + /** + * Test 'Disable Multishipping' plugin if 'Multishipping' mode is NOT changed. * * @return void */ - public function testExecuteTurnsOffMultishippingModeOnNotMultishippingQuote(): void + public function testPluginWithNotChangedMultishippingMode(): void { $subject = $this->createMock(Index::class); - $this->quoteMock->expects($this->once())->method('getIsMultiShipping')->willReturn(0); - $this->quoteMock->expects($this->never())->method('setIsMultiShipping'); - $this->cartMock->expects($this->never())->method('saveQuote'); + $this->disableMultishippingMock->expects($this->once()) + ->method('execute') + ->with($this->quoteMock) + ->willReturn(false); + $this->quoteMock->expects($this->never()) + ->method('getTotalsCollectedFlag'); + $this->quoteMock->expects($this->never()) + ->method('setTotalsCollectedFlag'); + $this->cartMock->expects($this->never()) + ->method('saveQuote'); + $this->object->beforeExecute($subject); } } diff --git a/app/code/Magento/Multishipping/etc/frontend/events.xml b/app/code/Magento/Multishipping/etc/frontend/events.xml new file mode 100644 index 0000000000000..219e358528ca2 --- /dev/null +++ b/app/code/Magento/Multishipping/etc/frontend/events.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="checkout_cart_save_before"> + <observer name="magento_multishipping_disabler" instance="Magento\Multishipping\Observer\DisableMultishippingObserver"/> + </event> +</config> diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Observer/DisableMultishippingObserverTest.php b/dev/tests/integration/testsuite/Magento/Multishipping/Observer/DisableMultishippingObserverTest.php new file mode 100644 index 0000000000000..3145f945b5009 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Observer/DisableMultishippingObserverTest.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Observer; + +use Magento\Checkout\Model\Cart; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * 'Disable Multishipping' observer integration tests. + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoAppArea frontend + */ +class DisableMultishippingObserverTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Cart + */ + private $cart; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->cart = $this->objectManager->get(Cart::class); + $this->prepareQuote(); + } + + /** + * Test that Quote totals are calculated correctly when Cart is saved with 'Multishipping' mode enabled. + * + * @return void + */ + public function testObserverWithEnabledMultishippingMode(): void + { + $quote = $this->cart->getQuote(); + $extensionAttributes = $quote->getExtensionAttributes(); + $this->assertEquals(1, (int)$quote->getItemsQty()); + $this->assertCount(1, $extensionAttributes->getShippingAssignments()); + + $quote->setIsMultiShipping(1); + $quoteItem = current($quote->getItems()); + $itemData = [$quoteItem->getId() => ['qty' => 2]]; + + $this->cart->updateItems($itemData)->save(); + + $this->assertEquals(2, (int)$quote->getItemsQty()); + $this->assertEquals(0, $quote->getIsMultiShipping()); + $this->assertCount(0, $extensionAttributes->getShippingAssignments()); + } + + /** + * Prepare Quote before test execution. + * + * @return void + */ + private function prepareQuote(): void + { + /** @var CartInterface $quote */ + $quote = $this->objectManager->get(GetQuoteByReservedOrderId::class) + ->execute('test_order_with_simple_product_without_address'); + $shippingAssignment = $this->objectManager->get(ShippingAssignmentInterface::class); + $quote->getExtensionAttributes()->setShippingAssignments([$shippingAssignment]); + /** @var CheckoutSession $checkoutSession */ + $checkoutSession = $this->objectManager->get(CheckoutSession::class); + $checkoutSession->clearQuote(); + $checkoutSession->setQuoteId($quote->getId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Multishipping/Plugin/DisableMultishippingModeTest.php b/dev/tests/integration/testsuite/Magento/Multishipping/Plugin/DisableMultishippingModeTest.php new file mode 100644 index 0000000000000..07e1fdd5a9d23 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Multishipping/Plugin/DisableMultishippingModeTest.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Multishipping\Plugin; + +use Magento\Checkout\Model\Cart; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Data\Form\FormKey; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * 'Disable Multishipping' plugin integration tests. + * + * @see DisableMultishippingMode + */ +class DisableMultishippingModeTest extends AbstractController +{ + /** + * @var Cart + */ + private $cart; + + /** + * @var FormKey + */ + private $formKey; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->cart = $this->_objectManager->get(Cart::class); + $this->formKey = $this->_objectManager->get(FormKey::class); + } + + /** + * Test that Quote totals are updated correctly when 'Multishipping' mode is enabled. + * + * @magentoDataFixture Magento/Catalog/_files/products.php + * @return void + */ + public function testPluginWithEnabledMultishippingMode(): void + { + $quote = $this->cart->getQuote(); + $postData = [ + 'qty' => '1', + 'product' => '1', + ]; + $this->getRequest()->setPostValue($postData) + ->setMethod(HttpRequest::METHOD_POST) + ->setParam('form_key', $this->formKey->getFormKey()); + + $this->dispatch('checkout/cart/add'); + $this->assertEquals(1, (int)$quote->getItemsQty()); + + $quote->setTotalsCollectedFlag(false) + ->setIsMultiShipping(true); + + $this->dispatch('checkout/cart/add'); + $this->assertEquals(2, (int)$quote->getItemsQty()); + $this->assertEquals(0, $quote->getIsMultiShipping()); + } +} From c15c7c74d80711be59fcd75baf6c0f7da7419507 Mon Sep 17 00:00:00 2001 From: Vasya Tsviklinskyi <tsviklinskyi@gmail.com> Date: Mon, 26 Oct 2020 13:13:15 +0200 Subject: [PATCH 0933/1013] MC-37954: PLP sort by name is case-sensitive with ElasticSearch --- .../Model/Indexer/ReindexAllTest.php | 39 ++++++ .../Elasticsearch/_files/case_sensitive.php | 126 ++++++++++++++++++ .../_files/case_sensitive_rollback.php | 35 +++++ 3 files changed, 200 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php create mode 100644 dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php index 9679b4f232ee2..6df4d8fbb2d92 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php @@ -130,6 +130,45 @@ public function testSort() $this->assertEquals($productSimpleId, $firstInSearchResults); } + /** + * Test sorting of products with lower and upper case names after full reindex + * + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest + * @magentoDataFixture Magento/Elasticsearch/_files/case_sensitive.php + */ + public function testSortCaseSensitive(): void + { + $productFirst = $this->productRepository->get('fulltext-1'); + $productSecond = $this->productRepository->get('fulltext-2'); + $productThird = $this->productRepository->get('fulltext-3'); + $productFourth = $this->productRepository->get('fulltext-4'); + $productFifth = $this->productRepository->get('fulltext-5'); + $correctSortedIds = [ + $productFirst->getId(), + $productFourth->getId(), + $productSecond->getId(), + $productFifth->getId(), + $productThird->getId(), + ]; + $this->reindexAll(); + $result = $this->sortByName(); + $firstInSearchResults = (int) $result[0]['_id']; + $secondInSearchResults = (int) $result[1]['_id']; + $thirdInSearchResults = (int) $result[2]['_id']; + $fourthInSearchResults = (int) $result[3]['_id']; + $fifthInSearchResults = (int) $result[4]['_id']; + $actualSortedIds = [ + $firstInSearchResults, + $secondInSearchResults, + $thirdInSearchResults, + $fourthInSearchResults, + $fifthInSearchResults + ]; + $this->assertCount(5, $result); + $this->assertEquals($correctSortedIds, $actualSortedIds); + } + /** * Test search of specific product after full reindex * diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php new file mode 100644 index 0000000000000..1b664f958dd46 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * 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\Visibility; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute.php'); + +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = Bootstrap::getObjectManager(); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->get('fulltext-1'); +} catch (NoSuchEntityException $e) { + /** @var $productFirst Product */ + $productFirst = $objectManager->create(Product::class); + $productFirst->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('A') + ->setSku('fulltext-1') + ->setPrice(10) + ->setMetaTitle('first meta title') + ->setMetaKeyword('first meta keyword') + ->setMetaDescription('first meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-2'); +} catch (NoSuchEntityException $e) { + /** @var $productSecond Product */ + $productSecond = $objectManager->create(Product::class); + $productSecond->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('B') + ->setSku('fulltext-2') + ->setPrice(20) + ->setMetaTitle('second meta title') + ->setMetaKeyword('second meta keyword') + ->setMetaDescription('second meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-3'); +} catch (NoSuchEntityException $e) { + /** @var $productThird Product */ + $productThird = $objectManager->create(Product::class); + $productThird->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('C') + ->setSku('fulltext-3') + ->setPrice(20) + ->setMetaTitle('third meta title') + ->setMetaKeyword('third meta keyword') + ->setMetaDescription('third meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-4'); +} catch (NoSuchEntityException $e) { + /** @var $productFourth Product */ + $productFourth = $objectManager->create(Product::class); + $productFourth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('a') + ->setSku('fulltext-4') + ->setPrice(20) + ->setMetaTitle('fourth meta title') + ->setMetaKeyword('fourth meta keyword') + ->setMetaDescription('fourth meta description') + ->setUrlKey('aa') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} + +try { + $productRepository->get('fulltext-5'); +} catch (NoSuchEntityException $e) { + /** @var $productFifth Product */ + $productFifth = $objectManager->create(Product::class); + $productFifth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('b') + ->setSku('fulltext-5') + ->setPrice(20) + ->setMetaTitle('fifth meta title') + ->setMetaKeyword('fifth meta keyword') + ->setMetaDescription('fifth meta description') + ->setUrlKey('bb') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php new file mode 100644 index 0000000000000..a97faa29a1588 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.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; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute_rollback.php'); + +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +$collection->addAttributeToSelect('id')->load(); +if ($collection->count() > 0) { + $collection->delete(); +} + +/** @var \Magento\Store\Model\Store $store */ +$store = $objectManager->create(\Magento\Store\Model\Store::class); +$storeCode = 'secondary'; +$store->load($storeCode); +if ($store->getId()) { + $store->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); From 50d421dc84511efa196e3e07267ff8cc0ae6cd6f Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Mon, 26 Oct 2020 13:21:55 +0200 Subject: [PATCH 0934/1013] Use namespaced event listener, to make it removable --- app/design/adminhtml/Magento/backend/web/js/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index e2b8d8cfc884d..ac49462803f77 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -347,7 +347,7 @@ define('globalSearch', [ self.field.addClass(self.options.fieldActiveClass); }); - $(document).keydown(function (event) { + $(document).on('keydown.activateGlobalSearchForm', function (event) { var inputs = [ 'input', 'select', From 3a19dcad46475e3aaf7e82c48f1e859b1e2cd4a4 Mon Sep 17 00:00:00 2001 From: Vova Yatsyuk <vova.yatsyuk@gmail.com> Date: Mon, 26 Oct 2020 13:24:28 +0200 Subject: [PATCH 0935/1013] Don't use whole jquery/ui when widget is needed only --- app/design/adminhtml/Magento/backend/web/js/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index ac49462803f77..069970deae681 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -313,7 +313,7 @@ define('globalNavigation', [ define('globalSearch', [ 'jquery', 'Magento_Ui/js/lib/key-codes', - 'jquery/ui' + 'jquery-ui-modules/widget' ], function ($, keyCodes) { 'use strict'; From 5f775d2d58f9296aeac3b27788e95a8475824865 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 26 Oct 2020 13:30:51 +0200 Subject: [PATCH 0936/1013] MC-38589: [MFTF] AdminEnhancedMediaGalleryVerifyAssetFilterTest failed because of bad design --- .../AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml index 90ae7a5f10368..18c52a884c6eb 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml @@ -32,7 +32,7 @@ <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImages"/> <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> @@ -41,7 +41,7 @@ <argument name="id" value="$category.id$"/> </actionGroup> <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFridFilters"/> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> <argument name="image" value="ImageUpload3"/> </actionGroup> @@ -56,7 +56,7 @@ <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategory"/> <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultViewAgain"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFridFiltersAgain"/> <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> <argument name="filterName" value="Asset"/> From 0b2e2db10b41030e82ce8be0cd78a736d266b31c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 26 Oct 2020 13:49:13 +0200 Subject: [PATCH 0937/1013] MC-38589: [MFTF] AdminEnhancedMediaGalleryVerifyAssetFilterTest failed because of bad design --- .../AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml index 18c52a884c6eb..a3208af0da238 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyFilterByAssetTest.xml @@ -32,7 +32,7 @@ <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImages"/> <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> - <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFridFilters"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> @@ -41,7 +41,7 @@ <argument name="id" value="$category.id$"/> </actionGroup> <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> - <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFridFilters"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> <argument name="image" value="ImageUpload3"/> </actionGroup> @@ -56,7 +56,7 @@ <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategory"/> <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> - <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFridFiltersAgain"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFiltersAgain"/> <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> <argument name="filterName" value="Asset"/> From 8d5f5c5d32a6b06039b5d265aa879eec4bb4c8b6 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 26 Oct 2020 15:27:55 +0200 Subject: [PATCH 0938/1013] MC-38620: Merge release branch into 2.4-develop fix tests after merge --- .../Address/CollectTotalsObserverTest.php | 5 ---- .../AddDownloadableProductToWishlistTest.php | 23 ------------------- 2 files changed, 28 deletions(-) diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index 8b72749d13b49..c817ec2a512c3 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -271,11 +271,6 @@ public function testDispatchWithDefaultCustomerGroupId() ->method('getCustomerGroupId') ->willReturn('customerGroupId'); $this->customerMock->expects($this->exactly(2))->method('getId')->willReturn('1'); - $this->groupManagementMock->expects($this->once()) - ->method('getDefaultGroup') - ->willReturn($this->groupInterfaceMock); - $this->groupInterfaceMock->expects($this->once()) - ->method('getId')->willReturn('defaultCustomerGroupId'); /** Assertions */ $this->quoteAddressMock->expects($this->once()) ->method('setPrevQuoteCustomerGroupId') diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php index aca3012768b06..0de45fb21b20b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -120,29 +120,6 @@ public function testAddDownloadableProductWithOptions(): void $this->assertEquals('Downloadable Product Sample', $wishlistItemSamples[0]['title']); } - /** - * @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 configuration is currently disabled.'); - $this->graphQlMutation($query, [], '', $this->getHeaderMap()); - } - /** * Function returns array of all product's links * From 4de0d0145ddbe855ff3e8c46b9acf4ce65005db5 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 26 Oct 2020 17:53:07 +0200 Subject: [PATCH 0939/1013] MC-38620: Merge release branch into 2.4-develop fix tests after merge --- .../Frontend/Quote/Address/CollectTotalsObserver.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php index 504a035049eb4..d938ad7d638f1 100644 --- a/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php +++ b/app/code/Magento/Quote/Observer/Frontend/Quote/Address/CollectTotalsObserver.php @@ -141,11 +141,9 @@ public function execute(Observer $observer) $address->setPrevQuoteCustomerGroupId($quote->getCustomerGroupId()); $quote->setCustomerGroupId($groupId); $this->customerSession->setCustomerGroupId($groupId); - if ($customer->getId() !== null) { - $customer->setGroupId($groupId); - $customer->setEmail($customer->getEmail() ?: $quote->getCustomerEmail()); - $quote->setCustomer($customer); - } + $customer->setGroupId($groupId); + $customer->setEmail($customer->getEmail() ?: $quote->getCustomerEmail()); + $quote->setCustomer($customer); } } } From 162b49ea1cde46618d856a9f7e8139113aeb65ba Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 26 Oct 2020 18:17:52 +0200 Subject: [PATCH 0940/1013] MC-38620: Merge release branch into 2.4-develop fix tests after merge --- .../testsuite/Magento/Sales/Helper/ReorderTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php index 5a21f551ff1a7..fa0e9e7bc4e1b 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Helper/ReorderTest.php @@ -68,13 +68,14 @@ public function testCanReorderForGuest(): void } /** - * @magentoDataFixture Magento/Sales/_files/customer_order_with_two_items.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Sales/_files/order.php * * @return void */ public function testCanReorderForLoggedCustomer(): void { - $order = $this->orderFactory->create()->loadByIncrementId('100000555'); + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); $this->customerSession->setCustomerId($order->getCustomerId()); $this->assertTrue($this->helper->canReorder($order->getId())); } From a15601591250d341c6b10647508f14c7977f2870 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 26 Oct 2020 22:03:03 +0200 Subject: [PATCH 0941/1013] MC-38620: Merge release branch into 2.4-develop fix tests after merge --- .../Frontend/Quote/Address/CollectTotalsObserverTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php index c817ec2a512c3..ae2a4734215ad 100644 --- a/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php +++ b/app/code/Magento/Quote/Test/Unit/Observer/Frontend/Quote/Address/CollectTotalsObserverTest.php @@ -270,7 +270,8 @@ public function testDispatchWithDefaultCustomerGroupId() $this->quoteMock->expects($this->exactly(2)) ->method('getCustomerGroupId') ->willReturn('customerGroupId'); - $this->customerMock->expects($this->exactly(2))->method('getId')->willReturn('1'); + $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); + /** Assertions */ $this->quoteAddressMock->expects($this->once()) ->method('setPrevQuoteCustomerGroupId') @@ -323,7 +324,6 @@ public function testDispatchWithCustomerCountryInEU() ->method('setPrevQuoteCustomerGroupId') ->with('customerGroupId'); - $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); $this->quoteMock->expects($this->once())->method('setCustomerGroupId')->with('customerGroupId'); $this->quoteMock->expects($this->once())->method('setCustomer')->with($this->customerMock); $this->customerDataFactoryMock->expects($this->any()) @@ -431,8 +431,6 @@ public function testDispatchWithEmptyShippingAddress() ->method('setPrevQuoteCustomerGroupId') ->with('customerGroupId'); - $this->customerMock->expects($this->once())->method('getId')->willReturn('1'); - $this->quoteMock->expects($this->once())->method('setCustomerGroupId')->with('customerGroupId'); $this->quoteMock->expects($this->once())->method('setCustomer')->with($this->customerMock); $this->customerDataFactoryMock->expects($this->any()) From 8f25d13db436bd2f6134dcd6bc8431b8f3e9dff9 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 26 Oct 2020 22:54:59 +0200 Subject: [PATCH 0942/1013] MC-38620: Merge release branch into 2.4-develop fix tests after merge --- .../GraphQl/Wishlist/UpdateProductsFromWishlistTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php index 0e5a9544b8e99..691c06782070f 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -99,14 +99,14 @@ public function testUpdateProductInWishlistWithZeroQty() { $wishlist = $this->getWishlist(); $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; $qty = 0; $description = 'Description for zero quantity'; $updateWishlistQuery = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); self::assertEquals(1, $response['updateProductsInWishlist']['wishlist']['items_count']); - self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items'], 'empty wish list items'); - self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items']); + self::assertNotEmpty($response['updateProductsInWishlist']['wishlist']['items_v2'], 'empty wish list items'); + self::assertCount(1, $response['updateProductsInWishlist']['wishlist']['items_v2']); self::assertArrayHasKey('user_errors', $response['updateProductsInWishlist']); self::assertCount(1, $response['updateProductsInWishlist']['user_errors']); $message = 'The quantity of a wish list item cannot be 0'; @@ -127,7 +127,7 @@ public function testUpdateProductWithValidQtyAndNoDescription() { $wishlist = $this->getWishlist(); $wishlistId = $wishlist['customer']['wishlist']['id']; - $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + $wishlistItem = $wishlist['customer']['wishlist']['items_v2'][0]; $qty = 2; $updateWishlistQuery = $this->getQueryWithNoDescription((int) $wishlistId, (int) $wishlistItem['id'], $qty); $response = $this->graphQlMutation($updateWishlistQuery, [], '', $this->getHeaderMap()); From 792875173830d061040e65cc41b66e9fb66826cc Mon Sep 17 00:00:00 2001 From: Dmytro Poperechnyy <dpoperechnyy@magento.com> Date: Mon, 26 Oct 2020 23:37:01 +0200 Subject: [PATCH 0943/1013] [AWS S3] MC-37601: Support by Magento ImportExport (#6231) --- app/code/Magento/AwsS3/Driver/AwsS3.php | 42 +++++-- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 19 ++- .../Model/Import/Product.php | 6 +- .../Model/Import/Uploader.php | 34 +++++- .../Test/Unit/Model/Import/ProductTest.php | 12 ++ .../Test/Unit/Model/Import/UploaderTest.php | 28 ++++- .../Helper/Uploader.php | 10 +- .../Adminhtml/Export/File/Download.php | 11 +- .../ImportExport/Model/Export/Consumer.php | 4 +- .../DataProvider/ExportFileDataProvider.php | 28 +++-- .../Magento/ImportExport/etc/adminhtml/di.xml | 2 + app/code/Magento/RemoteStorage/Filesystem.php | 15 +++ .../Magento/RemoteStorage/Plugin/Image.php | 85 ++++++++++--- .../Test/Unit/Plugin/ImageTest.php | 36 ++++-- app/code/Magento/RemoteStorage/composer.json | 5 +- app/code/Magento/RemoteStorage/etc/di.xml | 32 +++++ .../App/Filesystem/CreatePdfFileTest.php | 1 - .../Adminhtml/Export/File/DownloadTest.php | 2 +- .../App/Filesystem/DirectoryList.php | 8 +- .../Framework/Filesystem/Directory/Write.php | 6 +- lib/internal/Magento/Framework/Image.php | 9 +- .../Magento/Framework/Image/Adapter/Gd2.php | 112 +++++++++++------- .../Framework/Image/Adapter/ImageMagick.php | 7 ++ .../Test/Unit/Adapter/ImageMagickTest.php | 4 +- .../Unit/Adapter/_files/global_php_mock.php | 10 ++ 25 files changed, 405 insertions(+), 123 deletions(-) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index b7dee36488bb9..169a93580038c 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Phrase; +use Psr\Log\LoggerInterface; /** * Driver for AWS S3 IO operations. @@ -35,23 +36,35 @@ class AwsS3 implements DriverInterface */ private $streams = []; + /** + * @var LoggerInterface + */ + private $logger; + /** * @param AwsS3Adapter $adapter + * @param LoggerInterface $logger */ - public function __construct(AwsS3Adapter $adapter) - { + public function __construct( + AwsS3Adapter $adapter, + LoggerInterface $logger + ) { $this->adapter = $adapter; + $this->logger = $logger; } /** * Destroy opened streams. - * - * @throws FileSystemException */ public function __destruct() { - foreach ($this->streams as $stream) { - $this->fileClose($stream); + try { + foreach ($this->streams as $stream) { + $this->fileClose($stream); + } + } catch (\Exception $e) { + // log exception as throwing an exception from a destructor causes a fatal error + $this->logger->critical($e); } } @@ -521,12 +534,17 @@ public function fileRead($resource, $length): string */ public function fileGetCsv($resource, $length = 0, $delimiter = ',', $enclosure = '"', $escape = '\\') { - //phpcs:disable - $metadata = stream_get_meta_data($resource); - //phpcs:enable - $file = $this->adapter->read($metadata['uri'])['contents']; - - return str_getcsv($file, $delimiter, $enclosure, $escape); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = fgetcsv($resource, $length, $delimiter, $enclosure, $escape); + if ($result === null) { + throw new FileSystemException( + new Phrase( + 'The "%1" CSV handle is incorrect. Verify the handle and try again.', + [$this->getWarningMessage()] + ) + ); + } + return $result; } /** diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index e3e3e4208484d..173143b709519 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -13,6 +13,7 @@ use Magento\Framework\Exception\FileSystemException; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @see AwsS3 @@ -36,6 +37,11 @@ class AwsS3Test extends TestCase */ private $clientMock; + /** + * @var LoggerInterface + */ + private $logger; + /** * @inheritDoc */ @@ -43,6 +49,7 @@ protected function setUp(): void { $this->adapterMock = $this->createMock(AwsS3Adapter::class); $this->clientMock = $this->getMockForAbstractClass(S3ClientInterface::class); + $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); $this->adapterMock->method('applyPathPrefix') ->willReturnArgument(0); @@ -59,7 +66,7 @@ protected function setUp(): void return self::URL . $path; }); - $this->driver = new AwsS3($this->adapterMock); + $this->driver = new AwsS3($this->adapterMock, $this->logger); } /** @@ -149,6 +156,16 @@ public function getAbsolutePathDataProvider(): array '', self::URL . 'media/catalog/test.png', self::URL . 'media/catalog/test.png' + ], + [ + self::URL, + 'var/import/images', + self::URL . 'var/import/images' + ], + [ + self::URL . 'var/import/images/product_images/', + self::URL . 'var/import/images/product_images/1.png', + self::URL . 'var/import/images/product_images/1.png' ] ]; } diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index f59bc338ced69..428961aa6ddf6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -9,7 +9,6 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config as CatalogConfig; use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Model\ResourceModel\Product\Link; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor; use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; @@ -2209,6 +2208,11 @@ protected function _getUploader() $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + // make media folder a primary folder for media in external storages + if (!is_a($this->_mediaDirectory->getDriver(), File::class)) { + $dirAddon = DirectoryList::MEDIA; + } + $tmpPath = $this->getImportDir(); if (!$fileUploader->setTmpDir($tmpPath)) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 5b90ced62b0eb..d2a0019349ef2 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverPool; /** @@ -116,6 +117,11 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ private $maxFilenameLength = 255; + /** + * @var TargetDirectory + */ + private $targetDirectory; + /** * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb * @param \Magento\MediaStorage\Helper\File\Storage $coreFileStorage @@ -125,6 +131,7 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader * @param Filesystem\File\ReadFactory $readFactory * @param string|null $filePath * @param \Magento\Framework\Math\Random|null $random + * @param TargetDirectory|null $targetDirectory * @throws \Magento\Framework\Exception\FileSystemException * @throws \Magento\Framework\Exception\LocalizedException */ @@ -136,7 +143,8 @@ public function __construct( Filesystem $filesystem, Filesystem\File\ReadFactory $readFactory, $filePath = null, - \Magento\Framework\Math\Random $random = null + \Magento\Framework\Math\Random $random = null, + TargetDirectory $targetDirectory = null ) { $this->_imageFactory = $imageFactory; $this->_coreFileStorageDb = $coreFileStorageDb; @@ -149,6 +157,7 @@ public function __construct( $this->_setUploadFile($filePath); } $this->random = $random ?: ObjectManager::getInstance()->get(\Magento\Framework\Math\Random::class); + $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); } /** @@ -188,7 +197,8 @@ public function move($fileName, $renameFileOff = false) } $this->_setUploadFile($tmpFilePath); - $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); + $rootDirectory = $this->getTargetDirectory()->getDirectoryRead(DirectoryList::ROOT); + $destDir = $rootDirectory->getAbsolutePath($this->getDestDir()); $result = $this->save($destDir); unset($result['path']); $result['name'] = self::getCorrectFileName($result['name']); @@ -243,6 +253,20 @@ private function downloadFileFromUrl($url, $driver) return $tmpFilePath; } + /** + * Retrieves target directory. + * + * @return TargetDirectory + */ + private function getTargetDirectory(): TargetDirectory + { + if (!isset($this->targetDirectory)) { + $this->targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); + } + + return $this->targetDirectory; + } + /** * Prepare information about the file for moving * @@ -381,7 +405,8 @@ public function getDestDir() */ public function setDestDir($path) { - if (is_string($path) && $this->_directory->isWritable($path)) { + $directoryRoot = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + if (is_string($path) && $directoryRoot->isWritable($path)) { $this->_destDir = $path; return true; } @@ -404,7 +429,8 @@ protected function _moveFile($tmpPath, $destPath) $destinationRealPath = $this->_directory->getDriver()->getRealPath($destPath); $relativeDestPath = $this->_directory->getRelativePath($destPath); $isSameFile = $tmpRealPath === $destinationRealPath; - return $isSameFile ?: $this->_directory->copyFile($tmpPath, $relativeDestPath); + $rootDirectory = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + return $isSameFile ?: $this->_directory->copyFile($tmpPath, $relativeDestPath, $rootDirectory); } else { return false; } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index 52769859a74ac..2eb8c86a34686 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -39,6 +39,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File as DriverFile; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Json\Helper\Data; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; @@ -207,6 +208,9 @@ class ProductTest extends AbstractImportTestCase /** @var ImageTypeProcessor|MockObject */ protected $imageTypeProcessor; + /** @var DriverFile|MockObject */ + private $driverFile; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -374,6 +378,10 @@ protected function setUp(): void $this->errorAggregator = $this->getErrorAggregatorObject(); + $this->driverFile = $this->getMockBuilder(DriverFile::class) + ->disableOriginalConstructor() + ->getMock(); + $this->data = []; $this->imageTypeProcessor = $this->getMockBuilder(ImageTypeProcessor::class) @@ -1336,6 +1344,10 @@ public function testFillUploaderObject($isRead, $isWrite, $message) ->with('pub/media/catalog/product') ->willReturn($isWrite); + $this->_mediaDirectory + ->method('getDriver') + ->willReturn($this->driverFile); + $this->_mediaDirectory ->method('getRelativePath') ->willReturnMap( diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index 2d482938949bc..bc8fba5e2b919 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -9,6 +9,7 @@ use Magento\CatalogImportExport\Model\Import\Uploader; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Driver\Http; use Magento\Framework\Filesystem\Driver\Https; @@ -73,6 +74,11 @@ class UploaderTest extends TestCase */ protected $uploader; + /** + * @var TargetDirectory|MockObject + */ + private $targetDirectory; + protected function setUp(): void { $this->coreFileStorageDb = $this->getMockBuilder(Database::class) @@ -115,6 +121,13 @@ protected function setUp(): void ->setMethods(['getRandomString']) ->getMock(); + $this->targetDirectory = $this->getMockBuilder(TargetDirectory::class) + ->disableOriginalConstructor() + ->setMethods(['getDirectoryWrite', 'getDirectoryRead']) + ->getMock(); + $this->targetDirectory->method('getDirectoryWrite')->willReturn($this->directoryMock); + $this->targetDirectory->method('getDirectoryRead')->willReturn($this->directoryMock); + $this->uploader = $this->getMockBuilder(Uploader::class) ->setConstructorArgs( [ @@ -125,7 +138,8 @@ protected function setUp(): void $this->filesystem, $this->readFactory, null, - $this->random + $this->random, + $this->targetDirectory ] ) ->setMethods(['_setUploadFile', 'save', 'getTmpDir', 'checkAllowedExtension']) @@ -274,9 +288,9 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive ->addMethods(['readAll']) ->onlyMethods(['isExists']) ->getMock(); - $driverMock->expects($this->any())->method('isExists')->willReturn(true); - $driverMock->expects($this->any())->method('readAll')->willReturn(null); - $driverPool->expects($this->any())->method('getDriver')->willReturn($driverMock); + $driverMock->method('isExists')->willReturn(true); + $driverMock->method('readAll')->willReturn(null); + $driverPool->method('getDriver')->willReturn($driverMock); $readFactory = $this->getMockBuilder(ReadFactory::class) ->setConstructorArgs( @@ -287,10 +301,11 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive ->setMethods(['create']) ->getMock(); - $readFactory->expects($this->any())->method('create') + $readFactory->method('create') ->with($expectedHost, $expectedScheme) ->willReturn($driverMock); + /** @var Uploader $uploaderMock */ $uploaderMock = $this->getMockBuilder(Uploader::class) ->setConstructorArgs( [ @@ -300,6 +315,9 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive $this->validator, $this->filesystem, $readFactory, + null, + $this->random, + $this->targetDirectory ] ) ->getMock(); diff --git a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php index e6ead5d5cc021..3450376365cd0 100644 --- a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php +++ b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php @@ -6,6 +6,7 @@ namespace Magento\DownloadableImportExport\Helper; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Driver\File; /** * Uploader helper for downloadable products @@ -82,6 +83,11 @@ public function getUploader($type, $parameters) $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + // make media folder a primary folder for media in external storages + if (!is_a($this->mediaDirectory->getDriver(), File::class)) { + $dirAddon = DirectoryList::MEDIA; + } + if (!empty($parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR])) { $tmpPath = $parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR]; } else { @@ -113,7 +119,9 @@ public function getUploader($type, $parameters) */ public function isFileExist(string $fileName): bool { - return $this->mediaDirectory->isExist($this->fileUploader->getDestDir().$fileName); + $fileName = '/' . ltrim($fileName, '/'); + + return $this->mediaDirectory->isExist($this->fileUploader->getDestDir() . $fileName); } /** diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php index 4107e19860328..26ee257c42ff2 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php @@ -67,13 +67,12 @@ public function execute() return $resultRedirect; } try { - $path = 'export/' . $fileName; - $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); - if ($directory->isFile($path)) { + $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_EXPORT); + if ($directory->isFile($fileName)) { return $this->fileFactory->create( - $path, - $directory->readFile($path), - DirectoryList::VAR_DIR + $fileName, + $directory->readFile($fileName), + DirectoryList::VAR_EXPORT ); } $this->messageManager->addErrorMessage(__('%1 is not a valid file', $fileName)); diff --git a/app/code/Magento/ImportExport/Model/Export/Consumer.php b/app/code/Magento/ImportExport/Model/Export/Consumer.php index 27019780269c4..955f96fe3de2e 100644 --- a/app/code/Magento/ImportExport/Model/Export/Consumer.php +++ b/app/code/Magento/ImportExport/Model/Export/Consumer.php @@ -70,8 +70,8 @@ public function process(ExportInfoInterface $exportInfo) try { $data = $this->exportManager->export($exportInfo); $fileName = $exportInfo->getFileName(); - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); - $directory->writeFile('export/' . $fileName, $data); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); + $directory->writeFile($fileName, $data); $this->notifier->addMajor( __('Your export file is ready'), diff --git a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php index 2b5af6ab5ca8d..71614bafd138e 100644 --- a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php +++ b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php @@ -13,6 +13,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Filesystem\Directory\WriteInterface; /** * Data provider for export grid. @@ -29,6 +30,11 @@ class ExportFileDataProvider extends DataProvider */ private $file; + /** + * @var WriteInterface + */ + private $directory; + /** * @var Filesystem */ @@ -48,6 +54,7 @@ class ExportFileDataProvider extends DataProvider * @param array $meta * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( string $name, @@ -78,6 +85,7 @@ public function __construct( ); $this->fileIO = $fileIO ?: ObjectManager::getInstance()->get(File::class); + $this->directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); } /** @@ -88,13 +96,12 @@ public function __construct( */ public function getData() { - $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); $emptyResponse = ['items' => [], 'totalRecords' => 0]; - if (!$this->file->isExists($directory->getAbsolutePath() . 'export/')) { + if (!$this->directory->isExist($this->directory->getAbsolutePath())) { return $emptyResponse; } - $files = $this->getExportFiles($directory->getAbsolutePath() . 'export/'); + $files = $this->getExportFiles($this->directory->getAbsolutePath()); if (empty($files)) { return $emptyResponse; } @@ -121,12 +128,15 @@ public function getData() */ private function getPathToExportFile($file): string { - $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); + $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_EXPORT); $delimiter = '/'; $cutPath = explode( $delimiter, - $directory->getAbsolutePath() . 'export' + $directory->getAbsolutePath() ); + // remove . from dirname if file path is not absolute in the file system but just a file name + $file['dirname'] = $file['dirname'] !== '.' ? $file['dirname'] : ''; + $filePath = explode( $delimiter, $file['dirname'] @@ -148,14 +158,14 @@ private function getPathToExportFile($file): string private function getExportFiles(string $directoryPath): array { $sortedFiles = []; - $files = $this->file->readDirectoryRecursively($directoryPath); + $files = $this->directory->getDriver()->readDirectoryRecursively($directoryPath); if (empty($files)) { return []; } foreach ($files as $filePath) { - if ($this->file->isFile($filePath)) { - //phpcs:ignore Magento2.Functions.DiscouragedFunction - $sortedFiles[filemtime($filePath)] = $filePath; + if ($this->directory->isFile($filePath)) { + $fileModificationTime = $this->directory->stat($filePath)['mtime']; + $sortedFiles[$fileModificationTime] = $filePath; } } //sort array elements using key value diff --git a/app/code/Magento/ImportExport/etc/adminhtml/di.xml b/app/code/Magento/ImportExport/etc/adminhtml/di.xml index 04ee726349123..7b124957d5f57 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -16,11 +16,13 @@ <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> </arguments> </type> + <!-- deprecated as file argument is not used anymore. Can be deleted in major release to avoid BIC.--> <type name="Magento\ImportExport\Controller\Adminhtml\Export\File\Delete"> <arguments> <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> </arguments> </type> + <!-- deprecated as file argument is not used anymore. Can be deleted in major release to avoid BIC.--> <type name="Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider"> <arguments> <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php index 1594e53392b76..f2d5237ea243b 100644 --- a/app/code/Magento/RemoteStorage/Filesystem.php +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -105,4 +105,19 @@ public function getDirectoryWrite($directoryCode, $driverCode = DriverPool::REMO return parent::getDirectoryWrite($directoryCode); } + + /** + * @inheritDoc + */ + public function getDirectoryReadByPath($path, $driverCode = DriverPool::REMOTE) + { + if ($driverCode === DriverPool::REMOTE && $this->isEnabled) { + return $this->readFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $path), + $driverCode + ); + } + + return parent::getDirectoryReadByPath($path); + } } diff --git a/app/code/Magento/RemoteStorage/Plugin/Image.php b/app/code/Magento/RemoteStorage/Plugin/Image.php index 8f554a3d8f8c3..66c5fe1a5ac67 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Image.php +++ b/app/code/Magento/RemoteStorage/Plugin/Image.php @@ -30,7 +30,7 @@ class Image /** * @var Filesystem\Directory\WriteInterface */ - private $targetDirectoryWrite; + private $remoteDirectoryWrite; /** * @var array @@ -69,7 +69,7 @@ public function __construct( LoggerInterface $logger ) { $this->tmpDirectoryWrite = $filesystem->getDirectoryWrite(DirectoryList::TMP); - $this->targetDirectoryWrite = $targetDirectory->getDirectoryWrite(DirectoryList::ROOT); + $this->remoteDirectoryWrite = $targetDirectory->getDirectoryWrite(DirectoryList::ROOT); $this->isEnabled = $config->isEnabled(); $this->ioFile = $ioFile; $this->logger = $logger; @@ -93,6 +93,42 @@ public function beforeOpen(AbstractAdapter $subject, $filename): array return [$filename]; } + /** + * Copy import file locally to validate + * + * @param AbstractAdapter $subject + * @param string $filePath + * @return string[] + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeValidateUploadFile(AbstractAdapter $subject, $filePath): array + { + if ($this->isEnabled) { + $filePath = $this->copyFileToTmp($filePath); + } + return [$filePath]; + } + + /** + * Copy watermark locally before adding it an image + * + * @param AbstractAdapter $subject + * @param string $filePath + * @return string[] + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeWatermark(AbstractAdapter $subject, $filePath): array + { + if ($this->isEnabled) { + $filePath = $this->copyFileToTmp($filePath); + } + return [$filePath]; + } + /** * Get filesystem tmp path for file and provide it to save() function * @@ -110,16 +146,15 @@ public function aroundSave( $newName = null ): void { if ($this->isEnabled) { - $relativePath = $this->targetDirectoryWrite->getRelativePath($destination); + $relativePath = $this->remoteDirectoryWrite->getRelativePath($destination); $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath($relativePath); $proceed($tmpPath, $newName); - $destination = $this->prepareDestination($subject, $destination, $newName); $this->tmpDirectoryWrite->getDriver()->rename( - $tmpPath, - $destination, - $this->targetDirectoryWrite->getDriver() + $this->prepareDestination($subject, $tmpPath, $newName), + $this->prepareDestination($subject, $destination, $newName), + $this->remoteDirectoryWrite->getDriver() ); } else { $proceed($destination, $newName); @@ -149,13 +184,14 @@ public function __destruct() */ private function copyFileToTmp($filePath): string { - $absolutePath = $this->targetDirectoryWrite->getAbsolutePath($filePath); - if ($this->targetDirectoryWrite->isFile($absolutePath)) { + if ($this->fileExistsInTmp($filePath)) { + return $filePath; + } + $absolutePath = $this->remoteDirectoryWrite->getAbsolutePath($filePath); + if ($this->remoteDirectoryWrite->isFile($absolutePath)) { $this->tmpDirectoryWrite->create(); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath() . basename($filePath); - $this->storeTmpName($tmpPath); - $content = $this->targetDirectoryWrite->getDriver()->fileGetContents($filePath); + $tmpPath = $this->storeTmpName($filePath); + $content = $this->remoteDirectoryWrite->getDriver()->fileGetContents($filePath); $filePath = $this->tmpDirectoryWrite->getDriver()->filePutContents($tmpPath, $content) ? $tmpPath : $filePath; @@ -166,11 +202,28 @@ private function copyFileToTmp($filePath): string /** * Store created tmp image path * - * @param string $path + * @param string $filePath + * @return string + */ + private function storeTmpName(string $filePath): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath() . basename($filePath); + + $this->tmpFiles[$filePath] = $tmpPath; + + return $tmpPath; + } + + /** + * Check is file exist in tmp folder + * + * @param string $filePath + * @return bool */ - private function storeTmpName(string $path): void + private function fileExistsInTmp(string $filePath): bool { - $this->tmpFiles[] = $path; + return in_array($filePath, $this->tmpFiles, true); } /** diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php index 4055422a8aa4e..13d170946e343 100644 --- a/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php +++ b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\RemoteStorage\Test\Unit\Plugin; use Magento\Framework\App\Filesystem\DirectoryList; @@ -41,8 +42,8 @@ class ImageTest extends TestCase private $targetDirectoryWrite; /** - * @throws \Magento\Framework\Exception\FileSystemException * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ protected function setUp(): void { @@ -80,11 +81,16 @@ protected function setUp(): void * @param string $destination * @param string $newDestination * @param string|null $newName + * @param string|null $oldName * @return void * @throws \Magento\Framework\Exception\FileSystemException */ - public function testAroundSaveWithNewName(string $destination, string $newDestination, ?string $newName): void - { + public function testAroundSaveWithNewName( + string $destination, + string $newDestination, + ?string $newName, + ?string $oldName + ): void { $tmpDestination = '/tmp/' . $destination; /** @var AbstractAdapter $subject */ $subject = $this->getMockBuilder(AbstractAdapter::class) @@ -96,7 +102,7 @@ public function testAroundSaveWithNewName(string $destination, string $newDestin ->disableOriginalConstructor() ->getMock(); $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getRelativePath') - ->willReturn($destination); + ->willReturn($destination . $oldName); $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') ->willReturn($targetDriver); $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') @@ -104,12 +110,18 @@ public function testAroundSaveWithNewName(string $destination, string $newDestin $driver = $this->getMockBuilder(DriverInterface::class) ->disableOriginalConstructor() ->getMock(); + $actualName = $newName ?? $oldName; $driver->expects(self::atLeastOnce())->method('rename') - ->with($tmpDestination, $newDestination, $driver); + ->with($tmpDestination . $actualName, $newDestination, $driver); $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver')->willReturn($driver); - $this->ioFile->expects(self::any())->method('getPathInfo')->with($destination) - ->willReturn(['dirname' => 'destination/', 'basename' => 'old_name.file']); - $this->plugin->aroundSave($subject, $proceed, $destination, $newName); + $this->ioFile->method('getPathInfo') + ->willReturnMap( + [ + [$tmpDestination, ['dirname' => $tmpDestination, 'basename' => 'old_name.file']], + [$destination . $oldName, ['dirname' => $destination, 'basename' => 'old_name.file']] + ] + ); + $this->plugin->aroundSave($subject, $proceed, $destination . $oldName, $newName); } /** @@ -121,12 +133,14 @@ public function aroundSaveDataProvider(): array 'with_new_name' => [ 'destination' => 'destination/', 'new_destination' => 'destination/new_name.file', - 'new_name' => 'new_name.file' + 'new_name' => 'new_name.file', + 'old_name' => null ], 'with_old_name' => [ - 'destination' => 'destination/old_name.file', + 'destination' => 'destination/', 'new_destination' => 'destination/old_name.file', - 'new_name' => null + 'new_name' => null, + 'old_name' => 'old_name.file' ] ]; } diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index c55923f6e2109..fa27e821c817c 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -10,7 +10,10 @@ "magento/module-sitemap": "*", "magento/module-cms": "*", "magento/module-downloadable": "*", - "magento/module-media-storage": "*" + "magento/module-media-storage": "*", + "magento/module-import-export": "*", + "magento/module-catalog-import-export": "*", + "magento/module-downloadable-import-export": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index fe16d1d4afca5..bb253bb5d18f7 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -26,6 +26,8 @@ <arguments> <argument name="directoryCodes" xsi:type="array"> <item name="media" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::MEDIA</item> + <item name="var_export" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::VAR_EXPORT</item> + <item name="var_import" xsi:type="string">Magento\Framework\App\Filesystem\DirectoryList::VAR_IMPORT</item> </argument> </arguments> </virtualType> @@ -95,4 +97,34 @@ <type name="Magento\Framework\Image\Adapter\AbstractAdapter"> <plugin name="remoteImageFile" type="Magento\RemoteStorage\Plugin\Image" sortOrder="10"/> </type> + <type name="Magento\ImportExport\Model\Import"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Model\Import\ImageDirectoryBaseProvider"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Helper\Report"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\CatalogImportExport\Model\Import\Product"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\CatalogImportExport\Model\Import\Uploader"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\DownloadableImportExport\Helper\Uploader"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> </config> diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php index d7b492bf5153c..7bd4b3a99d1bf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php @@ -8,7 +8,6 @@ namespace Magento\Framework\App\Filesystem; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Response\Http\FileFactory; use Magento\Framework\Filesystem; use Magento\TestFramework\Helper\Bootstrap; diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php index 277e6af871650..2128516189474 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php @@ -105,7 +105,7 @@ public function testExecute($file): void 'Incorrect response header "content-type"' ); $this->assertEquals( - 'attachment; filename="export/' . $this->fileName . '"', + 'attachment; filename="' . $this->fileName . '"', $contentDisposition->getFieldValue(), 'Incorrect response header "content-disposition"' ); diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php index 295ac50cf5687..fdf524348293b 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php @@ -62,6 +62,11 @@ class DirectoryList extends \Magento\Framework\Filesystem\DirectoryList */ const VAR_EXPORT = 'var_export'; + /** + * Storage of files which were imported. + */ + const VAR_IMPORT = 'var_import'; + /** * Temporary files */ @@ -151,7 +156,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::VAR_EXPORT => [parent::PATH => 'var/export', parent::URL_PATH => 'export'], self::CACHE => [parent::PATH => 'var/cache'], self::LOG => [parent::PATH => 'var/log'], self::DI => [parent::PATH => 'generated/metadata'], @@ -170,6 +175,7 @@ public static function getDefaultConfig() self::GENERATED => [parent::PATH => 'generated'], self::GENERATED_CODE => [parent::PATH => Io::DEFAULT_DIRECTORY], self::GENERATED_METADATA => [parent::PATH => 'generated/metadata'], + self::VAR_IMPORT => [parent::PATH => 'var/import', parent::URL_PATH => 'var/import'], ]; return parent::getDefaultConfig() + $result; } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 0ff25f868d7af..4d4bb3b6c7f5e 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -349,7 +349,11 @@ public function openFile($path, $mode = 'w') */ public function writeFile($path, $content, $mode = 'w+') { - return $this->openFile($path, $mode)->write($content); + $file = $this->openFile($path, $mode); + $result = $file->write($content); + $file->close(); + + return $result; } /** diff --git a/lib/internal/Magento/Framework/Image.php b/lib/internal/Magento/Framework/Image.php index a14f94b8f2733..5b49e9f303ca0 100644 --- a/lib/internal/Magento/Framework/Image.php +++ b/lib/internal/Magento/Framework/Image.php @@ -90,7 +90,7 @@ public function rotate($angle) /** * Crop an image. * - * @param int $top Default value is 0 + * @param int $top Default value is 0 * @param int $left Default value is 0 * @param int $right Default value is 0 * @param int $bottom Default value is 0 @@ -200,9 +200,6 @@ public function watermark( $watermarkImageOpacity = 30, $repeat = false ) { - if (!file_exists($watermarkImage)) { - throw new \Exception("Required file '{$watermarkImage}' does not exists."); - } $this->_adapter->watermark($watermarkImage, $positionX, $positionY, $watermarkImageOpacity, $repeat); } @@ -234,7 +231,7 @@ public function getImageType() * @access public * @return void */ - public function process() + public function process() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } @@ -244,7 +241,7 @@ public function process() * @access public * @return void */ - public function instruction() + public function instruction() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index c37cb89c30587..bebf64c56596a 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -6,6 +6,9 @@ namespace Magento\Framework\Image\Adapter; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Phrase; + /** * Gd2 adapter. * @@ -56,10 +59,15 @@ protected function _reset() * * @param string $filename * @return void - * @throws \OverflowException + * @throws \OverflowException|FileSystemException */ public function open($filename) { + if (!file_exists($filename)) { + throw new FileSystemException( + new Phrase('File "%1" does not exist.', [$this->_fileName]) + ); + } if (!$filename || filesize($filename) === 0 || !$this->validateURLScheme($filename)) { throw new \InvalidArgumentException('Wrong file'); } @@ -436,8 +444,6 @@ public function rotate($angle) * @param int $opacity * @param bool $tile * @return void - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -452,53 +458,42 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $merged = false; + $watermark = $this->createWatermarkBasedOnPosition($watermark, $positionX, $positionY, $merged, $tile); + + imagedestroy($watermark); + $this->refreshImageDimensions(); + } + + /** + * Create watermark based on it's image position. + * + * @param resource $watermark + * @param int $positionX + * @param int $positionY + * @param bool $merged + * @param bool $tile + * @return false|resource + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function createWatermarkBasedOnPosition( + $watermark, + int $positionX, + int $positionY, + bool $merged, + bool $tile + ) { if ($this->getWatermarkWidth() && $this->getWatermarkHeight() && $this->getWatermarkPosition() != self::POSITION_STRETCH ) { - $newWatermark = imagecreatetruecolor($this->getWatermarkWidth(), $this->getWatermarkHeight()); - imagealphablending($newWatermark, false); - $col = imagecolorallocate($newWatermark, 255, 255, 255); - imagecolortransparent($newWatermark, $col); - imagefilledrectangle($newWatermark, 0, 0, $this->getWatermarkWidth(), $this->getWatermarkHeight(), $col); - imagesavealpha($newWatermark, true); - imagecopyresampled( - $newWatermark, - $watermark, - 0, - 0, - 0, - 0, - $this->getWatermarkWidth(), - $this->getWatermarkHeight(), - imagesx($watermark), - imagesy($watermark) - ); - $watermark = $newWatermark; + $watermark = $this->createWaterMark($watermark, $this->getWatermarkWidth(), $this->getWatermarkHeight()); } if ($this->getWatermarkPosition() == self::POSITION_TILE) { $tile = true; } elseif ($this->getWatermarkPosition() == self::POSITION_STRETCH) { - $newWatermark = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight); - imagealphablending($newWatermark, false); - $col = imagecolorallocate($newWatermark, 255, 255, 255); - imagecolortransparent($newWatermark, $col); - imagefilledrectangle($newWatermark, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight, $col); - imagesavealpha($newWatermark, true); - imagecopyresampled( - $newWatermark, - $watermark, - 0, - 0, - 0, - 0, - $this->_imageSrcWidth, - $this->_imageSrcHeight, - imagesx($watermark), - imagesy($watermark) - ); - $watermark = $newWatermark; + $watermark = $this->createWaterMark($watermark, $this->_imageSrcWidth, $this->_imageSrcHeight); } elseif ($this->getWatermarkPosition() == self::POSITION_CENTER) { $positionX = $this->_imageSrcWidth / 2 - imagesx($watermark) / 2; $positionY = $this->_imageSrcHeight / 2 - imagesy($watermark) / 2; @@ -602,8 +597,39 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = } } - imagedestroy($watermark); - $this->refreshImageDimensions(); + return $watermark; + } + + /** + * Create watermark. + * + * @param resource $watermark + * @param string $width + * @param string $height + * @return false|resource + */ + private function createWaterMark($watermark, string $width, string $height) + { + $newWatermark = imagecreatetruecolor($width, $height); + imagealphablending($newWatermark, false); + $col = imagecolorallocate($newWatermark, 255, 255, 255); + imagecolortransparent($newWatermark, $col); + imagefilledrectangle($newWatermark, 0, 0, $width, $height, $col); + imagesavealpha($newWatermark, true); + imagecopyresampled( + $newWatermark, + $watermark, + 0, + 0, + 0, + 0, + $width, + $height, + imagesx($watermark), + imagesy($watermark) + ); + + return $newWatermark; } /** diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 7e36cdb334eb2..31793d281ac52 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -6,7 +6,9 @@ namespace Magento\Framework\Image\Adapter; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; /** * Image adapter from ImageMagick. @@ -88,6 +90,11 @@ public function backgroundColor($color = null) */ public function open($filename) { + if (!file_exists($filename)) { + throw new FileSystemException( + new Phrase('File "%1" does not exist.', [$this->_fileName]) + ); + } if (!empty($filename) && !$this->validateURLScheme($filename)) { throw new \InvalidArgumentException('Wrong file'); } 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 f21101f099200..355a221c5d368 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -80,7 +80,7 @@ public function watermarkDataProvider(): array { return [ ['', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], - [__DIR__ . '/not_exists', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], + ['not_exist', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], [ __DIR__ . '/_files/invalid_image.jpg', ImageMagick::ERROR_WRONG_IMAGE @@ -105,6 +105,8 @@ public function testSaveWithException() */ public function testOpenInvalidUrl() { + require_once __DIR__ . '/_files/global_php_mock.php'; + $this->expectException(\InvalidArgumentException::class); $this->imageMagic->open('bar://foo.bar'); diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php index a62909b495ab4..034e9c32c6954 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php @@ -48,6 +48,16 @@ function filesize($file) return Gd2Test::$imageSize; } +/** + * @param $file + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +function file_exists($file) +{ + return !($file === 'not_exist'); +} + /** * @param $real * @return int From 0761e1ad760d103090100664d21af055ca8d21ee Mon Sep 17 00:00:00 2001 From: Serhiy Yelahin <serhiy.yelahin@transoftgroup.com> Date: Tue, 27 Oct 2020 12:16:00 +0200 Subject: [PATCH 0944/1013] MC-38663: Korean postal code is now 5 digit not 6 --- app/code/Magento/Directory/etc/zip_codes.xml | 1 + .../Model/Country/Postcode/Config/ReaderTest.php | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/app/code/Magento/Directory/etc/zip_codes.xml b/app/code/Magento/Directory/etc/zip_codes.xml index de6c064815d7a..634d4abe06763 100644 --- a/app/code/Magento/Directory/etc/zip_codes.xml +++ b/app/code/Magento/Directory/etc/zip_codes.xml @@ -229,6 +229,7 @@ <zip countryCode="KR"> <codes> <code id="pattern_1" active="true" example="123-456">^[0-9]{3}-[0-9]{3}$</code> + <code id="pattern_2" active="true" example="12345">^[0-9]{5}$</code> </codes> </zip> <zip countryCode="KG"> diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php index 9146535ed5181..87dfd2a4a3981 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php @@ -49,5 +49,13 @@ public function testRead() $this->assertEquals('^[0-9]{4}$', $result['AR']['pattern_1']['pattern']); $this->assertEquals('A1234BCD', $result['AR']['pattern_2']['example']); $this->assertEquals('^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$', $result['AR']['pattern_2']['pattern']); + + $this->assertArrayHasKey('KR', $result); + $this->assertArrayHasKey('pattern_1', $result['KR']); + $this->assertArrayHasKey('pattern_2', $result['KR']); + $this->assertEquals('123-456', $result['KR']['pattern_1']['example']); + $this->assertEquals('^[0-9]{3}-[0-9]{3}$', $result['KR']['pattern_1']['pattern']); + $this->assertEquals('12345', $result['KR']['pattern_2']['example']); + $this->assertEquals('^[0-9]{5}$', $result['KR']['pattern_2']['pattern']); } } From 5ba03c224aa3ea69299a60a43e09f7fd6cd9c692 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky <engcom-vendorworker-foxtrot@adobe.com> Date: Tue, 27 Oct 2020 13:15:17 +0200 Subject: [PATCH 0945/1013] magento/magento2#30372: [Issue] MFTF: Admin Delete CMS Block Test. --- .../Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml | 2 +- .../Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml index a9c9a5943529c..f558619fa49ac 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml @@ -14,6 +14,6 @@ <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> - <element name="gridDataRow" type="input" selector=".data-row .data-grid-cell-content"/> + <element name="gridDataRow" type="input" selector="//table[@data-role='grid']//tr/td"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml index 529000dc44c3a..38281d4d6d1d6 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml @@ -20,7 +20,7 @@ <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"/> - <element name="delete" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Delete']" parameterized="true"/> - <element name="deleteConfirm" type="button" selector=".action-primary.action-accept" timeout="60"/> + <element name="delete" type="button" selector="//a[@data-action='item-delete']"/> + <element name="deleteConfirm" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="60"/> </section> </sections> From 63737e339c457f9611efa7fb7be60a91c680099c Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Tue, 27 Oct 2020 14:41:28 +0200 Subject: [PATCH 0946/1013] MC-38488: [MFTF] AdminMediaGalleryAssertUsedInLinkBlocksGridTest failed because of bad design --- ...minMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml index 63f6a483665c9..530605bb7e233 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertImageUsedInLinkBlocksGridTest.xml @@ -27,7 +27,7 @@ <deleteData createDataKey="createBlock" stepKey="deleteBlock"/> <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultViewAgain"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersOnStandaloneMediaGalleryPage"/> <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCreatedFolderAgain"> <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> @@ -40,7 +40,7 @@ <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersOnMediaGalleryPage"/> <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImagesAfterTest"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> @@ -50,7 +50,7 @@ </actionGroup> <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImageIcon" /> <waitForPageLoad stepKey="waitForInitialPageLoad" /> - <waitForPageLoad stepKey="waitForSecondaryPageLoad" /> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderNewCreateButton}}" stepKey="waitForNewFolderButton"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"> @@ -70,7 +70,7 @@ <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="saveBlock"/> <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> - <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersOnStandaloneMediaGallery"/> <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCreatedFolder"> <argument name="name" value="{{AdminMediaGalleryFolderData.name}}"/> </actionGroup> From 7569b08df6c47335f199a7226cb363f4ae562425 Mon Sep 17 00:00:00 2001 From: mastiuhin-olexandr <mastiuhin.olexandr@transoftgroup.com> Date: Tue, 27 Oct 2020 17:37:17 +0200 Subject: [PATCH 0947/1013] MC-33288: [2.4][MSI][MFTF] StorefrontLoggedInCustomerCreateOrderAllOptionQuantityConfigurableProductCustomStockTest fails because of bad design --- .../Magento/Catalog/Model/ResourceModel/Product/Relation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php index deba2b555d5cc..2f8c4a34f0087 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php @@ -152,7 +152,7 @@ public function getRelationsByChildren(array $childrenIds): array )->join( ['relation' => $this->getTable('catalog_product_relation')], 'relation.parent_id = cpe.' . $linkField - )->where('child_id IN(?)', $childrenIds); + )->where('relation.child_id IN(?)', $childrenIds); return $connection->fetchCol($select); } From 00a170a6a783d7d93e05d625548fb7aff4e1b4d1 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <duhon@users.noreply.github.com> Date: Tue, 27 Oct 2020 14:03:00 -0500 Subject: [PATCH 0948/1013] [performance] MC-38137 Custom option (#6275) --- .../Magento/Bundle/Model/Product/Type.php | 11 +++-- .../Option/Type/File/ValidatorFile.php | 13 +++--- .../Model/Product/Type/AbstractType.php | 44 +++++++++---------- .../Model/Product/Type/Configurable.php | 14 +++--- .../Downloadable/Model/Product/Type.php | 10 +++-- .../Model/Product/Type/Grouped.php | 8 +++- .../Option/Type/File/ValidatorFileTest.php | 4 +- 7 files changed, 57 insertions(+), 47 deletions(-) diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index fe120e9a179dd..6ee67859db015 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -13,6 +13,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\File\UploaderFactory; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\ArrayUtils; @@ -190,11 +191,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType * @param PriceCurrencyInterface $priceCurrency * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\CatalogInventory\Api\StockStateInterface $stockState - * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param Json|null $serializer * @param MetadataPool|null $metadataPool * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier * @param ArrayUtils|null $arrayUtility - * + * @param UploaderFactory $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -222,7 +223,8 @@ public function __construct( Json $serializer = null, MetadataPool $metadataPool = null, SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, - ArrayUtils $arrayUtility = null + ArrayUtils $arrayUtility = null, + UploaderFactory $uploaderFactory = null ) { $this->_catalogProduct = $catalogProduct; $this->_catalogData = $catalogData; @@ -254,7 +256,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php index fef4999a1174a..934ff48045097 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Math\Random; use Magento\Framework\App\ObjectManager; +use Magento\MediaStorage\Model\File\Uploader; /** * Validator class. Represents logic for validation file given from product option @@ -173,15 +174,11 @@ public function validate($processingParams, $option) $userValue = []; if ($upload->isUploaded($file) && $upload->isValid($file)) { - $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($fileInfo['name']); - $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); - - $filePath = $dispersion; - $tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP); - $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); $fileRandomName = $this->random->getRandomString(32); - $filePath .= '/' .$fileRandomName; + $fileName = Uploader::getCorrectFileName($fileRandomName); + $dispersion = Uploader::getDispersionPath($fileName); + $filePath = $dispersion . '/' . $fileName; $fileFullPath = $this->mediaDirectory->getAbsolutePath($this->quotePath . $filePath); $upload->addFilter(new \Zend_Filter_File_Rename(['target' => $fileFullPath, 'overwrite' => true])); @@ -216,6 +213,8 @@ public function validate($processingParams, $option) } } + $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); + $userValue = [ 'type' => $fileInfo['type'], 'title' => $fileInfo['name'], diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index eb4a71cb90a8c..e14a38e61a25f 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -7,9 +7,9 @@ namespace Magento\Catalog\Model\Product\Type; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\UploaderFactory; /** * Abstract model for product type implementation @@ -113,6 +113,11 @@ abstract class AbstractType */ protected $_cacheProductSetAttributes = '_cache_instance_product_set_attributes'; + /** + * @var UploaderFactory + */ + private $uploaderFactory; + /** * Delete data specific for this product type * @@ -175,8 +180,6 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ protected $serializer; /** - * Construct - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -187,6 +190,7 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ * @param \Psr\Log\LoggerInterface $logger * @param ProductRepositoryInterface $productRepository * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -199,7 +203,8 @@ public function __construct( \Magento\Framework\Registry $coreRegistry, \Psr\Log\LoggerInterface $logger, ProductRepositoryInterface $productRepository, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->_catalogProductOption = $catalogProductOption; $this->_eavConfig = $eavConfig; @@ -212,6 +217,7 @@ public function __construct( $this->productRepository = $productRepository; $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->uploaderFactory = $uploaderFactory ?: ObjectManager::getInstance()->get(UploaderFactory::class); } /** @@ -493,28 +499,20 @@ public function processFileQueue() if (isset($queueOptions['operation']) && ($operation = $queueOptions['operation'])) { switch ($operation) { case 'receive_uploaded_file': - $src = isset($queueOptions['src_name']) ? $queueOptions['src_name'] : ''; - $dst = isset($queueOptions['dst_name']) ? $queueOptions['dst_name'] : ''; + $src = $queueOptions['src_name'] ?? ''; + $dst = $queueOptions['dst_name'] ?? ''; /** @var $uploader \Zend_File_Transfer_Adapter_Http */ - $uploader = isset($queueOptions['uploader']) ? $queueOptions['uploader'] : null; - - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $path = dirname($dst); - - try { - $rootDir = $this->_filesystem->getDirectoryWrite( - DirectoryList::ROOT - ); - $rootDir->create($rootDir->getRelativePath($path)); - } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \Magento\Framework\Exception\LocalizedException( - __('We can\'t create the "%1" writeable directory.', $path) - ); + $uploader = $queueOptions['uploader'] ?? null; + $isUploaded = false; + if ($uploader && $uploader->isValid()) { + $path = pathinfo($dst, PATHINFO_DIRNAME); + $uploader = $this->uploaderFactory->create(['fileId' => $src]); + $uploader->setFilesDispersion(false); + $uploader->setAllowRenameFiles(true); + $isUploaded = $uploader->save($path, pathinfo($dst, PATHINFO_FILENAME)); } - $uploader->setDestination($path); - - if (empty($src) || empty($dst) || !$uploader->receive($src)) { + if (empty($src) || empty($dst) || !$isUploaded) { /** * @todo: show invalid option */ diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index c2ae381b345c6..79f6d1e47f1a2 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -17,6 +17,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\File\UploaderFactory; /** * Configurable product type implementation @@ -235,11 +236,12 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor * @param \Magento\Framework\Cache\FrontendInterface|null $cache * @param \Magento\Customer\Model\Session|null $customerSession - * @param \Magento\Framework\Serialize\Serializer\Json $serializer - * @param ProductInterfaceFactory $productFactory - * @param SalableProcessor $salableProcessor + * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param ProductInterfaceFactory|null $productFactory + * @param SalableProcessor|null $salableProcessor * @param ProductAttributeRepositoryInterface|null $productAttributeRepository * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -266,7 +268,8 @@ public function __construct( ProductInterfaceFactory $productFactory = null, SalableProcessor $salableProcessor = null, ProductAttributeRepositoryInterface $productAttributeRepository = null, - SearchCriteriaBuilder $searchCriteriaBuilder = null + SearchCriteriaBuilder $searchCriteriaBuilder = null, + UploaderFactory $uploaderFactory = null ) { $this->typeConfigurableFactory = $typeConfigurableFactory; $this->_eavAttributeFactory = $eavAttributeFactory; @@ -295,7 +298,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/Downloadable/Model/Product/Type.php b/app/code/Magento/Downloadable/Model/Product/Type.php index cb79dda3baccb..45a03b50d78b8 100644 --- a/app/code/Magento/Downloadable/Model/Product/Type.php +++ b/app/code/Magento/Downloadable/Model/Product/Type.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\File\UploaderFactory; /** * Downloadable product type model @@ -67,8 +68,6 @@ class Type extends \Magento\Catalog\Model\Product\Type\Virtual private $extensionAttributesJoinProcessor; /** - * Construct - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -87,6 +86,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\Virtual * @param TypeHandler\TypeHandlerInterface $typeHandler * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -107,7 +107,8 @@ public function __construct( \Magento\Downloadable\Model\LinkFactory $linkFactory, \Magento\Downloadable\Model\Product\TypeHandler\TypeHandlerInterface $typeHandler, JoinProcessorInterface $extensionAttributesJoinProcessor, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->_sampleResFactory = $sampleResFactory; $this->_linkResource = $linkResource; @@ -127,7 +128,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index 8eac8d0b0e163..b56e8657df722 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -8,6 +8,7 @@ namespace Magento\GroupedProduct\Model\Product\Type; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\File\UploaderFactory; /** * Grouped product type model @@ -102,6 +103,7 @@ class Grouped extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\App\State $appState * @param \Magento\Msrp\Helper\Data $msrpData * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -119,7 +121,8 @@ public function __construct( \Magento\Catalog\Model\Product\Attribute\Source\Status $catalogProductStatus, \Magento\Framework\App\State $appState, \Magento\Msrp\Helper\Data $msrpData, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->productLinks = $catalogProductLink; $this->_storeManager = $storeManager; @@ -136,7 +139,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php index 0be889f546a2b..64b009b5b8d13 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php @@ -362,8 +362,8 @@ protected function expectedValidate() return [ 'type' => 'image/jpeg', 'title' => 'test.jpg', - 'quote_path' => 'custom_options/quote/t/e/RandomString', - 'order_path' => 'custom_options/order/t/e/RandomString', + 'quote_path' => 'custom_options/quote/R/a/RandomString', + 'order_path' => 'custom_options/order/R/a/RandomString', 'size' => '3046', 'width' => 136, 'height' => 131, From 647628ac00897af4d312d131f0cfd3ecd93b5fbf Mon Sep 17 00:00:00 2001 From: Andrii Beziazychnyi <a.beziazychnyi@atwix.com> Date: Tue, 27 Oct 2020 23:12:58 +0200 Subject: [PATCH 0949/1013] Backward Compatibility fixes --- .../QuoteGraphQl/Model/Resolver/MergeCarts.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php index d53ab597eaba4..297fd25be1fae 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/MergeCarts.php @@ -7,6 +7,7 @@ namespace Magento\QuoteGraphQl\Model\Resolver; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; @@ -50,19 +51,21 @@ class MergeCarts implements ResolverInterface /** * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository - * @param CustomerCartResolver $customerCartResolver - * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + * @param CustomerCartResolver|null $customerCartResolver + * @param QuoteIdToMaskedQuoteIdInterface|null $quoteIdToMaskedQuoteId */ public function __construct( GetCartForUser $getCartForUser, CartRepositoryInterface $cartRepository, - CustomerCartResolver $customerCartResolver, - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + CustomerCartResolver $customerCartResolver = null, + QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId = null ) { $this->getCartForUser = $getCartForUser; $this->cartRepository = $cartRepository; - $this->customerCartResolver = $customerCartResolver; - $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->customerCartResolver = $customerCartResolver + ?: ObjectManager::getInstance()->get(CustomerCartResolver::class); + $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId + ?: ObjectManager::getInstance()->get(QuoteIdToMaskedQuoteIdInterface::class); } /** From de8edcfd37f42f2001c856320597aed9b4161ac6 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Tue, 27 Oct 2020 20:01:40 -0500 Subject: [PATCH 0950/1013] [AWS S3] Improvements (#6262) --- .gitignore | 2 + app/code/Magento/AwsS3/Driver/AwsS3.php | 66 +++++- .../Magento/AwsS3/Driver/AwsS3Factory.php | 14 +- .../AwsS3/Test/Mftf/Data/ConfigData.xml | 2 + .../AwsS3AdminAddImageForCategoryTest.xml | 27 +++ .../AwsS3AdminAddImageToWYSIWYGBlockTest.xml | 66 +----- .../AwsS3AdminAddImageToWYSIWYGCMSTest.xml | 61 +----- ...S3AdminAddImageToWYSIWYGNewsletterTest.xml | 28 +++ ...AddRemoveDefaultVideoSimpleProductTest.xml | 30 +++ ...nCreateDownloadableProductWithLinkTest.xml | 7 +- ...3AdminMarketingCreateSitemapEntityTest.xml | 47 +--- ...wsS3AdminMarketingSiteMapCreateNewTest.xml | 23 +- .../Mftf/Test/AwsS3CheckingRMAPrintTest.xml | 26 +++ ...tChildImageShouldBeShownOnWishListTest.xml | 29 +++ .../AwsS3StorefrontPrintOrderGuestTest.xml | 30 +++ ...S3UpdateImageFileCustomerAttributeTest.xml | 27 +++ app/code/Magento/AwsS3/composer.json | 3 + .../Catalog/Model/Category/FileInfo.php | 3 +- app/code/Magento/Catalog/etc/config.xml | 2 - .../Cms/Model/Wysiwyg/Images/Storage.php | 9 +- app/code/Magento/Cms/etc/config.xml | 1 + .../Model/CreateAssetFromFile.php | 36 +++- app/code/Magento/MediaStorage/App/Media.php | 50 ++++- .../MediaStorage/Model/File/Storage.php | 13 +- .../MediaStorage/Test/Unit/App/MediaTest.php | 189 ++++++++-------- .../Command/RemoteStorageEnableCommand.php | 12 +- .../RemoteStorage/Driver/DriverException.php | 17 ++ .../Driver/DriverFactoryInterface.php | 6 +- .../Driver/DriverFactoryPool.php | 4 +- .../RemoteStorage/Driver/DriverPool.php | 4 +- .../Driver/RemoteDriverInterface.php | 23 ++ .../Magento/RemoteStorage/Model/Config.php | 33 +-- .../Magento/RemoteStorage/Plugin/Image.php | 2 +- .../RemoteStorage/Plugin/MediaStorage.php | 37 ++-- .../Magento/RemoteStorage/Plugin/Scope.php | 2 +- .../Magento/RemoteStorage/Plugin/Sitemap.php | 67 ------ .../RemoteStorage/Setup/ConfigOptionsList.php | 201 ++++++++++++++++++ app/code/Magento/RemoteStorage/composer.json | 1 + .../RemoteStorage/etc/adminhtml/system.xml | 18 ++ app/code/Magento/RemoteStorage/etc/di.xml | 14 +- app/code/Magento/Sitemap/etc/config.xml | 7 + app/code/Magento/Sitemap/etc/di.xml | 12 ++ .../Theme/Model/Design/Backend/File.php | 2 +- .../Framework/Css/_files/css/test-input.html | 6 + .../Magento/Framework/Config/DocumentRoot.php | 2 +- .../Framework/Data/Collection/Filesystem.php | 8 +- lib/internal/Magento/Framework/File/Mime.php | 175 ++++++--------- .../Framework/File/Test/Unit/MimeTest.php | 97 +++++++-- .../Framework/Filesystem/Driver/File.php | 29 ++- .../Framework/Filesystem/Driver/File/Mime.php | 163 ++++++++++++++ .../Filesystem/ExtendedDriverInterface.php | 43 ++++ .../Test/Unit/Driver/File/MimeTest.php | 67 ++++++ .../Unit/Driver/File/_files/UPPERCASE.WEIRD | 1 + .../Test/Unit/Driver/File/_files/blank.html | 6 + .../Test/Unit/Driver/File/_files/file.weird | 1 + .../Unit/Driver/File/_files/javascript.js | 5 + .../Test/Unit/Driver/File/_files/magento | Bin 0 -> 55303 bytes nginx.conf.sample | 1 + pub/get.php | 13 +- pub/media/sitemap/.htaccess | 7 + 60 files changed, 1287 insertions(+), 590 deletions(-) create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml create mode 100644 app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml create mode 100644 app/code/Magento/RemoteStorage/Driver/DriverException.php create mode 100644 app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php delete mode 100644 app/code/Magento/RemoteStorage/Plugin/Sitemap.php create mode 100644 app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php create mode 100644 app/code/Magento/RemoteStorage/etc/adminhtml/system.xml create mode 100644 lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php create mode 100644 lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js create mode 100644 lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento create mode 100644 pub/media/sitemap/.htaccess diff --git a/.gitignore b/.gitignore index 8ec1104f25535..7092a568ba2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ atlassian* /pub/media/tmp/* !/pub/media/tmp/.htaccess /pub/media/captcha/* +/pub/media/sitemap/* +!/pub/media/sitemap/.htaccess /pub/static/* !/pub/static/.htaccess diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 169a93580038c..5dc1f7e8cb216 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -7,24 +7,29 @@ namespace Magento\AwsS3\Driver; +use Exception; use League\Flysystem\AwsS3v3\AwsS3Adapter; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Phrase; use Psr\Log\LoggerInterface; +use Magento\RemoteStorage\Driver\DriverException; +use Magento\RemoteStorage\Driver\RemoteDriverInterface; /** * Driver for AWS S3 IO operations. * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -class AwsS3 implements DriverInterface +class AwsS3 implements RemoteDriverInterface { public const TYPE_DIR = 'dir'; public const TYPE_FILE = 'file'; - private const CONFIG = ['ACL' => 'public-read']; + private const TEST_FLAG = 'storage.flag'; + + private const CONFIG = ['ACL' => 'private']; /** * @var AwsS3Adapter @@ -68,6 +73,18 @@ public function __destruct() } } + /** + * @inheritDoc + */ + public function test(): void + { + try { + $this->adapter->write(self::TEST_FLAG, '', new Config(self::CONFIG)); + } catch (Exception $exception) { + throw new DriverException(__($exception->getMessage()), $exception); + } + } + /** * @inheritDoc */ @@ -177,7 +194,7 @@ public function deleteDirectory($path): bool /** * @inheritDoc */ - public function filePutContents($path, $content, $mode = null, $context = null): int + public function filePutContents($path, $content, $mode = null): int { $path = $this->normalizeRelativePath($path); @@ -345,6 +362,7 @@ public function isDirectory($path): bool if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { return ($meta['type'] ?? null) === self::TYPE_DIR; } + return false; } @@ -416,10 +434,44 @@ public function stat($path): array 'blksize' => 0, 'blocks' => 0, 'size' => $metaInfo['size'] ?? 0, - 'type' => $metaInfo['type'] ?? 0, + 'type' => $metaInfo['type'] ?? '', 'mtime' => $metaInfo['timestamp'] ?? 0, - 'disposition' => null, - 'mimetype' => $metaInfo['mimetype'] ?? 0 + 'disposition' => null + ]; + } + + /** + * @inheritDoc + */ + public function getMetadata(string $path): array + { + $path = $this->normalizeRelativePath($path); + $metaInfo = $this->adapter->getMetadata($path); + + if (!$metaInfo) { + throw new FileSystemException(__('Cannot gather meta info! %1', [$this->getWarningMessage()])); + } + + $extra = [ + 'image-width' => 0, + 'image-height' => 0 + ]; + + if (isset($metaInfo['image-width'], $metaInfo['image-height'])) { + $extra['image-width'] = $metaInfo['image-width']; + $extra['image-height'] = $metaInfo['image-height']; + } + + return [ + 'path' => $metaInfo['path'], + 'dirname' => $metaInfo['dirname'], + 'basename' => $metaInfo['basename'], + 'extension' => $metaInfo['extension'], + 'filename' => $metaInfo['filename'], + 'timestamp' => $metaInfo['timestamp'], + 'size' => $metaInfo['size'], + 'mimetype' => $metaInfo['mimetype'], + 'extra' => $extra ]; } @@ -778,6 +830,7 @@ private function getSearchPattern(string $pattern, array $parentPattern, string '/\?/' => '.', '/\//' => '\/' ]; + return preg_replace(array_keys($replacement), array_values($replacement), $searchPattern); } @@ -816,6 +869,7 @@ private function getDirectoryContent( } } } + return $directoryContent; } } diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index d9efe6f7fd10e..2042e10090407 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -9,9 +9,9 @@ use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; -use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\ObjectManagerInterface; use Magento\RemoteStorage\Driver\DriverFactoryInterface; +use Magento\RemoteStorage\Driver\RemoteDriverInterface; /** * Creates a pre-configured instance of AWS S3 driver. @@ -36,13 +36,15 @@ public function __construct(ObjectManagerInterface $objectManager) * * @param array $config * @param string $prefix - * @return DriverInterface + * @return RemoteDriverInterface */ - public function create(array $config, string $prefix): DriverInterface + public function create(array $config, string $prefix): RemoteDriverInterface { - $config += [ - 'version' => 'latest' - ]; + $config['version'] = 'latest'; + + if (empty($config['credentials']['key']) || empty($config['credentials']['secret'])) { + unset($config['credentials']); + } return $this->objectManager->create( AwsS3::class, diff --git a/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml index bc43aff37a491..23be7918106ee 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml @@ -13,5 +13,7 @@ <data key="bucket">{{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}}</data> <data key="access_key">{{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}}</data> <data key="secret_key">{{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}}</data> + <data key="enable_options">--remote-storage-driver={{_ENV.REMOTE_STORAGE_AWSS3_DRIVER}} --remote-storage-bucket={{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}} --remote-storage-region={{_ENV.REMOTE_STORAGE_AWSS3_REGION}} --remote-storage-prefix={{_ENV.REMOTE_STORAGE_AWSS3_PREFIX}} --remote-storage-key={{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}} --remote-storage-secret={{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}} -n</data> + <data key="disable_options">--remote-storage-driver=file -n</data> </entity> </entities> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml new file mode 100644 index 0000000000000..a8f0d4da9e338 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * 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="AwsS3AdminAddImageForCategoryTest" extends="AdminAddImageForCategoryTest"> + <annotations> + <title value="AWS S3 Admin should be able to add image to a Category"/> + <stories value="Add/remove images and videos for all product types and category"/> + <description value="Admin should be able to add image to a Category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38688"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml index 54b7795a84cd3..13e0dcbf41c01 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml @@ -7,74 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminAddImageToWYSIWYGBlockTest"> + <test name="AwsS3AdminAddImageToWYSIWYGBlockTest" extends="AdminAddImageToWYSIWYGBlockTest"> <annotations> - <features value="Cms"/> - <stories value="MC-37460: Support by Magento CMS"/> - <group value="Cms"/> - <title value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> - <description value="Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <stories value="Default WYSIWYG toolbar configuration with Magento Media Gallery"/> + <description value="Admin should be able to add image to WYSIWYG content of Block"/> <severity value="BLOCKER"/> <testCaseId value="MC-38302"/> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> - <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> - <createData entity="_defaultBlock" stepKey="createPreReqBlock" /> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> - <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> - <actionGroup ref="AssignBlockToCMSPage" stepKey="assignBlockToCMSPage"> - <argument name="Block" value="$$createPreReqBlock$$"/> - <argument name="CmsPage" value="$$createCMSPage$$"/> - </actionGroup> - <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> - <argument name="CMSBlockPage" value="$$createPreReqBlock$$"/> - </actionGroup> - <selectOption selector="{{BlockNewPageBasicFieldsSection.storeView}}" userInput="All Store View" stepKey="selectAllStoreView" /> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE" /> - <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> - <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> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> - <argument name="Image" value="ImageUpload"/> - </actionGroup> - <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> - <argument name="Image" value="ImageUpload"/> - </actionGroup> - <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> - <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> - <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="clickSaveBlock"/> - <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> - <waitForPageLoad stepKey="waitForPageLoad11" /> - <!--see image on Storefront--> - <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"/> - <waitForPageLoad stepKey="waitForGridReload"/> - <deleteData createDataKey="createPreReqBlock" stepKey="deletePreReqBlock" /> - <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml index d361fb60f31c7..a56d5d0710d3a 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml @@ -7,69 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminAddImageToWYSIWYGCMSTest"> + <test name="AwsS3AdminAddImageToWYSIWYGCMSTest" extends="AdminAddImageToWYSIWYGCMSTest"> <annotations> - <features value="Cms"/> - <stories value="MC-37460: Support by Magento CMS"/> - <group value="Cms"/> - <title value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> - <description value="Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <stories value="Default WYSIWYG toolbar configuration with Magento Media Gallery"/> + <description value="Admin should be able to add image to WYSIWYG content of CMS Page"/> <severity value="BLOCKER"/> <testCaseId value="MC-38295"/> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}} --is-public true" stepKey="enableRemoteStorage"/> - <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYGBeforeTest" /> - <magentoCLI command='config:set cms/wysiwyg/enabled enabled' stepKey="enableWYSIWYGBeforeTest"/> - <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> <after> - <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> - <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> - <argument name="ImageFolder" value="ImageFolder"/> - </actionGroup> - <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> - <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYGAfterTest" /> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> - - <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> - <argument name="CMSPage" value="$$createCMSPage$$"/> - </actionGroup> - <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> - <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> - <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> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> - <argument name="Image" value="ImageUpload3"/> - </actionGroup> - <actionGroup ref="DeleteImageActionGroup" stepKey="deleteImage"/> - <actionGroup ref="AttachImageActionGroup" stepKey="attachImage2"> - <argument name="Image" value="ImageUpload3"/> - </actionGroup> - <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> - <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> - <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> - <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="$$createCMSPage.identifier$$" stepKey="fillFieldUrlKey"/> - <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> - <waitForElementVisible selector="{{CmsNewPagePageActionsSection.splitButtonMenu}}" stepKey="waitForSplitButtonMenuVisible"/> - <click selector="{{CmsNewPagePageActionsSection.savePage}}" stepKey="clickSavePage"/> - <see userInput="You saved the page." stepKey="seeSuccessMessage"/> - <amOnPage url="$$createCMSPage.identifier$$" stepKey="amOnPageTestPage"/> - <waitForPageLoad stepKey="wait4"/> - <seeElement selector="{{StorefrontCMSPageSection.mediaDescription}}" stepKey="assertMediaDescription"/> - <seeElementInDOM selector="{{StorefrontCMSPageSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml new file mode 100644 index 0000000000000..adc4eea8acf2e --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AwsS3AdminAddImageToWYSIWYGNewsletterTest" extends="AdminAddImageToWYSIWYGNewsletterTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Apply new WYSIWYG in Newsletter"/> + <group value="Newsletter"/> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of Newsletter"/> + <description value="Admin should be able to add image to WYSIWYG content Newsletter"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38716"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml new file mode 100644 index 0000000000000..2b46ddcacb94c --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.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="AwsS3AdminAddRemoveDefaultVideoSimpleProductTest" extends="AdminAddRemoveDefaultVideoSimpleProductTest"> + <annotations> + <title value="AWS S3Admin should be able to add/remove default product video for a Simple Product"/> + <stories value="Add/remove images and videos for all product types and category"/> + <description value="Admin should be able to add/remove default product video for a Simple Product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38693"/> + <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MC-33903"/> + </skip> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml index 449f00281c425..dd0fe36f44dde 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml @@ -20,9 +20,12 @@ <testCaseId value="MC-38039"/> <group value="Downloadable"/> <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MQE-2288" /> + </skip> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -31,7 +34,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> </before> <after> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> <!-- Delete customer --> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml index 4d411fcffc682..d9dc75c18ad4b 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml @@ -7,55 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminMarketingCreateSitemapEntityTest"> + <test name="AwsS3AdminMarketingCreateSitemapEntityTest" extends="AdminMarketingCreateSitemapEntityTest"> <annotations> - <features value="Sitemap"/> - <stories value="AWS S3 Admin Creates Sitemap Entity"/> - <title value="AWS S3 Sitemap Creation"/> + <stories value="Admin Creates Sitemap Entity"/> <description value="Sitemap Entity Creation"/> - <testCaseId value="MC-38319"/> <severity value="MAJOR"/> - <group value="sitemap"/> - <group value="mtf_migrated"/> + <title value="AWS S3 Sitemap Creation"/> + <testCaseId value="MC-38319"/> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> <after> - <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteCreatedSitemap"> - <argument name="filename" value="sitemap.xml"/> - </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> - - <!--TEST BODY --> - <!--Navigate to Marketing->Sitemap Page --> - <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToMarketingSiteMapPage"> - <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> - <argument name="submenuUiId" value="{{AdminMenuSEOAndSearchSiteMap.dataUiId}}"/> - </actionGroup> - <!-- Navigate to New Sitemap Creation Page --> - <actionGroup ref="AdminMarketingNavigateToNewSitemapPageActionGroup" stepKey="navigateToAddNewSitemap"/> - <!-- Create Sitemap Entity --> - <actionGroup ref="AdminMarketingCreateSitemapEntityActionGroup" stepKey="createSitemap"> - <argument name="filename" value="sitemap.xml"/> - <argument name="path" value="/"/> - </actionGroup> - <!-- Assert Success Message --> - <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="seeSuccessMessage"> - <argument name="message" value="You saved the sitemap."/> - <argument name="messageType" value="success"/> - </actionGroup> - <!-- Find Created Sitemap On Grid --> - <actionGroup ref="AdminMarketingSearchSitemapActionGroup" stepKey="findCreatedSitemapInGrid"> - <argument name="name" value="sitemap.xml"/> - </actionGroup> - <actionGroup ref="AssertAdminSitemapInGridActionGroup" stepKey="assertSitemapInGrid"> - <argument name="name" value="sitemap.xml"/> - </actionGroup> - <!--END TEST BODY --> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml index 43cf305e3fd17..bbdeb7ff1155a 100644 --- a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml @@ -7,33 +7,20 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AwsS3AdminMarketingSiteMapCreateNewTest"> + <test name="AwsS3AdminMarketingSiteMapCreateNewTest" extends="AdminMarketingSiteMapCreateNewTest"> <annotations> - <features value="Sitemap"/> - <stories value="AWS S3 Create Site Map"/> <title value="AWS S3 Create New Site Map with valid data"/> + <stories value="Create Site Map"/> <description value="Create New Site Map with valid data"/> - <testCaseId value="MC-38320" /> <severity value="CRITICAL"/> - <group value="sitemap"/> + <testCaseId value="MC-38320" /> <group value="remote_storage_aws_s3"/> </annotations> <before> - <magentoCLI command="remote-storage:enable {{RemoteStorageAwsS3ConfigData.driver}} {{RemoteStorageAwsS3ConfigData.bucket}} {{RemoteStorageAwsS3ConfigData.region}} {{RemoteStorageAwsS3ConfigData.prefix}} {{RemoteStorageAwsS3ConfigData.access_key}} {{RemoteStorageAwsS3ConfigData.secret_key}}" stepKey="enableRemoteStorage"/> - <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> </before> <after> - <actionGroup ref="AdminMarketingSiteDeleteByNameActionGroup" stepKey="deleteSiteMap"> - <argument name="filename" value="{{DefaultSiteMap.filename}}" /> - </actionGroup> - <actionGroup ref="AssertSiteMapDeleteSuccessActionGroup" stepKey="assertDeleteSuccessMessage"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="remote-storage:disable" stepKey="disableRemoteStorage"/> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> </after> - <actionGroup ref="AdminMarketingSiteMapNavigateNewActionGroup" stepKey="navigateNewSiteMap"/> - <actionGroup ref="AdminMarketingSiteMapFillFormActionGroup" stepKey="fillSiteMapForm"> - <argument name="sitemap" value="DefaultSiteMap" /> - </actionGroup> - <actionGroup ref="AssertSiteMapCreateSuccessActionGroup" stepKey="seeSuccessMessage"/> </test> </tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml new file mode 100644 index 0000000000000..6d9d89fd29be5 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AwsS3CheckingRMAPrintTest" extends="CheckingRMAPrintTest"> + <annotations> + <title value="AWS S3 Checking Returns Print"/> + <stories value="Exception when try to print RMA"/> + <description value="RMA file should be downloaded"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38694"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml new file mode 100644 index 0000000000000..049caa2180d69 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.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="AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest" extends="ConfigurableProductChildImageShouldBeShownOnWishListTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Configurable product child image should be Shown on wishlist"/> + <group value="wishlist"/> + <title value="AWS S3 when user add Configurable child product to WIshlist then child product image should be shown in Wishlist"/> + <description value="When user add Configurable child product to WIshlist then child product image should be shown in Wishlist"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38708"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml new file mode 100644 index 0000000000000..c8d2947632b59 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.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="AwsS3StorefrontPrintOrderGuestTest" extends="StorefrontPrintOrderGuestTest"> + <annotations> + <title value="AWS S3 Print Order from Guest on Frontend"/> + <stories value="Print Order"/> + <description value="Print Order from Guest on Frontend"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38689"/> + <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MQE-2288" /> + </skip> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml new file mode 100644 index 0000000000000..8e2ec348d4f41 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * 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="AwsS3UpdateImageFileCustomerAttributeTest" extends="UpdateImageFileCustomerAttributeTest"> + <annotations> + <title value="AWS S3 Update image file customer attribute test"/> + <stories value="Update Customer Custom Attributes"/> + <description value="Update image file customer attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38692"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json index 02733f01d2285..ce5396223f58d 100644 --- a/app/code/Magento/AwsS3/composer.json +++ b/app/code/Magento/AwsS3/composer.json @@ -1,6 +1,9 @@ { "name": "magento/module-aws-s-3", "description": "N/A", + "config": { + "sort-packages": true + }, "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "^100.0.2", diff --git a/app/code/Magento/Catalog/Model/Category/FileInfo.php b/app/code/Magento/Catalog/Model/Category/FileInfo.php index 7d679f2645be1..f5aec60b2fcc0 100644 --- a/app/code/Magento/Catalog/Model/Category/FileInfo.php +++ b/app/code/Magento/Catalog/Model/Category/FileInfo.php @@ -239,7 +239,8 @@ private function getMediaDirectoryPathRelativeToBaseDirectoryPath(string $filePa $mediaDirectoryRelativeSubpath = substr($mediaDirectoryPath, strlen($baseDirectoryPath)); $pubDirectory = $baseDirectory->getRelativePath($pubDirectoryPath); - if (strpos($mediaDirectoryRelativeSubpath, $pubDirectory) === 0 && strpos($filePath, $pubDirectory) !== 0) { + if ($pubDirectory && strpos($mediaDirectoryRelativeSubpath, $pubDirectory) === 0 + && strpos($filePath, $pubDirectory) !== 0) { $mediaDirectoryRelativeSubpath = substr($mediaDirectoryRelativeSubpath, strlen($pubDirectory)); } diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index b8ab4e32ec161..f5546a06dd235 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -67,8 +67,6 @@ <media_storage_configuration> <allowed_resources> <tmp_images_folder>tmp</tmp_images_folder> - <catalog_product_images>media/catalog/product/</catalog_product_images> - <catalog_product_images_tmp>media/tmp/catalog/product/</catalog_product_images_tmp> <catalog_images_folder>catalog</catalog_images_folder> <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 8b170ecdd5c04..2c94e2e76914f 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -363,7 +363,6 @@ public function getFilesCollection($path, $type = null) $collection->setFilesFilter('/\.(' . implode('|', $allowed) . ')$/i'); } - // prepare items foreach ($collection as $item) { $item->setId($this->_cmsWysiwygImages->idEncode($item->getBasename())); $item->setName($item->getBasename()); @@ -383,7 +382,9 @@ public function getFilesCollection($path, $type = null) } try { - $size = getimagesize($item->getFilename()); + $size = getimagesizefromstring( + $driver->fileGetContents($item->getFilename()) + ); if (is_array($size)) { $item->setWidth($size[0]); @@ -657,7 +658,7 @@ public function resizeFile($source, $keepRatio = true) $image->keepAspectRatio($keepRatio); - list($imageWidth, $imageHeight) = $this->getResizedParams($source); + [$imageWidth, $imageHeight] = $this->getResizedParams($source); $image->resize($imageWidth, $imageHeight); $dest = $targetDir . '/' . $this->ioFile->getPathInfo($source)['basename']; @@ -680,7 +681,7 @@ private function getResizedParams(string $source): array $configHeight = $this->_resizeParameters['height']; //phpcs:ignore Generic.PHP.NoSilencedErrors - list($imageWidth, $imageHeight) = @getimagesize($source); + [$imageWidth, $imageHeight] = @getimagesize($source); if ($imageWidth && $imageHeight) { $imageWidth = $configWidth > $imageWidth ? $imageWidth : $configWidth; diff --git a/app/code/Magento/Cms/etc/config.xml b/app/code/Magento/Cms/etc/config.xml index d7a9e172f59a6..c1b3717386454 100644 --- a/app/code/Magento/Cms/etc/config.xml +++ b/app/code/Magento/Cms/etc/config.xml @@ -31,6 +31,7 @@ <media_storage_configuration> <allowed_resources> <wysiwyg_image_folder>wysiwyg</wysiwyg_image_folder> + <preview_folder>.thumbs</preview_folder> </allowed_resources> </media_storage_configuration> </system> diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index b4c360c3e0538..48f2aad8fa746 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -10,7 +10,6 @@ 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\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; @@ -74,19 +73,36 @@ public function __construct( public function execute(string $path): AssetInterface { $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); - $file = $this->getFileInfo->execute($absolutePath); - [$width, $height] = getimagesize($absolutePath); + $driver = $this->getMediaDirectory()->getDriver(); + + if ($driver instanceof Filesystem\ExtendedDriverInterface) { + $meta = $driver->getMetadata($absolutePath); + } else { + /** + * SPL file info is not compatible with remote storages and must not be used. + */ + $file = $this->getFileInfo->execute($absolutePath); + $meta = [ + 'size' => $file->getSize(), + 'extension' => $file->getExtension(), + 'basename' => $file->getBasename(), + ]; + } + + [$width, $height] = getimagesizefromstring( + $this->getMediaDirectory()->readFile($absolutePath) + ); return $this->assetFactory->create( [ 'id' => null, 'path' => $path, - 'title' => $file->getBasename(), + 'title' => $meta['basename'], 'width' => $width, 'height' => $height, 'hash' => $this->getHash($path), - 'size' => $file->getSize(), - 'contentType' => 'image/' . $file->getExtension(), + 'size' => $meta['size'], + 'contentType' => 'image/' . $meta['extension'], 'source' => 'Local' ] ); @@ -105,12 +121,12 @@ private function getHash(string $path): string } /** - * Retrieve media directory instance with read access + * Retrieve media directory instance with write access * - * @return ReadInterface + * @return Filesystem\Directory\WriteInterface */ - private function getMediaDirectory(): ReadInterface + private function getMediaDirectory(): Filesystem\Directory\WriteInterface { - return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); } } diff --git a/app/code/Magento/MediaStorage/App/Media.php b/app/code/Magento/MediaStorage/App/Media.php index ca5ff458c52e9..f3a85cf3a9baa 100644 --- a/app/code/Magento/MediaStorage/App/Media.php +++ b/app/code/Magento/MediaStorage/App/Media.php @@ -11,6 +11,7 @@ use Closure; use Exception; use LogicException; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App; use Magento\Framework\App\Area; @@ -18,6 +19,7 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\App\State; use Magento\Framework\AppInterface; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Driver\File; @@ -103,6 +105,11 @@ class Media implements AppInterface */ private $imageResize; + /** + * @var string + */ + private $mediaUrlFormat; + /** * @param ConfigFactory $configFactory * @param SynchronizationFactory $syncFactory @@ -116,6 +123,8 @@ class Media implements AppInterface * @param State $state * @param ImageResize $imageResize * @param File $file + * @param CatalogMediaConfig $catalogMediaConfig + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -130,12 +139,19 @@ public function __construct( PlaceholderFactory $placeholderFactory, State $state, ImageResize $imageResize, - File $file + File $file, + CatalogMediaConfig $catalogMediaConfig = null ) { $this->response = $response; $this->isAllowed = $isAllowed; - $this->directoryPub = $filesystem->getDirectoryWrite(DirectoryList::PUB); - $this->directoryMedia = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->directoryPub = $filesystem->getDirectoryWrite( + DirectoryList::PUB, + Filesystem\DriverPool::FILE + ); + $this->directoryMedia = $filesystem->getDirectoryWrite( + DirectoryList::MEDIA, + Filesystem\DriverPool::FILE + ); $mediaDirectory = trim($mediaDirectory); if (!empty($mediaDirectory)) { // phpcs:ignore Magento2.Functions.DiscouragedFunction @@ -148,6 +164,9 @@ public function __construct( $this->placeholderFactory = $placeholderFactory; $this->appState = $state; $this->imageResize = $imageResize; + + $catalogMediaConfig = $catalogMediaConfig ?: App\ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaUrlFormat = $catalogMediaConfig->getMediaUrlFormat(); } /** @@ -174,10 +193,8 @@ public function launch(): ResponseInterface } try { - /** @var Synchronization $sync */ - $sync = $this->syncFactory->create(['directory' => $this->directoryPub]); - $sync->synchronize($this->relativeFileName); - $this->imageResize->resizeFromImageName($this->getOriginalImage($this->relativeFileName)); + $this->createLocalCopy(); + if ($this->directoryPub->isReadable($this->relativeFileName)) { $this->response->setFilePath($this->directoryPub->getAbsolutePath($this->relativeFileName)); } else { @@ -190,6 +207,25 @@ public function launch(): ResponseInterface return $this->response; } + /** + * Create local copy of file and perform resizing if necessary. + * + * @throws NotFoundException + */ + private function createLocalCopy(): void + { + $this->syncFactory->create(['directory' => $this->directoryPub]) + ->synchronize($this->relativeFileName); + + if ($this->directoryPub->isReadable($this->relativeFileName)) { + return; + } + + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $this->imageResize->resizeFromImageName($this->getOriginalImage($this->relativeFileName)); + } + } + /** * Check if media directory changed * diff --git a/app/code/Magento/MediaStorage/Model/File/Storage.php b/app/code/Magento/MediaStorage/Model/File/Storage.php index 861f2d82c7e7b..f93b9180fa23d 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage.php @@ -83,11 +83,9 @@ class Storage extends AbstractModel protected $_databaseFactory; /** - * Filesystem instance - * - * @var Filesystem + * @var Filesystem\Directory\ReadInterface */ - protected $filesystem; + private $localMediaDirectory; /** * @param \Magento\Framework\Model\Context $context @@ -124,7 +122,10 @@ public function __construct( $this->_fileFlag = $fileFlag; $this->_fileFactory = $fileFactory; $this->_databaseFactory = $databaseFactory; - $this->filesystem = $filesystem; + $this->localMediaDirectory = $filesystem->getDirectoryRead( + DirectoryList::MEDIA, + Filesystem\DriverPool::FILE + ); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -286,7 +287,7 @@ public function synchronize($storage) public function getScriptConfig() { $config = []; - $config['media_directory'] = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath(); + $config['media_directory'] = $this->localMediaDirectory->getAbsolutePath(); $allowedResources = $this->_coreConfig->getValue(self::XML_PATH_MEDIA_RESOURCE_WHITELIST, 'default'); foreach ($allowedResources as $allowedResource) { diff --git a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php index 7f70f5ba48e5c..068732a7225cd 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php @@ -10,21 +10,23 @@ use Exception; use LogicException; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\View\Asset\Placeholder; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\State; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\Read; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\MediaStorage\App\Media; use Magento\MediaStorage\Model\File\Storage\Config; use Magento\MediaStorage\Model\File\Storage\ConfigFactory; use Magento\MediaStorage\Model\File\Storage\Response; use Magento\MediaStorage\Model\File\Storage\Synchronization; use Magento\MediaStorage\Model\File\Storage\SynchronizationFactory; +use Magento\MediaStorage\Service\ImageResize; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -80,122 +82,106 @@ class MediaTest extends TestCase private $directoryMediaMock; /** - * @var \Magento\Framework\Filesystem\Directory\Read|MockObject + * @var Read|MockObject */ private $directoryPubMock; + /** + * @inheritDoc + */ protected function setUp(): void { $this->configMock = $this->createMock(Config::class); $this->sync = $this->createMock(Synchronization::class); - $this->configFactoryMock = $this->createPartialMock( - ConfigFactory::class, - ['create'] - ); - $this->configFactoryMock->expects($this->any()) - ->method('create') + $this->configFactoryMock = $this->createPartialMock(ConfigFactory::class, ['create']); + $this->responseMock = $this->createMock(Response::class); + $this->syncFactoryMock = $this->createPartialMock(SynchronizationFactory::class, ['create']); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->directoryPubMock = $this->getMockForAbstractClass(WriteInterface::class); + $this->directoryMediaMock = $this->getMockForAbstractClass(WriteInterface::class); + + $this->configFactoryMock->method('create') ->willReturn($this->configMock); - $this->syncFactoryMock = $this->createPartialMock( - SynchronizationFactory::class, - ['create'] - ); - $this->syncFactoryMock->expects($this->any()) - ->method('create') + $this->syncFactoryMock->method('create') ->willReturn($this->sync); - - $this->filesystemMock = $this->createMock(Filesystem::class); - $this->directoryPubMock = $this->getMockForAbstractClass( - WriteInterface::class, - [], - '', - false, - true, - true, - ['isReadable', 'getAbsolutePath'] - ); - $this->directoryMediaMock = $this->getMockForAbstractClass( - WriteInterface::class, - [], - '', - false, - true, - true, - ['getAbsolutePath'] - ); - $this->filesystemMock->expects($this->any()) - ->method('getDirectoryWrite') + $this->filesystemMock->method('getDirectoryWrite') ->willReturnMap([ [DirectoryList::PUB, DriverPool::FILE, $this->directoryPubMock], [DirectoryList::MEDIA, DriverPool::FILE, $this->directoryMediaMock], ]); - - $this->responseMock = $this->createMock(Response::class); } - protected function tearDown(): void + public function testProcessRequestCreatesConfigFileMediaDirectoryIsNotProvided(): void { - unset($this->mediaModel); - } - - public function testProcessRequestCreatesConfigFileMediaDirectoryIsNotProvided() - { - $this->mediaModel = $this->getMediaModel(); - $filePath = '/absolute/path/to/test/file.png'; - $this->directoryMediaMock->expects($this->once()) + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::once()) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); - $this->configMock->expects($this->once())->method('save'); - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryPubMock->expects($this->once()) + $this->configMock->expects(self::once()) + ->method('save'); + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(true); - $this->responseMock->expects($this->once())->method('setFilePath')->with($filePath); - $this->mediaModel->launch(); + $this->responseMock->expects(self::once()) + ->method('setFilePath') + ->with($filePath); + + $this->createMediaModel()->launch(); } - public function testProcessRequestReturnsFileIfItsProperlySynchronized() + public function testProcessRequestReturnsFileIfItsProperlySynchronized(): void { - $this->mediaModel = $this->getMediaModel(); + $this->mediaModel = $this->createMediaModel(); $filePath = '/absolute/path/to/test/file.png'; - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryMediaMock->expects($this->once()) + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(true); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::once()) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); - $this->responseMock->expects($this->once())->method('setFilePath')->with($filePath); - $this->assertSame($this->responseMock, $this->mediaModel->launch()); + $this->responseMock->expects(self::once()) + ->method('setFilePath') + ->with($filePath); + + self::assertSame($this->responseMock, $this->mediaModel->launch()); } - public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized() + public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized(): void { - $this->mediaModel = $this->getMediaModel(); + $this->mediaModel = $this->createMediaModel(); - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryMediaMock->expects($this->once()) + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(false); - $this->assertSame($this->responseMock, $this->mediaModel->launch()); + + self::assertSame($this->responseMock, $this->mediaModel->launch()); } /** @@ -204,7 +190,7 @@ public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized() * * @dataProvider catchExceptionDataProvider */ - public function testCatchException($isDeveloper, $setBodyCalls) + public function testCatchException(bool $isDeveloper, int $setBodyCalls): void { /** @var Bootstrap|MockObject $bootstrap */ $bootstrap = $this->createMock(Bootstrap::class); @@ -212,41 +198,39 @@ public function testCatchException($isDeveloper, $setBodyCalls) /** @var Exception|MockObject $exception */ $exception = $this->createMock(Exception::class); - $this->responseMock->expects($this->once()) + $this->responseMock->expects(self::once()) ->method('setHttpResponseCode') ->with(404); - $bootstrap->expects($this->once()) + $bootstrap->expects(self::once()) ->method('isDeveloperMode') ->willReturn($isDeveloper); - $this->responseMock->expects($this->exactly($setBodyCalls)) + $this->responseMock->expects(self::exactly($setBodyCalls)) ->method('setBody'); - $this->responseMock->expects($this->once()) + $this->responseMock->expects(self::once()) ->method('sendResponse'); - $this->mediaModel = $this->getMediaModel(); - - $this->mediaModel->catchException($bootstrap, $exception); + $this->createMediaModel()->catchException($bootstrap, $exception); } - public function testExceptionWhenIsAllowedReturnsFalse() + public function testExceptionWhenIsAllowedReturnsFalse(): void { - $this->mediaModel = $this->getMediaModel(false); - $this->directoryMediaMock->expects($this->once()) + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->configMock->expects($this->once())->method('save'); + $this->configMock->expects(self::once()) + ->method('save'); $this->expectException(LogicException::class); $this->expectExceptionMessage('The path is not allowed: ' . self::RELATIVE_FILE_PATH); - $this->mediaModel->launch(); + $this->createMediaModel(false)->launch(); } /** * @return array */ - public function catchExceptionDataProvider() + public function catchExceptionDataProvider(): array { return [ 'default mode' => [false, 0], @@ -260,35 +244,30 @@ public function catchExceptionDataProvider() * @param bool $isAllowed * @return Media */ - protected function getMediaModel(bool $isAllowed = true): Media + protected function createMediaModel(bool $isAllowed = true): Media { - $objectManager = new ObjectManager($this); - $isAllowedCallback = function () use ($isAllowed) { return $isAllowed; }; - /** @var Media $mediaClass */ - $mediaClass = $objectManager->getObject( - Media::class, - [ - 'configFactory' => $this->configFactoryMock, - 'syncFactory' => $this->syncFactoryMock, - 'response' => $this->responseMock, - 'isAllowed' => $isAllowedCallback, - 'mediaDirectory' => false, - 'configCacheFile' => self::CACHE_FILE_PATH, - 'relativeFileName' => self::RELATIVE_FILE_PATH, - 'filesystem' => $this->filesystemMock, - 'placeholderFactory' => $this->createConfiguredMock( - PlaceholderFactory::class, - [ - 'create' => $this->createMock(Placeholder::class) - ] - ), - ] - ); + $placeholderFactory = $this->createMock(PlaceholderFactory::class); + $placeholderFactory->method('create') + ->willReturn($this->createMock(Placeholder::class)); - return $mediaClass; + return new Media( + $this->configFactoryMock, + $this->syncFactoryMock, + $this->responseMock, + $isAllowedCallback, + false, + self::CACHE_FILE_PATH, + self::RELATIVE_FILE_PATH, + $this->filesystemMock, + $placeholderFactory, + $this->createMock(State::class), + $this->createMock(ImageResize::class), + $this->createMock(Filesystem\Driver\File::class), + $this->createMock(CatalogMediaConfig::class) + ); } } diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php index e308f609a3d69..bc21700cadee0 100644 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php @@ -28,8 +28,8 @@ class RemoteStorageEnableCommand extends Command private const ARG_DRIVER = 'driver'; private const ARGUMENT_BUCKET = 'bucket'; private const ARGUMENT_REGION = 'region'; - private const ARGUMENT_ACCESS_KEY = 'access-key'; - private const ARGUMENT_SECRET_KEY = 'secret-key'; + private const OPTION_ACCESS_KEY = 'access-key'; + private const OPTION_SECRET_KEY = 'secret-key'; private const ARGUMENT_PREFIX = 'prefix'; private const OPTION_IS_PUBLIC = 'is-public'; @@ -66,8 +66,8 @@ protected function configure(): void ->addArgument(self::ARGUMENT_BUCKET, InputArgument::OPTIONAL, 'Bucket') ->addArgument(self::ARGUMENT_REGION, InputArgument::OPTIONAL, 'Region') ->addArgument(self::ARGUMENT_PREFIX, InputArgument::OPTIONAL, 'Prefix', '') - ->addArgument(self::ARGUMENT_ACCESS_KEY, InputArgument::OPTIONAL, 'Access key') - ->addArgument(self::ARGUMENT_SECRET_KEY, InputArgument::OPTIONAL, 'Secret key') + ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_OPTIONAL, 'Access key') + ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_OPTIONAL, 'Secret key') ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_REQUIRED, 'Is public', false); } @@ -105,8 +105,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]; $isPublic = (bool)$input->getOption(self::OPTION_IS_PUBLIC); - if (($key = (string)$input->getArgument(self::ARGUMENT_ACCESS_KEY)) - && ($secret = (string)$input->getArgument(self::ARGUMENT_SECRET_KEY)) + if (($key = (string)$input->getOption(self::OPTION_ACCESS_KEY)) + && ($secret = (string)$input->getOption(self::OPTION_SECRET_KEY)) ) { $config['credentials']['key'] = $key; $config['credentials']['secret'] = $secret; diff --git a/app/code/Magento/RemoteStorage/Driver/DriverException.php b/app/code/Magento/RemoteStorage/Driver/DriverException.php new file mode 100644 index 0000000000000..b35a7e7c4d4da --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Remote storage driver. + */ +class DriverException extends LocalizedException +{ +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php index ab7a1bcaa6cc5..5268cbaea4a77 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -7,8 +7,6 @@ namespace Magento\RemoteStorage\Driver; -use Magento\Framework\Filesystem\DriverInterface; - /** * Factory for drivers with additional configuration. */ @@ -19,7 +17,7 @@ interface DriverFactoryInterface * * @param array $config * @param string $prefix - * @return DriverInterface + * @return RemoteDriverInterface */ - public function create(array $config, string $prefix): DriverInterface; + public function create(array $config, string $prefix): RemoteDriverInterface; } diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php index aa4e057af5383..d13f599387d90 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php @@ -22,7 +22,7 @@ class DriverFactoryPool /** * @param DriverFactoryInterface[] $pool */ - public function __construct(array $pool) + public function __construct(array $pool = []) { $this->pool = $pool; } @@ -49,7 +49,7 @@ public function has(string $name): bool public function get(string $name): DriverFactoryInterface { if (!$this->has($name)) { - throw new RuntimeException(__('Factory %1 does not exist', $name)); + throw new RuntimeException(__('Driver "%1" does not exist', $name)); } return $this->pool[$name]; diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index a1e758f170e7e..731ec6686e657 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -20,7 +20,7 @@ class DriverPool implements DriverPoolInterface { public const PATH_DRIVER = 'remote_storage/driver'; - public const PATH_IS_PUBLIC = 'remote_storage/is_public'; + public const PATH_EXPOSE_URLS = 'remote_storage/expose_urls'; public const PATH_PREFIX = 'remote_storage/prefix'; public const PATH_CONFIG = 'remote_storage/config'; @@ -68,7 +68,7 @@ public function __construct( * Retrieves remote driver. * * @param string $code - * @return DriverInterface + * @return RemoteDriverInterface * @throws RuntimeException * @throws FileSystemException */ diff --git a/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php b/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php new file mode 100644 index 0000000000000..fc108bb388cb5 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Filesystem\ExtendedDriverInterface; + +/** + * Remote storage driver. + */ +interface RemoteDriverInterface extends ExtendedDriverInterface +{ + /** + * Test storage connection. + * + * @throws DriverException + */ + public function test(): void; +} diff --git a/app/code/Magento/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php index 36238b20b38bb..41fbfdde15bd0 100644 --- a/app/code/Magento/RemoteStorage/Model/Config.php +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -34,13 +34,13 @@ public function __construct(DeploymentConfig $config) /** * Retrieve driver name. * - * @return string|null + * @return string * @throws FileSystemException * @throws RuntimeException */ - public function getDriver(): ?string + public function getDriver(): string { - return $this->config->get(DriverPool::PATH_DRIVER, null); + return $this->config->get(DriverPool::PATH_DRIVER, BaseDriverPool::FILE); } /** @@ -52,23 +52,11 @@ public function getDriver(): ?string */ public function isEnabled(): bool { - $driver = $this->config->get(DriverPool::PATH_DRIVER); + $driver = $this->getDriver(); return $driver && $driver !== BaseDriverPool::FILE; } - /** - * Use remote URL for public URLs. - * - * @return bool - * @throws FileSystemException - * @throws RuntimeException - */ - public function isPublic(): bool - { - return (bool)$this->config->get(DriverPool::PATH_IS_PUBLIC, false); - } - /** * Retrieves config. * @@ -85,6 +73,7 @@ public function getConfig(): array * Retrieves prefix. * * @return string + * * @throws FileSystemException * @throws RuntimeException */ @@ -92,4 +81,16 @@ public function getPrefix(): string { return (string)$this->config->get(DriverPool::PATH_PREFIX, ''); } + + /** + * Retrieves value for exposing URLs. + * + * @return bool + * @throws FileSystemException + * @throws RuntimeException + */ + public function getExposeUrls(): bool + { + return (bool)$this->config->get(DriverPool::PATH_EXPOSE_URLS, false); + } } diff --git a/app/code/Magento/RemoteStorage/Plugin/Image.php b/app/code/Magento/RemoteStorage/Plugin/Image.php index 66c5fe1a5ac67..013e3fd23e168 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Image.php +++ b/app/code/Magento/RemoteStorage/Plugin/Image.php @@ -182,7 +182,7 @@ public function __destruct() * @return string * @throws FileSystemException */ - private function copyFileToTmp($filePath): string + private function copyFileToTmp(string $filePath): string { if ($this->fileExistsInTmp($filePath)) { return $filePath; diff --git a/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php index 59e21a9c237d0..12837545c533b 100644 --- a/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php +++ b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php @@ -23,6 +23,11 @@ */ class MediaStorage { + /** + * @var Filesystem + */ + private $filesystem; + /** * @var bool */ @@ -31,12 +36,12 @@ class MediaStorage /** * @var WriteInterface */ - private $remoteDir; + private $remoteDirectory; /** * @var WriteInterface */ - private $localDir; + private $localDirectory; /** * @param Config $config @@ -47,12 +52,13 @@ class MediaStorage public function __construct(Config $config, Filesystem $filesystem) { $this->isEnabled = $config->isEnabled(); - $this->remoteDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, RemoteDriverPool::REMOTE); - $this->localDir = $filesystem->getDirectoryWrite(DirectoryList::PUB, LocalDriverPool::FILE); + $this->remoteDirectory = $filesystem->getDirectoryWrite(DirectoryList::PUB, RemoteDriverPool::REMOTE); + $this->localDirectory = $filesystem->getDirectoryWrite(DirectoryList::PUB, LocalDriverPool::FILE); } /** * Download remote file + * * @param Synchronization $subject * @param string $relativeFileName * @return null @@ -60,21 +66,18 @@ public function __construct(Config $config, Filesystem $filesystem) * @throws ValidatorException * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function beforeSynchronize(Synchronization $subject, string $relativeFileName) + public function beforeSynchronize(Synchronization $subject, string $relativeFileName): void { - if ($this->isEnabled) { - if ($this->remoteDir->isExist($relativeFileName)) { - $file = $this->localDir->openFile($relativeFileName, 'w'); - try { - $file->lock(); - $file->write($this->remoteDir->readFile($relativeFileName)); - $file->unlock(); - $file->close(); - } catch (FileSystemException $e) { - $file->close(); - } + if ($this->isEnabled && $this->remoteDirectory->isExist($relativeFileName)) { + $file = $this->localDirectory->openFile($relativeFileName, 'w'); + try { + $file->lock(); + $file->write($this->remoteDirectory->readFile($relativeFileName)); + $file->unlock(); + $file->close(); + } catch (FileSystemException $e) { + $file->close(); } } - return null; } } diff --git a/app/code/Magento/RemoteStorage/Plugin/Scope.php b/app/code/Magento/RemoteStorage/Plugin/Scope.php index ab723fa1d0c19..6a05b63dee3a6 100644 --- a/app/code/Magento/RemoteStorage/Plugin/Scope.php +++ b/app/code/Magento/RemoteStorage/Plugin/Scope.php @@ -36,7 +36,7 @@ class Scope */ public function __construct(Config $config, Filesystem $filesystem) { - $this->isEnabled = $config->isEnabled() && $config->isPublic(); + $this->isEnabled = $config->isEnabled() && $config->getExposeUrls(); $this->filesystem = $filesystem; } diff --git a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php b/app/code/Magento/RemoteStorage/Plugin/Sitemap.php deleted file mode 100644 index 2e93949b40fce..0000000000000 --- a/app/code/Magento/RemoteStorage/Plugin/Sitemap.php +++ /dev/null @@ -1,67 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Plugin; - -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\RemoteStorage\Driver\DriverPool; -use Magento\RemoteStorage\Filesystem; -use Magento\RemoteStorage\Model\Config; -use Magento\Sitemap\Model\Sitemap as BaseSitemap; - -/** - * Plugin to replace file URL with remote URL. - */ -class Sitemap -{ - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var bool - */ - private $isEnabled; - - /** - * @param Filesystem $filesystem - * @param Config $config - */ - public function __construct(Filesystem $filesystem, Config $config) - { - $this->filesystem = $filesystem; - $this->isEnabled = $config->isEnabled(); - } - - /** - * Modifies image URl to point to correct remote storage. - * - * @param BaseSitemap $subject - * @param string $result - * @param string $sitemapPath - * @param string $sitemapFileName - * @return string - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterGetSitemapUrl( - BaseSitemap $subject, - string $result, - string $sitemapPath, - string $sitemapFileName - ): string { - if ($this->isEnabled) { - $path = trim($sitemapPath . $sitemapFileName, '/'); - - return $this->filesystem->getDirectoryRead(DirectoryList::ROOT, DriverPool::REMOTE) - ->getAbsolutePath($path); - } - - return $result; - } -} diff --git a/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php b/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..b625661479962 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php @@ -0,0 +1,201 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverFactoryPool; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\TextConfigOption; +use Psr\Log\LoggerInterface; + +/** + * Remote storage options. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + private const OPTION_REMOTE_STORAGE_DRIVER = 'remote-storage-driver'; + private const CONFIG_PATH__REMOTE_STORAGE_DRIVER = RemoteDriverPool::PATH_DRIVER; + private const OPTION_REMOTE_STORAGE_PREFIX = 'remote-storage-prefix'; + private const CONFIG_PATH__REMOTE_STORAGE_PREFIX = RemoteDriverPool::PATH_PREFIX; + private const OPTION_REMOTE_STORAGE_BUCKET = 'remote-storage-bucket'; + private const CONFIG_PATH__REMOTE_STORAGE_BUCKET = RemoteDriverPool::PATH_CONFIG . '/bucket'; + private const OPTION_REMOTE_STORAGE_REGION = 'remote-storage-region'; + private const CONFIG_PATH__REMOTE_STORAGE_REGION = RemoteDriverPool::PATH_CONFIG . '/region'; + private const OPTION_REMOTE_STORAGE_ACCESS_KEY = 'remote-storage-key'; + private const CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY = RemoteDriverPool::PATH_CONFIG . '/credentials/key'; + private const OPTION_REMOTE_STORAGE_SECRET_KEY = 'remote-storage-secret'; + private const CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY = RemoteDriverPool::PATH_CONFIG . '/credentials/secret'; + + /** + * Map of option to config path relations. + * + * @var string[] + */ + private static $map = [ + self::OPTION_REMOTE_STORAGE_PREFIX => self::CONFIG_PATH__REMOTE_STORAGE_PREFIX, + self::OPTION_REMOTE_STORAGE_BUCKET => self::CONFIG_PATH__REMOTE_STORAGE_BUCKET, + self::OPTION_REMOTE_STORAGE_REGION => self::CONFIG_PATH__REMOTE_STORAGE_REGION, + self::OPTION_REMOTE_STORAGE_ACCESS_KEY => self::CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY, + self::OPTION_REMOTE_STORAGE_SECRET_KEY => self::CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY + ]; + + /** + * @var DriverFactoryPool + */ + private $driverFactoryPool; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param DriverFactoryPool $driverFactoryPool + * @param LoggerInterface $logger + */ + public function __construct(DriverFactoryPool $driverFactoryPool, LoggerInterface $logger) + { + $this->driverFactoryPool = $driverFactoryPool; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function getOptions(): array + { + return [ + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_DRIVER, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, + 'Remote storage driver', + DriverPool::FILE + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_PREFIX, + 'Remote storage prefix', + '' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_BUCKET, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_BUCKET, + 'Remote storage bucket' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_REGION, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_REGION, + 'Remote storage region' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_ACCESS_KEY, + TextConfigOption::FRONTEND_WIZARD_PASSWORD, + self::CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY, + 'Remote storage access key', + '' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_SECRET_KEY, + TextConfigOption::FRONTEND_WIZARD_PASSWORD, + self::CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY, + 'Remote storage secret key', + '' + ) + ]; + } + + /** + * @inheritDoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $driver = $options[self::OPTION_REMOTE_STORAGE_DRIVER] ?? DriverPool::FILE; + + if ($driver === DriverPool::FILE) { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $configData->set(self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, $driver); + } else { + $configData = $this->createConfigData($driver, $options); + } + + return [$configData]; + } + + /** + * @inheritDoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig): array + { + $driver = $options[self::OPTION_REMOTE_STORAGE_DRIVER] ?? DriverPool::FILE; + + if ($driver === DriverPool::FILE) { + return []; + } + + $errors = []; + + if (empty($options[self::OPTION_REMOTE_STORAGE_REGION])) { + $errors[] = 'Region is required'; + } + + if (empty($options[self::OPTION_REMOTE_STORAGE_BUCKET])) { + $errors[] = 'Bucket is required'; + } + + if (!$errors) { + $configData = $this->createConfigData($driver, $options); + + try { + $this->driverFactoryPool->get($driver)->create( + $configData->getData()['remote_storage']['config'], + $options[self::OPTION_REMOTE_STORAGE_PREFIX] + )->test(); + } catch (LocalizedException $exception) { + $message = $exception->getMessage(); + + $this->logger->critical($message); + + $errors[] = 'Adapter error: ' . $message; + } + } + + return $errors; + } + + /** + * Creates pre-configured config data object. + * + * @param string $driver + * @param array $options + * @return ConfigData + */ + private function createConfigData(string $driver, array $options): ConfigData + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $configData->set(self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, $driver); + + foreach (self::$map as $option => $configPath) { + if (!empty($options[$option])) { + $configData->set($configPath, $options[$option]); + } + } + + return $configData; + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json index fa27e821c817c..7345048a159e3 100644 --- a/app/code/Magento/RemoteStorage/composer.json +++ b/app/code/Magento/RemoteStorage/composer.json @@ -10,6 +10,7 @@ "magento/module-sitemap": "*", "magento/module-cms": "*", "magento/module-downloadable": "*", + "magento/module-catalog": "*", "magento/module-media-storage": "*", "magento/module-import-export": "*", "magento/module-catalog-import-export": "*", diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..5009a05d8b602 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_storage_configuration"> + <field id="media_storage"> + <comment><![CDATA[<strong style="color:red">Warning!</strong> Database media storage will be ignored if remote storage is enabled.]]></comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index bb253bb5d18f7..9684a3ac49dbc 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -43,7 +43,6 @@ <arguments> <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> - <plugin name="remote_sitemap" type="Magento\RemoteStorage\Plugin\Sitemap" /> </type> <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Save"> <arguments> @@ -60,9 +59,6 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> - <type name="Magento\Framework\Url\ScopeInterface"> - <plugin name="remote_url" type="Magento\RemoteStorage\Plugin\Scope" /> - </type> <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> @@ -77,7 +73,7 @@ </arguments> </type> <type name="Magento\MediaStorage\Model\File\Storage\Synchronization"> - <plugin name="remote_media" type="Magento\RemoteStorage\Plugin\MediaStorage" /> + <plugin name="remoteMedia" type="Magento\RemoteStorage\Plugin\MediaStorage" /> </type> <type name="Magento\Framework\Data\Collection\Filesystem"> <arguments> @@ -97,6 +93,14 @@ <type name="Magento\Framework\Image\Adapter\AbstractAdapter"> <plugin name="remoteImageFile" type="Magento\RemoteStorage\Plugin\Image" sortOrder="10"/> </type> + <type name="Magento\Catalog\Model\Category\FileInfo"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\Url\ScopeInterface"> + <plugin name="remoteUrl" type="Magento\RemoteStorage\Plugin\Scope"/> + </type> <type name="Magento\ImportExport\Model\Import"> <arguments> <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> diff --git a/app/code/Magento/Sitemap/etc/config.xml b/app/code/Magento/Sitemap/etc/config.xml index 36b2cc2207422..614421b9dd752 100644 --- a/app/code/Magento/Sitemap/etc/config.xml +++ b/app/code/Magento/Sitemap/etc/config.xml @@ -57,5 +57,12 @@ </jobs> </default> </crontab> + <system> + <media_storage_configuration> + <allowed_resources> + <sitemap_folder>sitemap</sitemap_folder> + </allowed_resources> + </media_storage_configuration> + </system> </default> </config> diff --git a/app/code/Magento/Sitemap/etc/di.xml b/app/code/Magento/Sitemap/etc/di.xml index 4c4a5f98f737a..4771da2f11144 100644 --- a/app/code/Magento/Sitemap/etc/di.xml +++ b/app/code/Magento/Sitemap/etc/di.xml @@ -52,4 +52,16 @@ <argument name="configReader" xsi:type="object">Magento\Sitemap\Model\ItemProvider\CmsPageConfigReader</argument> </arguments> </type> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="dirs" xsi:type="array"> + <item name="exclude" xsi:type="array"> + <item name="sitemap" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">media[/\\]+sitemap[/\\]*$</item> + </item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index 3ef113fa63fa7..143889364781f 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -160,7 +160,7 @@ public function afterLoad() 'size' => is_array($stat) ? $stat['size'] : 0, //phpcs:ignore Magento2.Functions.DiscouragedFunction 'name' => basename($value), - 'type' => $stat['mimetype'] ?? $this->getMimeType($fileName), + 'type' => $this->getMimeType($fileName), 'exists' => true, ] ]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html index ade4f52d5153f..518926ed52d69 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html @@ -1,3 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php index 363a48d822ace..d20604e27e5c9 100644 --- a/lib/internal/Magento/Framework/Config/DocumentRoot.php +++ b/lib/internal/Magento/Framework/Config/DocumentRoot.php @@ -10,7 +10,7 @@ /** * Document root detector. - * @deprecared Magento always uses the pub directory + * @deprecated Magento always uses the pub directory * @api */ class DocumentRoot diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index be00cb9f64c18..9b16687620e8f 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -131,11 +131,6 @@ class Filesystem extends \Magento\Framework\Data\Collection */ protected $_collectedFiles = []; - /** - * @var \Magento\Framework\Filesystem - */ - private $filesystem; - /** * @var WriteInterface */ @@ -150,7 +145,8 @@ public function __construct( \Magento\Framework\Filesystem $filesystem = null ) { $this->_entityFactory = $_entityFactory ?? ObjectManager::getInstance()->get(EntityFactoryInterface::class); - $this->filesystem = $filesystem ?? ObjectManager::getInstance()->get(\Magento\Framework\Filesystem::class); + + $filesystem = $filesystem ?? ObjectManager::getInstance()->get(\Magento\Framework\Filesystem::class); $this->rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); parent::__construct($this->_entityFactory); } diff --git a/lib/internal/Magento/Framework/File/Mime.php b/lib/internal/Magento/Framework/File/Mime.php index fe23969f32ce3..d61f5054990e8 100644 --- a/lib/internal/Magento/Framework/File/Mime.php +++ b/lib/internal/Magento/Framework/File/Mime.php @@ -7,10 +7,15 @@ namespace Magento\Framework\File; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; /** * Utility for mime type retrieval + * + * @deprecated + * @see Filesystem\ExtendedDriverInterface::getMetadata() */ class Mime { @@ -18,79 +23,52 @@ class Mime * Mime types * * @var array + * + * @deprecated */ protected $mimeTypes = [ - 'txt' => 'text/plain', - 'htm' => 'text/html', + 'txt' => 'text/plain', + 'htm' => 'text/html', 'html' => 'text/html', - 'php' => 'text/html', - 'css' => 'text/css', - 'js' => 'application/javascript', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', 'json' => 'application/json', - 'xml' => 'application/xml', - 'swf' => 'application/x-shockwave-flash', - 'flv' => 'video/x-flv', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', // images - 'png' => 'image/png', - 'jpe' => 'image/jpeg', + 'png' => 'image/png', + 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'gif' => 'image/gif', - 'bmp' => 'image/bmp', - 'ico' => 'image/vnd.microsoft.icon', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', 'tiff' => 'image/tiff', - 'tif' => 'image/tiff', - 'svg' => 'image/svg+xml', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', // archives - 'zip' => 'application/zip', - 'rar' => 'application/x-rar-compressed', - 'exe' => 'application/x-msdownload', - 'msi' => 'application/x-msdownload', - 'cab' => 'application/vnd.ms-cab-compressed', + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', // audio/video - 'mp3' => 'audio/mpeg', - 'qt' => 'video/quicktime', - 'mov' => 'video/quicktime', + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', // adobe - 'pdf' => 'application/pdf', - 'psd' => 'image/vnd.adobe.photoshop', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', - ]; - - /** - * List of mime types that can be defined by file extension. - * - * @var array - */ - private $defineByExtensionList = [ - 'txt' => 'text/plain', - 'htm' => 'text/html', - 'html' => 'text/html', - 'php' => 'text/html', - 'css' => 'text/css', - 'js' => 'application/javascript', - 'json' => 'application/json', - 'xml' => 'application/xml', - 'svg' => 'image/svg+xml', - ]; - - /** - * List of generic MIME types - * - * The file mime type should be detected by the file's extension if the native mime type is one of the listed below. - * - * @var array - */ - private $genericMimeTypes = [ - 'application/x-empty', - 'inode/x-empty', + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', ]; /** @@ -99,81 +77,48 @@ class Mime private $filesystem; /** - * Mime constructor. - * @param Filesystem $filesystem + * @param Filesystem|null $filesystem */ public function __construct(Filesystem $filesystem = null) { - $this->filesystem = $filesystem; + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** - * Get mime type of a file + * Get mime type of a file. * * @param string $file * @return string - * @throws \InvalidArgumentException + * @throws FileSystemException + * + * @deprecated */ public function getMimeType($file) { - $directoryRead = $this->filesystem->getDirectoryRead(DirectoryList::ROOT); - $fileExistsLocally = file_exists($file); - if (!$fileExistsLocally && !$directoryRead->isExist($file)) { - throw new \InvalidArgumentException("File '$file' doesn't exist"); + $driver = $this->filesystem->getDirectoryWrite( + DirectoryList::ROOT, + Filesystem\DriverPool::FILE + )->getDriver(); + + /** + * Try with non-local driver. + */ + if (!$driver->isExists($file)) { + $driver = $this->filesystem->getDirectoryWrite( + DirectoryList::ROOT + )->getDriver(); } - $result = null; - $extension = $this->getFileExtension($file); - - if (function_exists('mime_content_type') && $fileExistsLocally) { - $result = $this->getNativeMimeType($file); - } else { - $imageInfo = getimagesize($file); - $result = $imageInfo['mime']; + if (!$driver->isExists($file)) { + throw new FileSystemException(__("File '$file' doesn't exist")); } - if (null === $result && isset($this->mimeTypes[$extension])) { - $result = $this->mimeTypes[$extension]; - } elseif (null === $result) { - $result = 'application/octet-stream'; + if ($driver instanceof Filesystem\ExtendedDriverInterface) { + return $driver->getMetadata($file)['mimetype']; } - return $result; - } + $mime = new Filesystem\Driver\File\Mime(); - /** - * Get mime type by the native mime_content_type function. - * - * Search for extended mime type if mime_content_type() returned 'application/octet-stream' or 'text/plain' - * - * @param string $file - * @return string - */ - private function getNativeMimeType(string $file): string - { - $extension = $this->getFileExtension($file); - $result = mime_content_type($file); - if (isset($this->mimeTypes[$extension], $this->defineByExtensionList[$extension]) - && ( - strpos($result, 'text/') === 0 - || strpos($result, 'image/svg') === 0 - || in_array($result, $this->genericMimeTypes, true) - ) - ) { - $result = $this->mimeTypes[$extension]; - } - - return $result; - } - - /** - * Get file extension by file name. - * - * @param string $file - * @return string - */ - private function getFileExtension(string $file): string - { - return strtolower(pathinfo($file, PATHINFO_EXTENSION)); + return $mime->getMimeType($file); } } diff --git a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php index ff70f0fb9b0c9..db42e03363236 100644 --- a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php +++ b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php @@ -7,15 +7,16 @@ namespace Magento\Framework\File\Test\Unit; -use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test mime type utility for correct + * Test mime type utility for correct. + * + * @deprecated */ class MimeTest extends TestCase { @@ -25,31 +26,64 @@ class MimeTest extends TestCase private $object; /** - * @var ReadInterface|MockObject + * @var Filesystem\DriverInterface|MockObject */ - private $readInterface; + private $localDriverMock; + + /** + * @var Filesystem\DriverInterface|MockObject + */ + private $remoteDriverMock; + + /** + * @var Filesystem|MockObject + */ + private $filesystemMock; + + /** + * @var Filesystem\Directory\WriteInterface|MockObject + */ + private $localDirectoryMock; + + /** + * @var Filesystem\Directory\WriteInterface|MockObject + */ + private $remoteDirectoryMock; /** * @inheritDoc */ protected function setUp(): void { - $this->readInterface = $this->getMockBuilder(ReadInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $this->localDriverMock = $this->getMockForAbstractClass(Filesystem\DriverInterface::class); + $this->remoteDriverMock = $this->getMockForAbstractClass(Filesystem\ExtendedDriverInterface::class); + + $this->localDirectoryMock = $this->getMockForAbstractClass(Filesystem\Directory\WriteInterface::class); + $this->localDirectoryMock->method('getDriver') + ->willReturn($this->localDriverMock); + $this->remoteDirectoryMock = $this->getMockForAbstractClass(Filesystem\Directory\WriteInterface::class); + $this->remoteDirectoryMock->method('getDriver') + ->willReturn($this->remoteDriverMock); + /** @var Filesystem|MockObject $filesystem */ - $filesystem = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $filesystem->expects(self::any())->method('getDirectoryRead')->with(DirectoryList::ROOT) - ->willReturn($this->readInterface); - $this->object = new Mime($filesystem); + $this->filesystemMock = $this->createMock(Filesystem::class); + + $this->object = new Mime($this->filesystemMock); } public function testGetMimeTypeNonexistentFileException(): void { - $this->expectException('InvalidArgumentException'); + $this->expectException(FileSystemException::class); $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); + + $this->filesystemMock->method('getDirectoryWrite')->willReturn( + $this->localDirectoryMock + ); + $this->localDriverMock->expects(self::exactly(2)) + ->method('isExists') + ->with('nonexistent.file') + ->willReturn(true); + $file = 'nonexistent.file'; $this->object->getMimeType($file); } @@ -62,6 +96,14 @@ public function testGetMimeTypeNonexistentFileException(): void */ public function testGetMimeType($file, $expectedType): void { + $this->filesystemMock->method('getDirectoryWrite')->willReturn( + $this->localDirectoryMock + ); + $this->localDriverMock->expects(self::exactly(2)) + ->method('isExists') + ->with($file) + ->willReturn(true); + $actualType = $this->object->getMimeType($file); self::assertSame($expectedType, $actualType); } @@ -79,4 +121,29 @@ public function getMimeTypeDataProvider(): array 'tmp file mime type' => [__DIR__ . '/_files/magento', 'image/jpeg'], ]; } + + /** + * @param string $file + * @param string $expectedType + * + * @dataProvider getMimeTypeDataProvider + */ + public function testGetMimeTypeRemote($file, $expectedType): void + { + $this->filesystemMock->method('getDirectoryWrite')->willReturnOnConsecutiveCalls( + $this->localDirectoryMock, + $this->remoteDirectoryMock + ); + $this->localDriverMock->method('isExists') + ->willReturn(false); + $this->remoteDriverMock->expects(self::once()) + ->method('isExists') + ->with($file) + ->willReturn(true); + $this->remoteDriverMock->method('getMetadata') + ->willReturn(['mimetype' => $expectedType]); + + $actualType = $this->object->getMimeType($file); + self::assertSame($expectedType, $actualType); + } } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 1fdde276e4e51..07a0d1345a301 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -9,7 +9,9 @@ namespace Magento\Framework\Filesystem\Driver; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Driver\File\Mime; use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\ExtendedDriverInterface; use Magento\Framework\Filesystem\Glob; use Magento\Framework\Phrase; @@ -20,7 +22,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -class File implements DriverInterface +class File implements ExtendedDriverInterface { /** * @var string @@ -81,6 +83,26 @@ public function stat($path) return $result; } + /** + * @inheritDoc + */ + public function getMetadata(string $path): array + { + $fileInfo = new \SplFileInfo($path); + $mime = new Mime(); + + return [ + 'path' => $fileInfo->getPath(), + 'basename' => $fileInfo->getBasename('.' . $fileInfo->getExtension()), + 'extension' => $fileInfo->getExtension(), + 'filename' => $fileInfo->getFilename(), + 'dirname' => dirname($fileInfo->getFilename()), + 'timestamp' => $fileInfo->getMTime(), + 'size' => $fileInfo->getSize(), + 'mimetype' => $mime->getMimeType($path) + ]; + } + /** * Check permissions for reading file or directory * @@ -300,12 +322,13 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) { $result = false; $targetDriver = $targetDriver ?: $this; - if (get_class($targetDriver) == get_class($this)) { + if (get_class($targetDriver) === get_class($this)) { $result = @rename($this->getScheme() . $oldPath, $newPath); + $this->changePermissions($newPath, 0777 & ~umask()); } else { $content = $this->fileGetContents($oldPath); if (false !== $targetDriver->filePutContents($newPath, $content)) { - $result = $this->deleteFile($oldPath); + $result = $this->isFile($oldPath) ? $this->deleteFile($oldPath) : true; } } if (!$result) { diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php b/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php new file mode 100644 index 0000000000000..4634e3dd1018d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Driver\File; + +use Magento\Framework\Exception\FileSystemException; + +/** + * Mime type resolver. + */ +class Mime +{ + /** + * Mime types + * + * @var array + */ + private $mimeTypes = [ + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + ]; + + /** + * List of mime types that can be defined by file extension. + * + * @var array + */ + private $defineByExtensionList = [ + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'svg' => 'image/svg+xml', + ]; + + /** + * List of generic MIME types + * + * The file mime type should be detected by the file's extension if the native mime type is one of the listed below. + * + * @var array + */ + private $genericMimeTypes = [ + 'application/x-empty', + 'inode/x-empty', + ]; + + /** + * Get mime type of a file + * + * @param string $path Absolute file path + * @return string + * @throws FileSystemException + */ + public function getMimeType(string $path): string + { + if (!file_exists($path)) { + throw new FileSystemException(__("File '$path' doesn't exist")); + } + + $result = null; + $extension = $this->getFileExtension($path); + + if (function_exists('mime_content_type')) { + $result = $this->getNativeMimeType($path); + } else { + $imageInfo = getimagesize($path); + $result = $imageInfo['mime']; + } + + if (null === $result && isset($this->mimeTypes[$extension])) { + $result = $this->mimeTypes[$extension]; + } elseif (null === $result) { + $result = 'application/octet-stream'; + } + + return $result; + } + + /** + * Get mime type by the native mime_content_type function. + * + * Search for extended mime type if mime_content_type() returned 'application/octet-stream' or 'text/plain' + * + * @param string $file + * @return string + */ + private function getNativeMimeType(string $file): string + { + $extension = $this->getFileExtension($file); + $result = mime_content_type($file); + if (isset($this->mimeTypes[$extension], $this->defineByExtensionList[$extension]) + && ( + strpos($result, 'text/') === 0 + || strpos($result, 'image/svg') === 0 + || in_array($result, $this->genericMimeTypes, true) + ) + ) { + $result = $this->mimeTypes[$extension]; + } + + return $result; + } + + /** + * Get file extension by file name. + * + * @param string $path + * @return string + */ + private function getFileExtension(string $path): string + { + return strtolower(pathinfo($path, PATHINFO_EXTENSION)); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php new file mode 100644 index 0000000000000..a93d242dbe15a --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem; + +/** + * Provides extension for Driver interface. + * + * @see DriverInterface + * + * @deprecated Method will be moved to DriverInterface + * @see DriverInterface + */ +interface ExtendedDriverInterface extends DriverInterface +{ + /** + * Retrieve file metadata. + * + * Implementation must return associative array with next keys: + * + * ```php + * [ + * 'path', + * 'dirname', + * 'basename', + * 'extension', + * 'filename', + * 'timestamp', + * 'size', + * 'mimetype', + * ]; + * + * @param string $path Absolute path to file + * @return array + * + * @deprecated Method will be moved to DriverInterface + */ + public function getMetadata(string $path): array; +} diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php new file mode 100644 index 0000000000000..4e34d497d86af --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Test\Unit\Driver\File; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Driver\File\Mime; +use PHPUnit\Framework\TestCase; + +/** + * @see Mime + */ +class MimeTest extends TestCase +{ + /** + * @var Mime + */ + private $mime; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->mime = new Mime(); + } + + public function testGetMimeTypeNonexistentFileException(): void + { + $this->expectException(FileSystemException::class); + $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); + + $file = 'nonexistent.file'; + $this->mime->getMimeType($file); + } + + /** + * @param string $file + * @param string $expectedType + * @throws FileSystemException + * + * @dataProvider getMimeTypeDataProvider + */ + public function testGetMimeType(string $file, string $expectedType): void + { + $actualType = $this->mime->getMimeType($file); + self::assertSame($expectedType, $actualType); + } + + /** + * @return array + */ + public function getMimeTypeDataProvider(): array + { + return [ + 'javascript' => [__DIR__ . '/_files/javascript.js', 'application/javascript'], + 'weird extension' => [__DIR__ . '/_files/file.weird', 'application/octet-stream'], + 'weird uppercase extension' => [__DIR__ . '/_files/UPPERCASE.WEIRD', 'application/octet-stream'], + 'generic mime type' => [__DIR__ . '/_files/blank.html', 'text/html'], + 'tmp file mime type' => [__DIR__ . '/_files/magento', 'image/jpeg'], + ]; + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD new file mode 100644 index 0000000000000..b361f47e9c25d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD @@ -0,0 +1 @@ +� diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html new file mode 100644 index 0000000000000..2b699a9062611 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html @@ -0,0 +1,6 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird new file mode 100644 index 0000000000000..b361f47e9c25d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird @@ -0,0 +1 @@ +� diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js new file mode 100644 index 0000000000000..d168db0daa734 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js @@ -0,0 +1,5 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +var test = 10; diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento new file mode 100644 index 0000000000000000000000000000000000000000..c377daf8fb0b390d89aa4f6f111715fc9118ca1b GIT binary patch literal 55303 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 Hh5vs5{BvNq literal 0 HcmV?d00001 diff --git a/nginx.conf.sample b/nginx.conf.sample index 296f9fafd0a35..2dbba68c39c39 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -145,6 +145,7 @@ location /media/ { # # # Replace placeholders and uncomment the line below to serve product images from public S3 # # See examples of S3 authentication at https://github.com/anomalizer/ngx_aws_auth +# # resolver 8.8.8.8; # # proxy_pass https://<bucket-name>.<region-name>.amazonaws.com; # # set $width "-"; diff --git a/pub/get.php b/pub/get.php index 215a83b74fbca..c59365c98727c 100644 --- a/pub/get.php +++ b/pub/get.php @@ -43,13 +43,16 @@ // Serve file if it's materialized if ($mediaDirectory) { - if (!$isAllowed($relativePath, $allowedResources)) { + $fileAbsolutePath = __DIR__ . '/' . $relativePath; + $fileRelativePath = str_replace(rtrim($mediaDirectory, '/') . '/', '', $fileAbsolutePath); + + if (!$isAllowed($fileRelativePath, $allowedResources)) { require_once 'errors/404.php'; exit; } - $mediaAbsPath = $mediaDirectory . '/' . $relativePath; - if (is_readable($mediaAbsPath)) { - if (is_dir($mediaAbsPath)) { + + if (is_readable($fileAbsolutePath)) { + if (is_dir($fileAbsolutePath)) { require_once 'errors/404.php'; exit; } @@ -57,7 +60,7 @@ new \Magento\Framework\HTTP\PhpEnvironment\Response(), new \Magento\Framework\File\Mime() ); - $transfer->send($mediaAbsPath); + $transfer->send($fileAbsolutePath); exit; } } diff --git a/pub/media/sitemap/.htaccess b/pub/media/sitemap/.htaccess new file mode 100644 index 0000000000000..b97408bad3f2e --- /dev/null +++ b/pub/media/sitemap/.htaccess @@ -0,0 +1,7 @@ +<IfVersion < 2.4> + order allow,deny + deny from all +</IfVersion> +<IfVersion >= 2.4> + Require all denied +</IfVersion> From b47713413edc980fbdfb31d63a7f82d3e3504d8d Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 28 Oct 2020 12:59:32 +0200 Subject: [PATCH 0951/1013] MC-38651: [MFTF] AdminMediaGalleryAssertUsedInLinkPagesGridTest failed because of bad design --- ...inCmsPageFillOutBasicFieldsActionGroup.xml | 30 ++++++++ ...nSaveAndContinueEditCmsPageActionGroup.xml | 22 ++++++ .../SaveAndContinueEditCmsPageActionGroup.xml | 2 +- ...FillOutCustomCMSPageContentActionGroup.xml | 2 +- ...iaGalleryAssertUsedInLinkPagesGridTest.xml | 7 +- ...GalleryAssertUsedInLinkedPagesGridTest.xml | 75 ++++++++++++++++++ ...ediaGalleryCloseViewDetailsActionGroup.xml | 4 +- ...ediaGalleryDeletedAllImagesActionGroup.xml | 26 +++++++ ...ncedMediaGalleryUploadImageActionGroup.xml | 1 + ...ediaGalleryViewImageDetailsActionGroup.xml | 2 +- ...eryAssertFolderDoesNotExistActionGroup.xml | 4 +- ...ediaGalleryAssertFolderNameActionGroup.xml | 2 +- ...minMediaGalleryFolderDeleteActionGroup.xml | 4 +- ...minMediaGalleryFolderSelectActionGroup.xml | 8 +- ...ediaGalleryFromPageNoEditorActionGroup.xml | 2 +- .../Test/Mftf/Helper/MediaGalleryUiHelper.php | 77 +++++++++++++++++++ ...nhancedMediaGalleryImageActionsSection.xml | 2 +- ...nEnhancedMediaGalleryMassActionSection.xml | 1 + ...EnhancedMediaGalleryViewDetailsSection.xml | 3 +- .../AdminMediaGalleryFolderSection.xml | 8 +- .../AdminMediaGalleryMessagesSection.xml | 11 +++ 21 files changed, 273 insertions(+), 20 deletions(-) create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsPageFillOutBasicFieldsActionGroup.xml create mode 100644 app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSaveAndContinueEditCmsPageActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsPageFillOutBasicFieldsActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsPageFillOutBasicFieldsActionGroup.xml new file mode 100644 index 0000000000000..52b4cee37b03c --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminCmsPageFillOutBasicFieldsActionGroup.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="AdminCmsPageFillOutBasicFieldsActionGroup"> + <annotations> + <description>Fills out the Page details (Page Title, Content and URL Key) on the Admin Page creation/edit page.</description> + </annotations> + <arguments> + <argument name="title" type="string" defaultValue="{{_defaultCmsPage.title}}"/> + <argument name="contentHeading" type="string" defaultValue="{{_defaultCmsPage.content_heading}}"/> + <argument name="content" type="string" defaultValue="{{_defaultCmsPage.content}}"/> + <argument name="urlKey" type="string" defaultValue="{{_defaultCmsPage.identifier}}"/> + </arguments> + + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{title}}" stepKey="fillTitle"/> + <conditionalClick selector="{{CmsNewPagePageContentSection.header}}" dependentSelector="{{CmsNewPagePageContentSection.contentHeading}}" visible="false" stepKey="expandContentTabIfCollapsed"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{contentHeading}}" stepKey="fillContentHeading"/> + <scrollTo selector="{{CmsNewPagePageContentSection.content}}" stepKey="scrollToPageContent"/> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{content}}" stepKey="fillContent"/> + <conditionalClick selector="{{CmsNewPagePageSeoSection.header}}" dependentSelector="{{CmsNewPagePageSeoSection.urlKey}}" visible="false" stepKey="clickExpandSearchEngineOptimisationIfCollapsed"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{urlKey}}" stepKey="fillUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSaveAndContinueEditCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSaveAndContinueEditCmsPageActionGroup.xml new file mode 100644 index 0000000000000..5c8b390b59ba0 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSaveAndContinueEditCmsPageActionGroup.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="AdminSaveAndContinueEditCmsPageActionGroup"> + <annotations> + <description>Clicks on the Save and Continue button and see success message.</description> + </annotations> + + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <waitForElementVisible selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="waitForSaveAndContinueButton"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSaveAndContinueButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the page." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SaveAndContinueEditCmsPageActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SaveAndContinueEditCmsPageActionGroup.xml index a8dce19153a98..d5ccb4e1c1e71 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/SaveAndContinueEditCmsPageActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/SaveAndContinueEditCmsPageActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SaveAndContinueEditCmsPageActionGroup"> <annotations> - <description>Clicks on the Save and Continue button.</description> + <description>DEPRECATED. Use AdminSaveAndContinueEditCmsPageActionGroup instead. Clicks on the Save and Continue button.</description> </annotations> <waitForElementVisible time="10" selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="waitForSaveAndContinueVisibility"/> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml index f0938016d12f1..e0ec79394fdda 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml @@ -10,7 +10,7 @@ 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> + <description>DEPRECATED. Use AdminCmsPageFillOutBasicFieldsActionGroup and other AGs from CMS module. Fills out the Page details (Page Title, Content and URL Key)</description> </annotations> <arguments> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml index 9b37a84122227..c29ddbae0eb52 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkPagesGridTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryAssertUsedInLinkPagesGridTest"> + <test name="AdminMediaGalleryAssertUsedInLinkPagesGridTest" deprecated="Use AdminMediaGalleryAssertUsedInLinkedPagesGridTest instead"> <annotations> <features value="AdminMediaGalleryUsedInBlocksFilter"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> - <title value="Used in pages link"/> + <title value="DEPRECATED. Used in pages link"/> <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 pages"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryAssertUsedInLinkedPagesGridTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml new file mode 100644 index 0000000000000..2bf229d2a65d3 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * 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="AdminMediaGalleryAssertUsedInLinkedPagesGridTest"> + <annotations> + <features value="MediaGalleryCmsUi"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <title value="Used in pages link"/> + <description value="User filters assets used in pages"/> + <severity value="CRITICAL"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + </before> + + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> + <actionGroup ref="AdminCmsPageFillOutBasicFieldsActionGroup" stepKey="fillBasicPageFields"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + <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="AdminSaveAndContinueEditCmsPageActionGroup" stepKey="saveCmsPageAndContinue"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFiltersAgain"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInPages"> + <argument name="entityName" value="Pages"/> + </actionGroup> + <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> + <argument name="urlKey" value="{{_defaultCmsPage.identifier}}"/> + </actionGroup> + + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersInPageGrid"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFiltersAndAgain"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderAgain"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml index 3754eb319da44..effb574853bb7 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml @@ -10,10 +10,10 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup"> <annotations> - <description>Closes View Details panel</description> + <description>Closes View Details panel of Media Gallery image.</description> </annotations> <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.cancel}}" stepKey="clickCancel"/> - <wait time="1" stepKey="waitForElementRender"/> + <waitForElementNotVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.modalTitle}}" stepKey="waitForElementRender"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml new file mode 100644 index 0000000000000..4aa460327578a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeletedAllImagesActionGroup.xml @@ -0,0 +1,26 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <actionGroup name="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup"> + <annotations> + <description>Open Media Gallery page and delete all images</description> + </annotations> + <amOnPage url="{{AdminStandaloneMediaGalleryPage.url}}" stepKey="openMediaGalleryPage"/> + <!-- 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\MediaGalleryUi\Test\Mftf\Helper\MediaGalleryUiHelper" method="deleteAllImagesUsingMassAction" stepKey="deleteAllImagesUsingMassAction"> + <argument name="emptyRow">{{AdminMediaGalleryGridSection.noDataMessage}}</argument> + <argument name="deleteImagesButton">{{AdminEnhancedMediaGalleryMassActionSection.deleteImages}}</argument> + <argument name="checkImage">{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckboxAll}}</argument> + <argument name="deleteSelectedButton">{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}</argument> + <argument name="modalAcceptButton">{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}</argument> + <argument name="successMessageContainer">{{AdminMediaGalleryMessagesSection.success}}</argument> + <argument name="successMessage">been successfully deleted</argument> + </helper> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml index 053a1185b3fda..9a9c09cda9ab2 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml @@ -17,6 +17,7 @@ <argument name="image"/> </arguments> + <waitForPageLoad stepKey="waitForPageFullyLoaded"/> <attachFile selector="{{AdminEnhancedMediaGalleryActionsSection.upload}}" userInput="{{image.value}}" stepKey="uploadImage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml index b5c0bbac69bec..d216af75c7be1 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml @@ -15,6 +15,6 @@ <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.viewDetails}}" stepKey="viewDetails"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.modalTitle}}" stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml index d0d9817da6d34..488ea03ebd86c 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml @@ -12,7 +12,7 @@ <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"/> + <waitForPageLoad stepKey="waitForFolderTreeReloads"/> + <dontSeeElement selector="{{AdminMediaGalleryFolderSection.folderInTree(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 index 7d71c764bc8de..e42eb31dc00d8 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml @@ -12,6 +12,6 @@ <arguments> <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> </arguments> - <waitForElementVisible selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="waitForFolder"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderInTree(name)}}" stepKey="waitForFolder"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml index f7e8f551e681f..398003c7f2b02 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml @@ -9,8 +9,8 @@ <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.folderDeleteButtonActive}}" stepKey="waitBeforeDeleteButtonWillBeActive"/> + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButtonActive}}" stepKey="clickDeleteButton"/> <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderDeleteModalHeader}}" stepKey="waitBeforeModalAppears"/> <click selector="{{AdminMediaGalleryFolderSection.folderConfirmDeleteButton}}" stepKey="clickConfirmDeleteButton"/> </actionGroup> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml index b8ed1d4f1cd25..5751b8ec323da 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml @@ -9,11 +9,15 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminMediaGalleryFolderSelectActionGroup"> + <annotations> + <description>Wait for folder name appeared in tree and then click it.</description> + </annotations> <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"/> + + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderInTree(name)}}" stepKey="waitBeforeClickOnFolder"/> + <click selector="{{AdminMediaGalleryFolderSection.folderInTree(name)}}" stepKey="selectFolder"/> <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml index a434aa72679e4..287b6219115f2 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml @@ -9,7 +9,7 @@ <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"/> + <conditionalClick selector="{{CmsNewPagePageContentSection.header}}" dependentSelector="{{CmsNewPagePageContentSection.contentHeading}}" visible="false" stepKey="clickExpandContent"/> <waitForElementVisible selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="waitForInsertImageButton" /> <scrollTo selector="{{CmsWYSIWYGSection.InsertImageBtn}}" x="0" y="-80" stepKey="scrollToInsertImageButton"/> <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImage" /> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php b/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php new file mode 100644 index 0000000000000..4059a8460bb51 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Helper/MediaGalleryUiHelper.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\Remote\RemoteWebElement; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for MediaGalleryUi module. + */ +class MediaGalleryUiHelper extends Helper +{ + /** + * Delete all images using mass action. + * + * @param string $emptyRow + * @param string $deleteImagesButton + * @param string $checkImage + * @param string $deleteSelectedButton + * @param string $modalAcceptButton + * @param string $successMessageContainer + * @param string $successMessage + * + * @return void + */ + public function deleteAllImagesUsingMassAction( + string $emptyRow, + string $deleteImagesButton, + string $checkImage, + string $deleteSelectedButton, + string $modalAcceptButton, + 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($emptyRow)); + while (empty($rows)) { + $magentoWebDriver->click($deleteImagesButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($deleteSelectedButton, 10); + + // Check all images + /** @var RemoteWebElement[] $images */ + $imagesCheckboxes = $webDriver->findElements(WebDriverBy::cssSelector($checkImage)); + /** @var RemoteWebElement $image */ + foreach ($imagesCheckboxes as $imageCheckbox) { + $imageCheckbox->click(); + } + + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteSelectedButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForElementVisible($successMessageContainer, 10); + $magentoWebDriver->see($successMessage, $successMessageContainer); + + $rows = $webDriver->findElements(WebDriverBy::cssSelector($emptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index f36fca88dc760..1a8f6f553d4ce 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -10,7 +10,7 @@ <section name="AdminEnhancedMediaGalleryImageActionsSection"> <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> - <element name="viewDetails" type="button" selector="[data-ui-id='action-image-details']"/> + <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[text()='View Details']" timeout="30"/> <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"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml index 07f2dc23530e1..9018ccb4ddd69 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml @@ -13,5 +13,6 @@ <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"/> + <element name="massActionCheckboxAll" type="checkbox" selector="[data-id='media-gallery-masonry-grid'] .mediagallery-massaction-checkbox input[type='checkbox']"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml index 92f3323214065..9b9dc157bbc27 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml @@ -8,6 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEnhancedMediaGalleryViewDetailsSection"> + <element name="modalTitle" type="text" selector="//aside[contains(@class, 'media-gallery-image-details') and contains(@class, '_show')]//header[contains(@class, 'modal-header')]//h1[contains(@class, 'modal-title') and contains(., 'Image Details')]"/> <element name="title" type="text" selector=".image-title"/> <element name="contentType" type="text" selector="span[data-ui-id='content-type']"/> <element name="type" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Type')]/following-sibling::div"/> @@ -22,7 +23,7 @@ <element name="usedIn" type="button" selector="//div[@class='attribute']/span[contains(text(), 'Used In')]"/> <element name="updatedAtDate" type="button" selector="//div[@class='attribute']/span[contains(text(), 'Modified')]/following-sibling::div"/> <element name="addImage" type="button" selector=".add-image-action"/> - <element name="cancel" type="button" selector="#image-details-action-cancel"/> + <element name="cancel" type="button" selector="#image-details-action-cancel" timeout="10"/> <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 index 2e6919f692042..acc378dac6e67 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml @@ -12,13 +12,15 @@ <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="folderDeleteButton" type="button" selector="#delete_folder" timeout="30"/> + <element name="folderDeleteButtonActive" type="button" selector="#delete_folder:not(.disabled)" timeout="30"/> + <element name="folderConfirmDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'OK')]" timeout="30"/> <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="folderConfirmCreateButton" type="button" selector="//button/span[contains(text(),'Confirm')]" timeout="30"/> <element name="folderNameValidationMessage" type="block" selector="label.mage-error"/> <element name="folderArrow" type="button" selector="#{{id}} > .jstree-icon" parameterized="true"/> <element name="checkIfFolderArrowExpand" type="button" selector="//li[@id='{{id}}' and contains(@class,'jstree-closed')]" parameterized="true"/> + <element name="folderInTree" type="text" selector="//div[contains(@class, 'media-directory-container')]//ul//li//a[contains(text(), '{{name}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml new file mode 100644 index 0000000000000..659fb51080225 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryMessagesSection.xml @@ -0,0 +1,11 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <section name="AdminMediaGalleryMessagesSection"> + <element name="success" type="text" selector=".media-gallery-container ul.messages div.message.message-success span"/> + </section> +</sections> From 87d936894839ac92528b6f4fcf476ee2ba4eb550 Mon Sep 17 00:00:00 2001 From: Vitaliy Prokopov <vitaliyprokopov@Vitaliys-MacBook-Pro.local> Date: Wed, 28 Oct 2020 16:16:27 +0200 Subject: [PATCH 0952/1013] fixed url issue for image --- .../Model/Resolver/Category/Image.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php index 5de7fdc10ff4a..549b1311000ec 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Image.php @@ -7,14 +7,16 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Category; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category\FileInfo; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DirectoryList; 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\Framework\Exception\LocalizedException; +use Magento\Framework\UrlInterface; use Magento\Store\Api\Data\StoreInterface; -use Magento\Framework\Filesystem\DirectoryList; -use Magento\Catalog\Model\Category\FileInfo; -use Magento\Framework\GraphQl\Exception\GraphQlInputException; /** * Resolve category image to a fully qualified URL @@ -52,7 +54,7 @@ public function resolve( if (!isset($value['model'])) { throw new LocalizedException(__('"model" value should be specified')); } - /** @var \Magento\Catalog\Model\Category $category */ + /** @var Category $category */ $category = $value['model']; $imagePath = $category->getData('image'); if (empty($imagePath)) { @@ -60,7 +62,7 @@ public function resolve( } /** @var StoreInterface $store */ $store = $context->getExtensionAttributes()->getStore(); - $baseUrl = $store->getBaseUrl(); + $baseUrl = $store->getBaseUrl(UrlInterface::URL_TYPE_WEB); $filenameWithMedia = $this->fileInfo->isBeginsWithMediaDirectoryPath($imagePath) ? $imagePath : $this->formatFileNameWithMediaCategoryFolder($imagePath); From 5ef4711270360c3b418f38a1c05730919a5dd91f Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 29 Oct 2020 12:01:35 +0200 Subject: [PATCH 0953/1013] MC-38589: [MFTF] AdminEnhancedMediaGalleryVerifyAssetFilterTest failed because of bad design --- .../AdminMediaGalleryClickAddSelectedActionGroup.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml index f575e346a8ca0..711a271c9d4ec 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml @@ -10,8 +10,8 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminMediaGalleryClickAddSelectedActionGroup"> <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="waitForAddSelectedButton"/> - <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="clickAddSelected"/> - <waitForPageLoad stepKey="waitForPageLoad"/> - <waitForElementVisible selector="{{MediaGallerySection.insertEditImageModalWindow}}" stepKey="waitForInsertEditImageWindow"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="ClickAddSelected"/> + <waitForPageLoad stepKey="waitForImageToBeAdded"/> + <waitForElementVisible selector="{{MediaGallerySection.insertEditImageModalWindow}}" stepKey="waitForLoadingMaskDisappear"/> </actionGroup> </actionGroups> From 07062f4ad5d5f8d3d5b52b4e080d8ec76fd2f316 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 29 Oct 2020 12:40:17 +0200 Subject: [PATCH 0954/1013] MC-37213: [MFTF] AdminMediaGalleryCatalogUiUsedInProductFilterTest is flaky --- .../Test/Mftf/Suite/MediaGalleryUiSuite.xml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml index bda9b6ad08e43..e81dc807d0f48 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml @@ -11,12 +11,16 @@ <suite name="MediaGalleryUiSuite"> <before> <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYG" /> - <magentoCLI command="config:set {{MediaGalleryConfigDataEnabled.path}} {{MediaGalleryConfigDataEnabled.value}}" stepKey="enableEnhancedMediaGallery"/> - <magentoCLI command="config:set {{MediaGalleryRenditionsDataEnabled.path}} {{MediaGalleryRenditionsDataEnabled.value}}" stepKey="enableMediaGalleryRenditions"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="enableEnhancedMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryRenditionsEnableActionGroup" stepKey="enableMediaGalleryRenditions"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </before> <after> - <magentoCLI command="config:set {{MediaGalleryRenditionsDataDisabled.path}} {{MediaGalleryRenditionsDataDisabled.value}}" stepKey="disableMediaGalleryRenditions"/> - <magentoCLI command="config:set {{MediaGalleryConfigDataDisabled.path}} {{MediaGalleryConfigDataDisabled.value}}" stepKey="disableEnhancedMediaGallery"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYG" /> </after> <include> From 7adea3a008c8cb16bc5099ae8c59ed290db70d74 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Thu, 29 Oct 2020 14:55:29 +0200 Subject: [PATCH 0955/1013] MC-38050: Price Sorting Is not working --- .../Magento/Bundle/Model/ResourceModel/Indexer/Price.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index 96d68d7e74117..55bef4980098b 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -17,6 +17,7 @@ use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogInventory\Model\Stock; /** * Bundle products Price indexer resource model @@ -624,6 +625,13 @@ private function calculateDynamicBundleSelectionPrice($dimensions) 'tier_price' => $tierExpr, ] ); + $select->join( + ['si' => $this->getTable('cataloginventory_stock_status')], + 'si.product_id = bs.product_id', + [] + ); + $select->where('si.stock_status = ?', Stock::STOCK_IN_STOCK); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); } From 645cf2323e1c44af0b8badc51e3af647e988a31d Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 29 Oct 2020 15:09:59 +0200 Subject: [PATCH 0956/1013] MC-37093: Create automated test for "Collect data default data definition by cron" --- .../Analytics/Cron/CollectDataTest.php | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php new file mode 100644 index 0000000000000..227474edfc2e1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Analytics\Cron; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks data collection process behaviour + * + * @see \Magento\Analytics\Cron\CollectData + * + * @magentoAppArea adminhtml + */ +class CollectDataTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CollectData */ + private $collectDataService; + + /** @var Filesystem */ + private $fileSystem; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->collectDataService = $this->objectManager->get(CollectData::class); + $this->fileSystem = $this->objectManager->get(Filesystem::class); + $this->removeAnalyticsDirectory(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->removeAnalyticsDirectory(); + + parent::tearDown(); + } + + /** + * @magentoConfigFixture current_store analytics/subscription/enabled 1 + * @magentoConfigFixture default/analytics/general/token 123 + * + * @return void + */ + public function testExecute(): void + { + $this->collectDataService->execute(); + $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->assertTrue( + $mediaDirectory->isDirectory('analytics'), + 'Analytics was not created' + ); + $files = $mediaDirectory->getDriver()->readDirectoryRecursively($mediaDirectory->getAbsolutePath('analytics')); + $file = array_filter($files, function ($element) { + return substr($element, -8) === 'data.tgz'; + }); + $this->assertNotEmpty($file, 'File was not created'); + } + + /** + * Remove Analytics directory + * + * @return void + */ + private function removeAnalyticsDirectory(): void + { + $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); + $directoryToRemove = $mediaDirectory->getAbsolutePath('analytics'); + if ($mediaDirectory->isDirectory($directoryToRemove)) { + $mediaDirectory->delete($directoryToRemove); + } + } +} From 871e62d023b3b227153a8d82bdc22d7ac25862d2 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 29 Oct 2020 15:43:07 +0200 Subject: [PATCH 0957/1013] MC-38651: [MFTF] AdminMediaGalleryAssertUsedInLinkPagesGridTest failed because of bad design --- .../Test/Mftf/Section/AdminMediaGalleryFolderSection.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml index acc378dac6e67..be12829229663 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml @@ -21,6 +21,6 @@ <element name="folderNameValidationMessage" type="block" selector="label.mage-error"/> <element name="folderArrow" type="button" selector="#{{id}} > .jstree-icon" parameterized="true"/> <element name="checkIfFolderArrowExpand" type="button" selector="//li[@id='{{id}}' and contains(@class,'jstree-closed')]" parameterized="true"/> - <element name="folderInTree" type="text" selector="//div[contains(@class, 'media-directory-container')]//ul//li//a[contains(text(), '{{name}}')]" parameterized="true"/> + <element name="folderInTree" type="text" selector="//div[contains(@class, 'media-directory-container')]//ul//li//a[normalize-space(text())='{{name}}']" parameterized="true"/> </section> </sections> From ecf59ac44640a8b3af9d391c3bdb87f22c7d171e Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Thu, 29 Oct 2020 15:46:14 +0200 Subject: [PATCH 0958/1013] MC-38651: [MFTF] AdminMediaGalleryAssertUsedInLinkPagesGridTest failed because of bad design --- .../Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml index 2bf229d2a65d3..539d6ccb6c0dd 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml @@ -20,7 +20,7 @@ </annotations> <before> - <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> </before> From 3a95f15db88c04747fcc838aba909c09de00252f Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Thu, 29 Oct 2020 11:41:41 -0500 Subject: [PATCH 0959/1013] MC-37726: Price filter uses base currency for ranges --- .../GraphQl/Catalog/ProductPriceTest.php | 60 +++++++++++++++++-- .../Catalog/ProductSearchAggregationsTest.php | 31 ++-------- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php index f1c1be44ccd13..7a3829a325c59 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php @@ -78,6 +78,54 @@ public function testProductWithSinglePrice() $this->assertPrices($expectedPriceRange, $product['price_range']); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/products.php + * @magentoConfigFixture default_store currency/options/allow EUR,USD + */ + public function testProductWithSinglePriceNonDefaultCurrency() + { + $skus = ['simple']; + $query = $this->getProductQuery($skus); + $headerMap = [ + 'Content-Currency' => 'EUR' + ]; + $result = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertNotEmpty($result['products']['items']); + $product = $result['products']['items'][0]; + $this->assertNotEmpty($product['price_range']); + + $expectedPriceRange = [ + "minimum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 20 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ], + "maximum_price" => [ + "regular_price" => [ + "value" => 20 + ], + "final_price" => [ + "value" => 20 + ], + "discount" => [ + "amount_off" => 0, + "percent_off" => 0 + ] + ] + ]; + + $this->assertPrices($expectedPriceRange, $product['price_range'], 'EUR'); + } + /** * Pricing for Simple, Grouped and Configurable products with no special or tier prices configured * @@ -909,7 +957,7 @@ private function getQueryConfigurableProductAndVariants(array $sku): string name sku price_range { - minimum_price {regular_price + minimum_price {regular_price { value currency @@ -949,13 +997,13 @@ private function getQueryConfigurableProductAndVariants(array $sku): string ... on ConfigurableProduct{ variants{ product{ - + sku price_range { minimum_price {regular_price {value} final_price { value - + } discount { amount_off @@ -965,11 +1013,11 @@ private function getQueryConfigurableProductAndVariants(array $sku): string maximum_price { regular_price { value - + } final_price { value - + } discount { amount_off @@ -985,7 +1033,7 @@ private function getQueryConfigurableProductAndVariants(array $sku): string final_price{value} quantity } - + } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index 075388b0f023c..57b6b24472bf7 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -89,31 +89,12 @@ function ($a) { } /** - * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php * @magentoApiDataFixture Magento/Catalog/_files/products_for_search.php + * @magentoConfigFixture default_store currency/options/allow EUR,USD */ public function testAggregationPriceRangesWithCurrencyHeader() { - // add USD as allowed (not default) currency - $objectManager = Bootstrap::getObjectManager(); - /* @var Store $store */ - $store = $objectManager->create(Store::class); - $store->load('fixture_second_store'); - /** @var Config $configResource */ - $configResource = $objectManager->get(Config::class); - $configResource->saveConfig( - Currency::XML_PATH_CURRENCY_ALLOW, - 'USD', - ScopeInterface::SCOPE_STORES, - $store->getId() - ); - // Configuration cache clean is required to reload currency setting - /** @var System $config */ - $config = $objectManager->get(System::class); - $config->clean(); - - $headerMap['Store'] = 'fixture_second_store'; - $headerMap['Content-Currency'] = 'USD'; + $headerMap['Content-Currency'] = 'EUR'; $query = $this->getGraphQlQuery( '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"' ); @@ -131,10 +112,10 @@ function ($a) { $this->assertEquals('Price', $priceAggregation['label']); $this->assertEquals(4, $priceAggregation['count']); $expectedOptions = [ - ['label' => '10-20', 'value'=> '10_20', 'count' => '2'], - ['label' => '20-30', 'value'=> '20_30', 'count' => '1'], - ['label' => '30-40', 'value'=> '30_40', 'count' => '1'], - ['label' => '40-50', 'value'=> '40_50', 'count' => '1'] + ['label' => '20-40', 'value'=> '20_40', 'count' => '2'], + ['label' => '40-60', 'value'=> '40_60', 'count' => '1'], + ['label' => '60-80', 'value'=> '60_80', 'count' => '1'], + ['label' => '80-100', 'value'=> '80_100', 'count' => '1'] ]; $this->assertEquals($expectedOptions, $priceAggregation['options']); } From d835452dd2d859c43ad149ece77390a879a6ddda Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Thu, 29 Oct 2020 12:46:40 -0500 Subject: [PATCH 0960/1013] MC-37726: Price filter uses base currency for ranges --- .../Magento/GraphQl/Catalog/ProductPriceTest.php | 15 ++++++++------- .../Catalog/ProductSearchAggregationsTest.php | 13 +++++++------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php index 7a3829a325c59..b6c4b55dc1d23 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductPriceTest.php @@ -80,14 +80,15 @@ public function testProductWithSinglePrice() /** * @magentoApiDataFixture Magento/Catalog/_files/products.php - * @magentoConfigFixture default_store currency/options/allow EUR,USD + * @magentoApiDataFixture Magento/Directory/_files/usd_cny_rate.php + * @magentoConfigFixture default_store currency/options/allow CNY,USD */ public function testProductWithSinglePriceNonDefaultCurrency() { $skus = ['simple']; $query = $this->getProductQuery($skus); $headerMap = [ - 'Content-Currency' => 'EUR' + 'Content-Currency' => 'CNY' ]; $result = $this->graphQlQuery($query, [], '', $headerMap); @@ -99,10 +100,10 @@ public function testProductWithSinglePriceNonDefaultCurrency() $expectedPriceRange = [ "minimum_price" => [ "regular_price" => [ - "value" => 20 + "value" => 70 ], "final_price" => [ - "value" => 20 + "value" => 70 ], "discount" => [ "amount_off" => 0, @@ -111,10 +112,10 @@ public function testProductWithSinglePriceNonDefaultCurrency() ], "maximum_price" => [ "regular_price" => [ - "value" => 20 + "value" => 70 ], "final_price" => [ - "value" => 20 + "value" => 70 ], "discount" => [ "amount_off" => 0, @@ -123,7 +124,7 @@ public function testProductWithSinglePriceNonDefaultCurrency() ] ]; - $this->assertPrices($expectedPriceRange, $product['price_range'], 'EUR'); + $this->assertPrices($expectedPriceRange, $product['price_range'], 'CNY'); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index 57b6b24472bf7..9ff33795cb868 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -90,11 +90,12 @@ function ($a) { /** * @magentoApiDataFixture Magento/Catalog/_files/products_for_search.php - * @magentoConfigFixture default_store currency/options/allow EUR,USD + * @magentoApiDataFixture Magento/Directory/_files/usd_cny_rate.php + * @magentoConfigFixture default_store currency/options/allow CNY,USD */ public function testAggregationPriceRangesWithCurrencyHeader() { - $headerMap['Content-Currency'] = 'EUR'; + $headerMap['Content-Currency'] = 'CNY'; $query = $this->getGraphQlQuery( '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"' ); @@ -112,10 +113,10 @@ function ($a) { $this->assertEquals('Price', $priceAggregation['label']); $this->assertEquals(4, $priceAggregation['count']); $expectedOptions = [ - ['label' => '20-40', 'value'=> '20_40', 'count' => '2'], - ['label' => '40-60', 'value'=> '40_60', 'count' => '1'], - ['label' => '60-80', 'value'=> '60_80', 'count' => '1'], - ['label' => '80-100', 'value'=> '80_100', 'count' => '1'] + ['label' => '70-140', 'value'=> '70_140', 'count' => '2'], + ['label' => '140-210', 'value'=> '140_210', 'count' => '1'], + ['label' => '210-280', 'value'=> '210_280', 'count' => '1'], + ['label' => '280-350', 'value'=> '280_350', 'count' => '1'] ]; $this->assertEquals($expectedOptions, $priceAggregation['options']); } From 908456b80bc687c8268e3c5ec9d4cfaa14c65999 Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Thu, 29 Oct 2020 12:55:24 -0500 Subject: [PATCH 0961/1013] MC-37726: Price filter uses base currency for ranges --- .../GraphQl/Catalog/ProductSearchAggregationsTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index 9ff33795cb868..bd4530d0724a1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -7,12 +7,6 @@ namespace Magento\GraphQl\Catalog; -use Magento\Config\App\Config\Type\System; -use Magento\Config\Model\ResourceModel\Config; -use Magento\Directory\Model\Currency; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\Store; -use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\GraphQlAbstract; class ProductSearchAggregationsTest extends GraphQlAbstract From 44fa566db29f8a1911e64371b06849587e60d1d9 Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Thu, 29 Oct 2020 15:59:21 -0500 Subject: [PATCH 0962/1013] MC-37726: Price filter uses base currency for ranges --- app/code/Magento/CatalogGraphQl/composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index 46d7454a6d7e2..463f974056749 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -7,6 +7,7 @@ "magento/module-eav": "*", "magento/module-catalog": "*", "magento/module-catalog-inventory": "*", + "magento/module-directory": "*", "magento/module-search": "*", "magento/module-store": "*", "magento/module-eav-graph-ql": "*", From ebbf23a009ff0a65456512f9ab1502af8b638123 Mon Sep 17 00:00:00 2001 From: Cristian Partica <cpartica@magento.com> Date: Thu, 29 Oct 2020 16:00:05 -0500 Subject: [PATCH 0963/1013] added tests for seo disabled --- .../Catalog/CategoriesQuery/CategoriesFilterTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 a3daf89631c17..09f39bf1441f5 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 @@ -395,8 +395,9 @@ public function testMinimumMatchQueryLength() * Test category image full name is returned * * @magentoApiDataFixture Magento/Catalog/_files/catalog_category_with_long_image_name.php + * @magentoConfigFixture default_store web/seo/use_rewrites 0 */ - public function testCategoryImageName() + public function testCategoryImageNameAndSeoDisabled() { /** @var CategoryCollection $categoryCollection */ $categoryCollection = Bootstrap::getObjectManager()->get(CategoryCollection::class); @@ -427,14 +428,13 @@ public function testCategoryImageName() $categories = $response['categories']; $this->assertArrayNotHasKey('errors', $response); $this->assertNotEmpty($response['categories']['items']); - $expectedImageUrl = str_replace('index.php/', '', $expectedImageUrl); - $categories['items'][0]['image'] = str_replace('index.php/', '', $categories['items'][0]['image']); $this->assertEquals('Parent Image Category', $categories['items'][0]['name']); $this->assertEquals($expectedImageUrl, $categories['items'][0]['image']); } /** * @magentoApiDataFixture Magento/Catalog/_files/categories.php + * @magentoConfigFixture default_store web/seo/use_rewrites 1 */ public function testFilterByUrlPathTopLevelCategory() { From 9094403c198214c1dcc34021e0d4bac3bc8b091e Mon Sep 17 00:00:00 2001 From: Cari Spruiell <spruiell@adobe.com> Date: Thu, 29 Oct 2020 16:00:52 -0500 Subject: [PATCH 0964/1013] MC-37726: Price filter uses base currency for ranges --- app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php index cddb95d5ba765..cc76855cc6d20 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Aggregations.php @@ -37,8 +37,8 @@ class Aggregations implements ResolverInterface /** * @param \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider - * @param PriceCurrency $priceCurrency * @param LayerBuilder $layerBuilder + * @param PriceCurrency $priceCurrency */ public function __construct( \Magento\CatalogGraphQl\Model\Resolver\Layer\DataProvider\Filters $filtersDataProvider, From cf5e1563a1290ad638b22511666a3af53a4a4ae2 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Fri, 30 Oct 2020 09:53:31 +0200 Subject: [PATCH 0965/1013] MC-35740: Using API to capture payment --- app/code/Magento/Sales/etc/webapi_soap/di.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 1a8478438b04a..12ad410279a08 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -19,7 +19,7 @@ </argument> </arguments> </type> - <type name="Magento\Sales\Model\Order\ShipmentRepository"> - <plugin name="process_order_and_shipment_via_api" type="Magento\Sales\Plugin\ProcessOrderAndShipmentViaAPI" /> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> </type> </config> From 320aee986572290d5a6f4c5324fd2d9cbf12af5c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 30 Oct 2020 10:08:58 +0200 Subject: [PATCH 0966/1013] MC-38589: [MFTF] AdminEnhancedMediaGalleryVerifyAssetFilterTest failed because of bad design --- .../AdminMediaGalleryClickAddSelectedActionGroup.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml index 711a271c9d4ec..6cfc7b07831d0 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml @@ -12,6 +12,6 @@ <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="waitForAddSelectedButton"/> <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="ClickAddSelected"/> <waitForPageLoad stepKey="waitForImageToBeAdded"/> - <waitForElementVisible selector="{{MediaGallerySection.insertEditImageModalWindow}}" stepKey="waitForLoadingMaskDisappear"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskDisappear"/> </actionGroup> </actionGroups> From 2be562a6502015f13de5829b940e7a48e0fc919c Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 30 Oct 2020 12:04:09 +0200 Subject: [PATCH 0967/1013] MC-33288: [2.4][MSI][MFTF] StorefrontLoggedInCustomerCreateOrderAllOptionQuantityConfigurableProductCustomStockTest fails because of bad design --- .../Catalog/Model/ResourceModel/Product/Relation.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php index 2f8c4a34f0087..392a4aeedfeb3 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Relation.php @@ -5,6 +5,7 @@ */ namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\ResourceModel\Db\AbstractDb; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\EntityManager\MetadataPool; @@ -24,16 +25,16 @@ class Relation extends AbstractDb /** * @param Context $context - * @param null $connectionName + * @param string $connectionName * @param MetadataPool $metadataPool */ public function __construct( Context $context, $connectionName = null, - MetadataPool $metadataPool + MetadataPool $metadataPool = null ) { parent::__construct($context, $connectionName); - $this->metadataPool = $metadataPool; + $this->metadataPool = $metadataPool ?: ObjectManager::getInstance()->get(MetadataPool::class); } /** From 9a011bf016020c58a1c77737a153301cc33d93a6 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 30 Oct 2020 12:30:14 +0200 Subject: [PATCH 0968/1013] MC-37093: Create automated test for "Collect data default data definition by cron" --- .../Analytics/Cron/CollectDataTest.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php index 227474edfc2e1..a1d2b24d54b0e 100644 --- a/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php +++ b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -28,8 +29,8 @@ class CollectDataTest extends TestCase /** @var CollectData */ private $collectDataService; - /** @var Filesystem */ - private $fileSystem; + /** @var WriteInterface */ + private $mediaDirectory; /** * @inheritdoc @@ -40,7 +41,7 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->collectDataService = $this->objectManager->get(CollectData::class); - $this->fileSystem = $this->objectManager->get(Filesystem::class); + $this->mediaDirectory = $this->objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); $this->removeAnalyticsDirectory(); } @@ -63,12 +64,12 @@ protected function tearDown(): void public function testExecute(): void { $this->collectDataService->execute(); - $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); $this->assertTrue( - $mediaDirectory->isDirectory('analytics'), + $this->mediaDirectory->isDirectory('analytics'), 'Analytics was not created' ); - $files = $mediaDirectory->getDriver()->readDirectoryRecursively($mediaDirectory->getAbsolutePath('analytics')); + $files = $this->mediaDirectory->getDriver() + ->readDirectoryRecursively($this->mediaDirectory->getAbsolutePath('analytics')); $file = array_filter($files, function ($element) { return substr($element, -8) === 'data.tgz'; }); @@ -82,10 +83,9 @@ public function testExecute(): void */ private function removeAnalyticsDirectory(): void { - $mediaDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); - $directoryToRemove = $mediaDirectory->getAbsolutePath('analytics'); - if ($mediaDirectory->isDirectory($directoryToRemove)) { - $mediaDirectory->delete($directoryToRemove); + $directoryToRemove = $this->mediaDirectory->getAbsolutePath('analytics'); + if ($this->mediaDirectory->isDirectory($directoryToRemove)) { + $this->mediaDirectory->delete($directoryToRemove); } } } From 7cd05c324b61dddaced8467c69780a59edbe50f7 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Fri, 30 Oct 2020 12:38:04 +0200 Subject: [PATCH 0969/1013] MC-38050: Price Sorting Is not working --- .../Model/ResourceModel/Indexer/PriceTest.php | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php new file mode 100644 index 0000000000000..afb0e66558aaa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Model\ResourceModel\Indexer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Price; +use Magento\Customer\Model\Group; +use Magento\Framework\Indexer\ActionInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Catalog\Model\Product\Price\GetPriceIndexDataByProductId; +use Magento\CatalogInventory\Model\Indexer\Stock; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class PriceTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ActionInterface + */ + private $indexer; + + /** + * @var GetPriceIndexDataByProductId + */ + private $getPriceIndexDataByProductId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var WebsiteRepositoryInterface + */ + private $websiteRepository; + + /** + * @var Stock + */ + private $stockIndexer; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->indexer = $this->objectManager->get(Price::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->getPriceIndexDataByProductId = $this->objectManager->get(GetPriceIndexDataByProductId::class); + $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->stockIndexer = $this->objectManager->get(Stock::class); + } + + /** + * Test get bundle index price if enabled show out off stock + * + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Bundle/_files/bundle_product_with_dynamic_price.php + * @magentoConfigFixture default_store cataloginventory/options/show_out_of_stock 1 + * + * @return void + */ + public function testExecuteRowWithShowOutOfStock(): void + { + + $expectedPrices = [ + 'price' => 0, + 'final_price' => 0, + 'min_price' => 15.99, + 'max_price' => 15.99, + 'tier_price' => null + ]; + $product = $this->productRepository->get('simple1'); + $product->setStockData(['qty' => 0]); + $this->productRepository->save($product); + $this->stockIndexer->executeRow($product->getId()); + $bundleProduct = $this->productRepository->get('bundle_product_with_dynamic_price'); + $this->indexer->executeRow($bundleProduct->getId()); + $this->assertIndexTableData($bundleProduct->getId(), $expectedPrices); + } + + /** + * Asserts price data in index table. + * + * @param int $productId + * @param array $expectedPrices + * @return void + */ + private function assertIndexTableData(int $productId, array $expectedPrices): void + { + $data = $this->getPriceIndexDataByProductId->execute( + $productId, + Group::NOT_LOGGED_IN_ID, + (int)$this->websiteRepository->get('base')->getId() + ); + $data = reset($data); + foreach ($expectedPrices as $column => $price) { + $this->assertEquals($price, $data[$column]); + } + } +} From d180f78d5ebc04c43ef76f466843a7cb9b123570 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Fri, 30 Oct 2020 14:04:16 +0200 Subject: [PATCH 0970/1013] MC-38814: [MFTF] AdminMediaGalleryInsertLargeImageFileSizeTest failed because of bad design --- ...diaGalleryInsertImageLargeFileSizeTest.xml | 58 +++++++++++++++++++ ...diaGalleryInsertLargeImageFileSizeTest.xml | 7 ++- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertImageLargeFileSizeTest.xml diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertImageLargeFileSizeTest.xml b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertImageLargeFileSizeTest.xml new file mode 100644 index 0000000000000..2d80b490e2d9b --- /dev/null +++ b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertImageLargeFileSizeTest.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="AdminMediaGalleryInsertImageLargeFileSizeTest"> + <annotations> + <features value="MediaGalleryRenditions"/> + <stories value="User inserts image rendition to the content"/> + <title value="Admin user should see correct image file size after rendition"/> + <description value="Admin user should see correct image file size after rendition"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1507933/scenarios/5200023"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1806"/> + <severity value="AVERAGE"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImages"/> + </before> + + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllImages"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <!-- Open category page --> + <actionGroup ref="GoToAdminCategoryPageByIdActionGroup" stepKey="openCategoryPage"> + <argument name="id" value="$category.id$"/> + </actionGroup> + + <!-- Add image to category from gallery --> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFiltersAgain"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="addCategoryImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImage"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="addSelected"/> + + <!-- Assert added image size --> + <actionGroup ref="AdminAssertImageUploadFileSizeThanActionGroup" stepKey="assertSize"> + <argument name="fileSize" value="26 KB"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.xml b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.xml index d859f4852aaaf..061f062eabe6b 100644 --- a/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.xml +++ b/app/code/Magento/MediaGalleryRenditions/Test/Mftf/Test/AdminMediaGalleryInsertLargeImageFileSizeTest.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="AdminMediaGalleryInsertLargeImageFileSizeTest"> + <test name="AdminMediaGalleryInsertLargeImageFileSizeTest" deprecated="Use AdminMediaGalleryInsertImageLargeFileSizeTest instead"> <annotations> <features value="AdminMediaGalleryInsertLargeImageFileSizeTest"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1806"/> - <title value="Admin user should see correct image file size after rendition"/> + <title value="DEPRECATED. Admin user should see correct image file size after rendition"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1507933/scenarios/5200023"/> <stories value="User inserts image rendition to the content"/> <description value="Admin user should see correct image file size after rendition"/> <severity value="AVERAGE"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryInsertImageLargeFileSizeTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> From 5e2e8e9fc9d47928ce66aea9dc502319dc5dd5b6 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Fri, 30 Oct 2020 19:38:51 +0200 Subject: [PATCH 0971/1013] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Magento/Cms/Test/Mftf/Data/BlockData.xml | 7 ++ ...dminUseQuickSearchInAdminDataGridsTest.xml | 109 ++++++++++++++++++ ...sertNumberOfRecordsInUiGridActionGroup.xml | 20 ++++ .../AdminGridHeadersSection.xml | 1 + 4 files changed, 137 insertions(+) create mode 100644 app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml create mode 100644 app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml index dea047ec43568..31dd27b0f599c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml @@ -15,4 +15,11 @@ <data key="content">sales25off everything!</data> <data key="is_active">0</data> </entity> + <entity name="TestBlock" type="block"> + <data key="title" unique="suffix">Test Block</data> + <data key="identifier" unique="suffix">TestBlock</data> + <data key="store_id">All Store Views</data> + <data key="content">Test Block content</data> + <data key="is_active">1</data> + </entity> </entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml new file mode 100644 index 0000000000000..85bbf6042b686 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUseQuickSearchInAdminDataGridsTest"> + <annotations> + <features value="CmsPage"/> + <stories value="Create CMS Page"/> + <title value="[CMS Grids] Use quick search in Admin data grids"/> + <description value="Verify that Merchant can use quick search in order to simplify the data grid filtering in Admin"/> + <testCaseId value="MC-27559" /> + <severity value="MAJOR"/> + <group value="cms"/> + <group value="ui"/> + </annotations> + <before> + <createData entity="simpleCmsPage" stepKey="createFirstCMSPage" /> + <createData entity="_newDefaultCmsPage" stepKey="createSecondCMSPage" /> + <createData entity="_emptyCmsPage" stepKey="createThirdCMSPage" /> + <createData entity="Sales25offBlock" stepKey="createFirstCmsBlock"/> + <createData entity="TestBlock" stepKey="createSecondCmsBlock"/> + <createData entity="_emptyCmsBlock" stepKey="createThirdCmsBlock"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstCMSPage" stepKey="deleteFirstCMSPage" /> + <deleteData createDataKey="createSecondCMSPage" stepKey="deleteSecondCMSPage" /> + <deleteData createDataKey="createThirdCMSPage" stepKey="deleteThirdCMSPage" /> + <deleteData createDataKey="createFirstCmsBlock" stepKey="deleteFirstCmsBlock" /> + <deleteData createDataKey="createSecondCmsBlock" stepKey="deleteSecondCmsBlock" /> + <deleteData createDataKey="createThirdCmsBlock" stepKey="deleteThirdCmsBlock" /> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCMSPageGrid"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearCmsPagesGridFilters"/> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="navigateToCmsBlockGrid"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearCmsBlockGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Go to "Cms Pages Grid" page and filter by title--> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstCmsPage"> + <argument name="keyword" value="$createFirstCMSPage.title$"/> + </actionGroup> + <see userInput="$createFirstCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeFirstCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsInCmsPageGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchSecondCmsPage"> + <argument name="keyword" value="$createSecondCMSPage.title$"/> + </actionGroup> + <see userInput="$createSecondCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeSecondCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringSecondCmsPage"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFilters"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsCmsPagesBeforeClickSearchButton"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSearchMagnifierButton"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsCmsPagesAfterClickSearchButton"/> + <assertEquals stepKey="assertTotalRecordsCmsPages"> + <expectedResult type="string">$grabTotalRecordsCmsPagesBeforeClickSearchButton</expectedResult> + <actualResult type="string">$grabTotalRecordsCmsPagesAfterClickSearchButton</actualResult> + </assertEquals> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="enterNonExistentEntityInQuickSearch"> + <argument name="keyword" value="TestQueryNonExistentEntity"/> + </actionGroup> + <dontSeeElement selector="{{AdminDataGridTableSection.rows}}" stepKey="dontSeeResultRows"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringNonExistentCmsPage"> + <argument name="number" value="0"/> + </actionGroup> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchThirdCmsPage"> + <argument name="keyword" value="$createThirdCMSPage.title$"/> + </actionGroup> + <see userInput="$createThirdCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeThirdCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringThirdCmsPage"/> + + <!--Go to "Cms Blocks Grid" page and filter by title--> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="navigateToCmsBlockGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstCmsBlock"> + <argument name="keyword" value="$createFirstCmsBlock.title$"/> + </actionGroup> + <see userInput="$createFirstCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeFirstCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsInBlockGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchSecondCmsBlock"> + <argument name="keyword" value="$createSecondCmsBlock.title$"/> + </actionGroup> + <see userInput="$createSecondCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeSecondCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringSecondBlock"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersOnBlocksGridPage"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsBlocksBeforeClickSearchButton"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSearchMagnifierButtonOnBlocksGridPage"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsBlocksAfterClickSearchButton"/> + <assertEquals stepKey="assertTotalRecordsBlocks"> + <expectedResult type="string">$grabTotalRecordsBlocksBeforeClickSearchButton</expectedResult> + <actualResult type="string">$grabTotalRecordsBlocksAfterClickSearchButton</actualResult> + </assertEquals> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="enterNonExistentEntityInQuickSearchOnBlocksGridPage"> + <argument name="keyword" value="TestQueryNonExistentEntity"/> + </actionGroup> + <dontSeeElement selector="{{AdminDataGridTableSection.rows}}" stepKey="dontSeeResultRowsOnBlocksGrid"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringNonExistentCmsBlock"> + <argument name="number" value="0"/> + </actionGroup> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchThirdCmsBlock"> + <argument name="keyword" value="$createThirdCmsBlock.title$"/> + </actionGroup> + <see userInput="$createThirdCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeThirdCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringThirdBlock"/> + </test> +</tests> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml new file mode 100644 index 0000000000000..5928833bf4794 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.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="AdminAssertNumberOfRecordsInUiGridActionGroup"> + <annotations> + <description>Validates that the Number of Records listed on the Ui grid page is present and correct.</description> + </annotations> + <arguments> + <argument name="number" type="string" defaultValue="1"/> + </arguments> + <see userInput="{{number}} records found" selector="{{AdminGridHeaders.totalRecords}}" stepKey="seeRecords"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml index 89831359657bf..851e65e61bb1c 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml @@ -11,5 +11,6 @@ <element name="title" type="text" selector=".page-title-wrapper h1"/> <element name="headerByName" type="text" selector="//div[@data-role='grid-wrapper']//span[@class='data-grid-cell-content' and contains(text(), '{{var1}}')]/parent::*" parameterized="true"/> <element name="columnsNames" type="text" selector="[data-role='grid-wrapper'] .data-grid-th > span"/> + <element name="totalRecords" type="text" selector="div.admin__data-grid-header-row .row>div:nth-child(1)"/> </section> </sections> From 3464b87a6954dd939bf8b97b2a10022bc8cfb425 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Mon, 2 Nov 2020 11:55:30 +0200 Subject: [PATCH 0972/1013] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml index 85bbf6042b686..65df6fd80e865 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="AdminUseQuickSearchInAdminDataGridsTest"> <annotations> - <features value="CmsPage"/> + <features value="Cms"/> <stories value="Create CMS Page"/> <title value="[CMS Grids] Use quick search in Admin data grids"/> <description value="Verify that Merchant can use quick search in order to simplify the data grid filtering in Admin"/> From 5fe07d4224a809791e6cb64903d5024ca944b07a Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Mon, 2 Nov 2020 17:09:14 +0200 Subject: [PATCH 0973/1013] MC-38651: [MFTF] AdminMediaGalleryAssertUsedInLinkPagesGridTest failed because of bad design --- .../Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml index 539d6ccb6c0dd..4e589faef9a1f 100644 --- a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryAssertUsedInLinkedPagesGridTest.xml @@ -60,8 +60,8 @@ <actionGroup ref="AdminAssertMediaGalleryFilterPlaceHolderGridActionGroup" stepKey="assertFilterApplied"> <argument name="filterPlaceholder" value="{{ImageMetadata.title}}"/> </actionGroup> - <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> - <argument name="urlKey" value="{{_defaultCmsPage.identifier}}"/> + <actionGroup ref="AdminDeleteCMSPageByUrlKeyActionGroup" stepKey="deleteCmsPage"> + <argument name="pageUrlKey" value="{{_defaultCmsPage.identifier}}"/> </actionGroup> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersInPageGrid"/> From 362eba826be3a7aff9ce9de579893e639ffc2cfd Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <duhon@users.noreply.github.com> Date: Mon, 2 Nov 2020 11:03:20 -0600 Subject: [PATCH 0974/1013] [performance] MC-37936: Performance generator for images (#6295) --- app/code/Magento/MediaStorage/App/Media.php | 2 +- app/code/Magento/Swatches/etc/config.xml | 7 +++++++ setup/src/Magento/Setup/Fixtures/ImagesFixture.php | 13 +++++++++++-- .../Fixtures/ImagesGenerator/ImagesGenerator.php | 13 +++++++++---- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/code/Magento/MediaStorage/App/Media.php b/app/code/Magento/MediaStorage/App/Media.php index f3a85cf3a9baa..34c20aab40bcb 100644 --- a/app/code/Magento/MediaStorage/App/Media.php +++ b/app/code/Magento/MediaStorage/App/Media.php @@ -255,7 +255,7 @@ private function setPlaceholderImage(): void */ private function getOriginalImage(string $resizedImagePath): string { - return preg_replace('|^.*((?:/[^/]+){3})$|', '$1', $resizedImagePath); + return preg_replace('|^.*?((?:/([^/])/([^/])/\2\3)?/?[^/]+$)|', '$1', $resizedImagePath); } /** diff --git a/app/code/Magento/Swatches/etc/config.xml b/app/code/Magento/Swatches/etc/config.xml index 9d36d9692b295..236e9237fb29b 100644 --- a/app/code/Magento/Swatches/etc/config.xml +++ b/app/code/Magento/Swatches/etc/config.xml @@ -14,6 +14,13 @@ <show_swatch_tooltip>1</show_swatch_tooltip> </frontend> </catalog> + <system> + <media_storage_configuration> + <allowed_resources> + <swatches_folder>attribute</swatches_folder> + </allowed_resources> + </media_storage_configuration> + </system> <general> <validator_data> <input_types> diff --git a/setup/src/Magento/Setup/Fixtures/ImagesFixture.php b/setup/src/Magento/Setup/Fixtures/ImagesFixture.php index 1878a48977156..cd403897de07a 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesFixture.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesFixture.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\ValidatorException; +use Magento\MediaStorage\Service\ImageResize; use Symfony\Component\Console\Output\OutputInterface; /** @@ -106,6 +107,10 @@ class ImagesFixture extends Fixture * @var array */ private $tableCache = []; + /** + * @var ImageResize + */ + private $imageResize; /** * @param FixtureModel $fixtureModel @@ -117,6 +122,7 @@ class ImagesFixture extends Fixture * @param \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory * @param \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param ImageResize $imageResize */ public function __construct( FixtureModel $fixtureModel, @@ -127,7 +133,8 @@ public function __construct( \Magento\Eav\Model\AttributeRepository $attributeRepository, \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory, \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + ImageResize $imageResize ) { parent::__construct($fixtureModel); @@ -139,6 +146,7 @@ public function __construct( $this->expressionFactory = $expressionFactory; $this->batchInsertFactory = $batchInsertFactory; $this->metadataPool = $metadataPool; + $this->imageResize = $imageResize; } /** @@ -147,9 +155,10 @@ public function __construct( */ public function execute() { - if (!$this->checkIfImagesExists()) { + if (!$this->checkIfImagesExists() && $this->getImagesToGenerate()) { $this->createImageEntities(); $this->assignImagesToProducts(); + iterator_to_array($this->imageResize->resizeFromThemes(), false); } } diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index 9b42548c4e105..bc6d57a869b5a 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -36,9 +36,9 @@ public function __construct( /** * Generates image from $data and puts its to /tmp folder - * * @param array $config * @return string $imagePath + * @throws \Exception */ public function generate($config) { @@ -70,9 +70,14 @@ public function generate($config) $relativePathToMedia = $mediaDirectory->getRelativePath($this->mediaConfig->getBaseTmpMediaPath()); $mediaDirectory->create($relativePathToMedia); - $absolutePathToMedia = $mediaDirectory->getAbsolutePath($this->mediaConfig->getBaseTmpMediaPath()); - $imagePath = $absolutePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; - imagejpeg($image, $imagePath, 100); + $imagePath = $relativePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; + $memory = fopen('php://memory', 'r+'); + if(!imagejpeg($image, $memory)) { + throw new \Exception('Could not create picture ' . $imagePath); + } + $mediaDirectory->writeFile($imagePath, stream_get_contents($memory, -1, 0)); + fclose($memory); + imagedestroy($image); // phpcs:enable return $imagePath; From c6627449678753ab930879d9e6faf3af6ed14d27 Mon Sep 17 00:00:00 2001 From: Dmytro Poperechnyy <dpoperechnyy@magento.com> Date: Mon, 2 Nov 2020 20:16:09 +0200 Subject: [PATCH 0975/1013] MC-37822: Support by TaxImportExport (#6269) --- .../TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php | 5 ++++- .../Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php index 844cfc535cfb2..f23fe8ffae7ae 100644 --- a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php +++ b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php @@ -81,7 +81,10 @@ public function execute() $content .= $rate->toString($template) . "\n"; } - return $this->fileFactory->create('tax_rates.csv', $content, DirectoryList::VAR_DIR); + // pass 'rm' parameter to delete a file after download + $fileContent = ['type' => 'string', 'value' => $content, 'rm' => true]; + + return $this->fileFactory->create('tax_rates.csv', $fileContent, DirectoryList::VAR_DIR); } /** diff --git a/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php index 0c8d0cf80544b..f4d31f3e421eb 100644 --- a/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php +++ b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php @@ -101,10 +101,11 @@ public function testExecute() ]); $rateCollectionMock->expects($this->once())->method('joinCountryTable')->willReturnSelf(); $rateCollectionMock->expects($this->once())->method('joinRegionTable')->willReturnSelf(); + $fileContent = ['type' => 'string', 'value' => $content, 'rm' => true]; $this->fileFactoryMock ->expects($this->once()) ->method('create') - ->with('tax_rates.csv', $content, DirectoryList::VAR_DIR); + ->with('tax_rates.csv', $fileContent, DirectoryList::VAR_DIR); $this->controller->execute(); } } From 672768c1061940d320ad62ab0c84847235054aff Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Tue, 3 Nov 2020 13:31:19 +0200 Subject: [PATCH 0976/1013] MC-38851:[MFTF] AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest failed because of bad design --- ...alogUiVerifyUsedInLinkCategoryGridTest.xml | 7 +- ...ogUiVerifyUsedInLinkedCategoryGridTest.xml | 102 ++++++++++++++++++ ...ediaGalleryImageDetailsEditActionGroup.xml | 2 +- ...ediaGalleryImageDetailsSaveActionGroup.xml | 2 +- ...EnhancedMediaGalleryEditDetailsSection.xml | 3 +- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml index f9ffda43d2547..a3f1bd7c01136 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest"> + <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest" deprecated="Use AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest instead"> <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"/> + <title value="DEPRECATED. 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"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml new file mode 100644 index 0000000000000..8e197b740bb11 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.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="AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest"> + <annotations> + <features value="MediaGalleryCatalogUi"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <title value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <description value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + </before> + + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="GoToAdminCategoryPageByIdActionGroup" stepKey="openCategoryPage"> + <argument name="id" value="$category.id$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + <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="AdminSaveCategoryFormActionGroup" stepKey="saveCategory"/> + + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderToVerifyLink"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCategoryImageFolder"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInCategories"> + <argument name="entityName" value="Categories"/> + </actionGroup> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageNumberOfRecordsActionGroup" stepKey="assertOneRecordInGrid"> + <argument name="numberOfRecords" value="1 records found"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageImageColumnActionGroup" stepKey="assertCategoryGridPageImageColumn"> + <argument name="file" value="{{UpdatedImageDetails.file}}"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageDetailsActionGroup" stepKey="assertCategoryInGrid"> + <argument name="category" value="$category$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetCategoriesGridFilters"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setAssetFilter"> + <argument name="filterName" value="Asset"/> + <argument name="optionName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterAppliedAfterUrlFilterApplier"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="openCategoryImageFolder"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml index 931da0ee06fef..5f7ab2d2d008f 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml @@ -13,6 +13,6 @@ <description>Edit image from the View Details panel</description> </annotations> <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.edit}}" stepKey="editImage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryEditDetailsSection.modalTitle}}" 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 index 0da3de9501c13..69e8d94522cde 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml @@ -19,6 +19,6 @@ <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"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.modalTitle}}" stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml index b0bed4563003e..351367055e62b 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -16,6 +16,7 @@ <element name="addNewKeyword" type="input" selector="[data-ui-id='add-keyword']"/> <element name="removeSelectedKeyword" type="button" selector="//span[contains(text(), '{{keyword}}')]/following-sibling::button[@data-action='remove-selected-item']" parameterized="true"/> <element name="cancel" type="button" selector="#image-details-action-cancel"/> - <element name="save" type="button" selector="#image-details-action-save"/> + <element name="save" type="button" selector="#image-details-action-save" timeout="30"/> + <element name="modalTitle" type="text" selector="//aside[contains(@class, 'media-gallery-edit-image-details') and contains(@class, '_show')]//h1[contains(., 'Edit Image')]"/> </section> </sections> From 0f9ac99527af2fcde10956668d1789f50abb4311 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Tue, 3 Nov 2020 13:40:53 +0200 Subject: [PATCH 0977/1013] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../Customer/Address/Billing/Address.php | 54 +++++++++++++++++++ ...rder_create_load_block_billing_address.xml | 1 + .../templates/order/create/form/address.phtml | 9 +++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php diff --git a/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php new file mode 100644 index 0000000000000..a7ec8e6587d70 --- /dev/null +++ b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\ViewModel\Customer\Address\Billing; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Sales\Model\AdminOrder\Create; +use Magento\Quote\Model\Quote\Address as QuoteAddress; + +/** + * Customer address formatter + */ +class Address implements ArgumentInterface +{ + /** + * @var Create + */ + protected $orderCreate; + + /** + * Customer billing address + * + * @param Create $orderCreate + */ + public function __construct( + Create $orderCreate + ) { + $this->orderCreate = $orderCreate; + } + + /** + * Return billing address object + * + * @return QuoteAddress + */ + public function getAddress(): QuoteAddress + { + return $this->orderCreate->getBillingAddress(); + } + + /** + * Get save billing address in the address book + * + * @return int + */ + public function getSaveInAddressBook(): int + { + return (int)$this->getAddress()->getSaveInAddressBook(); + } +} diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index c52f81d5cb56d..edefa8de55c7a 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -12,6 +12,7 @@ <arguments> <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + <argument name="customerBillingAddress" xsi:type="object">Magento\Sales\ViewModel\Customer\Address\Billing\Address</argument> </arguments> </block> </referenceContainer> 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 12927dcf526a3..bdb1a6c8cba94 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 @@ -24,6 +24,11 @@ endif; */ $customerAddressFormatter = $block->getData('customerAddressFormatter'); +/** + * @var \Magento\Sales\ViewModel\Customer\Address\Billing\Address $billingAddress + */ +$billingAddress = $block->getData('customerBillingAddress'); + /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address| * \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block @@ -114,7 +119,9 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if (!$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> + <?php if ($billingAddress && $billingAddress->getSaveInAddressBook()): ?> + checked="checked" + <?php elseif ($block->getIsShipping() && !$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> checked="checked" <?php endif; ?> class="admin__control-checkbox"/> From 88051820ac133b5921f85f7b18ddbebd14d5b0e5 Mon Sep 17 00:00:00 2001 From: OlgaVasyltsun <olga.vasyltsun@transoftgroup.com> Date: Tue, 3 Nov 2020 13:52:05 +0200 Subject: [PATCH 0978/1013] MC-38682: Can't create shipping label for existing order with FedEx shipping --- .../adminhtml/templates/order/packaging/popup_content.phtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c3418049a38a0..71299b33ff159 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 @@ -227,8 +227,8 @@ <div class="grid_prepare admin__page-subsection"></div> </div> </section> - <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <div id="packages_content"></div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <?php $scriptString = <<<script require(['jquery'], function($){ $("div#packages_content").on('click', "button[data-action='package-save-items']", From 9fb6e86600e53f6c46692e4db5ae91f0633e9a13 Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Tue, 3 Nov 2020 13:59:02 +0200 Subject: [PATCH 0979/1013] MC-37816: Performance - endless scheduled export of catalog with 100k+ products --- .../DownloadableImportExport/Model/Export/RowCustomizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php index daa874e829e54..3b805fef73434 100644 --- a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php @@ -82,7 +82,7 @@ public function prepareData($collection, $productIds): void ->addAttributeToSelect('samples_title'); // set global scope during export $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); - foreach ($collection as $product) { + foreach ($productCollection as $product) { $productLinks = $this->linkRepository->getLinksByProduct($product); $productSamples = $this->sampleRepository->getSamplesByProduct($product); $this->downloadableData[$product->getId()] = []; From afab3b5843b66b966f05d5f516e8e8a71049c643 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Tue, 3 Nov 2020 14:40:29 +0200 Subject: [PATCH 0980/1013] MC-37542: Create automated test for "[API] Create CMS page using API service" --- .../Magento/Cms/Api/PageRepositoryTest.php | 449 +++++++++++++----- 1 file changed, 341 insertions(+), 108 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index 757530c4da693..773a5d3fd8596 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -11,15 +11,19 @@ use Magento\Authorization\Model\RulesFactory; use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\Data\PageInterfaceFactory; +use Magento\Cms\Ui\Component\DataProvider as CmsDataProvider; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Api\AdminTokenServiceInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -83,18 +87,55 @@ class PageRepositoryTest extends WebapiAbstract */ private $createdPages = []; + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var CmsDataProvider + */ + private $cmsUiDataProvider; + + /** + * @var GetPageByIdentifierInterface + */ + private $getPageByIdentifier; + /** * @inheritdoc */ protected function setUp(): void { - $this->pageFactory = Bootstrap::getObjectManager()->create(PageInterfaceFactory::class); - $this->pageRepository = Bootstrap::getObjectManager()->create(PageRepositoryInterface::class); - $this->dataObjectHelper = Bootstrap::getObjectManager()->create(DataObjectHelper::class); - $this->dataObjectProcessor = Bootstrap::getObjectManager()->create(DataObjectProcessor::class); - $this->roleFactory = Bootstrap::getObjectManager()->get(RoleFactory::class); - $this->rulesFactory = Bootstrap::getObjectManager()->get(RulesFactory::class); - $this->adminTokens = Bootstrap::getObjectManager()->get(AdminTokenServiceInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->pageFactory = $this->objectManager->create(PageInterfaceFactory::class); + $this->pageRepository = $this->objectManager->create(PageRepositoryInterface::class); + $this->dataObjectHelper = $this->objectManager->create(DataObjectHelper::class); + $this->dataObjectProcessor = $this->objectManager->create(DataObjectProcessor::class); + $this->roleFactory = $this->objectManager->get(RoleFactory::class); + $this->rulesFactory = $this->objectManager->get(RulesFactory::class); + $this->adminTokens = $this->objectManager->get(AdminTokenServiceInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->filterBuilder = $this->objectManager->get(FilterBuilder::class); + $this->cmsUiDataProvider = $this->objectManager->create( + CmsDataProvider::class, + [ + 'name' => 'cms_page_listing_data_source', + 'primaryFieldName' => 'page_id', + 'requestFieldName' => 'id', + ] + ); + $this->getPageByIdentifier = $this->objectManager->get(GetPageByIdentifierInterface::class); } /** @@ -127,17 +168,11 @@ public function testGet(): void ->setIdentifier($pageIdentifier); $this->currentPage = $this->pageRepository->save($pageDataObject); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $this->currentPage->getId(), - 'httpMethod' => Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'GetById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'GetById', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . '/' . $this->currentPage->getId() + ); $page = $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $this->currentPage->getId()]); $this->assertNotNull($page['id']); @@ -147,6 +182,41 @@ public function testGet(): void $this->assertEquals($pageData->getIdentifier(), $pageIdentifier); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testGetByStores(string $requestStore): void + { + $page = $this->getPageByIdentifier->execute('page100', 0); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->updatePage($page, ['store_id' => $store->getId()]); + $page = $this->getPageByIdentifier->execute('page100', $store->getId()); + $comparedFields = $this->getPageRequestData()['page']; + $expectedData = array_intersect_key( + $this->dataObjectProcessor->buildOutputDataArray($page, PageInterface::class), + $comparedFields + ); + $serviceInfo = $this->getServiceInfo( + 'GetById', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = []; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData[PageInterface::PAGE_ID] = $page->getId(); + } + + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertNotNull($page['id']); + $actualData = array_intersect_key($page, $comparedFields); + $this->assertEquals($expectedData, $actualData, 'Error while getting page.'); + } + /** * Test create page * @@ -161,17 +231,7 @@ public function testCreate(): void $pageDataObject->setTitle($pageTitle) ->setIdentifier($pageIdentifier); - $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', - ], - ]; + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ 'page' => [ @@ -187,10 +247,44 @@ public function testCreate(): void $this->assertEquals($this->currentPage->getIdentifier(), $pageIdentifier); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testCreateByStores(string $requestStore): void + { + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); + $requestData = $this->getPageRequestData(); + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertNotNull($page['id']); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->currentPage = $this->getPageByIdentifier->execute( + $requestData['page'][PageInterface::IDENTIFIER], + $store->getId() + ); + $actualData = array_intersect_key($page, $requestData['page']); + $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); + if ($requestStore != 'all') { + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + $pageGridData = $this->cmsUiDataProvider->getData(); + $this->assertTrue( + $this->isPageInArray($pageGridData['items'], $page['id']), + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + ); + } + /** * Test update \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testUpdate() + public function testUpdate(): void { $pageTitle = self::PAGE_TITLE; $newPageTitle = self::PAGE_TITLE_NEW; @@ -210,17 +304,10 @@ public function testUpdate() PageInterface::class ); - $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', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_POST + ); $page = $this->_webApiCall($serviceInfo, ['page' => $pageData]); $this->assertNotNull($page['id']); @@ -249,17 +336,11 @@ public function testUpdateOneField(): void $this->currentPage = $this->pageRepository->save($pageDataObject); $pageId = $this->currentPage->getId(); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $pageId, - 'httpMethod' => Request::HTTP_METHOD_PUT, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_PUT, + self::RESOURCE_PATH . '/' . $pageId + ); $data = [ 'page' => [ @@ -283,12 +364,54 @@ public function testUpdateOneField(): void $this->assertEquals($page['content'], $content); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testUpdateByStores(string $requestStore): void + { + $page = $this->getPageByIdentifier->execute('page100', 0); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->updatePage($page, ['store_id' => $store->getId()]); + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_PUT, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = $this->getPageRequestData(); + + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertNotNull($page['id']); + $this->currentPage = $this->getPageByIdentifier->execute( + $requestData['page'][PageInterface::IDENTIFIER], + $store->getId() + ); + $actualData = array_intersect_key($page, $requestData['page']); + $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); + if ($requestStore != 'all') { + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + $pageGridData = $this->cmsUiDataProvider->getData(); + $this->assertTrue( + $this->isPageInArray($pageGridData['items'], $page['id']), + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + ); + } + /** * Test delete \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testDelete() + public function testDelete(): void { - $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); + $this->expectException(NoSuchEntityException::class); $pageTitle = self::PAGE_TITLE; $pageIdentifier = self::PAGE_IDENTIFIER_PREFIX . uniqid(); @@ -298,34 +421,66 @@ public function testDelete() ->setIdentifier($pageIdentifier); $this->currentPage = $this->pageRepository->save($pageDataObject); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $this->currentPage->getId(), - 'httpMethod' => Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'DeleteById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $this->currentPage->getId() + ); $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $this->currentPage->getId()]); $this->pageRepository->getById($this->currentPage['id']); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testDeleteByStores(string $requestStore): void + { + $page = $this->getPageByIdentifier->execute('page100', 0); + $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; + $store = $this->storeManager->getStore($storeCode); + $this->updatePage($page, ['store_id' => $store->getId()]); + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = []; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData[PageInterface::PAGE_ID] = $page->getId(); + } + $pageResponse = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertTrue($pageResponse); + if ($requestStore != 'all') { + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + $pageGridData = $this->cmsUiDataProvider->getData(); + $this->assertFalse( + $this->isPageInArray($pageGridData['items'], $page->getId()), + sprintf('The "%s" page should not be present on the "%s" store', $page->getTitle(), $storeCode) + ); + } + /** * Test search \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testSearch() + public function testSearch(): void { $cmsPages = $this->prepareCmsPages(); /** @var FilterBuilder $filterBuilder */ - $filterBuilder = Bootstrap::getObjectManager()->create(FilterBuilder::class); + $filterBuilder = $this->objectManager->create(FilterBuilder::class); /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = Bootstrap::getObjectManager() + $searchCriteriaBuilder = $this->objectManager ->create(SearchCriteriaBuilder::class); $filter1 = $filterBuilder @@ -351,7 +506,7 @@ public function testSearch() $searchCriteriaBuilder->addFilters([$filter3, $filter4]); /** @var SortOrderBuilder $sortOrderBuilder */ - $sortOrderBuilder = Bootstrap::getObjectManager()->create(SortOrderBuilder::class); + $sortOrderBuilder = $this->objectManager->create(SortOrderBuilder::class); /** @var SortOrder $sortOrder */ $sortOrder = $sortOrderBuilder->setField(PageInterface::IDENTIFIER) @@ -365,17 +520,11 @@ public function testSearch() $searchData = $searchCriteriaBuilder->create()->__toArray(); $requestData = ['searchCriteria' => $searchData]; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . "/search" . '?' . http_build_query($requestData), - 'httpMethod' => Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'GetList', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'GetList', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . "/search" . '?' . http_build_query($requestData) + ); $searchResult = $this->_webApiCall($serviceInfo, $requestData); $this->assertEquals(2, $searchResult['total_count']); @@ -388,8 +537,10 @@ public function testSearch() /** * Create page with the same identifier after one was removed. + * + * @return void */ - public function testCreateSamePage() + public function testCreateSamePage(): void { $pageIdentifier = self::PAGE_IDENTIFIER_PREFIX . uniqid(); @@ -399,10 +550,30 @@ public function testCreateSamePage() $this->currentPage = $this->pageRepository->getById($id); } + /** + * Get stores for CRUD operations + * + * @return array + */ + public function byStoresProvider(): array + { + return [ + 'default_store' => [ + 'request_store' => 'default', + ], + /*'second_store' => [ + 'request_store' => 'fixture_second_store', + ], + 'all' => [ + 'request_store' => 'all', + ],*/ + ]; + } + /** * @return PageInterface[] */ - private function prepareCmsPages() + private function prepareCmsPages(): array { $result = []; @@ -437,19 +608,9 @@ private function prepareCmsPages() * @param string $identifier * @return string */ - private function createPageWithIdentifier($identifier) + private function createPageWithIdentifier($identifier): string { - $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', - ], - ]; + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ 'page' => [ PageInterface::IDENTIFIER => $identifier, @@ -466,19 +627,13 @@ private function createPageWithIdentifier($identifier) * @param string $pageId * @return void */ - private function deletePageByIdentifier($pageId) + private function deletePageByIdentifier($pageId): void { - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $pageId, - 'httpMethod' => Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'DeleteById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $pageId + ); $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $pageId]); } @@ -547,7 +702,7 @@ public function testSaveDesign(): void //Updating the user role to allow access to design properties. /** @var Rules $rules */ - $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules = $this->objectManager->create(Rules::class); $rules->setRoleId($role->getId()); $rules->setResources(['Magento_Cms::page', 'Magento_Cms::save_design']); $rules->saveRel(); @@ -562,7 +717,7 @@ public function testSaveDesign(): void //Updating our role to remove design properties access. /** @var Rules $rules */ - $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules = $this->objectManager->create(Rules::class); $rules->setRoleId($role->getId()); $rules->setResources(['Magento_Cms::page']); $rules->saveRel(); @@ -587,4 +742,82 @@ public function testSaveDesign(): void //We don't have permissions to do that. $this->assertEquals('You are not allowed to change CMS pages design settings', $exceptionMessage); } + + /** + * Get service info array + * + * @param string $soapOperation + * @param string $httpMethod + * @param string $resourcePath + * @return array + */ + private function getServiceInfo( + string $soapOperation, + string $httpMethod, + string $resourcePath = self::RESOURCE_PATH + ): array { + return [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => $httpMethod, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . $soapOperation, + ], + ]; + } + + /** + * Check that the page is in the page grid data + * + * @param array $pageGridData + * @param int $pageId + * @return bool + */ + private function isPageInArray(array $pageGridData, int $pageId): bool + { + $isPagePresent = false; + foreach ($pageGridData as $pageData) { + if ($pageData['page_id'] == $pageId) { + $isPagePresent = true; + break; + } + } + + return $isPagePresent; + } + + /** + * Update page with data + * + * @param PageInterface $page + * @param array $pageData + * @return PageInterface + */ + private function updatePage(PageInterface $page, array $pageData): PageInterface + { + $page->addData($pageData); + + return $this->pageRepository->save($page); + } + + /** + * Get request data for create or update page + * + * @return array + */ + private function getPageRequestData(): array + { + return [ + 'page' => [ + PageInterface::IDENTIFIER => self::PAGE_IDENTIFIER_PREFIX . uniqid(), + PageInterface::TITLE => self::PAGE_TITLE . uniqid(), + 'active' => true, + PageInterface::PAGE_LAYOUT => '1column', + PageInterface::CONTENT => self::PAGE_CONTENT, + ] + ]; + } } From c4953900027ae2523c15e35d7ab5c6e7fc5c2954 Mon Sep 17 00:00:00 2001 From: Bohdan Shevchenko <1408sheva@gmail.com> Date: Tue, 3 Nov 2020 14:57:25 +0200 Subject: [PATCH 0981/1013] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml | 4 ++-- .../Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml | 2 +- .../AdminGridControlsSection/AdminGridHeadersSection.xml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml index 31dd27b0f599c..bf9f199634078 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml @@ -15,9 +15,9 @@ <data key="content">sales25off everything!</data> <data key="is_active">0</data> </entity> - <entity name="TestBlock" type="block"> + <entity name="ActiveTestBlock" type="block"> <data key="title" unique="suffix">Test Block</data> - <data key="identifier" unique="suffix">TestBlock</data> + <data key="identifier" unique="suffix">ActiveTestBlock</data> <data key="store_id">All Store Views</data> <data key="content">Test Block content</data> <data key="is_active">1</data> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml index 65df6fd80e865..245b1486058b8 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -23,7 +23,7 @@ <createData entity="_newDefaultCmsPage" stepKey="createSecondCMSPage" /> <createData entity="_emptyCmsPage" stepKey="createThirdCMSPage" /> <createData entity="Sales25offBlock" stepKey="createFirstCmsBlock"/> - <createData entity="TestBlock" stepKey="createSecondCmsBlock"/> + <createData entity="ActiveTestBlock" stepKey="createSecondCmsBlock"/> <createData entity="_emptyCmsBlock" stepKey="createThirdCmsBlock"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml index 851e65e61bb1c..c7aa7604d7ade 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml @@ -11,6 +11,6 @@ <element name="title" type="text" selector=".page-title-wrapper h1"/> <element name="headerByName" type="text" selector="//div[@data-role='grid-wrapper']//span[@class='data-grid-cell-content' and contains(text(), '{{var1}}')]/parent::*" parameterized="true"/> <element name="columnsNames" type="text" selector="[data-role='grid-wrapper'] .data-grid-th > span"/> - <element name="totalRecords" type="text" selector="div.admin__data-grid-header-row .row>div:nth-child(1)"/> + <element name="totalRecords" type="text" selector="div.admin__data-grid-header-row.row.row-gutter div.row div.admin__control-support-text"/> </section> </sections> From 1886177acf3e36a2f3c8c666cad53555d3a17eb3 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Tue, 3 Nov 2020 17:22:21 +0200 Subject: [PATCH 0982/1013] MC-37902: Create automated test for "Paging and sort by function on widget grid" --- .../Block/Adminhtml/Widget/InstanceTest.php | 308 ++++++++++++++++++ .../Magento/Widget/_files/widgets.php | 103 ++++++ .../Widget/_files/widgets_rollback.php | 25 ++ 3 files changed, 436 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php create mode 100644 dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php new file mode 100644 index 0000000000000..3e01305d9f39f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php @@ -0,0 +1,308 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Widget\Block\Adminhtml\Widget; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme as ThemeResource; +use Magento\Theme\Model\ThemeFactory; +use PHPUnit\Framework\TestCase; + +/** + * Checks widget grid filtering and sorting + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class InstanceTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var PageFactory */ + private $pageFactory; + + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + } + + /** + * @dataProvider gridFiltersDataProvider + * + * @magentoDataFixture Magento/Widget/_files/widgets.php + * + * @param array $filter + * @param array $expectedWidgets + * @return void + */ + public function testGridFiltering(array $filter, array $expectedWidgets): void + { + $this->request->setParams($filter); + $collection = $this->getGridCollection(); + + $this->assertWidgets($expectedWidgets, $collection); + } + + /** + * @return array + */ + public function gridFiltersDataProvider(): array + { + return [ + 'first_page' => [ + 'filter' => [ + 'limit' => 2, + 'page' => 1, + ], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + ], + ], + 'second_page' => [ + 'filter' => [ + 'limit' => 2, + 'page' => 2, + ], + 'expected_widgets' => [ + 'recently compared products', + ], + ], + 'filter_by_title' => [ + 'filter' => [ + 'filter' => base64_encode('title=product link widget title'), + ], + 'expected_widgets' => [ + 'product link widget title', + ], + ], + 'filter_by_type' => [ + 'filter' => [ + 'filter' => base64_encode('type=Magento%5CCms%5CBlock%5CWidget%5CPage%5CLink'), + ], + 'expected_widgets' => [ + 'cms page widget title', + ], + ], + 'filter_by_theme' => [ + 'filter' => [ + 'filter' => base64_encode('theme_id=' . $this->loadThemeIdByCode('Magento/blank')), + ], + 'expected_widgets' => [ + 'recently compared products', + ], + ], + 'filter_by_sort_order' => [ + 'filter' => [ + 'filter' => base64_encode('sort_order=1'), + ], + 'expected_widgets' => [ + 'recently compared products' + ], + ], + 'filter_by_multiple_filters' => [ + 'filter' => [ + 'filter' => base64_encode( + 'type=Magento%5CCatalog%5CBlock%5CWidget%5CRecentlyCompared&sort_order=1' + ), + ], + 'expected_widgets' => [ + 'recently compared products' + ], + ], + ]; + } + + /** + * @dataProvider gridSortDataProvider + * + * @magentoDataFixture Magento/Widget/_files/widgets.php + * + * @param array $filter + * @param array $expectedWidgets + * @return void + */ + public function testGridSorting(array $filter, array $expectedWidgets): void + { + $this->request->setParams($filter); + $collection = $this->getGridCollection(); + $this->assertCount(count($expectedWidgets), $collection); + $this->assertEquals($expectedWidgets, $collection->getColumnValues('title')); + } + + /** + * @return array + */ + public function gridSortDataProvider(): array + { + return [ + 'sort_by_id_asc' => [ + 'filter' => ['sort' => 'instance_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_id_desc' => [ + 'filter' => ['sort' => 'instance_id', 'dir' => 'desc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_title_asc' => [ + 'filter' => ['sort' => 'title', 'dir' => 'asc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_title_desc' => [ + 'filter' => ['sort' => 'title', 'dir' => 'desc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_type_asc' => [ + 'filter' => ['sort' => 'type', 'dir' => 'asc'], + 'expected_widgets' => [ + 'product link widget title', + 'recently compared products', + 'cms page widget title', + ], + ], + 'sort_by_type_desc' => [ + 'filter' => ['sort' => 'type', 'dir' => 'desc'], + 'expected_widgets' => [ + 'cms page widget title', + 'recently compared products', + 'product link widget title', + ], + ], + 'sort_by_sort_order_asc' => [ + 'filter' => ['sort' => 'sort_order', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_sort_order_desc' => [ + 'filter' => ['sort' => 'sort_order', 'dir' => 'desc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_theme_asc' => [ + 'filter' => ['sort' => 'theme_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'cms page widget title', + 'product link widget title', + ], + ], + 'sort_by_theme_desc' => [ + 'filter' => ['sort' => 'theme_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'cms page widget title', + 'product link widget title', + ], + ], + ]; + } + + /** + * Load theme by theme id + * + * @param string $code + * @return int + */ + private function loadThemeIdByCode(string $code): int + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ThemeFactory $themeFactory */ + $themeFactory = $objectManager->get(ThemeFactory::class); + /** @var ThemeResource $themeResource */ + $themeResource = $objectManager->get(ThemeResource::class); + $theme = $themeFactory->create(); + $themeResource->load($theme, $code, 'code'); + + return (int)$theme->getId(); + } + + /** + * Assert widget instances + * + * @param $expectedWidgets + * @param AbstractCollection $collection + * @return void + */ + private function assertWidgets($expectedWidgets, AbstractCollection $collection): void + { + $this->assertCount(count($expectedWidgets), $collection); + foreach ($expectedWidgets as $widgetTitle) { + $item = $collection->getItemByColumnValue('title', $widgetTitle); + $this->assertNotNull($item); + } + } + + /** + * Prepare page layout + * + * @return LayoutInterface + */ + private function preparePageLayout(): LayoutInterface + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'adminhtml_widget_instance_index', + ]); + + return $page->getLayout()->generateXml(); + } + + /** + * Get prepared grid collection + * + * @return AbstractCollection + */ + private function getGridCollection(): AbstractCollection + { + $layout = $this->preparePageLayout(); + $containerBlock = $layout->getBlock('adminhtml.widget.instance.grid.container'); + $grid = $containerBlock->getChildBlock('grid'); + $this->assertNotFalse($grid); + + return $grid->getPreparedCollection(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php new file mode 100644 index 0000000000000..fa0f7d9fe9918 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php @@ -0,0 +1,103 @@ +<?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\Block\Product\Widget\Link as ProductLink; +use Magento\Catalog\Block\Widget\RecentlyCompared; +use Magento\Cms\Api\GetPageByIdentifierInterface; +use Magento\Cms\Block\Widget\Page\Link as PageLink; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme as ThemeResource; +use Magento\Theme\Model\ThemeFactory; +use Magento\Widget\Model\ResourceModel\Widget\Instance as InstanceResource; +use Magento\Widget\Model\Widget\InstanceFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ThemeFactory $themeFactory */ +$themeFactory = $objectManager->get(ThemeFactory::class); +/** @var ThemeResource $themeResource */ +$themeResource = $objectManager->get(ThemeResource::class); +$lumaTheme = $themeFactory->create(); +$themeResource->load($lumaTheme, 'Magento/luma', 'code'); +$blankTheme = $themeFactory->create(); +$themeResource->load($blankTheme, 'Magento/blank', 'code'); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$defaultStoreId = (int)$storeManager->getStore('default')->getId(); +/** @var GetPageByIdentifierInterface $getPageByIdentifier */ +$getPageByIdentifier = $objectManager->get(GetPageByIdentifierInterface::class); +$homePage = $getPageByIdentifier->execute('home', $defaultStoreId); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$productId = (int)$productRepository->get('simple2')->getId(); +/** @var InstanceFactory $widgetFactory */ +$widgetFactory = $objectManager->get(InstanceFactory::class); +/** @var InstanceResource $widgetResource */ +$widgetResource = $objectManager->get(InstanceResource::class); +$cmsPageWidget = $widgetFactory->create(); +$cmsPageWidgetData = [ + 'instance_type' => PageLink::class, + 'instance_code' => 'cms_page_link', + 'theme_id' => $lumaTheme->getId(), + 'title' => 'cms page widget title', + 'sort_order' => 3, + 'store_ids' => [$defaultStoreId], + 'widget_parameters' => [ + 'page_id' => $homePage->getId(), + ], +]; +$cmsPageWidget->setData($cmsPageWidgetData); +$widgetResource->save($cmsPageWidget); + +$productLinkWidget = $widgetFactory->create(); +$productLinkWidgetData = [ + 'instance_type' => ProductLink::class, + 'instance_code' => 'catalog_product_link', + 'theme_id' => $lumaTheme->getId(), + 'title' => 'product link widget title', + 'sort_order' => 2, + 'store_ids' => [$defaultStoreId], + 'pages_groups' => [ + 'page_group' => 'all_pages', + 'all_pages' => [ + 'page_id' => 0, + 'layout_handle' => 'default', + 'for' => 'all', + 'block' => 'content', + 'template' => 'product/widget/link/link_block.phtml', + ], + ], + 'widget_parameters' => [ + 'product/' . $productId, + ], +]; + +$productLinkWidget->setData($productLinkWidgetData); +$widgetResource->save($productLinkWidget); + +$recentlyComparedProductWidget = $widgetFactory->create(); +$recentlyComparedProductWidgetData = [ + 'instance_type' => RecentlyCompared::class, + 'instance_code' => 'catalog_recently_compared', + 'theme_id' => $blankTheme->getId(), + 'title' => 'recently compared products', + 'store_ids' => [$defaultStoreId], + 'sort_order' => 1, + 'widget_parameters' => [ + 'uiComponent' => 'widget_recently_compared', + 'page_size' => 5, + 'show_attributes' => ['name'], + 'show_buttons' => ['add_to_cart'], + ], +]; +$recentlyComparedProductWidget->setData($recentlyComparedProductWidgetData); +$widgetResource->save($recentlyComparedProductWidget); diff --git a/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php new file mode 100644 index 0000000000000..63c8183fe3431 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_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\TestFramework\Helper\Bootstrap; +use Magento\Widget\Model\ResourceModel\Widget\Instance; +use Magento\Widget\Model\ResourceModel\Widget\Instance\CollectionFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CollectionFactory $collectionFactory */ +$collectionFactory = $objectManager->get(CollectionFactory::class); +/** @var Instance $widgetResourceModel */ +$widgetResourceModel = $objectManager->get(Instance::class); + +$titles = ['cms page widget title', 'product link widget title', 'recently compared products']; +$widgets = $collectionFactory->create()->addFieldToFilter('title', $titles); +foreach ($widgets as $widget) { + $widgetResourceModel->delete($widget); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); From 987f4b8bf5c628eb4474ac553d74792d8fd6f2ab Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Tue, 3 Nov 2020 18:27:23 +0200 Subject: [PATCH 0983/1013] MC-38851:[MFTF] AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest failed because of bad design --- ...ediaGalleryImageDetailsSaveActionGroup.xml | 2 +- ...nhancedMediaGalleryImageActionsSection.xml | 4 +- ...diaGalleryEditImageDetailsFromGridTest.xml | 52 ++++++++++++++++ .../AdminMediaGalleryEditImageDetailsTest.xml | 7 ++- ...aGalleryEditImageDetailsFromDialogTest.xml | 61 +++++++++++++++++++ ...daloneMediaGalleryEditImageDetailsTest.xml | 7 ++- 6 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml create mode 100644 app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml index 69e8d94522cde..0da3de9501c13 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml @@ -19,6 +19,6 @@ <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.title}}" userInput="{{image.title}}" stepKey="setTitle" /> <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.description}}" userInput="{{image.description}}" stepKey="setDescription" /> <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.save}}" stepKey="saveDetails"/> - <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.modalTitle}}" stepKey="waitForLoadingMaskToDisappear"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index 1a8f6f553d4ce..17c3e82144d6f 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -11,8 +11,8 @@ <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[text()='View Details']" timeout="30"/> - <element name="delete" type="button" selector="[data-ui-id='action-delete']"/> - <element name="edit" type="button" selector="[data-ui-id='action-edit']"/> + <element name="delete" type="button" selector="//ul[@class='action-menu _active']//a[text()='Delete']"/> + <element name="edit" type="button" selector="//ul[@class='action-menu _active']//a[text()='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/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml new file mode 100644 index 0000000000000..91a17a7c1167c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.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="AdminMediaGalleryEditImageDetailsFromGridTest"> + <annotations> + <features value="MediaGalleryUi"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <title value="User edits image meta data in media gallery"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + </before> + + <after> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <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/AdminMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml index 960443998d010..34c3159ab769e 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryEditImageDetailsTest"> + <test name="AdminMediaGalleryEditImageDetailsTest" deprecated="Use AdminMediaGalleryEditImageDetailsFromGridTest instead"> <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"/> + <title value="DEPRECATED. 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"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryEditImageDetailsFromGridTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml new file mode 100644 index 0000000000000..250b42c5510a7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.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="AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest"> + <annotations> + <features value="MediaGalleryUi"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in standalone media gallery"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + </before> + + <after> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <generateDate date="now" format="s" stepKey="secondsFromMinuteStart"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="clickViewDetails"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> + + <executeJS function="return 60 - {$secondsFromMinuteStart} + 5" stepKey="calcWaitPeriod"/> + <wait time="$calcWaitPeriod" stepKey="waitTillEndOfAMinute"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <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="AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup" stepKey="assertUpdatedAtTimeChanged" /> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml index 58c6f32b8d72f..039e9212945e2 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminStandaloneMediaGalleryEditImageDetailsTest"> + <test name="AdminStandaloneMediaGalleryEditImageDetailsTest" deprecated="Use AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest instead"> <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"/> + <title value="DEPRECATED. 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"/> + <skip> + <issueId value="DEPRECATED">Use AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> From 458ac648faab828c1b225a2c94d6db1edd5ca936 Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oposyniak@magento.com> Date: Tue, 3 Nov 2020 12:19:03 -0600 Subject: [PATCH 0984/1013] [AWS S3] Simple cache (#6299) --- app/code/Magento/AwsS3/Driver/AwsS3.php | 57 +- .../Magento/AwsS3/Driver/AwsS3Factory.php | 24 +- .../AwsS3/Test/Unit/Driver/AwsS3Test.php | 34 +- app/code/Magento/AwsS3/composer.json | 3 +- .../Driver/DriverFactoryInterface.php | 2 + composer.json | 3 +- composer.lock | 838 +++++++++--------- pub/media/sitemap/.htaccess | 8 +- 8 files changed, 455 insertions(+), 514 deletions(-) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 5dc1f7e8cb216..d0c054b637530 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -8,7 +8,7 @@ namespace Magento\AwsS3\Driver; use Exception; -use League\Flysystem\AwsS3v3\AwsS3Adapter; +use League\Flysystem\AdapterInterface; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem\DriverInterface; @@ -32,30 +32,38 @@ class AwsS3 implements RemoteDriverInterface private const CONFIG = ['ACL' => 'private']; /** - * @var AwsS3Adapter + * @var AdapterInterface */ private $adapter; + /** + * @var LoggerInterface + */ + private $logger; + /** * @var array */ private $streams = []; /** - * @var LoggerInterface + * @var string */ - private $logger; + private $objectUrl; /** - * @param AwsS3Adapter $adapter + * @param AdapterInterface $adapter * @param LoggerInterface $logger + * @param string $objectUrl */ public function __construct( - AwsS3Adapter $adapter, - LoggerInterface $logger + AdapterInterface $adapter, + LoggerInterface $logger, + string $objectUrl ) { $this->adapter = $adapter; $this->logger = $logger; + $this->objectUrl = $objectUrl; } /** @@ -282,26 +290,27 @@ public function getAbsolutePath($basePath, $path, $scheme = null) * @param string $path Relative path * @return string Absolute path */ - private function normalizeAbsolutePath(string $path = '.'): string + private function normalizeAbsolutePath(string $path = '/'): string { $path = ltrim($path, '/'); - $path = str_replace( - $this->adapter->getClient()->getObjectUrl( - $this->adapter->getBucket(), - $this->adapter->applyPathPrefix('.') - ), - '', - $path - ); + $path = str_replace($this->getObjectUrl(''), '', $path); if (!$path) { - $path = '.'; + $path = '/'; } - return $this->adapter->getClient()->getObjectUrl( - $this->adapter->getBucket(), - $this->adapter->applyPathPrefix($path) - ); + return $this->getObjectUrl($path); + } + + /** + * Retrieves object URL from cache. + * + * @param string $path + * @return string + */ + private function getObjectUrl(string $path): string + { + return $this->objectUrl . ltrim($path, '/'); } /** @@ -312,11 +321,7 @@ private function normalizeAbsolutePath(string $path = '.'): string */ private function normalizeRelativePath(string $path): string { - return str_replace( - $this->normalizeAbsolutePath(), - '', - $path - ); + return str_replace($this->normalizeAbsolutePath(), '', $path); } /** diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php index 2042e10090407..87efd7c13f398 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -10,6 +10,7 @@ use Aws\S3\S3Client; use League\Flysystem\AwsS3v3\AwsS3Adapter; use Magento\Framework\ObjectManagerInterface; +use Magento\RemoteStorage\Driver\DriverException; use Magento\RemoteStorage\Driver\DriverFactoryInterface; use Magento\RemoteStorage\Driver\RemoteDriverInterface; @@ -32,11 +33,7 @@ public function __construct(ObjectManagerInterface $objectManager) } /** - * Creates an instance of AWS S3 driver. - * - * @param array $config - * @param string $prefix - * @return RemoteDriverInterface + * @inheritDoc */ public function create(array $config, string $prefix): RemoteDriverInterface { @@ -46,17 +43,18 @@ public function create(array $config, string $prefix): RemoteDriverInterface unset($config['credentials']); } + if (empty($config['bucket']) || empty($config['region'])) { + throw new DriverException(__('Bucket and region are required values')); + } + + $client = new S3Client($config); + $adapter = new AwsS3Adapter($client, $config['bucket'], $prefix); + return $this->objectManager->create( AwsS3::class, [ - 'adapter' => $this->objectManager->create( - AwsS3Adapter::class, - [ - 'client' => $this->objectManager->create(S3Client::class, ['args' => $config]), - 'bucket' => $config['bucket'], - 'prefix' => $prefix - ] - ) + 'adapter' => $adapter, + 'objectUrl' => $client->getObjectUrl($adapter->getBucket(), $adapter->applyPathPrefix('.')) ] ); } diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php index 173143b709519..20bc28be4583c 100644 --- a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -7,8 +7,8 @@ namespace Magento\AwsS3\Test\Unit\Driver; -use Aws\S3\S3ClientInterface; use League\Flysystem\AwsS3v3\AwsS3Adapter; +use League\Flysystem\Cached\CachedAdapter; use Magento\AwsS3\Driver\AwsS3; use Magento\Framework\Exception\FileSystemException; use PHPUnit\Framework\MockObject\MockObject; @@ -32,41 +32,15 @@ class AwsS3Test extends TestCase */ private $adapterMock; - /** - * @var S3ClientInterface|MockObject - */ - private $clientMock; - - /** - * @var LoggerInterface - */ - private $logger; - /** * @inheritDoc */ protected function setUp(): void { - $this->adapterMock = $this->createMock(AwsS3Adapter::class); - $this->clientMock = $this->getMockForAbstractClass(S3ClientInterface::class); - $this->logger = $this->getMockForAbstractClass(LoggerInterface::class); - - $this->adapterMock->method('applyPathPrefix') - ->willReturnArgument(0); - $this->adapterMock->method('getBucket') - ->willReturn('test'); - $this->adapterMock->method('getClient') - ->willReturn($this->clientMock); - $this->clientMock->method('getObjectUrl') - ->willReturnCallback(function (string $bucket, string $path) { - if ($path === '.') { - $path = ''; - } - - return self::URL . $path; - }); + $this->adapterMock = $this->createMock(CachedAdapter::class); + $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $this->driver = new AwsS3($this->adapterMock, $this->logger); + $this->driver = new AwsS3($this->adapterMock, $loggerMock, self::URL); } /** diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json index ce5396223f58d..6e72ac37f8ba6 100644 --- a/app/code/Magento/AwsS3/composer.json +++ b/app/code/Magento/AwsS3/composer.json @@ -9,7 +9,8 @@ "magento/framework": "^100.0.2", "magento/module-remote-storage": "*", "league/flysystem": "^1.0", - "league/flysystem-aws-s3-v3": "^1.0" + "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-cached-adapter": "^1.0" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php index 5268cbaea4a77..b9074efc527f0 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -18,6 +18,8 @@ interface DriverFactoryInterface * @param array $config * @param string $prefix * @return RemoteDriverInterface + * + * @throws DriverException */ public function create(array $config, string $prefix): RemoteDriverInterface; } diff --git a/composer.json b/composer.json index 985bf0d9e16ea..b5a484d3828b8 100644 --- a/composer.json +++ b/composer.json @@ -82,7 +82,8 @@ "webonyx/graphql-php": "^0.13.8", "wikimedia/less.php": "~1.8.0", "league/flysystem": "^1.0", - "league/flysystem-aws-s3-v3": "^1.0" + "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-cached-adapter": "^1.0" }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", diff --git a/composer.lock b/composer.lock index f7e0df29a8159..8f855e574a834 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3eb0d410285c05a9f2649b65d8b9a1d5", + "content-hash": "50fd3418a729ef9b577d214fe6c9b0b1", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.158.7", + "version": "3.158.16", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "9afc422ad4ef0f1de9fa2a0be4b856c1dfa31123" + "reference": "6e8fc20ff7bc21b28e80815a9818b6ca7928ae3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9afc422ad4ef0f1de9fa2a0be4b856c1dfa31123", - "reference": "9afc422ad4ef0f1de9fa2a0be4b856c1dfa31123", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6e8fc20ff7bc21b28e80815a9818b6ca7928ae3a", + "reference": "6e8fc20ff7bc21b28e80815a9818b6ca7928ae3a", "shasum": "" }, "require": { @@ -89,7 +89,7 @@ "s3", "sdk" ], - "time": "2020-10-15T18:16:19+00:00" + "time": "2020-10-28T20:19:05+00:00" }, { "name": "colinmollenhour/cache-backend-file", @@ -309,16 +309,16 @@ }, { "name": "composer/composer", - "version": "1.10.15", + "version": "1.10.16", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12" + "reference": "217f0272673c72087862c40cf91ac07eb438d778" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/547c9ee73fe26c77af09a0ea16419176b1cdbd12", - "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12", + "url": "https://api.github.com/repos/composer/composer/zipball/217f0272673c72087862c40cf91ac07eb438d778", + "reference": "217f0272673c72087862c40cf91ac07eb438d778", "shasum": "" }, "require": { @@ -399,7 +399,7 @@ "type": "tidelift" } ], - "time": "2020-10-13T13:59:09+00:00" + "time": "2020-10-24T07:55:59+00:00" }, { "name": "composer/semver", @@ -552,16 +552,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6e076a124f7ee146f2487554a94b6a19a74887ba", + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba", "shasum": "" }, "require": { @@ -606,7 +606,7 @@ "type": "tidelift" } ], - "time": "2020-08-19T10:27:58+00:00" + "time": "2020-10-24T12:39:10+00:00" }, { "name": "container-interop/container-interop", @@ -2060,23 +2060,23 @@ }, { "name": "laminas/laminas-i18n", - "version": "2.10.3", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-i18n.git", - "reference": "94ff957a1366f5be94f3d3a9b89b50386649e3ae" + "reference": "85678f444b6dcb48e8a04591779e11c24e5bb901" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/94ff957a1366f5be94f3d3a9b89b50386649e3ae", - "reference": "94ff957a1366f5be94f3d3a9b89b50386649e3ae", + "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/85678f444b6dcb48e8a04591779e11c24e5bb901", + "reference": "85678f444b6dcb48e8a04591779e11c24e5bb901", "shasum": "" }, "require": { "ext-intl": "*", "laminas/laminas-stdlib": "^2.7 || ^3.0", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ~8.0.0" }, "conflict": { "phpspec/prophecy": "<1.9.0" @@ -2090,10 +2090,10 @@ "laminas/laminas-config": "^2.6", "laminas/laminas-eventmanager": "^2.6.2 || ^3.0", "laminas/laminas-filter": "^2.6.1", - "laminas/laminas-servicemanager": "^2.7.5 || ^3.0.3", + "laminas/laminas-servicemanager": "^3.2.1", "laminas/laminas-validator": "^2.6", "laminas/laminas-view": "^2.6.3", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16" + "phpunit/phpunit": "^9.3" }, "suggest": { "laminas/laminas-cache": "Laminas\\Cache component", @@ -2107,10 +2107,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" - }, "laminas": { "component": "Laminas\\I18n", "config-provider": "Laminas\\I18n\\ConfigProvider" @@ -2131,7 +2127,13 @@ "i18n", "laminas" ], - "time": "2020-03-29T12:51:08+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-10-24T13:14:32+00:00" }, { "name": "laminas/laminas-inputfilter", @@ -3589,18 +3591,65 @@ "description": "Flysystem adapter for the AWS S3 SDK v3.x", "time": "2020-10-08T18:58:37+00:00" }, + { + "name": "league/flysystem-cached-adapter", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-cached-adapter.git", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-cached-adapter/zipball/d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "shasum": "" + }, + "require": { + "league/flysystem": "~1.0", + "psr/cache": "^1.0.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7", + "predis/predis": "~1.0", + "tedivm/stash": "~0.12" + }, + "suggest": { + "ext-phpredis": "Pure C implemented extension for PHP" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Cached\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "frankdejonge", + "email": "info@frenky.net" + } + ], + "description": "An adapter decorator to enable meta-data caching.", + "time": "2020-07-25T15:56:04+00:00" + }, { "name": "league/mime-type-detection", - "version": "1.5.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28" + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ea2fbfc988bade315acd5967e6d02274086d0f28", - "reference": "ea2fbfc988bade315acd5967e6d02274086d0f28", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa", + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa", "shasum": "" }, "require": { @@ -3638,7 +3687,7 @@ "type": "tidelift" } ], - "time": "2020-09-21T18:10:53+00:00" + "time": "2020-10-18T11:50:25+00:00" }, { "name": "magento/composer", @@ -4374,6 +4423,52 @@ ], "time": "2020-09-08T04:24:43+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.0.0", @@ -4793,16 +4888,16 @@ }, { "name": "symfony/console", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124" + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/90933b39c7b312fc3ceaa1ddeac7eb48cb953124", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124", + "url": "https://api.github.com/repos/symfony/console/zipball/20f73dd143a5815d475e0838ff867bce1eebd9d5", + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5", "shasum": "" }, "require": { @@ -4837,11 +4932,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" @@ -4880,31 +4970,26 @@ "type": "tidelift" } ], - "time": "2020-09-15T07:58:55+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9" + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/e544e24472d4c97b2d11ade7caacd446727c6bf9", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\CssSelector\\": "" @@ -4947,20 +5032,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd" + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e17bb5e0663dc725f7cdcafc932132735b4725cd", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4204f13d2d0b7ad09454f221bb2195fccdf1fe98", + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98", "shasum": "" }, "require": { @@ -4989,11 +5074,6 @@ "symfony/http-kernel": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -5032,7 +5112,7 @@ "type": "tidelift" } ], - "time": "2020-09-18T14:07:46+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5112,16 +5192,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae" + "reference": "df08650ea7aee2d925380069c131a66124d79177" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/1a8697545a8d87b9f2f6b1d32414199cc5e20aae", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/df08650ea7aee2d925380069c131a66124d79177", + "reference": "df08650ea7aee2d925380069c131a66124d79177", "shasum": "" }, "require": { @@ -5129,11 +5209,6 @@ "symfony/polyfill-ctype": "~1.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" @@ -5172,31 +5247,26 @@ "type": "tidelift" } ], - "time": "2020-09-27T14:02:37+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/finder", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8" + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", + "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Finder\\": "" @@ -5235,24 +5305,24 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-ctype": "For best performance" @@ -5260,7 +5330,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5311,26 +5381,25 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251" + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117", + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117", "shasum": "" }, "require": { - "php": ">=5.3.3", + "php": ">=7.1", "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php70": "^1.10", "symfony/polyfill-php72": "^1.10" }, "suggest": { @@ -5339,7 +5408,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5396,24 +5465,24 @@ "type": "tidelift" } ], - "time": "2020-08-04T06:02:08+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + "reference": "727d1096295d807c309fb01a851577302394c897" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", + "reference": "727d1096295d807c309fb01a851577302394c897", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -5421,7 +5490,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5477,24 +5546,24 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-mbstring": "For best performance" @@ -5502,7 +5571,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5554,106 +5623,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" - }, - "dist": { - "type": "zip", - "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.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "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" - ], - "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" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "639447d008615574653fb3bc60d1986d7172eaae" + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", - "reference": "639447d008615574653fb3bc60d1986d7172eaae", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5704,29 +5696,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5780,29 +5772,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", "shasum": "" }, "require": { - "php": ">=7.0.8" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5860,31 +5852,26 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/process", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "9b887acc522935f77555ae8813495958c7771ba7" + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/9b887acc522935f77555ae8813495958c7771ba7", - "reference": "9b887acc522935f77555ae8813495958c7771ba7", + "url": "https://api.github.com/repos/symfony/process/zipball/2f4b049fb80ca5e9874615a2a85dc2a502090f05", + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05", "shasum": "" }, "require": { "php": ">=7.1.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -5923,7 +5910,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:08:58+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/service-contracts", @@ -6692,16 +6679,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.8", + "version": "4.1.9", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "41036e8af66e727c4587012f0366b7f0576a99da" + "reference": "5782e342b978a3efd0b7a776b7808902840b8213" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/41036e8af66e727c4587012f0366b7f0576a99da", - "reference": "41036e8af66e727c4587012f0366b7f0576a99da", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5782e342b978a3efd0b7a776b7808902840b8213", + "reference": "5782e342b978a3efd0b7a776b7808902840b8213", "shasum": "" }, "require": { @@ -6713,7 +6700,7 @@ "ext-json": "*", "ext-mbstring": "*", "guzzlehttp/psr7": "~1.4", - "php": ">=5.6.0 <8.0", + "php": ">=5.6.0 <9.0", "symfony/console": ">=2.7 <6.0", "symfony/css-selector": ">=2.7 <6.0", "symfony/event-dispatcher": ">=2.7 <6.0", @@ -6779,26 +6766,26 @@ "type": "open_collective" } ], - "time": "2020-10-11T17:54:58+00:00" + "time": "2020-10-23T17:59:47+00:00" }, { "name": "codeception/lib-asserts", - "version": "1.13.1", + "version": "1.13.2", "source": { "type": "git", "url": "https://github.com/Codeception/lib-asserts.git", - "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10" + "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/263ef0b7eff80643e82f4cf55351eca553a09a10", - "reference": "263ef0b7eff80643e82f4cf55351eca553a09a10", + "url": "https://api.github.com/repos/Codeception/lib-asserts/zipball/184231d5eab66bc69afd6b9429344d80c67a33b6", + "reference": "184231d5eab66bc69afd6b9429344d80c67a33b6", "shasum": "" }, "require": { "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0", "ext-dom": "*", - "php": ">=5.6.0 <8.0" + "php": ">=5.6.0 <9.0" }, "type": "library", "autoload": { @@ -6829,33 +6816,30 @@ "keywords": [ "codeception" ], - "time": "2020-08-28T07:49:36+00:00" + "time": "2020-10-21T16:26:20+00:00" }, { "name": "codeception/module-asserts", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-asserts.git", - "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667" + "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/32e5be519faaeb60ed3692383dcd1b3390ec2667", - "reference": "32e5be519faaeb60ed3692383dcd1b3390ec2667", + "url": "https://api.github.com/repos/Codeception/module-asserts/zipball/59374f2fef0cabb9e8ddb53277e85cdca74328de", + "reference": "59374f2fef0cabb9e8ddb53277e85cdca74328de", "shasum": "" }, "require": { "codeception/codeception": "*@dev", "codeception/lib-asserts": "^1.13.1", - "php": ">=5.6.0 <8.0" + "php": ">=5.6.0 <9.0" }, "conflict": { "codeception/codeception": "<4.0" }, - "require-dev": { - "codeception/util-robohelpers": "dev-master" - }, "type": "library", "autoload": { "classmap": [ @@ -6885,7 +6869,7 @@ "asserts", "codeception" ], - "time": "2020-08-28T08:06:29+00:00" + "time": "2020-10-21T16:48:15+00:00" }, { "name": "codeception/module-sequence", @@ -6932,26 +6916,23 @@ }, { "name": "codeception/module-webdriver", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "d055c645f600e991e33d1f289a9645eee46c384e" + "reference": "b7dc227f91730e7abb520439decc9ad0677b8a55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/d055c645f600e991e33d1f289a9645eee46c384e", - "reference": "d055c645f600e991e33d1f289a9645eee46c384e", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/b7dc227f91730e7abb520439decc9ad0677b8a55", + "reference": "b7dc227f91730e7abb520439decc9ad0677b8a55", "shasum": "" }, "require": { "codeception/codeception": "^4.0", - "php": ">=5.6.0 <8.0", + "php": ">=5.6.0 <9.0", "php-webdriver/webdriver": "^1.6.0" }, - "require-dev": { - "codeception/util-robohelpers": "dev-master" - }, "suggest": { "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" }, @@ -6983,7 +6964,7 @@ "browser-testing", "codeception" ], - "time": "2020-10-11T18:54:47+00:00" + "time": "2020-10-24T15:41:19+00:00" }, { "name": "codeception/phpunit-wrapper", @@ -7213,16 +7194,16 @@ }, { "name": "doctrine/annotations", - "version": "1.10.4", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f" + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/bfe91e31984e2ba76df1c1339681770401ec262f", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", "shasum": "" }, "require": { @@ -7232,13 +7213,14 @@ }, "require-dev": { "doctrine/cache": "1.*", + "doctrine/coding-standard": "^6.0 || ^8.1", "phpstan/phpstan": "^0.12.20", "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9.x-dev" + "dev-master": "1.11.x-dev" } }, "autoload": { @@ -7273,13 +7255,13 @@ } ], "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", "keywords": [ "annotations", "docblock", "parser" ], - "time": "2020-08-10T19:35:50+00:00" + "time": "2020-10-26T10:28:16+00:00" }, { "name": "doctrine/cache", @@ -7592,27 +7574,27 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.4", + "version": "v2.16.7", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4e35806a6d7d8510d6842ae932e8832363d22c87", + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87", "shasum": "" }, "require": { - "composer/semver": "^1.4", + "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^1.2", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", - "php": "^5.6 || ^7.0", + "php": "^7.1", "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", + "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", "symfony/finder": "^3.0 || ^4.0 || ^5.0", @@ -7625,14 +7607,14 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.2", + "keradus/cli-executor": "^1.4", "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.1", + "php-coveralls/php-coveralls": "^2.4.1", "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", - "phpunitgoodpractices/traits": "^1.8", + "phpunitgoodpractices/traits": "^1.9.1", "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, @@ -7685,7 +7667,7 @@ "type": "github" } ], - "time": "2020-06-27T23:57:46+00:00" + "time": "2020-10-27T22:44:27+00:00" }, { "name": "hoa/consistency", @@ -9781,16 +9763,16 @@ }, { "name": "phpunit/php-text-template", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84" + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/18c887016e60e52477e54534956d7b47bc52cd84", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { @@ -9832,7 +9814,7 @@ "type": "github" } ], - "time": "2020-09-28T06:03:05+00:00" + "time": "2020-10-26T05:33:50+00:00" }, { "name": "phpunit/php-timer", @@ -10043,52 +10025,6 @@ ], "time": "2020-05-22T13:54:05+00:00" }, - { - "name": "psr/cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "time": "2016-08-06T20:24:11+00:00" - }, { "name": "psr/simple-cache", "version": "1.0.1", @@ -10139,16 +10075,16 @@ }, { "name": "sebastian/code-unit", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab" + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/59236be62b1bb9919e6d7f60b0b832dc05cef9ab", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { @@ -10187,7 +10123,7 @@ "type": "github" } ], - "time": "2020-10-02T14:47:54+00:00" + "time": "2020-10-26T13:08:54+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -10242,16 +10178,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120" + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7a8ff306445707539c1a6397372a982a1ec55120", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", "shasum": "" }, "require": { @@ -10308,20 +10244,20 @@ "type": "github" } ], - "time": "2020-09-30T06:47:25+00:00" + "time": "2020-10-26T15:49:45+00:00" }, { "name": "sebastian/diff", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092" + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ffc949a1a2aae270ea064453d7535b82e4c32092", - "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", "shasum": "" }, "require": { @@ -10370,7 +10306,7 @@ "type": "github" } ], - "time": "2020-09-28T05:32:55+00:00" + "time": "2020-10-26T13:10:38+00:00" }, { "name": "sebastian/environment", @@ -10607,16 +10543,16 @@ }, { "name": "sebastian/object-enumerator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f6f5957013d84725427d361507e13513702888a4" + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f6f5957013d84725427d361507e13513702888a4", - "reference": "f6f5957013d84725427d361507e13513702888a4", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { @@ -10656,20 +10592,20 @@ "type": "github" } ], - "time": "2020-09-28T05:55:06+00:00" + "time": "2020-10-26T13:12:34+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5" + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { @@ -10707,7 +10643,7 @@ "type": "github" } ], - "time": "2020-09-28T05:56:16+00:00" + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/phpcpd", @@ -10762,16 +10698,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7" + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", - "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", "shasum": "" }, "require": { @@ -10817,7 +10753,7 @@ "type": "github" } ], - "time": "2020-09-28T05:17:32+00:00" + "time": "2020-10-26T13:17:30+00:00" }, { "name": "sebastian/resource-operations", @@ -10872,16 +10808,16 @@ }, { "name": "sebastian/type", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909" + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fa592377f3923946cb90bf1f6a71ba2e5f229909", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", "shasum": "" }, "require": { @@ -10920,7 +10856,7 @@ "type": "github" } ], - "time": "2020-10-06T08:41:03+00:00" + "time": "2020-10-26T13:18:59+00:00" }, { "name": "sebastian/version", @@ -11044,16 +10980,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.6", + "version": "3.5.8", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0" + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0", - "reference": "e97627871a7eab2f70e59166072a6b767d5834e0", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", "shasum": "" }, "require": { @@ -11091,20 +11027,20 @@ "phpcs", "standards" ], - "time": "2020-08-10T04:50:15+00:00" + "time": "2020-10-23T02:01:07+00:00" }, { "name": "symfony/config", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191" + "reference": "11baeefa4c179d6908655a7b6be728f62367c193" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/6ad8be6e1280f6734150d8a04a9160dd34ceb191", - "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191", + "url": "https://api.github.com/repos/symfony/config/zipball/11baeefa4c179d6908655a7b6be728f62367c193", + "reference": "11baeefa4c179d6908655a7b6be728f62367c193", "shasum": "" }, "require": { @@ -11128,11 +11064,6 @@ "symfony/yaml": "To use the yaml reference dumper" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Config\\": "" @@ -11171,20 +11102,20 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b" + "reference": "829ca6bceaf68036a123a13a979f3c89289eae78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2dea4a3ef2eb79138354c1d49e9372cc921af20b", - "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/829ca6bceaf68036a123a13a979f3c89289eae78", + "reference": "829ca6bceaf68036a123a13a979f3c89289eae78", "shasum": "" }, "require": { @@ -11217,11 +11148,6 @@ "symfony/yaml": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" @@ -11260,7 +11186,7 @@ "type": "tidelift" } ], - "time": "2020-10-01T12:14:45+00:00" + "time": "2020-10-27T10:11:13+00:00" }, { "name": "symfony/deprecation-contracts", @@ -11328,16 +11254,16 @@ }, { "name": "symfony/http-foundation", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b" + "reference": "a2860ec970404b0233ab1e59e0568d3277d32b6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/353b42e7b4fd1c898aab09a059466c9cea74039b", - "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a2860ec970404b0233ab1e59e0568d3277d32b6f", + "reference": "a2860ec970404b0233ab1e59e0568d3277d32b6f", "shasum": "" }, "require": { @@ -11356,11 +11282,6 @@ "symfony/mime": "To use the file extension guesser" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" @@ -11399,20 +11320,20 @@ "type": "tidelift" } ], - "time": "2020-09-27T14:14:57+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/mime", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "4404d6545125863561721514ad9388db2661eec5" + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/4404d6545125863561721514ad9388db2661eec5", - "reference": "4404d6545125863561721514ad9388db2661eec5", + "url": "https://api.github.com/repos/symfony/mime/zipball/f5485a92c24d4bcfc2f3fc648744fb398482ff1b", + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b", "shasum": "" }, "require": { @@ -11429,11 +11350,6 @@ "symfony/dependency-injection": "^4.4|^5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Mime\\": "" @@ -11476,20 +11392,20 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd" + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4c7e155bf7d93ea4ba3824d5a14476694a5278dd", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", "shasum": "" }, "require": { @@ -11498,11 +11414,6 @@ "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" @@ -11546,20 +11457,85 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:44:28+00:00" + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "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" + ], + "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-10-23T14:02:19+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/3d9f57c89011f0266e6b1d469e5c0110513859d5", + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5", "shasum": "" }, "require": { @@ -11567,11 +11543,6 @@ "symfony/service-contracts": "^1.0|^2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Stopwatch\\": "" @@ -11610,20 +11581,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/yaml", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a" + "reference": "f284e032c3cefefb9943792132251b79a6127ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f284e032c3cefefb9943792132251b79a6127ca6", + "reference": "f284e032c3cefefb9943792132251b79a6127ca6", "shasum": "" }, "require": { @@ -11644,11 +11615,6 @@ "Resources/bin/yaml-lint" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" @@ -11687,20 +11653,20 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:44:28+00:00" + "time": "2020-10-24T12:03:25+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.3.1", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a6b795aeb367c90cc6ed88dadb4cdcac436377c2" + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a6b795aeb367c90cc6ed88dadb4cdcac436377c2", - "reference": "a6b795aeb367c90cc6ed88dadb4cdcac436377c2", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", "shasum": "" }, "require": { @@ -11822,7 +11788,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-10-08T08:40:29+00:00" + "time": "2020-10-28T17:51:34+00:00" }, { "name": "theseer/fdomdocument", diff --git a/pub/media/sitemap/.htaccess b/pub/media/sitemap/.htaccess index b97408bad3f2e..187517e43efb2 100644 --- a/pub/media/sitemap/.htaccess +++ b/pub/media/sitemap/.htaccess @@ -1,7 +1 @@ -<IfVersion < 2.4> - order allow,deny - deny from all -</IfVersion> -<IfVersion >= 2.4> - Require all denied -</IfVersion> +Allow From All From 9828790d30ff5f91ebf2e7c0ee4b7da8c6841715 Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Tue, 3 Nov 2020 12:50:21 -0600 Subject: [PATCH 0985/1013] MC-38416: Stabilize CMS tests on S3 (#6309) --- .../Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml index c3d84fafd071c..f3cf259842e1b 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml @@ -15,8 +15,9 @@ <arguments> <argument name="FolderName" type="string"/> </arguments> - + <conditionalClick selector="{{MediaGallerySection.StorageRootArrow}}" dependentSelector="{{MediaGallerySection.checkIfArrowExpand}}" stepKey="clickArrowIfClosed" visible="true"/> + <waitForPageLoad time="10" stepKey="waitForDirectoriesTreeBuilding"/> <waitForText userInput="{{FolderName}}" stepKey="waitForNewFolder"/> <click userInput="{{FolderName}}" stepKey="clickOnCreatedFolder"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> From 010211f03a0a6e4072f871aafa1336c634e7a9f2 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 4 Nov 2020 09:17:52 +0200 Subject: [PATCH 0986/1013] MC-38893: Avoid BIC making introduced const DDL_EXISTS private in lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php --- lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index c5e17a97c9f01..53f09fda19471 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -56,7 +56,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface public const DDL_CREATE = 2; public const DDL_INDEX = 3; public const DDL_FOREIGN_KEY = 4; - public const DDL_EXISTS = 5; + private const DDL_EXISTS = 5; public const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; public const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; From ba82de1f5db35ed522926dc3977f36b30dde520f Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 4 Nov 2020 09:38:26 +0200 Subject: [PATCH 0987/1013] MC-38894: [MFTF] AdminMediaGalleryCatalogUiEditCategoryGridPageTest failed because of bad design --- ...yCatalogUiEditCategoryFromGridPageTest.xml | 39 +++++++++++++++++++ ...lleryCatalogUiEditCategoryGridPageTest.xml | 7 +++- 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml new file mode 100644 index 0000000000000..2beb0ad12e5d0 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest"> + <annotations> + <features value="MediaGalleryCatalogUi"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <title value="User Edits Category from Category grid"/> + <description value="Edit Category from Media Gallery Category Grid"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/5034526"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> + <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"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetGridFilters"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + <actionGroup ref="AdminAssertCategoryPageTitleActionGroup" stepKey="assertCategoryByName"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml index 2a606d8ab6a9e..739b25d1ce0ed 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCatalogUiEditCategoryGridPageTest"> + <test name="AdminMediaGalleryCatalogUiEditCategoryGridPageTest" deprecated="Use AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest instead"> <annotations> <features value="AdminMediaGalleryCategoryGrid"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> - <title value="User Edits Category from Category grid"/> + <title value="DEPRECATED. 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"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> From 375a49ff47d3a74112618830c12af6b7178667f3 Mon Sep 17 00:00:00 2001 From: engcom-Kilo <mikola.malevanec@transoftgroup.com> Date: Tue, 3 Nov 2020 17:54:38 +0200 Subject: [PATCH 0988/1013] MC-38833: Region field visible for Country when "Allow to Choose State if It is Optional for Country" is disabled. --- .../view/base/web/js/form/element/region.js | 46 +++++++++++++++---- .../Ui/base/js/form/element/region.test.js | 34 ++++++++++++++ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/region.js b/app/code/Magento/Ui/view/base/web/js/form/element/region.js index cd9c2aee85dc6..68b480d25a38c 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/region.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/region.js @@ -23,6 +23,22 @@ define([ } }, + /** + * {@inheritdoc} + */ + initialize: function () { + var option; + + this._super(); + + option = _.find(this.countryOptions, function (row) { + return row['is_default'] === true; + }); + this.hideRegion(option); + + return this; + }, + /** * Method called every time country selector's value gets changed. * Updates all validations and requirements for certain country. @@ -42,16 +58,9 @@ define([ return; } - defaultPostCodeResolver.setUseDefaultPostCode(!option['is_zipcode_optional']); - - if (option['is_region_visible'] === false) { - // Hide select and corresponding text input field if region must not be shown for selected country. - this.setVisible(false); + this.hideRegion(option); - if (this.customEntry) { // eslint-disable-line max-depth - this.toggleInput(false); - } - } + defaultPostCodeResolver.setUseDefaultPostCode(!option['is_zipcode_optional']); isRegionRequired = !this.skipValidation && !!option['is_region_required']; @@ -67,7 +76,24 @@ define([ input.validation['required-entry'] = isRegionRequired; input.validation['validate-not-number-first'] = !this.options().length; }.bind(this)); + }, + + /** + * Hide select and corresponding text input field if region must not be shown for selected country. + * + * @private + * @param {Object}option + */ + hideRegion: function (option) { + if (!option || option['is_region_visible'] !== false) { + return; + } + + this.setVisible(false); + + if (this.customEntry) { + this.toggleInput(false); + } } }); }); - diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js index a957db5d1c119..517e13281d402 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js @@ -47,6 +47,40 @@ define([ }); }); + describe('initialize method', function () { + it('Hides region field when it should be hidden for default country', function () { + model.countryOptions = { + 'DefaultCountryCode': { + 'is_default': true, + 'is_region_visible': false + }, + 'NonDefaultCountryCode': { + 'is_region_visible': true + } + }; + + model.initialize(); + + expect(model.visible()).toEqual(false); + }); + + it('Shows region field when it should be visible for default country', function () { + model.countryOptions = { + 'CountryCode': { + 'is_default': true, + 'is_region_visible': true + }, + 'NonDefaultCountryCode': { + 'is_region_visible': false + } + }; + + model.initialize(); + + expect(model.visible()).toEqual(true); + }); + }); + describe('update method', function () { it('makes field optional when there is no corresponding country', function () { var value = 'Value'; From 304e5a1b09bd5a256e4a907821e4f92864c1e86a Mon Sep 17 00:00:00 2001 From: Serhii Balko <serhii.balko@transoftgroup.com> Date: Wed, 4 Nov 2020 12:10:57 +0200 Subject: [PATCH 0989/1013] MC-37816: Performance - endless scheduled export of catalog with 100k+ products --- .../Export/Product/RowCustomizerTest.php | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php diff --git a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php new file mode 100644 index 0000000000000..110451aa19f1a --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/Product/RowCustomizerTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableImportExport\Test\Unit\Model\Export\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Downloadable\Model\LinkRepository; +use Magento\Downloadable\Model\Product\Type as Type; +use Magento\Downloadable\Model\SampleRepository; +use Magento\DownloadableImportExport\Model\Export\RowCustomizer; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class to test Customizes output during export + */ +class RowCustomizerTest extends TestCase +{ + /** + * @var LinkRepository|MockObject + */ + private $linkRepository; + + /** + * @var SampleRepository|MockObject + */ + private $sampleRepository; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var RowCustomizer + */ + private $rowCustomizer; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->linkRepository = $this->createMock(LinkRepository::class); + $this->sampleRepository = $this->createMock(SampleRepository::class); + $this->storeManager = $this->createMock(StoreManagerInterface::class); + + $this->rowCustomizer = new RowCustomizer( + $this->storeManager, + $this->linkRepository, + $this->sampleRepository + ); + } + + /** + * Test to Prepare downloadable data for export + */ + public function testPrepareData() + { + $productIds = [1, 2, 3]; + $collection = $this->createMock(ProductCollection::class); + $collection->expects($this->at(0)) + ->method('addAttributeToFilter') + ->with('entity_id', ['in' => $productIds]) + ->willReturnSelf(); + $collection->expects($this->at(1)) + ->method('addAttributeToFilter') + ->with('type_id', ['eq' => Type::TYPE_DOWNLOADABLE]) + ->willReturnSelf(); + $collection->method('addAttributeToSelect')->willReturnSelf(); + $collection->method('getIterator')->willReturn(new \ArrayIterator([])); + + $this->storeManager->expects($this->once()) + ->method('setCurrentStore') + ->with(Store::DEFAULT_STORE_ID); + + $this->rowCustomizer->prepareData($collection, $productIds); + } +} From 8f2a8be7962bcceb12790805b3171f874abdf5d4 Mon Sep 17 00:00:00 2001 From: Serhii Bohomaz <serhii.bohomaz@transoftgroup.com> Date: Wed, 4 Nov 2020 13:07:30 +0200 Subject: [PATCH 0990/1013] MC-37545: Create automated test for "Edit Category on Store View Level" --- .../Catalog/Block/Category/TitleTest.php | 136 ++++++++++++++++++ .../Category/CategoryUrlRewriteTest.php | 108 ++++++++++++++ .../Model/Category/DataProviderTest.php | 77 +++++++--- .../_files/category_on_second_store.php | 29 ++++ .../category_on_second_store_rollback.php | 11 ++ .../Controller/Store/SwitchActionTest.php | 126 ++++++++++++---- 6 files changed, 441 insertions(+), 46 deletions(-) create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php create mode 100644 dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php new file mode 100644 index 0000000000000..7bc359935bf60 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Category; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +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; + +/** + * Category title check + * + * @magentoAppArea frontend + * @see \Magento\Theme\Block\Html\Title + */ +class TitleTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var Registry */ + private $registry; + + /** @var PageFactory */ + private $pageFactory; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_category'); + + parent::tearDown(); + } + + /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Store/_files/store.php + * @return void + */ + public function testCategoryNameOnStoreView(): void + { + $id = 333; + $categoryNameForSecondStore = 'Category Name For Second Store'; + $this->executeInStoreContext->execute( + 'test', + [$this, 'updateCategoryName'], + $this->categoryRepository->get($id), + $categoryNameForSecondStore + ); + $this->registerCategory($this->categoryRepository->get($id)); + $this->assertStringContainsString('Category 1', $this->getBlockTitle(), 'Wrong category name'); + $this->registerCategory($this->categoryRepository->get($id, $this->storeManager->getStore('test')->getId())); + $this->assertStringContainsString($categoryNameForSecondStore, $this->getBlockTitle(), 'Wrong category name'); + } + + /** + * Update category name + * + * @param CategoryInterface $category + * @param string $categoryName + * @return void + */ + public function updateCategoryName(CategoryInterface $category, string $categoryName): void + { + $category->setName($categoryName); + $this->categoryRepository->save($category); + } + + /** + * Get title block + * + * @return string + */ + private function getBlockTitle(): string + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'catalog_category_view', + ]); + $page->getLayout()->generateXml(); + $block = $page->getLayout()->getBlock('page.main.title'); + $this->assertNotFalse($block); + + return $block->stripTags($block->toHtml()); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('current_category'); + $this->registry->register('current_category', $category); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php index ad62a4ec2df29..931bbf835521e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php @@ -7,16 +7,25 @@ namespace Magento\Catalog\Controller\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Layer\Category; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Session as CatalogSession; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Response\Http; use Magento\Framework\Registry; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\Response; +use Magento\TestFramework\Store\ExecuteInStoreContext; use Magento\TestFramework\TestCase\AbstractController; /** * Checks category availability on storefront by url rewrite * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 * @magentoDbIsolation enabled */ @@ -31,6 +40,18 @@ class CategoryUrlRewriteTest extends AbstractController /** @var string */ private $categoryUrlSuffix; + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var CatalogSession */ + private $catalogSession; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -44,6 +65,10 @@ protected function setUp(): void CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE ); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->catalogSession = $this->_objectManager->get(CatalogSession::class); + $this->executeInStoreContext = $this->_objectManager->get(ExecuteInStoreContext::class); } /** @@ -87,4 +112,87 @@ public function categoryRewriteProvider(): array ], ]; } + + /** + * Test category url on different store view + * + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Store/_files/store.php + * @return void + */ + public function testCategoryUrlOnStoreView(): void + { + $id = 333; + $secondStoreUrlKey = 'category-1-second'; + $currentStore = $this->storeManager->getStore(); + $secondStore = $this->storeManager->getStore('test'); + $this->executeInStoreContext->execute( + $secondStore, + [$this, 'updateCategoryUrlKey'], + $id, + (int)$secondStore->getId(), + $secondStoreUrlKey + ); + $url = sprintf('/' . $secondStoreUrlKey . '%s', $this->categoryUrlSuffix); + $this->executeInStoreContext->execute($secondStore, [$this, 'dispatch'], $url); + $this->assertCategoryIsVisible(); + $this->assertEquals( + $secondStoreUrlKey, + $this->categoryRepository->get($id, (int)$secondStore->getId())->getUrlKey(), + 'Wrong category is registered' + ); + $this->cleanUpCachedObjects(); + $defaultStoreUrlKey = $this->categoryRepository->get($id, $currentStore->getId())->getUrlKey(); + $this->dispatch(sprintf($defaultStoreUrlKey . '%s', $this->categoryUrlSuffix)); + $this->assertCategoryIsVisible(); + } + + /** + * Assert that category is available in storefront + * + * @return void + */ + private function assertCategoryIsVisible(): void + { + $this->assertEquals( + Response::STATUS_CODE_200, + $this->getResponse()->getHttpResponseCode(), + 'Wrong response code is returned' + ); + $this->assertNotNull((int)$this->catalogSession->getData('last_viewed_category_id')); + } + + /** + * Clean up cached objects + * + * @return void + */ + private function cleanUpCachedObjects(): void + { + $this->catalogSession->clearStorage(); + $this->registry->unregister('current_category'); + $this->registry->unregister('category'); + $this->_objectManager->removeSharedInstance(Request::class); + $this->_objectManager->removeSharedInstance(Response::class); + $this->_objectManager->removeSharedInstance(Resolver::class); + $this->_objectManager->removeSharedInstance(Category::class); + $this->_objectManager->removeSharedInstance('categoryFilterList'); + $this->_response = null; + $this->_request = null; + } + + /** + * Update category url key + * + * @param int $id + * @param int $storeId + * @param string $categoryUrlKey + * @return void + */ + public function updateCategoryUrlKey(int $id, int $storeId, string $categoryUrlKey): void + { + $category = $this->categoryRepository->get($id, $storeId); + $category->setUrlKey($categoryUrlKey); + $this->categoryRepository->save($category); + } } 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 1d846fc154fc0..6ae6669956f62 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php @@ -3,14 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; 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; @@ -19,42 +22,36 @@ use PHPUnit\Framework\TestCase; /** + * Testing category form data provider. + * * @magentoDbIsolation enabled * @magentoAppIsolation enabled * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DataProviderTest extends TestCase { - /** - * @var DataProvider - */ + /** @var DataProvider */ private $dataProvider; - /** - * @var Registry - */ + /** @var Registry */ private $registry; - /** - * @var CategoryFactory - */ + /** @var CategoryFactory */ private $categoryFactory; - /** - * @var CategoryLayoutUpdateManager - */ + /** @var CategoryLayoutUpdateManager */ private $fakeFiles; - /** - * @var ScopeConfigInterface - */ + /** @var ScopeConfigInterface */ private $scopeConfig; - /** - * @var StoreManagerInterface - */ + /** @var StoreManagerInterface */ private $storeManager; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + /** * Create subject instance. * @@ -80,8 +77,7 @@ protected function setUp(): void $objectManager = Bootstrap::getObjectManager(); $objectManager->configure([ 'preferences' => [ - \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class - => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + LayoutUpdateManager::class => CategoryLayoutUpdateManager::class ] ]); parent::setUp(); @@ -91,6 +87,15 @@ protected function setUp(): void $this->fakeFiles = $objectManager->get(CategoryLayoutUpdateManager::class); $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); $this->storeManager = $objectManager->get(StoreManagerInterface::class); + $this->categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('category'); } /** @@ -267,7 +272,7 @@ public function testExistingCategoryLayoutUnaffectedByDefaults(): void /** * Check if category page layout default value setting will apply to the new category during it's creation * - * @throws NoSuchEntityException + * @return void */ public function testNewCategoryLayoutMatchesDefault(): void { @@ -288,4 +293,32 @@ public function testNewCategoryLayoutMatchesDefault(): void $this->assertEquals($categoryDefaultPageLayout, $categoryPageLayout); } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_on_second_store.php + * @return void + */ + public function testCategoryStoreView(): void + { + $id = 333; + $secondStore = $this->storeManager->getStore('test'); + $category = $this->categoryRepository->get($id, $secondStore->getId()); + $this->registerCategory($category); + $data = $this->dataProvider->getData(); + $this->assertNotEmpty($data); + $this->assertEquals('Category 1 Second', $data[$id]['name']); + $this->assertEquals('category-1-second-url-key', $data[$id]['url_key']); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('category'); + $this->registry->register('category', $category); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php new file mode 100644 index 0000000000000..0b094ba29290e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.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\CategoryRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/store.php'); + +$objectManager = Bootstrap::getObjectManager(); +$storeManager = $objectManager->get(StoreManagerInterface::class); +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +$executeInStoreContext = $objectManager->get(ExecuteInStoreContext::class); + +$currentStore = $storeManager->getStore(); +$secondStore = $storeManager->getStore('test'); +$category = $categoryRepository->get(333); +$category->setName('Category 1 Second'); +$category->setUrlKey('category-1-second-url-key'); +$executeInStoreContext->execute($secondStore, function ($categoryRepository, $category) { + $categoryRepository->save($category); +}, $categoryRepository, $category); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php new file mode 100644 index 0000000000000..b7b8491612fec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_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/category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php index e4d78de54d308..c506b77e45442 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php @@ -3,9 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Store\Controller\Store; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Response\RedirectInterface; use Magento\Framework\Encryption\UrlCoder; use Magento\Framework\Interception\InterceptorInterface; use Magento\Store\Api\StoreResolverInterface; @@ -16,8 +21,11 @@ use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractController; use PHPUnit\Framework\MockObject\MockObject; +use Magento\Store\Api\Data\StoreInterfaceFactory; +use Magento\Store\Model\ResourceModel\Store as StoreResource; /** * Test for store switch controller. @@ -27,23 +35,42 @@ */ class SwitchActionTest extends AbstractController { - /** - * @var RedirectDataPreprocessorInterface - */ + /** @var RedirectDataPreprocessorInterface */ private $preprocessor; - /** - * @var MockObject - */ + + /** @var MockObject */ private $preprocessorMock; - /** - * @var RedirectDataPostprocessorInterface - */ + + /** @var RedirectDataPostprocessorInterface */ private $postprocessor; - /** - * @var MockObject - */ + + /** @var MockObject */ private $postprocessorMock; + /** @var RedirectDataGenerator */ + private $redirectDataGenerator; + + /** @var ContextInterfaceFactory */ + private $contextFactory; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var UrlCoder */ + private $urlEncoder; + + /** @var RedirectInterface */ + private $redirect; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreResource */ + private $storeResource; + + /** @var StoreInterfaceFactory */ + private $storeFactory; + /** * @inheritDoc */ @@ -53,10 +80,17 @@ protected function setUp(): void $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); $this->_objectManager->addSharedInstance($this->preprocessorMock, $this->getClassName($this->preprocessor)); - $this->postprocessor = $this->_objectManager->get(RedirectDataPostprocessorInterface::class); $this->postprocessorMock = $this->createMock(RedirectDataPostprocessorInterface::class); $this->_objectManager->addSharedInstance($this->postprocessorMock, $this->getClassName($this->postprocessor)); + $this->redirectDataGenerator = $this->_objectManager->get(RedirectDataGenerator::class); + $this->contextFactory = $this->_objectManager->get(ContextInterfaceFactory::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->urlEncoder = $this->_objectManager->get(UrlCoder::class); + $this->redirect = $this->_objectManager->get(RedirectInterface::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->storeResource = $this->_objectManager->get(StoreResource::class); + $this->storeFactory = $this->_objectManager->get(StoreInterfaceFactory::class); } /** @@ -80,8 +114,9 @@ protected function tearDown(): void * @magentoConfigFixture fixture_second_store_store web/unsecure/base_link_url http://second_store.test/ * @magentoConfigFixture fixture_second_store_store web/secure/base_url http://second_store.test/ * @magentoConfigFixture fixture_second_store_store web/secure/base_link_url http://second_store.test/ + * @return void */ - public function testSwitch() + public function testSwitch(): void { $data = ['key1' => 'value1', 'key2' => 1]; $this->preprocessorMock->method('process') @@ -131,6 +166,7 @@ function (ContextInterface $context) { * Return class name of the given object * * @param mixed $instance + * @return string */ private function getClassName($instance): string { @@ -150,19 +186,20 @@ private function getClassName($instance): string * incorrect work of page cache. * * @magentoDbIsolation enabled + * @return void */ - public function testExecuteWithCustomDefaultStore() + public function testExecuteWithCustomDefaultStore(): void { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + Bootstrap::getInstance()->reinitialize(); $defaultStoreCode = 'default'; $modifiedDefaultCode = 'modified_default_code'; $this->changeStoreCode($defaultStoreCode, $modifiedDefaultCode); $this->dispatch('stores/store/switch'); - /** @var \Magento\Framework\App\Http\Context $httpContext */ - $httpContext = $this->_objectManager->get(\Magento\Framework\App\Http\Context::class); - $httpContext->unsValue(\Magento\Store\Model\Store::ENTITY); - $this->assertEquals($modifiedDefaultCode, $httpContext->getValue(\Magento\Store\Model\Store::ENTITY)); + /** @var Context $httpContext */ + $httpContext = $this->_objectManager->get(Context::class); + $httpContext->unsValue(Store::ENTITY); + $this->assertEquals($modifiedDefaultCode, $httpContext->getValue(Store::ENTITY)); $this->changeStoreCode($modifiedDefaultCode, $defaultStoreCode); } @@ -172,13 +209,54 @@ public function testExecuteWithCustomDefaultStore() * * @param string $from * @param string $to + * @return void */ - private function changeStoreCode($from, $to) + private function changeStoreCode(string $from, string $to): void { /** @var Store $store */ - $store = $this->_objectManager->create(Store::class); - $store->load($from, 'code'); + $store = $this->storeFactory->create(); + $this->storeResource->load($store, $from, 'code'); $store->setCode($to); - $store->save(); + $this->storeResource->save($store); + } + + /** + * Switch to category on second store + * + * @magentoDataFixture Magento/Catalog/_files/category_on_second_store.php + * @magentoDbIsolation disabled + * @return void + */ + public function testSwitchToCategoryOnSecondStore(): void + { + $id = 333; + $fromStore = $this->storeManager->getStore(); + $targetStore = $this->storeManager->getStore('test'); + $category = $this->categoryRepository->get($id, $fromStore->getId()); + + $redirectData = $this->redirectDataGenerator->generate( + $this->contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => $this->redirect->getRedirectUrl(), + ] + ) + ); + + $this->getRequest()->setParams( + [ + '___from_store' => $fromStore->getCode(), + StoreManagerInterface::PARAM_NAME => $targetStore->getCode(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($category->getUrl()), + 'data' => $redirectData->getData(), + 'time_stamp' => $redirectData->getTimestamp(), + 'signature' => $redirectData->getSignature(), + ] + ); + + $this->dispatch('stores/store/switch'); + $categorySecond = $this->categoryRepository->get($id, $targetStore->getId()); + $this->assertRedirect($this->stringContains($categorySecond->getUrlKey())); } } From b44339725c65a6047e007bd50267d19fc6551c4a Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Wed, 4 Nov 2020 13:19:55 +0200 Subject: [PATCH 0991/1013] MC-37902: Create automated test for "Paging and sort by function on widget grid" --- .../Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php index 3e01305d9f39f..57d4322ded9a3 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php @@ -128,7 +128,7 @@ public function gridFiltersDataProvider(): array ), ], 'expected_widgets' => [ - 'recently compared products' + 'recently compared products', ], ], ]; @@ -147,7 +147,6 @@ public function testGridSorting(array $filter, array $expectedWidgets): void { $this->request->setParams($filter); $collection = $this->getGridCollection(); - $this->assertCount(count($expectedWidgets), $collection); $this->assertEquals($expectedWidgets, $collection->getColumnValues('title')); } From b8ec1a32e7a589fbe6377523088df07ce714ca43 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Wed, 4 Nov 2020 15:37:07 +0200 Subject: [PATCH 0992/1013] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../{Address.php => AddressDataProvider.php} | 19 +-- ...rder_create_load_block_billing_address.xml | 2 +- .../templates/order/create/form/address.phtml | 10 +- .../Adminhtml/Order/Create/ReorderTest.php | 153 ++++++++++++++++++ 4 files changed, 164 insertions(+), 20 deletions(-) rename app/code/Magento/Sales/ViewModel/Customer/Address/Billing/{Address.php => AddressDataProvider.php} (62%) create mode 100644 dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php diff --git a/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php similarity index 62% rename from app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php rename to app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php index a7ec8e6587d70..c539e965b9df9 100644 --- a/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/Address.php +++ b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php @@ -9,17 +9,16 @@ use Magento\Framework\View\Element\Block\ArgumentInterface; use Magento\Sales\Model\AdminOrder\Create; -use Magento\Quote\Model\Quote\Address as QuoteAddress; /** - * Customer address formatter + * Customer billing address data provider */ -class Address implements ArgumentInterface +class AddressDataProvider implements ArgumentInterface { /** * @var Create */ - protected $orderCreate; + private $orderCreate; /** * Customer billing address @@ -32,16 +31,6 @@ public function __construct( $this->orderCreate = $orderCreate; } - /** - * Return billing address object - * - * @return QuoteAddress - */ - public function getAddress(): QuoteAddress - { - return $this->orderCreate->getBillingAddress(); - } - /** * Get save billing address in the address book * @@ -49,6 +38,6 @@ public function getAddress(): QuoteAddress */ public function getSaveInAddressBook(): int { - return (int)$this->getAddress()->getSaveInAddressBook(); + return (int)$this->orderCreate->getBillingAddress()->getSaveInAddressBook(); } } diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index edefa8de55c7a..91148d86055fc 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -12,7 +12,7 @@ <arguments> <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> - <argument name="customerBillingAddress" xsi:type="object">Magento\Sales\ViewModel\Customer\Address\Billing\Address</argument> + <argument name="billingAddressDataProvider" xsi:type="object">Magento\Sales\ViewModel\Customer\Address\Billing\AddressDataProvider</argument> </arguments> </block> </referenceContainer> 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 bdb1a6c8cba94..3b6b789cabccf 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 @@ -25,9 +25,9 @@ endif; $customerAddressFormatter = $block->getData('customerAddressFormatter'); /** - * @var \Magento\Sales\ViewModel\Customer\Address\Billing\Address $billingAddress + * @var \Magento\Sales\ViewModel\Customer\Address\Billing\AddressDataProvider $billingAddressDataProvider */ -$billingAddress = $block->getData('customerBillingAddress'); +$billingAddressDataProvider = $block->getData('billingAddressDataProvider'); /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address| @@ -119,9 +119,11 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if ($billingAddress && $billingAddress->getSaveInAddressBook()): ?> + <?php if ($billingAddressDataProvider && $billingAddressDataProvider->getSaveInAddressBook()): ?> checked="checked" - <?php elseif ($block->getIsShipping() && !$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> + <?php elseif ($block->getIsShipping() + && !$block->getDontSaveInAddressBook() + && !$block->getAddressId()): ?> checked="checked" <?php endif; ?> class="admin__control-checkbox"/> diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php new file mode 100644 index 0000000000000..0856e58c308d5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -0,0 +1,153 @@ +<?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\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Registry; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\OrderFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Test load block for order create controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Index + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class ReorderTest extends AbstractBackendController +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var CustomerInterfaceFactory + */ + private $customerFactory; + + /** + * @var AccountManagementInterface + */ + private $accountManagement; + + /** + * @var OrderFactory + */ + private $orderFactory; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var array + */ + private $customerIds = []; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class); + $this->customerFactory = $this->_objectManager->get(CustomerInterfaceFactory::class); + $this->accountManagement = $this->_objectManager->get(AccountManagementInterface::class); + $this->orderFactory = $this->_objectManager->get(OrderFactory::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + parent::tearDown(); + foreach ($this->customerIds as $customerId) { + try { + $this->customerRepository->deleteById($customerId); + } catch (NoSuchEntityException $e) { + //customer already deleted + } + } + } + + /** + * Test load billing address by reorder for delegating customer + * + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_address.php + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testLoadBillingAddressAfterReorderWithDelegatingCustomer(): void + { + $orderId = $this->getOrderWithDelegatingCustomer()->getId(); + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/loadBlock/block/billing_address'); + $html = $this->getResponse()->getBody(); + $this->assertEquals( + 0, + Xpath::getElementsCountForXpath( + '//*[@id="order-billing_address_save_in_address_book" and contains(@checked, "checked")]', + $html + ), + 'Billing address checked "Save in address book"' + ); + } + + /** + * Get Order with delegating customer + * + * @return OrderInterface + */ + private function getOrderWithDelegatingCustomer(): OrderInterface + { + $orderAutoincrementId = '100000001'; + /** @var Order $orderModel */ + $orderModel = $this->orderFactory->create(); + $orderModel->loadByIncrementId($orderAutoincrementId); + //Saving new customer with prepared data from order. + /** @var CustomerInterface $customer */ + $customer = $this->customerFactory->create(); + $customer->setWebsiteId(1) + ->setEmail('customer_order_delegate@example.com') + ->setGroupId(1) + ->setStoreId(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setTaxvat('12') + ->setGender(0); + $createdCustomer = $this->accountManagement->createAccount( + $customer, + '12345abcD' + ); + $this->customerIds[] = $createdCustomer->getId(); + $orderModel->setCustomerId($createdCustomer->getId()); + + return $this->orderRepository->save($orderModel); + } +} From 3a64d1457a8d552d7183a528d45b93fc86c3c95f Mon Sep 17 00:00:00 2001 From: Leonid Poluianov <46716220+le0n4ik@users.noreply.github.com> Date: Wed, 4 Nov 2020 08:57:56 -0600 Subject: [PATCH 0993/1013] MC-38835: Fix failing random tests (#6312) --- .../testsuite/Magento/Framework/Error/ProcessorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php index 3a2a02a0a5776..917b79588312c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php @@ -150,6 +150,6 @@ public function testGetViewFileUrl(): void $this->processor->_errorDir = __DIR__ . '/version2/magento2'; $this->assertStringNotContainsString('version2/magento2', $this->processor->getViewFileUrl()); - $this->assertStringContainsString('pub/errors/', $this->processor->getViewFileUrl()); + $this->assertStringContainsString('errors/', $this->processor->getViewFileUrl()); } } From c02fa441dadea2a88688ba6f0722ebd513eac3b6 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Wed, 4 Nov 2020 18:31:27 +0200 Subject: [PATCH 0994/1013] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../adminhtml/templates/order/create/form/address.phtml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 3b6b789cabccf..69b26d70e684a 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 @@ -119,11 +119,8 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if ($billingAddressDataProvider && $billingAddressDataProvider->getSaveInAddressBook()): ?> - checked="checked" - <?php elseif ($block->getIsShipping() - && !$block->getDontSaveInAddressBook() - && !$block->getAddressId()): ?> + <?php if ($billingAddressDataProvider && $billingAddressDataProvider->getSaveInAddressBook() || + $block->getIsShipping() && !$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> checked="checked" <?php endif; ?> class="admin__control-checkbox"/> From bdf9e6b370db408b456cb62ef4af2fe72da9c219 Mon Sep 17 00:00:00 2001 From: Myroslav Dobra <dmaraptor@gmail.com> Date: Wed, 4 Nov 2020 19:21:08 +0200 Subject: [PATCH 0995/1013] MC-38893: Avoid BIC making introduced const DDL_EXISTS private in lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php --- lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 53f09fda19471..5765a3a7fe1b2 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -665,11 +665,9 @@ protected function _prepareQuery(&$sql, &$bind = []) } // Mixed bind is not supported - so remember whether it is named bind, to normalize later if required - $isNamedBind = false; if ($bind) { foreach ($bind as $k => $v) { if (!is_int($k)) { - $isNamedBind = true; if ($k[0] != ':') { $bind[":{$k}"] = $v; unset($bind[$k]); From d6ccfb50a4d2d7202cacbf83620ae31d7a5bfc3a Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Tue, 3 Nov 2020 21:51:35 -0600 Subject: [PATCH 0996/1013] MC-38613: Support by Magento CatalogGraphQl --- app/code/Magento/AwsS3/Driver/AwsS3.php | 6 +++++- .../Model/Product/Gallery/ProcessorTest.php | 7 ++++--- .../Magento/Catalog/Model/ProductTest.php | 2 +- .../Catalog/_files/catalog_category_image.php | 11 ++++++----- .../_files/catalog_tmp_category_image.php | 3 +-- .../Magento/Catalog/_files/product_image.php | 5 ++--- .../Catalog/_files/product_simple_with_image.php | 2 +- .../Catalog/_files/validate_image_info.php | 10 +++++----- .../Catalog/controllers/_files/products.php | 2 +- lib/internal/Magento/Framework/Api/Uploader.php | 16 ---------------- 10 files changed, 26 insertions(+), 38 deletions(-) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index d0c054b637530..8b0469862a4e9 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -344,7 +344,11 @@ public function isFile($path): bool $path = $this->normalizeRelativePath($path); $path = rtrim($path, '/'); - return $this->adapter->has($path) && $this->adapter->getMetadata($path)['type'] === self::TYPE_FILE; + if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { + return ($meta['type'] ?? null) === self::TYPE_FILE; + } + + return false; } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php index f836fe9cbb96a..fb384253e27a7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php @@ -46,9 +46,10 @@ public static function setUpBeforeClass(): void $mediaDirectory->create($config->getBaseTmpMediaPath()); $mediaDirectory->create($config->getBaseMediaPath()); - copy($fixtureDir . "/magento_image.jpg", self::$_mediaTmpDir . "/magento_image.jpg"); - copy($fixtureDir . "/magento_image.jpg", self::$_mediaDir . "/magento_image.jpg"); - copy($fixtureDir . "/magento_small_image.jpg", self::$_mediaTmpDir . "/magento_small_image.jpg"); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaTmpDir . "/magento_image.jpg", file_get_contents($fixtureDir . "/magento_image.jpg")); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaDir . "/magento_image.jpg", file_get_contents($fixtureDir . "/magento_image.jpg")); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaTmpDir . "/magento_small_image.jpg", file_get_contents($fixtureDir . "/magento_small_image.jpg")); + } public static function tearDownAfterClass(): void diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index b0f36f250991b..8acb243a706c2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -246,7 +246,7 @@ protected function _copyFileToBaseTmpMediaPath($sourceFile) $mediaDirectory->create($config->getBaseTmpMediaPath()); $targetFile = $config->getTmpMediaPath(basename($sourceFile)); - copy($sourceFile, $mediaDirectory->getAbsolutePath($targetFile)); + $mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($targetFile), file_get_contents($sourceFile)); return $targetFile; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php index 3491065323c9f..7a2ad0fefac8a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php @@ -10,13 +10,14 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ -$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(DirectoryList::MEDIA); +$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); $fileName = 'magento_small_image.jpg'; $fileNameLong = 'magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg'; $filePath = 'catalog/category/' . $fileName; $filePathLong = 'catalog/category/' . $fileNameLong; $mediaDirectory->create('catalog/category'); - -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($filePath)); -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileNameLong, $mediaDirectory->getAbsolutePath($filePathLong)); +$shortImageContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileName); +$longImageContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileNameLong); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($filePath), $shortImageContent); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($filePathLong), $longImageContent); +unset($shortImageContent, $longImageContent); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php index 2562acdda2dc3..ce688f38ed1ec 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php @@ -15,5 +15,4 @@ $fileName = 'magento_small_image.jpg'; $tmpFilePath = 'catalog/tmp/category/' . $fileName; $mediaDirectory->create('catalog/tmp/category'); - -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($tmpFilePath)); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($tmpFilePath), file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileName)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php index 962a66f11f532..1794530832d04 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php @@ -24,12 +24,11 @@ $images = ['magento_image.jpg', 'magento_small_image.jpg', 'magento_thumbnail.jpg']; foreach ($images as $image) { - $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . DIRECTORY_SEPARATOR . $targetTmpDirPath - . DIRECTORY_SEPARATOR . $image; + $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . $targetTmpDirPath . $image; $sourceFilePath = __DIR__ . DIRECTORY_SEPARATOR . $image; + $mediaDirectory->getDriver()->filePutContents($targetTmpFilePath, file_get_contents($sourceFilePath)); - copy($sourceFilePath, $targetTmpFilePath); // Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir $database->saveFile($targetTmpFilePath); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php index 252f99c97b787..688a3bd199570 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php @@ -38,7 +38,7 @@ $mediaDirectory->create($targetTmpDirPath); $dist = $mediaDirectory->getAbsolutePath($mediaConfig->getBaseMediaPath() . DIRECTORY_SEPARATOR . 'magento_image.jpg'); -copy(__DIR__ . '/magento_image.jpg', $dist); +$mediaDirectory->getDriver()->filePutContents($dist, file_get_contents(__DIR__ . '/magento_image.jpg')); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php index 96ddb797a6dea..945f582b8cbdd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php @@ -12,10 +12,10 @@ /** @var Magento\Catalog\Model\Product\Media\Config $config */ $config = $objectManager->get(\Magento\Catalog\Model\Product\Media\Config::class); -/** @var $tmpDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ -$tmpDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); -$tmpDirectory->create($config->getBaseTmpMediaPath()); +/** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ +$mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); +$mediaDirectory->create($config->getBaseTmpMediaPath()); -$targetTmpFilePath = $tmpDirectory->getAbsolutePath($config->getBaseTmpMediaPath() . '/magento_small_image.jpg'); -copy(__DIR__ . '/magento_small_image.jpg', $targetTmpFilePath); +$targetTmpFilePath = $mediaDirectory->getAbsolutePath($config->getBaseTmpMediaPath() . '/magento_small_image.jpg'); +$mediaDirectory->getDriver()->filePutContents($targetTmpFilePath, file_get_contents(__DIR__ . '/magento_small_image.jpg')); // Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir diff --git a/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php b/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php index 3878cd2e5176e..4a3c8f2e6b96c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php @@ -24,7 +24,7 @@ $baseTmpMediaPath = $config->getBaseTmpMediaPath(); $mediaDirectory->create($baseTmpMediaPath); -copy(__DIR__ . '/product_image.png', $mediaDirectory->getAbsolutePath($baseTmpMediaPath . '/product_image.png')); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($baseTmpMediaPath . '/product_image.png'), file_get_contents(__DIR__ . '/product_image.png')); /** @var $productOne \Magento\Catalog\Model\Product */ $productOne = $objectManager->create(\Magento\Catalog\Model\Product::class); diff --git a/lib/internal/Magento/Framework/Api/Uploader.php b/lib/internal/Magento/Framework/Api/Uploader.php index 3a4019b9caf84..3f98b38bc2fdf 100644 --- a/lib/internal/Magento/Framework/Api/Uploader.php +++ b/lib/internal/Magento/Framework/Api/Uploader.php @@ -39,20 +39,4 @@ public function processFileAttributes($fileAttributes) $this->_fileExists = true; } } - - /** - * Move files from TMP folder into destination folder - * - * @param string $tmpPath - * @param string $destPath - * @return bool|void - */ - protected function _moveFile($tmpPath, $destPath) - { - if (is_uploaded_file($tmpPath)) { - return move_uploaded_file($tmpPath, $destPath); - } elseif (is_file($tmpPath)) { - return rename($tmpPath, $destPath); - } - } } From f6c739275e5d55065a235edb97f631645286b183 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Wed, 4 Nov 2020 19:52:56 -0600 Subject: [PATCH 0997/1013] MC-38613: Support by Magento CatalogGraphQl --- .../visual_swatch_attribute_with_different_options_type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php index 8d2b427d7f7f3..a4a755c4b92db 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php @@ -24,7 +24,7 @@ $imagesGenerator = Bootstrap::getObjectManager()->get(ImagesGenerator::class); /** @var SwatchesMedia $swatchesMedia */ $swatchesMedia = Bootstrap::getObjectManager()->get(SwatchesMedia::class); -$imageName = '/visual_swatch_attribute_option_type_image.jpg'; +$imageName = 'visual_swatch_attribute_option_type_image.jpg'; $imagesGenerator->generate([ 'image-width' => 110, 'image-height' => 90, From 8dc9c1a909f442f1f504dbf2353b3bf42b4c770e Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Thu, 5 Nov 2020 12:13:22 +0200 Subject: [PATCH 0998/1013] MC-37542: Create automated test for "[API] Create CMS page using API service" --- .../Magento/Cms/Api/PageRepositoryTest.php | 159 +++++++++++------- 1 file changed, 97 insertions(+), 62 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index f8d146dcc2bf7..53b1c56616403 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Api; use Magento\Authorization\Model\Role; @@ -191,15 +193,12 @@ public function testGet(): void */ public function testGetByStores(string $requestStore): void { - $page = $this->getPageByIdentifier->execute('page100', 0); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->updatePage($page, ['store_id' => $store->getId()]); - $page = $this->getPageByIdentifier->execute('page100', $store->getId()); - $comparedFields = $this->getPageRequestData()['page']; + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $this->updatePage('page100', 0, ['store_id' => $newStoreId]); + $page = $this->getPageByIdentifier->execute('page100', $newStoreId); $expectedData = array_intersect_key( $this->dataObjectProcessor->buildOutputDataArray($page, PageInterface::class), - $comparedFields + $this->getPageRequestData()['page'] ); $serviceInfo = $this->getServiceInfo( 'GetById', @@ -212,9 +211,7 @@ public function testGetByStores(string $requestStore): void } $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->assertNotNull($page['id']); - $actualData = array_intersect_key($page, $comparedFields); - $this->assertEquals($expectedData, $actualData, 'Error while getting page.'); + $this->assertResponseData($page, $expectedData); } /** @@ -255,27 +252,17 @@ public function testCreate(): void */ public function testCreateByStores(string $requestStore): void { + $newStoreId = $this->getStoreIdByRequestStore($requestStore); $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = $this->getPageRequestData(); + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->assertNotNull($page['id']); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->currentPage = $this->getPageByIdentifier->execute( - $requestData['page'][PageInterface::IDENTIFIER], - $store->getId() - ); - $actualData = array_intersect_key($page, $requestData['page']); - $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); - if ($requestStore != 'all') { - $this->cmsUiDataProvider->addFilter( - $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() - ); - } - $pageGridData = $this->cmsUiDataProvider->getData(); + $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->assertResponseData($page, $requestData['page']); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( $this->isPageInArray($pageGridData['items'], $page['id']), - sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $requestStore) ); } @@ -373,10 +360,8 @@ public function testUpdateOneField(): void */ public function testUpdateByStores(string $requestStore): void { - $page = $this->getPageByIdentifier->execute('page100', 0); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->updatePage($page, ['store_id' => $store->getId()]); + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $page = $this->updatePage('page100', 0, ['store_id' => $newStoreId]); $serviceInfo = $this->getServiceInfo( 'Save', Request::HTTP_METHOD_PUT, @@ -385,22 +370,12 @@ public function testUpdateByStores(string $requestStore): void $requestData = $this->getPageRequestData(); $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->assertNotNull($page['id']); - $this->currentPage = $this->getPageByIdentifier->execute( - $requestData['page'][PageInterface::IDENTIFIER], - $store->getId() - ); - $actualData = array_intersect_key($page, $requestData['page']); - $this->assertEquals($requestData['page'], $actualData, 'The page was saved with an error.'); - if ($requestStore != 'all') { - $this->cmsUiDataProvider->addFilter( - $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() - ); - } - $pageGridData = $this->cmsUiDataProvider->getData(); + $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->assertResponseData($page, $requestData['page']); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( $this->isPageInArray($pageGridData['items'], $page['id']), - sprintf('The "%s" page is missing from the "%s" store', $page['title'], $storeCode) + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $requestStore) ); } @@ -440,10 +415,8 @@ public function testDelete(): void */ public function testDeleteByStores(string $requestStore): void { - $page = $this->getPageByIdentifier->execute('page100', 0); - $storeCode = $requestStore == 'all' ? 'admin' : $requestStore; - $store = $this->storeManager->getStore($storeCode); - $this->updatePage($page, ['store_id' => $store->getId()]); + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $page = $this->updatePage('page100', 0, ['store_id' => $newStoreId]); $serviceInfo = $this->getServiceInfo( 'DeleteById', Request::HTTP_METHOD_DELETE, @@ -453,17 +426,13 @@ public function testDeleteByStores(string $requestStore): void if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { $requestData[PageInterface::PAGE_ID] = $page->getId(); } + $pageResponse = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); $this->assertTrue($pageResponse); - if ($requestStore != 'all') { - $this->cmsUiDataProvider->addFilter( - $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() - ); - } - $pageGridData = $this->cmsUiDataProvider->getData(); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertFalse( - $this->isPageInArray($pageGridData['items'], $page->getId()), - sprintf('The "%s" page should not be present on the "%s" store', $page->getTitle(), $storeCode) + $this->isPageInArray($pageGridData['items'], (int)$page->getId()), + sprintf('The "%s" page should not be present on the "%s" store', $page->getTitle(), $requestStore) ); } @@ -561,12 +530,12 @@ public function byStoresProvider(): array 'default_store' => [ 'request_store' => 'default', ], - /*'second_store' => [ + 'second_store' => [ 'request_store' => 'fixture_second_store', ], 'all' => [ 'request_store' => 'all', - ],*/ + ], ]; } @@ -606,9 +575,9 @@ private function prepareCmsPages(): array /** * Create page with hard-coded identifier to test with create-delete-create flow. * @param string $identifier - * @return string + * @return int */ - private function createPageWithIdentifier($identifier): string + private function createPageWithIdentifier($identifier): int { $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ @@ -792,12 +761,14 @@ private function isPageInArray(array $pageGridData, int $pageId): bool /** * Update page with data * - * @param PageInterface $page + * @param string $pageIdentifier + * @param int $storeId * @param array $pageData * @return PageInterface */ - private function updatePage(PageInterface $page, array $pageData): PageInterface + private function updatePage(string $pageIdentifier, int $storeId, array $pageData): PageInterface { + $page = $this->getPageByIdentifier->execute($pageIdentifier, $storeId); $page->addData($pageData); return $this->pageRepository->save($page); @@ -820,4 +791,68 @@ private function getPageRequestData(): array ] ]; } + + /** + * Get store id by request store code + * + * @param string $requestStoreCode + * @return int + */ + private function getStoreIdByRequestStore(string $requestStoreCode): int + { + $storeCode = $requestStoreCode === 'all' ? 'admin' : $requestStoreCode; + $store = $this->storeManager->getStore($storeCode); + + return (int)$store->getId(); + } + + /** + * Check that the response data is as expected + * + * @param array $page + * @param array $expectedData + * @return void + */ + private function assertResponseData(array $page, array $expectedData): void + { + $this->assertNotNull($page['id']); + $actualData = array_intersect_key($page, $expectedData); + $this->assertEquals($expectedData, $actualData, 'Response data does not match expected.'); + } + + /** + * Get page grid data of cms ui dataprovider filtering by store code + * + * @param string $requestStore + * @return array + */ + private function getPageGridDataByStoreCode(string $requestStore): array + { + if ($requestStore !== 'all') { + $store = $this->storeManager->getStore($requestStore); + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + + return $this->cmsUiDataProvider->getData(); + } + + /** + * Get page by identifier without throw exception + * + * @param string $identifier + * @param int $storeId + * @return PageInterface|null + */ + private function getPageByIdentifier(string $identifier, int $storeId): ?PageInterface + { + $page = null; + try { + $page = $this->getPageByIdentifier->execute($identifier, $storeId); + } catch (NoSuchEntityException $exception) { + } + + return $page; + } } From 42e7daff1e966e2f95c51ba1665d9b73a9b13fa6 Mon Sep 17 00:00:00 2001 From: Roman Zhupanyn <roma.dj.elf@gmail.com> Date: Thu, 5 Nov 2020 15:11:53 +0200 Subject: [PATCH 0999/1013] MC-37542: Create automated test for "[API] Create CMS page using API service" --- .../Magento/Cms/Api/PageRepositoryTest.php | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php index 53b1c56616403..8751f2a39921d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -13,6 +13,7 @@ use Magento\Authorization\Model\RulesFactory; use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\Data\PageInterfaceFactory; +use Magento\Cms\Model\ResourceModel\Page as PageResource; use Magento\Cms\Ui\Component\DataProvider as CmsDataProvider; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; @@ -85,7 +86,7 @@ class PageRepositoryTest extends WebapiAbstract private $adminTokens; /** - * @var array + * @var PageInterface[] */ private $createdPages = []; @@ -110,9 +111,9 @@ class PageRepositoryTest extends WebapiAbstract private $cmsUiDataProvider; /** - * @var GetPageByIdentifierInterface + * @var PageResource */ - private $getPageByIdentifier; + private $pageResource; /** * @inheritdoc @@ -137,7 +138,7 @@ protected function setUp(): void 'requestFieldName' => 'id', ] ); - $this->getPageByIdentifier = $this->objectManager->get(GetPageByIdentifierInterface::class); + $this->pageResource = $this->objectManager->get(PageResource::class); } /** @@ -151,7 +152,9 @@ protected function tearDown(): void } foreach ($this->createdPages as $page) { - $this->pageRepository->delete($page); + if ($page->getId()) { + $this->pageRepository->delete($page); + } } } @@ -195,7 +198,7 @@ public function testGetByStores(string $requestStore): void { $newStoreId = $this->getStoreIdByRequestStore($requestStore); $this->updatePage('page100', 0, ['store_id' => $newStoreId]); - $page = $this->getPageByIdentifier->execute('page100', $newStoreId); + $page = $this->loadPageByIdentifier('page100', $newStoreId); $expectedData = array_intersect_key( $this->dataObjectProcessor->buildOutputDataArray($page, PageInterface::class), $this->getPageRequestData()['page'] @@ -257,7 +260,10 @@ public function testCreateByStores(string $requestStore): void $requestData = $this->getPageRequestData(); $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->createdPages[] = $this->loadPageByIdentifier( + $requestData['page'][PageInterface::IDENTIFIER], + $newStoreId + ); $this->assertResponseData($page, $requestData['page']); $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( @@ -370,7 +376,10 @@ public function testUpdateByStores(string $requestStore): void $requestData = $this->getPageRequestData(); $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); - $this->currentPage = $this->getPageByIdentifier($requestData['page'][PageInterface::IDENTIFIER], $newStoreId); + $this->createdPages[] = $this->loadPageByIdentifier( + $requestData['page'][PageInterface::IDENTIFIER], + $newStoreId + ); $this->assertResponseData($page, $requestData['page']); $pageGridData = $this->getPageGridDataByStoreCode($requestStore); $this->assertTrue( @@ -768,7 +777,7 @@ private function isPageInArray(array $pageGridData, int $pageId): bool */ private function updatePage(string $pageIdentifier, int $storeId, array $pageData): PageInterface { - $page = $this->getPageByIdentifier->execute($pageIdentifier, $storeId); + $page = $this->loadPageByIdentifier($pageIdentifier, $storeId); $page->addData($pageData); return $this->pageRepository->save($page); @@ -839,19 +848,17 @@ private function getPageGridDataByStoreCode(string $requestStore): array } /** - * Get page by identifier without throw exception + * Load page by identifier and store id * * @param string $identifier * @param int $storeId - * @return PageInterface|null + * @return PageInterface */ - private function getPageByIdentifier(string $identifier, int $storeId): ?PageInterface + private function loadPageByIdentifier(string $identifier, int $storeId): PageInterface { - $page = null; - try { - $page = $this->getPageByIdentifier->execute($identifier, $storeId); - } catch (NoSuchEntityException $exception) { - } + $page = $this->pageFactory->create(); + $page->setStoreId($storeId); + $this->pageResource->load($page, $identifier, PageInterface::IDENTIFIER); return $page; } From f047427d64d8b5f43e7ad9f6be25c8dc14e39167 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 5 Nov 2020 15:32:27 +0200 Subject: [PATCH 1000/1013] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Cms/Ui/Component/DataProviderTest.php | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php diff --git a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php new file mode 100644 index 0000000000000..2a559566be786 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Ui\Component; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks Cms UI component data provider behaviour + * + * @magentoAppArea adminhtml + */ +class DataProviderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var UiComponentFactory */ + private $componentFactory; + + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->componentFactory = $this->objectManager->get(UiComponentFactory::class); + } + + /** + * @dataProvider pageFilterDataProvider + * + * @magentoDataFixture Magento/Cms/_files/pages.php + * + * @param array $filter + * @param string $expectedPage + * @return void + */ + public function testPageFiltering(array $filter, string $expectedPage): void + { + $this->request->setParams(['filters' => $filter]); + $data = $this->getComponentProvidedData('cms_page_listing'); + $this->assertCount(1, $data['items']); + $this->assertEquals(reset($data['items'])[PageInterface::IDENTIFIER], $expectedPage); + } + + /** + * @return array + */ + public function pageFilterDataProvider(): array + { + return [ + 'partial_title_filter' => [ + 'filter' => ['title' => 'Cms Page 1'], + 'expected_item' => 'page100', + ], + 'multiple_filter' => [ + 'filter' => [ + 'title' => 'Cms Page', + 'meta_title' => 'Cms Meta title for Blank page', + ], + 'expected_item' => 'page_design_blank', + ], + ]; + } + + /** + * @dataProvider blockFilterDataProvider + * + * @magentoDataFixture Magento/Cms/_files/blocks.php + * + * @return void + */ + public function testBlockFiltering(array $filter, string $expectedBlock): void + { + $this->request->setParams(['filters' => $filter]); + $data = $this->getComponentProvidedData('cms_block_listing'); + $this->assertCount(1, $data['items']); + $this->assertEquals(reset($data['items'])[BlockInterface::IDENTIFIER], $expectedBlock); + } + + /** + * @return array + */ + public function blockFilterDataProvider(): array + { + return [ + 'partial_title_filter' => [ + 'filter' => ['title' => 'Enabled CMS Block'], + 'expected_item' => 'enabled_block', + ], + 'multiple_filter' => [ + 'filter' => [ + 'title' => 'CMS Block Title', + 'is_active' => [0], + ], + 'expected_item' => 'disabled_block', + ], + ]; + } + + /** + * Call prepare method in the child components + * + * @param UiComponentInterface $component + * @return void + */ + private function prepareChildComponents(UiComponentInterface $component) + { + foreach ($component->getChildComponents() as $child) { + $this->prepareChildComponents($child); + } + + $component->prepare(); + } + + /** + * Get component provided data + * + * @param string $namespace + * @return array + */ + private function getComponentProvidedData(string $namespace): array + { + $component = $this->componentFactory->create($namespace); + $this->prepareChildComponents($component); + + return $component->getContext()->getDataProvider()->getData(); + } +} From 079f111b87a139c90becc3f5a8c53a28f221685a Mon Sep 17 00:00:00 2001 From: Ji Lu <jilu1@adobe.com> Date: Thu, 5 Nov 2020 11:37:25 -0600 Subject: [PATCH 1001/1013] MQE-2366: composer.lock updade for mftf 3.2.0 release --- composer.lock | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index b06e0e9fa9e5c..898039b4c8fc5 100644 --- a/composer.lock +++ b/composer.lock @@ -7947,16 +7947,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.1.1", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "c6760313811f2c04545a261c706d2a73dd727b9a" + "reference": "0ec0c87335af996cbf3c0aace375d4e659e7a6dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/c6760313811f2c04545a261c706d2a73dd727b9a", - "reference": "c6760313811f2c04545a261c706d2a73dd727b9a", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/0ec0c87335af996cbf3c0aace375d4e659e7a6dc", + "reference": "0ec0c87335af996cbf3c0aace375d4e659e7a6dc", "shasum": "" }, "require": { @@ -8034,7 +8034,7 @@ "magento", "testing" ], - "time": "2020-09-28T18:26:59+00:00" + "time": "2020-11-05T15:57:52+00:00" }, { "name": "mikey179/vfsstream", @@ -11297,6 +11297,5 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } From 6a2a4e2844c8fcb87abf412eaad2619250f05b53 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Thu, 5 Nov 2020 20:55:23 +0200 Subject: [PATCH 1002/1013] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../Cms/Ui/Component/DataProviderTest.php | 65 ++++--------------- 1 file changed, 11 insertions(+), 54 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php index 2a559566be786..3e7b1ed4d4c55 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php @@ -20,6 +20,7 @@ * Checks Cms UI component data provider behaviour * * @magentoAppArea adminhtml + * @magentoDbIsolation enabled */ class DataProviderTest extends TestCase { @@ -45,75 +46,31 @@ protected function setUp(): void } /** - * @dataProvider pageFilterDataProvider - * * @magentoDataFixture Magento/Cms/_files/pages.php * - * @param array $filter - * @param string $expectedPage * @return void */ - public function testPageFiltering(array $filter, string $expectedPage): void + public function testPageFilteringByTitlePart(): void { - $this->request->setParams(['filters' => $filter]); + $this->request->setParams(['search' => 'Cms Page 1']); $data = $this->getComponentProvidedData('cms_page_listing'); - $this->assertCount(1, $data['items']); - $this->assertEquals(reset($data['items'])[PageInterface::IDENTIFIER], $expectedPage); - } - - /** - * @return array - */ - public function pageFilterDataProvider(): array - { - return [ - 'partial_title_filter' => [ - 'filter' => ['title' => 'Cms Page 1'], - 'expected_item' => 'page100', - ], - 'multiple_filter' => [ - 'filter' => [ - 'title' => 'Cms Page', - 'meta_title' => 'Cms Meta title for Blank page', - ], - 'expected_item' => 'page_design_blank', - ], - ]; + $items = $data['items']; + $this->assertCount(1, $items); + $this->assertEquals('page100', reset($items)[PageInterface::IDENTIFIER]); } /** - * @dataProvider blockFilterDataProvider - * * @magentoDataFixture Magento/Cms/_files/blocks.php * * @return void */ - public function testBlockFiltering(array $filter, string $expectedBlock): void + public function testBlockFilteringByTitlePart(): void { - $this->request->setParams(['filters' => $filter]); + $this->request->setParams(['search' => 'Enabled CMS Block']); $data = $this->getComponentProvidedData('cms_block_listing'); - $this->assertCount(1, $data['items']); - $this->assertEquals(reset($data['items'])[BlockInterface::IDENTIFIER], $expectedBlock); - } - - /** - * @return array - */ - public function blockFilterDataProvider(): array - { - return [ - 'partial_title_filter' => [ - 'filter' => ['title' => 'Enabled CMS Block'], - 'expected_item' => 'enabled_block', - ], - 'multiple_filter' => [ - 'filter' => [ - 'title' => 'CMS Block Title', - 'is_active' => [0], - ], - 'expected_item' => 'disabled_block', - ], - ]; + $items = $data['items']; + $this->assertCount(1, $items); + $this->assertEquals('enabled_block', reset($items)[BlockInterface::IDENTIFIER]); } /** From 4b7bffac1f021e5b51f1b8d6bf783d9a332b6f2b Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Thu, 5 Nov 2020 18:36:29 -0600 Subject: [PATCH 1003/1013] MC-38613: Support by Magento CatalogGraphQl --- app/code/Magento/Eav/Model/AttributeRepository.php | 2 +- app/code/Magento/Swatches/Helper/Media.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/code/Magento/Eav/Model/AttributeRepository.php b/app/code/Magento/Eav/Model/AttributeRepository.php index bb307d5581121..ee2db9a9b6b35 100644 --- a/app/code/Magento/Eav/Model/AttributeRepository.php +++ b/app/code/Magento/Eav/Model/AttributeRepository.php @@ -88,7 +88,7 @@ public function save(\Magento\Eav\Api\Data\AttributeInterface $attribute) try { $this->eavResource->save($attribute); } catch (\Exception $e) { - throw new StateException(__("The attribute can't be saved.")); + throw new StateException(__("The attribute can't be saved."), $e); } return $attribute; } diff --git a/app/code/Magento/Swatches/Helper/Media.php b/app/code/Magento/Swatches/Helper/Media.php index bfcb354b41dfb..6787fba534893 100644 --- a/app/code/Magento/Swatches/Helper/Media.php +++ b/app/code/Magento/Swatches/Helper/Media.php @@ -197,7 +197,7 @@ protected function getUniqueFileName($file) $file ); } else { - $destFile = dirname($file) . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( + $destFile = rtrim(dirname($file), '/.') . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( $this->getOriginalFilePath($file) ); } From d108146652dcb46db68d4262146730c18a9be0bd Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Thu, 5 Nov 2020 21:04:31 -0600 Subject: [PATCH 1004/1013] MC-38613: Support by Magento CatalogGraphQl --- .../visual_swatch_attribute_with_different_options_type.php | 2 +- .../Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php index a4a755c4b92db..8d2b427d7f7f3 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php @@ -24,7 +24,7 @@ $imagesGenerator = Bootstrap::getObjectManager()->get(ImagesGenerator::class); /** @var SwatchesMedia $swatchesMedia */ $swatchesMedia = Bootstrap::getObjectManager()->get(SwatchesMedia::class); -$imageName = 'visual_swatch_attribute_option_type_image.jpg'; +$imageName = '/visual_swatch_attribute_option_type_image.jpg'; $imagesGenerator->generate([ 'image-width' => 110, 'image-height' => 90, diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index bc6d57a869b5a..dc730b69f8775 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -71,6 +71,7 @@ public function generate($config) $mediaDirectory->create($relativePathToMedia); $imagePath = $relativePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; + $imagePath = preg_replace('|/{2,}|', '/', $imagePath); $memory = fopen('php://memory', 'r+'); if(!imagejpeg($image, $memory)) { throw new \Exception('Could not create picture ' . $imagePath); From d38d0575ce79c0802e0818b3c56d45139dfd9e15 Mon Sep 17 00:00:00 2001 From: Andrii Lugovyi <alugovyi@adobe.com> Date: Thu, 5 Nov 2020 21:48:51 -0600 Subject: [PATCH 1005/1013] MC-38613: Support by Magento CatalogGraphQl --- .../visual_swatch_attribute_with_different_options_type.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php index 8d2b427d7f7f3..77b3e198bd5ab 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php @@ -30,7 +30,7 @@ 'image-height' => 90, 'image-name' => $imageName, ]); -$imagePath = substr($swatchesMedia->moveImageFromTmp($imageName), 1); +$imagePath = $swatchesMedia->moveImageFromTmp($imageName); $swatchesMedia->generateSwatchVariations($imagePath); // Add attribute data From ae9c0364d2c9c3887c893871ed66dbe33c0b8e63 Mon Sep 17 00:00:00 2001 From: Roman Hanin <rganin@adobe.com> Date: Fri, 6 Nov 2020 00:33:14 -0600 Subject: [PATCH 1006/1013] B2B-964: Create MFTF Test for Purchase Order Creation with Online Payment Methods --- ...ayPalExpressCheckoutDisableActionGroup.xml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml new file mode 100644 index 0000000000000..f70af75660f94 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.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="SampleConfigPayPalExpressCheckoutDisableActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="No" stepKey="enableSolution"/> + <!--Save configuration--> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + </actionGroup> +</actionGroups> From e8205458686ae20ee6159ced11a972070a08c96c Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Fri, 6 Nov 2020 08:38:41 +0200 Subject: [PATCH 1007/1013] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../Adminhtml/Order/Create/ReorderTest.php | 105 ++++++++++++++---- 1 file changed, 81 insertions(+), 24 deletions(-) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php index 0856e58c308d5..6390e5aeaba5f 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -7,61 +7,65 @@ namespace Magento\Sales\Controller\Adminhtml\Order\Create; -use Magento\Backend\Model\Session\Quote; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractBackendController; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerInterfaceFactory; use Magento\Framework\App\Request\Http; -use Magento\Framework\Registry; use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; use Magento\Sales\Model\OrderFactory; -use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Xpath; -use Magento\TestFramework\TestCase\AbstractBackendController; use Magento\Sales\Api\Data\OrderInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\Exception\NoSuchEntityException; /** - * Test load block for order create controller. - * - * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Index + * Test for reorder controller. * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder * @magentoAppArea adminhtml - * @magentoDbIsolation enabled */ class ReorderTest extends AbstractBackendController { - /** - * @var OrderRepositoryInterface - */ - private $orderRepository; + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CartInterface */ + private $quote; /** - * @var CustomerInterfaceFactory + * @var CustomerRepositoryInterface */ - private $customerFactory; + private $customerRepository; /** - * @var AccountManagementInterface + * @var array */ - private $accountManagement; + private $customerIds = []; /** - * @var OrderFactory + * @var OrderRepositoryInterface */ - private $orderFactory; + private $orderRepository; /** - * @var CustomerRepositoryInterface + * @var CustomerInterfaceFactory */ - private $customerRepository; + private $customerFactory; /** - * @var array + * @var AccountManagementInterface */ - private $customerIds = []; + private $accountManagement; /** * @inheritdoc @@ -69,10 +73,11 @@ class ReorderTest extends AbstractBackendController protected function setUp(): void { parent::setUp(); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); $this->orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class); $this->customerFactory = $this->_objectManager->get(CustomerInterfaceFactory::class); $this->accountManagement = $this->_objectManager->get(AccountManagementInterface::class); - $this->orderFactory = $this->_objectManager->get(OrderFactory::class); $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); } @@ -81,7 +86,9 @@ protected function setUp(): void */ protected function tearDown(): void { - parent::tearDown(); + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } foreach ($this->customerIds as $customerId) { try { $this->customerRepository->deleteById($customerId); @@ -89,6 +96,24 @@ protected function tearDown(): void //customer already deleted } } + parent::tearDown(); + } + + /** + * Reorder with JS calendar options + * + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @magentoDataFixture Magento/Sales/_files/order_with_date_time_option_product.php + * + * @return void + */ + public function testReorderAfterJSCalendarEnabled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('backend/sales/order_create')); + $this->quote = $this->getQuote('customer@example.com'); + $this->assertTrue(!empty($this->quote)); } /** @@ -150,4 +175,36 @@ private function getOrderWithDelegatingCustomer(): OrderInterface return $this->orderRepository->save($orderModel); } + + /** + * Dispatch reorder request. + * + * @param null|int $orderId + * @return void + */ + private function dispatchReorderRequest(?int $orderId = null): void + { + $this->getRequest()->setMethod(Request::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/reorder'); + } + + /** + * Gets quote by reserved order id. + * + * @return \Magento\Quote\Api\Data\CartInterface + */ + private function getQuote(string $customerEmail): \Magento\Quote\Api\Data\CartInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('customer_email', $customerEmail) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } } From ee08b091b4cd38e30f63a937e75244dc97b783e5 Mon Sep 17 00:00:00 2001 From: "rostyslav.hymon" <rostyslav.hymon@transoftgroup.com> Date: Fri, 6 Nov 2020 10:56:55 +0200 Subject: [PATCH 1008/1013] MC-38498: "Save to Address Book" in Admin checkout causes duplicate address book entries --- .../Sales/Controller/Adminhtml/Order/Create/ReorderTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php index 6390e5aeaba5f..27423c67ffe19 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -30,6 +30,7 @@ * * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ReorderTest extends AbstractBackendController { From 533a39d6061034a42d1989f9bb350ff0175baf76 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 6 Nov 2020 11:38:00 +0200 Subject: [PATCH 1009/1013] MC-37540: Create automated test for "[CMS Grids] Use quick search in Admin data grids" --- .../testsuite/Magento/Cms/Ui/Component/DataProviderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php index 3e7b1ed4d4c55..710c49241d82c 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php @@ -79,7 +79,7 @@ public function testBlockFilteringByTitlePart(): void * @param UiComponentInterface $component * @return void */ - private function prepareChildComponents(UiComponentInterface $component) + private function prepareChildComponents(UiComponentInterface $component): void { foreach ($component->getChildComponents() as $child) { $this->prepareChildComponents($child); From 40ebd2d2b99b85c5ded30bbb43e1ae3dfc36e1b3 Mon Sep 17 00:00:00 2001 From: Yurii Sapiha <yurasapiga93@gmail.com> Date: Fri, 6 Nov 2020 12:12:54 +0200 Subject: [PATCH 1010/1013] MC-37074: Create automated test for "Dynamic charts on Magento dashboard --- .../Block/Dashboard/Chart/PeriodTest.php | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php new file mode 100644 index 0000000000000..c9ad4827c2838 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Block\Dashboard\Chart; + +use Magento\Backend\ViewModel\ChartsPeriod; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Checks chart periods on Magento dashboard + * + * @magentoAppArea adminhtml + */ +class PeriodTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Template */ + private $block; + + /** @var LayoutInterface */ + private $layout; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Template::class); + $this->block->setTemplate("Magento_Backend::dashboard/chart/period.phtml"); + $this->block->setData('view_model', $this->objectManager->get(ChartsPeriod::class)); + } + + /** + * @return void + */ + public function testChartPeriodOptions(): void + { + $html = $this->block->toHtml(); + $dropDownList = [ + __('Last 24 Hours'), + __('Last 7 Days'), + __('Current Month'), + __('YTD'), + __('2YTD') + ]; + foreach ($dropDownList as $item) { + $xPath = "//select[@id='dashboard_chart_period']/option[normalize-space(text())='{$item}']"; + $this->assertEquals(1, Xpath::getElementsCountForXpath($xPath, $html)); + } + } +} From d64fe798a69adf933eb28fe2fdde8ad4d6b2bf9e Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Fri, 6 Nov 2020 09:57:02 -0600 Subject: [PATCH 1011/1013] B2B-964: Create MFTF Test for Purchase Order Creation with Online Payment Methods - Addressing PR feedback --- ... => AdminConfigPayPalExpressCheckoutDisableActionGroup.xml} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename app/code/Magento/Paypal/Test/Mftf/ActionGroup/{SampleConfigPayPalExpressCheckoutDisableActionGroup.xml => AdminConfigPayPalExpressCheckoutDisableActionGroup.xml} (91%) diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml similarity index 91% rename from app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml rename to app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml index f70af75660f94..42dbf1d1c6061 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutDisableActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.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="SampleConfigPayPalExpressCheckoutDisableActionGroup"> + <actionGroup name="AdminConfigPayPalExpressCheckoutDisableActionGroup"> <annotations> <description>Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.</description> </annotations> @@ -22,5 +22,6 @@ <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="No" stepKey="enableSolution"/> <!--Save configuration--> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> </actionGroup> </actionGroups> From d3bd1ffbf8bec185c19047cd3c01654b6eb9f260 Mon Sep 17 00:00:00 2001 From: David Haecker <dhaecker@magento.com> Date: Fri, 6 Nov 2020 10:05:22 -0600 Subject: [PATCH 1012/1013] B2B-964: Create MFTF Test for Purchase Order Creation with Online Payment Methods - Deprecating old PayPal actiongroup --- ...yPalExpressCheckoutDisableActionGroup.xml} | 3 +- ...PayPalExpressCheckoutEnableActionGroup.xml | 34 +++++++++++++++++++ ...ConfigPayPalExpressCheckoutActionGroup.xml | 2 +- ...ResolutionForPayPalInUnitedKingdomTest.xml | 2 +- 4 files changed, 37 insertions(+), 4 deletions(-) rename app/code/Magento/Paypal/Test/Mftf/ActionGroup/{AdminConfigPayPalExpressCheckoutDisableActionGroup.xml => AdminPayPalExpressCheckoutDisableActionGroup.xml} (92%) create mode 100644 app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.xml similarity index 92% rename from app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml rename to app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.xml index 42dbf1d1c6061..c927bfc50120e 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminConfigPayPalExpressCheckoutDisableActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.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="AdminConfigPayPalExpressCheckoutDisableActionGroup"> + <actionGroup name="AdminPayPalExpressCheckoutDisableActionGroup"> <annotations> <description>Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.</description> </annotations> @@ -20,7 +20,6 @@ <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="No" stepKey="enableSolution"/> - <!--Save configuration--> <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> <waitForPageLoad stepKey="waitForPageLoad2"/> </actionGroup> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml new file mode 100644 index 0000000000000..b6b44abd7b794 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.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="AdminPayPalExpressCheckoutEnableActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="credentials" defaultValue="SamplePaypalExpressConfig"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.email(countryCode)}}" userInput="{{credentials.paypal_express_email}}" stepKey="inputEmailAssociatedWithPayPalMerchantAccount"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.apiMethod(countryCode)}}" userInput="API Signature" stepKey="inputAPIAuthenticationMethods"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.username(countryCode)}}" userInput="{{credentials.paypal_express_api_username}}" stepKey="inputAPIUsername"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.password(countryCode)}}" userInput="{{credentials.paypal_express_api_password}}" stepKey="inputAPIPassword"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature(countryCode)}}" userInput="{{credentials.paypal_express_api_signature}}" stepKey="inputAPISignature"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.paypal_express_merchantID}}" stepKey="inputMerchantID"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml index 23d956c8e9b8f..a7ccf0a19263b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.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="SampleConfigPayPalExpressCheckoutActionGroup"> + <actionGroup name="SampleConfigPayPalExpressCheckoutActionGroup" deprecated="Use AdminPayPalExpressCheckoutEnableActionGroup instead"> <annotations> <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> </annotations> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml index ebdfb9e91ecf1..a616c0bb2c68b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml @@ -19,7 +19,7 @@ </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="SampleConfigPayPalExpressCheckoutActionGroup" stepKey="ConfigPayPalExpress"> + <actionGroup ref="AdminPayPalExpressCheckoutEnableActionGroup" stepKey="ConfigPayPalExpress"> <argument name="credentials" value="SamplePaypalExpressConfig"/> </actionGroup> </before> From 1ab716627dbe07ce5484ac03524e4bc008838faf Mon Sep 17 00:00:00 2001 From: Oleg Posyniak <oleg.posyniak@gmail.com> Date: Mon, 9 Nov 2020 11:58:23 -0600 Subject: [PATCH 1013/1013] MC-38112: Add synchronization mechanism between local storage and remote storage (#357) --- app/code/Magento/AwsS3/Driver/AwsS3.php | 62 ++++---- .../Magento/AwsS3/etc/adminhtml/system.xml | 35 ----- .../Magento/Backup/Model/Fs/Collection.php | 5 - .../Reader/Source/Deployed/DocumentRoot.php | 20 +-- .../MediaGalleryRenditions/etc/config.xml | 5 + .../Model/CreateAssetFromFile.php | 13 +- .../MediaStorage/Model/File/Storage.php | 9 ++ .../Command/RemoteStorageDisableCommand.php | 74 --------- .../Command/RemoteStorageEnableCommand.php | 144 ------------------ .../RemoteStorageSynchronizeCommand.php | 83 ++++++++++ .../RemoteStorage/Driver/DriverPool.php | 21 ++- app/code/Magento/RemoteStorage/Filesystem.php | 10 +- .../RemoteStorage/FilesystemInterface.php | 19 +++ .../RemoteStorage/Model/Synchronizer.php | 104 +++++++++++++ .../Test/Unit/Model/SynchronizerTest.php | 109 +++++++++++++ .../_files/test/.dot_directory/child_file.txt | 0 .../Test/Unit/Model/_files/test/.dot_file.txt | 0 .../Test/Unit/Model/_files/test/root_file.txt | 0 app/code/Magento/RemoteStorage/etc/di.xml | 9 +- app/etc/di.xml | 1 - .../Magento/Framework/Config/DocumentRoot.php | 50 ------ .../Magento/Framework/File/Uploader.php | 27 +--- .../Filesystem/Directory/ReadFactory.php | 7 +- .../Filesystem/Directory/WriteFactory.php | 7 +- .../Framework/Filesystem/Driver/File.php | 27 +--- .../Framework/Filesystem/DriverPool.php | 2 +- .../Filesystem/DriverPoolInterface.php | 4 +- .../Filesystem/ExtendedDriverInterface.php | 11 +- .../Framework/Filesystem/File/ReadFactory.php | 8 +- .../Filesystem/File/WriteFactory.php | 5 +- 30 files changed, 423 insertions(+), 448 deletions(-) delete mode 100644 app/code/Magento/AwsS3/etc/adminhtml/system.xml delete mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php delete mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php create mode 100644 app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php create mode 100644 app/code/Magento/RemoteStorage/FilesystemInterface.php create mode 100644 app/code/Magento/RemoteStorage/Model/Synchronizer.php create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt create mode 100644 app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt delete mode 100644 lib/internal/Magento/Framework/Config/DocumentRoot.php diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php index 8b0469862a4e9..dcf52b3188404 100644 --- a/app/code/Magento/AwsS3/Driver/AwsS3.php +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -8,6 +8,7 @@ namespace Magento\AwsS3\Driver; use Exception; +use Generator; use League\Flysystem\AdapterInterface; use League\Flysystem\Config; use Magento\Framework\Exception\FileSystemException; @@ -165,7 +166,7 @@ private function createDirectoryRecursively(string $path): bool $this->createDirectoryRecursively($parentDir); } - return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config([])); + return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config(self::CONFIG)); } /** @@ -205,8 +206,16 @@ public function deleteDirectory($path): bool public function filePutContents($path, $content, $mode = null): int { $path = $this->normalizeRelativePath($path); + $config = self::CONFIG; - return $this->adapter->write($path, $content, new Config(self::CONFIG))['size']; + if (false !== ($imageSize = @getimagesizefromstring($content))) { + $config['Metadata'] = [ + 'image-width' => $imageSize[0], + 'image-height' => $imageSize[1] + ]; + } + + return $this->adapter->write($path, $content, new Config($config))['size']; } /** @@ -461,16 +470,6 @@ public function getMetadata(string $path): array throw new FileSystemException(__('Cannot gather meta info! %1', [$this->getWarningMessage()])); } - $extra = [ - 'image-width' => 0, - 'image-height' => 0 - ]; - - if (isset($metaInfo['image-width'], $metaInfo['image-height'])) { - $extra['image-width'] = $metaInfo['image-width']; - $extra['image-height'] = $metaInfo['image-height']; - } - return [ 'path' => $metaInfo['path'], 'dirname' => $metaInfo['dirname'], @@ -480,7 +479,10 @@ public function getMetadata(string $path): array 'timestamp' => $metaInfo['timestamp'], 'size' => $metaInfo['size'], 'mimetype' => $metaInfo['mimetype'], - 'extra' => $extra + 'extra' => [ + 'image-width' => $metaInfo['metadata']['image-width'] ?? 0, + 'image-height' => $metaInfo['metadata']['image-height'] ?? 0 + ] ]; } @@ -489,21 +491,23 @@ public function getMetadata(string $path): array */ public function search($pattern, $path): array { - return $this->glob(rtrim($path, '/') . '/' . ltrim($pattern, '/')); + return iterator_to_array( + $this->glob(rtrim($path, '/') . '/' . ltrim($pattern, '/')), + false + ); } /** * Emulate php glob function for AWS S3 storage * * @param string $pattern - * @return array + * @return Generator * @throws FileSystemException */ - private function glob(string $pattern): array + private function glob(string $pattern): Generator { - $directoryContent = []; - $patternFound = preg_match('(\*|\?|\[.+\])', $pattern, $parentPattern, PREG_OFFSET_CAPTURE); + if ($patternFound) { // phpcs:ignore Magento2.Functions.DiscouragedFunction $parentDirectory = \dirname(substr($pattern, 0, $parentPattern[0][1] + 1)); @@ -512,13 +516,11 @@ private function glob(string $pattern): array $searchPattern = $this->getSearchPattern($pattern, $parentPattern, $parentDirectory, $index); if ($this->isDirectory($parentDirectory . '/')) { - $directoryContent = $this->getDirectoryContent($parentDirectory, $searchPattern, $leftover, $index); + yield from $this->getDirectoryContent($parentDirectory, $searchPattern, $leftover, $index); } } elseif ($this->isDirectory($pattern) || $this->isFile($pattern)) { - $directoryContent[] = $pattern; + yield $pattern; } - - return $directoryContent; } /** @@ -526,7 +528,7 @@ private function glob(string $pattern): array */ public function symlink($source, $destination, DriverInterface $targetDriver = null): bool { - throw new FileSystemException(__('Method %1 is not supported', __METHOD__)); + return $this->copy($source, $destination, $targetDriver); } /** @@ -850,7 +852,7 @@ private function getSearchPattern(string $pattern, array $parentPattern, string * @param string $searchPattern * @param string $leftover * @param int|bool $index - * @return array + * @return Generator * @throws FileSystemException */ private function getDirectoryContent( @@ -858,7 +860,7 @@ private function getDirectoryContent( string $searchPattern, string $leftover, $index - ): array { + ): Generator { $items = $this->readDirectory($parentDirectory . '/'); $directoryContent = []; foreach ($items as $item) { @@ -866,15 +868,9 @@ private function getDirectoryContent( // phpcs:ignore Magento2.Functions.DiscouragedFunction && strpos(basename($item), '.') !== 0) { if ($index === false || \strlen($leftover) === $index + 1) { - $directoryContent[] = $this->isDirectory($item) - ? rtrim($item, '/') . '/' - : $item; + yield $this->isDirectory($item) ? rtrim($item, '/') . '/' : $item; } elseif (strlen($leftover) > $index + 1) { - // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $directoryContent = array_merge( - $directoryContent, - $this->glob("{$parentDirectory}/{$item}" . substr($leftover, $index)) - ); + yield from $this->glob("{$parentDirectory}/{$item}" . substr($leftover, $index)); } } } diff --git a/app/code/Magento/AwsS3/etc/adminhtml/system.xml b/app/code/Magento/AwsS3/etc/adminhtml/system.xml deleted file mode 100644 index 0f97b96107ed3..0000000000000 --- a/app/code/Magento/AwsS3/etc/adminhtml/system.xml +++ /dev/null @@ -1,35 +0,0 @@ -<?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="file_system"> - <field id="access_key" translate="label comment" type="password" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Access Key</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - <field id="secret_key" translate="label comment" type="password" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Secret Key</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - <field id="bucket" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Bucket</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - <field id="region" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Region</label> - <validate>required-entry</validate> - <depends><field id="driver">aws-s3</field></depends> - </field> - </group> - </section> - </system> -</config> diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index 6102a63ec2f69..41a497495f687 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -40,11 +40,6 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem */ protected $_backup = null; - /** - * @var \Magento\Framework\Filesystem - */ - protected $_filesystem; - /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Magento\Backup\Helper\Data $backupData 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 2e50bbb8ef3c9..fb78de35569ac 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 @@ -6,8 +6,7 @@ namespace Magento\Config\Model\Config\Reader\Source\Deployed; use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Config\DocumentRoot as BaseDocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; /** * Document root detector. @@ -15,25 +14,18 @@ * @api * @since 101.0.0 * - * @deprecated Use new implementation - * @see \Magento\Framework\Config\DocumentRoot + * @deprecated Magento always uses the pub directory + * @see DirectoryList::PUB */ class DocumentRoot { - /** - * @var BaseDocumentRoot - */ - private $documentRoot; - /** * @param DeploymentConfig $config - * @param BaseDocumentRoot $documentRoot * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function __construct(DeploymentConfig $config, BaseDocumentRoot $documentRoot = null) + public function __construct(DeploymentConfig $config) { - $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(BaseDocumentRoot::class); } /** @@ -44,7 +36,7 @@ public function __construct(DeploymentConfig $config, BaseDocumentRoot $document */ public function getPath() { - return $this->documentRoot->getPath(); + return DirectoryList::PUB; } /** @@ -55,6 +47,6 @@ public function getPath() */ public function isPub() { - return $this->documentRoot->isPub(); + return true; } } diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml index 6b4f2351b8b10..871571a049875 100644 --- a/app/code/Magento/MediaGalleryRenditions/etc/config.xml +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml @@ -13,6 +13,11 @@ <width>1000</width> <height>1000</height> </media_gallery_renditions> + <media_storage_configuration> + <allowed_resources> + <renditions_folder>.renditions</renditions_folder> + </allowed_resources> + </media_storage_configuration> </system> </default> </config> diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index 48f2aad8fa746..19c2569695d56 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -82,24 +82,25 @@ public function execute(string $path): AssetInterface * SPL file info is not compatible with remote storages and must not be used. */ $file = $this->getFileInfo->execute($absolutePath); + [$width, $height] = getimagesize($absolutePath); $meta = [ 'size' => $file->getSize(), 'extension' => $file->getExtension(), 'basename' => $file->getBasename(), + 'extra' => [ + 'image-width' => $width, + 'image-height' => $height + ] ]; } - [$width, $height] = getimagesizefromstring( - $this->getMediaDirectory()->readFile($absolutePath) - ); - return $this->assetFactory->create( [ 'id' => null, 'path' => $path, 'title' => $meta['basename'], - 'width' => $width, - 'height' => $height, + 'width' => $meta['extra']['image-width'], + 'height' => $meta['extra']['image-height'], 'hash' => $this->getHash($path), 'size' => $meta['size'], 'contentType' => 'image/' . $meta['extension'], diff --git a/app/code/Magento/MediaStorage/Model/File/Storage.php b/app/code/Magento/MediaStorage/Model/File/Storage.php index f93b9180fa23d..f5ebda4a8d55c 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage.php @@ -82,6 +82,13 @@ class Storage extends AbstractModel */ protected $_databaseFactory; + /** + * @var Filesystem + * + * @deprecated + */ + protected $filesystem; + /** * @var Filesystem\Directory\ReadInterface */ @@ -122,6 +129,8 @@ public function __construct( $this->_fileFlag = $fileFlag; $this->_fileFactory = $fileFactory; $this->_databaseFactory = $databaseFactory; + $this->filesystem = $filesystem; + $this->localMediaDirectory = $filesystem->getDirectoryRead( DirectoryList::MEDIA, Filesystem\DriverPool::FILE diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php deleted file mode 100644 index e87ca584299e7..0000000000000 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageDisableCommand.php +++ /dev/null @@ -1,74 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Console\Command; - -use Magento\Framework\App\DeploymentConfig\Writer; -use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Console\Cli; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem\DriverPool; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Remote storage configuration disablement. - */ -class RemoteStorageDisableCommand extends Command -{ - private const NAME = 'remote-storage:disable'; - - /** - * @var Writer - */ - private $writer; - - /** - * @param Writer $writer - */ - public function __construct(Writer $writer) - { - $this->writer = $writer; - - parent::__construct(); - } - - /** - * @inheritDoc - */ - protected function configure(): void - { - $this->setName(self::NAME) - ->setDescription('Disable remote storage'); - } - - /** - * Executes command. - * - * @param InputInterface $input - * @param OutputInterface $output - * @return int - * @throws FileSystemException - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $this->writer->saveConfig([ - ConfigFilePool::APP_ENV => [ - 'remote_storage' => [ - 'driver' => DriverPool::FILE, - ] - ] - ], true); - - $output->writeln('<info>Config was saved.</info>'); - - return Cli::RETURN_SUCCESS; - } -} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php deleted file mode 100644 index bc21700cadee0..0000000000000 --- a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageEnableCommand.php +++ /dev/null @@ -1,144 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\RemoteStorage\Console\Command; - -use Magento\Framework\App\DeploymentConfig\Writer; -use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Console\Cli; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem\DriverPool; -use Magento\RemoteStorage\Driver\DriverFactoryPool; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Remote storage configuration enablement. - */ -class RemoteStorageEnableCommand extends Command -{ - private const NAME = 'remote-storage:enable'; - private const ARG_DRIVER = 'driver'; - private const ARGUMENT_BUCKET = 'bucket'; - private const ARGUMENT_REGION = 'region'; - private const OPTION_ACCESS_KEY = 'access-key'; - private const OPTION_SECRET_KEY = 'secret-key'; - private const ARGUMENT_PREFIX = 'prefix'; - private const OPTION_IS_PUBLIC = 'is-public'; - - /** - * @var Writer - */ - private $writer; - - /** - * @var DriverFactoryPool - */ - private $driverFactoryPool; - - /** - * @param Writer $writer - * @param DriverFactoryPool $driverFactoryPool - */ - public function __construct(Writer $writer, DriverFactoryPool $driverFactoryPool) - { - $this->writer = $writer; - $this->driverFactoryPool = $driverFactoryPool; - - parent::__construct(); - } - - /** - * @inheritDoc - */ - protected function configure(): void - { - $this->setName(self::NAME) - ->setDescription('Enable remote storage integration') - ->addArgument(self::ARG_DRIVER, InputArgument::OPTIONAL, 'Remote driver', DriverPool::FILE) - ->addArgument(self::ARGUMENT_BUCKET, InputArgument::OPTIONAL, 'Bucket') - ->addArgument(self::ARGUMENT_REGION, InputArgument::OPTIONAL, 'Region') - ->addArgument(self::ARGUMENT_PREFIX, InputArgument::OPTIONAL, 'Prefix', '') - ->addOption(self::OPTION_ACCESS_KEY, null, InputOption::VALUE_OPTIONAL, 'Access key') - ->addOption(self::OPTION_SECRET_KEY, null, InputOption::VALUE_OPTIONAL, 'Secret key') - ->addOption(self::OPTION_IS_PUBLIC, null, InputOption::VALUE_REQUIRED, 'Is public', false); - } - - /** - * Executes command. - * - * @param InputInterface $input - * @param OutputInterface $output - * @return int - * @throws FileSystemException - */ - protected function execute(InputInterface $input, OutputInterface $output): int - { - $driver = $input->getArgument(self::ARG_DRIVER); - - if ($driver === DriverPool::FILE) { - $output->writeln(sprintf( - 'Driver "%s" was specified. Skipping', - $driver - )); - - return Cli::RETURN_SUCCESS; - } - - if (!$this->driverFactoryPool->has($driver)) { - $output->writeln('Driver %s was not found', $driver); - - return Cli::RETURN_FAILURE; - } - - $prefix = (string)$input->getArgument(self::ARGUMENT_PREFIX); - $config = [ - 'bucket' => (string)$input->getArgument(self::ARGUMENT_BUCKET), - 'region' => (string)$input->getArgument(self::ARGUMENT_REGION), - ]; - $isPublic = (bool)$input->getOption(self::OPTION_IS_PUBLIC); - - if (($key = (string)$input->getOption(self::OPTION_ACCESS_KEY)) - && ($secret = (string)$input->getOption(self::OPTION_SECRET_KEY)) - ) { - $config['credentials']['key'] = $key; - $config['credentials']['secret'] = $secret; - } - - try { - $this->driverFactoryPool->get($driver)->create($config, $prefix); - } catch (\Exception $exception) { - $output->writeln(sprintf( - '<error>Config cannot be set: %s</error>', - $exception->getMessage() - )); - - return Cli::RETURN_FAILURE; - } - - $this->writer->saveConfig([ - ConfigFilePool::APP_ENV => [ - 'remote_storage' => [ - 'driver' => $driver, - 'prefix' => $prefix, - 'is_public' => $isPublic, - 'config' => $config - ] - ] - ], true); - - $output->writeln(sprintf( - '<info>Config for driver "%s" was saved.</info>', - $driver - )); - - return Cli::RETURN_SUCCESS; - } -} diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php new file mode 100644 index 0000000000000..a53a203b6d550 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Console\Command; + +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\LocalizedException; +use Magento\RemoteStorage\Model\Synchronizer; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\RemoteStorage\Model\Config; + +/** + * Synchronizes local storage with remote storage. + */ +class RemoteStorageSynchronizeCommand extends Command +{ + private const NAME = 'remote-storage:sync'; + + /** + * @var Synchronizer + */ + private $synchronizer; + + /** + * @var Config + */ + private $config; + + /** + * @param Synchronizer $synchronizer + * @param Config $config + */ + public function __construct( + Synchronizer $synchronizer, + Config $config + ) { + $this->synchronizer = $synchronizer; + $this->config = $config; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure(): void + { + $this->setDescription('Synchronize media files with remote storage.'); + } + + /** + * Run synchronization. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws LocalizedException + */ + public function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->config->isEnabled()) { + $output->writeln('<error>Remote storage is not enabled.</error>'); + + return Cli::RETURN_FAILURE; + } + + $output->writeln('<info>Uploading media files to remote storage.</info>'); + + foreach ($this->synchronizer->execute() as $file) { + $output->writeln('- ' . $file); + } + + $output->writeln('<info>End of upload.</info>'); + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php index 731ec6686e657..0c085da78ddac 100644 --- a/app/code/Magento/RemoteStorage/Driver/DriverPool.php +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -17,7 +17,7 @@ /** * The remote driver pool. */ -class DriverPool implements DriverPoolInterface +class DriverPool extends BaseDriverPool implements DriverPoolInterface { public const PATH_DRIVER = 'remote_storage/driver'; public const PATH_EXPOSE_URLS = 'remote_storage/expose_urls'; @@ -39,11 +39,6 @@ class DriverPool implements DriverPoolInterface */ private $driverFactoryPool; - /** - * @var DriverPool - */ - private $driverPool; - /** * @var array */ @@ -52,25 +47,27 @@ class DriverPool implements DriverPoolInterface /** * @param Config $config * @param DriverFactoryPool $driverFactoryPool - * @param BaseDriverPool $driverPool + * @param array $extraTypes */ public function __construct( Config $config, DriverFactoryPool $driverFactoryPool, - BaseDriverPool $driverPool + array $extraTypes = [] ) { $this->config = $config; $this->driverFactoryPool = $driverFactoryPool; - $this->driverPool = $driverPool; + + parent::__construct($extraTypes); } /** * Retrieves remote driver. * * @param string $code - * @return RemoteDriverInterface - * @throws RuntimeException + * @return DriverInterface + * @throws DriverException * @throws FileSystemException + * @throws RuntimeException */ public function getDriver($code = self::REMOTE): DriverInterface { @@ -91,6 +88,6 @@ public function getDriver($code = self::REMOTE): DriverInterface throw new RuntimeException(__('Remote driver is not available.')); } - return $this->driverPool->getDriver($code); + return parent::getDriver($code); } } diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php index f2d5237ea243b..01af39cfc50a3 100644 --- a/app/code/Magento/RemoteStorage/Filesystem.php +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -16,7 +16,7 @@ /** * Filesystem implementation for remote storage. */ -class Filesystem extends BaseFilesystem +class Filesystem extends BaseFilesystem implements FilesystemInterface { /** * @var bool @@ -120,4 +120,12 @@ public function getDirectoryReadByPath($path, $driverCode = DriverPool::REMOTE) return parent::getDirectoryReadByPath($path); } + + /** + * @inheritDoc + */ + public function getDirectoryCodes(): array + { + return $this->directoryCodes; + } } diff --git a/app/code/Magento/RemoteStorage/FilesystemInterface.php b/app/code/Magento/RemoteStorage/FilesystemInterface.php new file mode 100644 index 0000000000000..42669200c0caf --- /dev/null +++ b/app/code/Magento/RemoteStorage/FilesystemInterface.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage; + +/** + * Provides extension for applicable directory codes. + */ +interface FilesystemInterface +{ + /** + * Retrieve directory codes. + */ + public function getDirectoryCodes(): array; +} diff --git a/app/code/Magento/RemoteStorage/Model/Synchronizer.php b/app/code/Magento/RemoteStorage/Model/Synchronizer.php new file mode 100644 index 0000000000000..4276c7a1a2ffd --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Synchronizer.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Model; + +use Generator; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\Glob; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\RemoteStorage\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; + +/** + * Synchronize files from local filesystem. + */ +class Synchronizer +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem) + { + $this->filesystem = $filesystem; + } + + /** + * File upload. + * + * @return Generator + * @throws FileSystemException + * @throws ValidatorException + */ + public function execute(): Generator + { + foreach ($this->filesystem->getDirectoryCodes() as $directoryCode) { + $directory = $this->filesystem->getDirectoryWrite($directoryCode, DriverPool::FILE); + $remoteDirectory = $this->filesystem->getDirectoryWrite($directoryCode, RemoteDriverPool::REMOTE); + + yield from $this->copyRecursive($directory, $remoteDirectory, $directory->getAbsolutePath()); + } + } + + /** + * Recursive file upload. + * + * @param WriteInterface $directory + * @param WriteInterface $remoteDirectory + * @param string $path + * @param string $pattern + * @param int $flags + * @return Generator + * @throws FileSystemException + */ + private function copyRecursive( + WriteInterface $directory, + WriteInterface $remoteDirectory, + string $path, + string $pattern = '*.*', + int $flags = Glob::GLOB_NOSORT + ): Generator { + $path = rtrim($path, '/'); + $localDriver = $directory->getDriver(); + $remoteDriver = $remoteDirectory->getDriver(); + + foreach (Glob::glob($path . '/' . $pattern, $flags) as $file) { + /** + * Extracting relative path in local system to apply it for remote system. + */ + $relativeFile = $directory->getRelativePath($file); + $destination = $remoteDirectory->getAbsolutePath($relativeFile); + + if (!$remoteDirectory->isExist($destination)) { + $localDriver->copy($file, $destination, $remoteDriver); + + yield $relativeFile; + } + } + + foreach (Glob::glob($path . '/{,.}[!.,!..]*', + $flags | Glob::GLOB_ONLYDIR | Glob::GLOB_BRACE) as $childDirectory) { + $relativeDirectory = $directory->getRelativePath($childDirectory); + $destinationDirectory = $remoteDirectory->getAbsolutePath($relativeDirectory); + + if (!$remoteDirectory->isDirectory($destinationDirectory)) { + $remoteDriver->createDirectory($destinationDirectory); + + yield $relativeDirectory; + } + + yield from $this->copyRecursive($directory, $remoteDirectory, $childDirectory, $pattern, $flags); + } + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php new file mode 100644 index 0000000000000..5c3ddb74bb0cf --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Test\Unit\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\RemoteStorage\Filesystem; +use Magento\RemoteStorage\Model\Synchronizer; +use PHPUnit\Framework\TestCase; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; + +/** + * @see Synchronizer + */ +class SynchronizerTest extends TestCase +{ + /** + * @var Synchronizer + */ + private $synchronizer; + + /** + * @var Filesystem + */ + private $filesystemMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->filesystemMock = $this->createMock(Filesystem::class); + + $this->synchronizer = new Synchronizer( + $this->filesystemMock + ); + } + + /** + * @throws FileSystemException + * @throws ValidatorException + */ + public function testExecute(): void + { + $this->filesystemMock->method('getDirectoryCodes') + ->willReturn(['test']); + + $localDriver = $this->createMock(DriverInterface::class); + $remoteDriver = $this->createMock(DriverInterface::class); + + $localDirectory = $this->createMock(WriteInterface::class); + $localDirectory->method('getDriver') + ->willReturn($localDriver); + $remoteDirectory = $this->createMock(WriteInterface::class); + $remoteDirectory->method('getDriver') + ->willReturn($remoteDriver); + + $this->filesystemMock->method('getDirectoryWrite') + ->willReturnMap([ + ['test', DriverPool::FILE, $localDirectory], + ['test', RemoteDriverPool::REMOTE, $remoteDirectory] + ]); + $localDirectory->method('getAbsolutePath') + ->willReturnMap([ + [null, __DIR__ . '/_files/test'] + ]); + $localDirectory->method('getRelativePath') + ->willReturnCallback(function ($arg) { + return str_replace(__DIR__, '', $arg); + }); + $remoteDirectory->expects(self::exactly(2)) + ->method('isExist') + ->willReturnMap([ + [ + 'remote:/_files/test/root_file.txt', + false + ], + [ + 'remote:/_files/test/.dot_directory/child_file.txt', + true + ] + ]); + $remoteDirectory->method('getAbsolutePath') + ->willReturnCallback(function ($arg) { + return 'remote:' . $arg; + }); + $localDriver->expects(self::once()) + ->method('copy') + ->withConsecutive( + [__DIR__ . '/_files/test/root_file.txt', 'remote:/_files/test/root_file.txt', $remoteDriver] + ); + + self::assertSame( + [ + '/_files/test/root_file.txt', + '/_files/test/.dot_directory' + ], + iterator_to_array($this->synchronizer->execute(), false) + ); + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml index 9684a3ac49dbc..9fdde517b952c 100644 --- a/app/code/Magento/RemoteStorage/etc/di.xml +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -27,7 +27,6 @@ <argument name="directoryCodes" xsi:type="array"> <item name="media" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::MEDIA</item> <item name="var_export" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::VAR_EXPORT</item> - <item name="var_import" xsi:type="string">Magento\Framework\App\Filesystem\DirectoryList::VAR_IMPORT</item> </argument> </arguments> </virtualType> @@ -62,8 +61,7 @@ <type name="Magento\Framework\Console\CommandListInterface"> <arguments> <argument name="commands" xsi:type="array"> - <item name="remoteStorageEnable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageEnableCommand</item> - <item name="remoteStorageDisable" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageDisableCommand</item> + <item name="remoteStorageSync" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageSynchronizeCommand</item> </argument> </arguments> </type> @@ -131,4 +129,9 @@ <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> </arguments> </type> + <type name="Magento\RemoteStorage\Model\Synchronizer"> + <arguments> + <argument name="filesystem" xsi:type="object">customRemoteFilesystem</argument> + </arguments> + </type> </config> diff --git a/app/etc/di.xml b/app/etc/di.xml index d1282ff3ab961..fe7d86c4f599d 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -212,7 +212,6 @@ <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" /> - <preference for="Magento\Framework\Filesystem\DriverPoolInterface" type="Magento\Framework\Filesystem\DriverPool" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> diff --git a/lib/internal/Magento/Framework/Config/DocumentRoot.php b/lib/internal/Magento/Framework/Config/DocumentRoot.php deleted file mode 100644 index d20604e27e5c9..0000000000000 --- a/lib/internal/Magento/Framework/Config/DocumentRoot.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -namespace Magento\Framework\Config; - -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\App\DeploymentConfig; - -/** - * Document root detector. - * @deprecated Magento always uses the pub directory - * @api - */ -class DocumentRoot -{ - /** - * @var DeploymentConfig - */ - private $config; - - /** - * @param DeploymentConfig $config - */ - public function __construct(DeploymentConfig $config) - { - $this->config = $config; - } - - /** - * A shortcut to load the document root path from the DirectoryList. - * - * @return string - */ - public function getPath(): string - { - return DirectoryList::PUB; - } - - /** - * Checks if root folder is /pub. - * - * @return bool - */ - public function isPub(): bool - { - return true; - } -} diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index 706d6efef44b9..c3246bfbf1e48 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -7,7 +7,6 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; -use Magento\Framework\Config\DocumentRoot; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\TargetDirectory; @@ -188,11 +187,6 @@ class Uploader */ private $targetDirectory; - /** - * @var DocumentRoot - */ - private $documentRoot; - /** * Init upload * @@ -201,7 +195,6 @@ class Uploader * @param DirectoryList|null $directoryList * @param DriverPool|null $driverPool * @param TargetDirectory|null $targetDirectory - * @param DocumentRoot|null $documentRoot * @throws \DomainException */ public function __construct( @@ -209,8 +202,7 @@ public function __construct( Mime $fileMime = null, DirectoryList $directoryList = null, DriverPool $driverPool = null, - TargetDirectory $targetDirectory = null, - DocumentRoot $documentRoot = null + TargetDirectory $targetDirectory = null ) { $this->directoryList = $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); @@ -224,7 +216,6 @@ public function __construct( $this->fileMime = $fileMime ?: ObjectManager::getInstance()->get(Mime::class); $this->driverPool = $driverPool ?: ObjectManager::getInstance()->get(DriverPool::class); $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); - $this->documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); } /** @@ -342,7 +333,7 @@ protected function chmod($file) */ protected function _moveFile($tmpPath, $destPath) { - $rootCode = $this->getDocumentRoot()->getPath(); + $rootCode = DirectoryList::PUB; if (strpos($destPath, $this->getDirectoryList()->getPath($rootCode)) !== 0) { $rootCode = DirectoryList::ROOT; @@ -372,20 +363,6 @@ private function getTargetDirectory(): TargetDirectory return $this->targetDirectory; } - /** - * Retrieves document root. - * - * @return DocumentRoot - */ - private function getDocumentRoot(): DocumentRoot - { - if (!isset($this->documentRoot)) { - $this->documentRoot = ObjectManager::getInstance()->get(DocumentRoot::class); - } - - return $this->documentRoot; - } - /** * Retrieves directory list. * diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php index a3364d3be1c8c..33b025389945a 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php @@ -6,7 +6,6 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\Filesystem\DriverPoolInterface; /** * The factory of the filesystem directory instances for read operations. @@ -16,16 +15,16 @@ class ReadFactory /** * Pool of filesystem drivers * - * @var DriverPoolInterface + * @var DriverPool */ private $driverPool; /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php index 6f6bfe558176d..1cb642c5c3a2a 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php @@ -6,7 +6,6 @@ namespace Magento\Framework\Filesystem\Directory; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\Filesystem\DriverPoolInterface; /** * The factory of the filesystem directory instances for write operations. @@ -16,16 +15,16 @@ class WriteFactory /** * Pool of filesystem drivers * - * @var DriverPoolInterface + * @var DriverPool */ private $driverPool; /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 07a0d1345a301..bc08f67228849 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -5,13 +5,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\Filesystem\Driver; use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem\Driver\File\Mime; use Magento\Framework\Filesystem\DriverInterface; -use Magento\Framework\Filesystem\ExtendedDriverInterface; use Magento\Framework\Filesystem\Glob; use Magento\Framework\Phrase; @@ -22,7 +19,7 @@ * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -class File implements ExtendedDriverInterface +class File implements DriverInterface { /** * @var string @@ -83,26 +80,6 @@ public function stat($path) return $result; } - /** - * @inheritDoc - */ - public function getMetadata(string $path): array - { - $fileInfo = new \SplFileInfo($path); - $mime = new Mime(); - - return [ - 'path' => $fileInfo->getPath(), - 'basename' => $fileInfo->getBasename('.' . $fileInfo->getExtension()), - 'extension' => $fileInfo->getExtension(), - 'filename' => $fileInfo->getFilename(), - 'dirname' => dirname($fileInfo->getFilename()), - 'timestamp' => $fileInfo->getMTime(), - 'size' => $fileInfo->getSize(), - 'mimetype' => $mime->getMimeType($path) - ]; - } - /** * Check permissions for reading file or directory * @@ -354,7 +331,7 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) public function copy($source, $destination, DriverInterface $targetDriver = null) { $targetDriver = $targetDriver ?: $this; - if (get_class($targetDriver) == get_class($this)) { + if (get_class($targetDriver) === get_class($this)) { $result = @copy($this->getScheme() . $source, $destination); } else { $content = $this->fileGetContents($source); diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPool.php b/lib/internal/Magento/Framework/Filesystem/DriverPool.php index dfd1e2abce01a..435e51c26b012 100644 --- a/lib/internal/Magento/Framework/Filesystem/DriverPool.php +++ b/lib/internal/Magento/Framework/Filesystem/DriverPool.php @@ -9,7 +9,7 @@ /** * A pool of stream wrappers */ -class DriverPool implements DriverPoolInterface +class DriverPool { /**#@+ * Available driver types diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php index b88db7518ba17..d99b285be0f67 100644 --- a/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php @@ -8,7 +8,7 @@ namespace Magento\Framework\Filesystem; /** - * A pool of stream wrappers + * A pool of stream wrappers. */ interface DriverPoolInterface { @@ -18,5 +18,5 @@ interface DriverPoolInterface * @param string $code * @return DriverInterface */ - public function getDriver($code); + public function getDriver($code): DriverInterface; } diff --git a/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php index a93d242dbe15a..c2643d7c54e79 100644 --- a/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php @@ -7,6 +7,8 @@ namespace Magento\Framework\Filesystem; +use Magento\Framework\Exception\FileSystemException; + /** * Provides extension for Driver interface. * @@ -22,7 +24,7 @@ interface ExtendedDriverInterface extends DriverInterface * * Implementation must return associative array with next keys: * - * ```php + * ``` * [ * 'path', * 'dirname', @@ -32,10 +34,15 @@ interface ExtendedDriverInterface extends DriverInterface * 'timestamp', * 'size', * 'mimetype', - * ]; + * 'extra' => [ + * 'image-width', + * 'image-height' + * ] + * ]; * * @param string $path Absolute path to file * @return array + * @throws FileSystemException * * @deprecated Method will be moved to DriverInterface */ diff --git a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php index b442d6d1c05c3..e46b00bc5c74f 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Filesystem\File; use Magento\Framework\Filesystem\DriverInterface; -use Magento\Framework\Filesystem\DriverPoolInterface; +use Magento\Framework\Filesystem\DriverPool; /** * Opens a file for reading @@ -18,16 +18,16 @@ class ReadFactory /** * Pool of filesystem drivers * - * @var DriverPoolInterface + * @var DriverPool */ private $driverPool; /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { $this->driverPool = $driverPool; } diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php index 8bc62cb6573c4..7a9596586f56a 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php @@ -7,7 +7,6 @@ use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\Filesystem\DriverPoolInterface; /** * Opens a file for reading and/or writing @@ -26,9 +25,9 @@ class WriteFactory extends ReadFactory /** * Constructor * - * @param DriverPoolInterface $driverPool + * @param DriverPool $driverPool */ - public function __construct(DriverPoolInterface $driverPool) + public function __construct(DriverPool $driverPool) { parent::__construct($driverPool); $this->driverPool = $driverPool;