From 912b3f7737259d5977ce3fa64781a3280c712bfe Mon Sep 17 00:00:00 2001 From: Pieter Cappelle Date: Wed, 5 Feb 2020 13:38:28 +0100 Subject: [PATCH 001/671] 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 002/671] 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 zcmd^lby!qg+wX>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|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;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)& zvtaWKXp*rFPc5J zj+W1Rrw@^*>~ypQxi#{3A+wiu8(mCJ^B3s5zJD3 zXwF(%;atw|W&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&wczSkc(f46!FoZW0(9CQol(6a8hJc>bTvc#~cf1!+xn~ z-;>G$NmP6{pB1aG#%GBL>I-{Mu1v%m_-J)45wsuqqNr3g47B z+S7_ww~jyM%j(uDsKqN#X|*l;SQJ0coy7YotEXxjs{0G_MZrLhL>!J5TwArQfW`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+90%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^E6L%{(bH4638_mj6T9! z_QLzjF;_YX9zx;^skeAUxtM7+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~}00?y&iQDw#= zJ6P~LW*%(Ch|e(#4DmQ*WF)EY6ZhaPaU18~`OZ&b1}yMBmaN+}lV5neGwu$*Jiq00 z(G_|-xIvT*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 zqlfksTNeZ3e~`UvQJu9OZpoV03C-Q(=Y0_D^pm z5nr`d)DkOKDZ7)|QOD)^-tQN^Zo`qQxLH{Ct8iF^sdn$4j|=KrRDffyF^PeMMBRU* zDCym<&lAKOXOMooJC!cmi~e~%9Ax?;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<*W^xgO4%&p-+QEqqdNLFHd z7coG;P`~3+mq z;m!>vRldcAc=yY5Fn1~)ZGw?VMbBf1EO8lG)P_O5L)3h*XK*0b5W2&D?S%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 zc*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^=CyzGLjG^5{d_?ogLDcQkmQN^cFsl z4KGS`2!CQ6s+)u;)GO%uzH4ZXn|Hxq~4lBJ`4)FteKSMms!b_m z(LM5VE^i;&=5*8R=BM~aVW?l5bPj}(hXz33j@O-#MrUSrNPjr9IzUDIT@4!M>3Dd& zN*o=K;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^`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-Wvx5uaM6iiAzXH4r|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_>}#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@ayBq=6#Sv*=;3Dr+r<5?35^Ms2e~lmgF5}3>A#zQaIoed}pq}n-E@HQLP+T>>UMv z8y8YNz`*)OBojJD_L&Q(ihl&>vlDj4v*sgO8PP*5Nl6V((Dy@Hhsz#Xr%Y$suEISQA};aj z;~TzwrWVR%RJGsq&!eJ{G|7sjNfZop;DSt%l;d*pKVa6f4D`|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#@GUqG zopz2`;4>?$Xt^J*S-V`s7gnS6Zp&hU`ib<;MgtUIrJ^rbhWi+ILg-ORDwDxd6+79n zD0SRccT`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&*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~cvtA`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&@@3oWtraov;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 zq`=~B=QkMJy_#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*ZwqxQj$S@zaD4hN>h2ab>l7t~b)Hb_h3G$=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_lq^a45_Ok&3f2Fdv#m#xI-Og+c%O{lP z3??CJBB@bN$UQdkIuGxZO1oJ2Q0NEHanssS#-SQ7`)*PQe^t5wksv)lbGFl)=(o_`G5<@}zksQXB<*@-J ze+F3>yaF1VxxxM<9QFP}m0L_c$uNdEEYA4X7`?l7!h{@n5L8K$g?w%f?j~K#xkLyt z5hFHOnm(=^Q`Vj4HI&n39Qw$%j zkc4LpZ_&MC@PTyTG_PSwLL!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 zucZg^wQm=uoVJ*ZI=gGZB zLHY7DhKC_HPb0eTU({l|tbMHFbV?A59yXfKRq;sNAc!>j5|5*yYo9zCn=>}0RJ%r0o5v=y~6K2=1Z8j0R`rdWfDCL`>3 zbD*w}bl(ez=JT-G%{j#PY)d?%s=CX z^U>@9Na{hIy9L4|_dV|PxNS3q9_!8o=WTt4fU}Q2Ttw#7-%&&1995@V5!AMWYJ{SO z>6Us9CQQ$k_Sgy}4B)%ANw-QYjty&#-=-KKJA%-_(r1Z zn;S;43ge2QUp=|%!a6Y2UL3tJ#c$+7#|K@&H~8DTbu^-8_aW20DD{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?znu$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#zn^av3_Wxc>15T z!1>YRI=rykmYIoJV>cyAC^jwwI0cu&@Jy!CwMMsQc#c?I$G$ zD^`V%g)5MJw1Q;}93ycx!+g)TAs)k)7TTY5Hk)7N4tu9q6PVT4R-lKIPx zu*XDC5Xe`~7#m6?cWEEckrMX4Vtxp1O2>xlmWy16?N 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 003/671] 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 004/671] 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 1000822c16ddb32a483b3966bd5e01e7cfcad536 Mon Sep 17 00:00:00 2001 From: Rafael Kassner Date: Thu, 26 Mar 2020 12:10:12 +0100 Subject: [PATCH 005/671] 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 006/671] 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 007/671] 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 008/671] 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 009/671] 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 a128f392b45f559316460962444bd4b1834e8f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szubert?= Date: Fri, 8 May 2020 12:22:00 +0200 Subject: [PATCH 010/671] 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 4fd92e7edc9a62c3e09f7ed0461da4f36aba4d27 Mon Sep 17 00:00:00 2001 From: Per Date: Sat, 9 May 2020 17:09:57 +0200 Subject: [PATCH 011/671] 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 cf5d73b89648c31fa0832fc6a1957f13b338a512 Mon Sep 17 00:00:00 2001 From: Alexander Steshuk Date: Mon, 18 May 2020 12:40:15 +0300 Subject: [PATCH 012/671] #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 013/671] #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 014/671] #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 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 015/671] 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 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 016/671] 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 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 017/671] 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 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 018/671] 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 019/671] 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 020/671] 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 021/671] 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 022/671] 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 023/671] 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 024/671] 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 025/671] 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 026/671] 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 027/671] 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 028/671] 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 029/671] 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 030/671] 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 031/671] 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 032/671] 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 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 033/671] 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":"CAaC,SAAWA,gBAGV,IAAIC,EAASD,EAAEE,KAAOF,EAAEG,UAQxB,SAASC,EAAgBC,GACvB,QAAOJ,GAASA,EAAOG,gBAAgBC,GASzC,SAASC,EAAgBC,GACvB,QAAON,GAASA,EAAOK,gBAAgBC,GASzC,SAASC,EAAaD,EAAKE,IACrBF,GAA2B,UAApBA,EAAIG,MAAM,EAAG,IAAoBD,GAAWA,EAAQE,UAC7DL,EAAgBC,GAapB,SAASK,EAASC,EAAMC,EAAQC,EAASC,GACvC,IAAKhB,EAAEiB,WAAY,OAAO,EAC1B,IAAIC,EAAS,IAAID,WACjBC,EAAOJ,OAAS,WACdA,EAAOK,KAAKD,EAAQE,KAAKC,SAEvBN,IACFG,EAAOI,QAAUJ,EAAOH,QAAU,WAChCA,EAAQI,KAAKD,EAAQE,KAAKG,SAG9B,IAAIC,EAAeN,EAAOF,GAAU,iBACpC,OAAIQ,GACFA,EAAaL,KAAKD,EAAQL,GACnBK,QAFT,EAaF,SAASO,EAAaC,EAAMC,GAE1B,OAAOC,OAAOC,UAAUC,SAASX,KAAKQ,KAAS,WAAaD,EAAO,IAerE,SAASK,EAAUlB,EAAMmB,EAAUvB,GAQjC,SAASwB,EAASC,EAASC,GACzB,IACI5B,EADA6B,EAAMC,SAASC,cAAc,OASjC,SAASC,EAAeH,EAAKI,GACvBN,IAAYC,EAILC,aAAeK,MACxBN,EAAOC,KAGTI,EAAOA,GAAQ,IACVE,MAAQN,EACbF,EAAQM,IARFN,GAASA,EAAQE,EAAKI,GAgB9B,SAASG,EAAkBtC,EAAMuC,GAC3BA,GAAO5C,EAAE6C,SAASA,QAAQC,IAAIF,GAC9BvC,GAAQoB,EAAa,OAAQpB,GAE/BE,EAAMH,EADNS,EAAOR,IAGPE,EAAMM,EACFJ,GAAWA,EAAQsC,cACrBX,EAAIW,YAActC,EAAQsC,cAG9BX,EAAIY,IAAMzC,EAkBZ,OAhBA6B,EAAIrB,QAAU,SAAUkC,GACtBzC,EAAaD,EAAKE,GACd0B,GAAQA,EAAOhB,KAAKiB,EAAKa,IAE/Bb,EAAItB,OAAS,WACXN,EAAaD,EAAKE,GAClB,IAAI+B,EAAO,CACTU,cAAed,EAAIe,cAAgBf,EAAIgB,MACvCC,eAAgBjB,EAAIkB,eAAiBlB,EAAImB,QAE3C,IACExB,EAAUyB,UAAUpB,EAAK3B,EAAS8B,EAAgB1B,EAAM2B,GACxD,MAAOjB,GACHY,GAAQA,EAAOZ,KAGH,iBAATV,GACLkB,EAAU0B,iBAAiBhD,GAC7BsB,EAAU2B,UAAU7C,EAAM8B,EAAmBlC,GAE7CkC,IAEKP,GACEX,EAAa,OAAQZ,IAASY,EAAa,OAAQZ,IAC5DN,EAAMH,EAAgBS,KAEpBuB,EAAIY,IAAMzC,EACH6B,GAEFxB,EACLC,EACA,SAAUN,GACR6B,EAAIY,IAAMzC,GAEZ4B,QAXG,EAeT,OAAInC,EAAE2D,SAA+B,mBAAb3B,GACtBvB,EAAUuB,EACH,IAAI2B,QAAQ1B,IAEdA,EAASD,EAAUA,GAK5BD,EAAU0B,iBAAmB,SAAUhD,GACrC,OAAOA,GAAWA,EAAQmD,MAM5B7B,EAAU2B,UAAY,SAAUnD,EAAKyB,GACnCA,KAGFD,EAAUyB,UAAY,SAAUpB,EAAK3B,EAASuB,EAAUnB,EAAM2B,GAC5DR,EAASI,EAAKI,IAGhBT,EAAU8B,OAAS7D,EACnB+B,EAAUnB,SAAWA,EACrBmB,EAAUN,aAAeA,EACzBM,EAAU3B,gBAAkBA,EAC5B2B,EAAUzB,gBAAkBA,EAEN,mBAAXwD,QAAyBA,OAAOC,IACzCD,OAAO,WACL,OAAO/B,IAEkB,iBAAXiC,QAAuBA,OAAOC,QAC9CD,OAAOC,QAAUlC,EAEjB/B,EAAE+B,UAAYA,EArNjB,CAuNqB,oBAAXmC,QAA0BA,QAAW9C,MCvN/C,SAAW+C,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,gBAAiBK,GACE,iBAAXH,QAAuBA,OAAOC,QAC9CE,EAAQC,QAAQ,iBAGhBD,EAAQD,OAAOnC,WATlB,CAWE,SAAUA,gBAGX,IAAIsC,EAAoBtC,EAAUyB,UAElCzB,EAAUuC,aAAe,SAAUlB,EAAOG,EAAQgB,GAChD,GAAIA,GAAaxC,EAAU8B,OAAOW,gBAChC,OAAO,IAAIA,gBAAgBpB,EAAOG,GAEpC,IAAIkB,EAASpC,SAASC,cAAc,UAGpC,OAFAmC,EAAOrB,MAAQA,EACfqB,EAAOlB,OAASA,EACTkB,GAGT1C,EAAUyB,UAAY,SAAUpB,EAAK3B,EAASuB,EAAUnB,EAAM2B,GAC5D6B,EAAkBlD,KAChBY,EACAA,EAAU2C,MAAMtC,EAAK3B,EAAS+B,GAC9B/B,EACAuB,EACAnB,EACA2B,IAOJT,EAAU4C,qBAAuB,aAKjC5C,EAAU6C,sBAAwB,SAAUxC,EAAK3B,GAC/C,IACIoE,EACAC,EACA1B,EACAG,EAJAwB,EAActE,EAAQsE,YAK1B,IAAKA,EACH,OAAOtE,EAGT,IAAKqE,KADLD,EAAa,GACHpE,EACJmB,OAAOC,UAAUmD,eAAe7D,KAAKV,EAASqE,KAChDD,EAAWC,GAAKrE,EAAQqE,IAa5B,OAVAD,EAAWI,MAAO,EAGGF,GAFrB3B,EAAQhB,EAAIe,cAAgBf,EAAIgB,QAChCG,EAASnB,EAAIkB,eAAiBlB,EAAImB,SAEhCsB,EAAWK,SAAW3B,EAASwB,EAC/BF,EAAWM,UAAY5B,IAEvBsB,EAAWK,SAAW9B,EACtByB,EAAWM,UAAY/B,EAAQ2B,GAE1BF,GAIT9C,EAAUqD,UAAY,SACpBhD,EACAqC,EACAY,EACAC,EACAC,EACAC,EACAC,EACAC,EACAjF,GAEA,IAAIkF,EAAMlB,EAAOmB,WAAW,MAkB5B,OAjBsC,IAAlCnF,EAAQoF,uBACVF,EAAIG,yBAA0B,EAC9BH,EAAIE,uBAAwB,GACnBpF,EAAQsF,wBACjBJ,EAAII,sBAAwBtF,EAAQsF,uBAEtCJ,EAAIP,UACFhD,EACAiD,EACAC,EACAC,EACAC,EACA,EACA,EACAC,EACAC,GAEKC,GAIT5D,EAAUiE,eAAiB,SAAUvF,GACnC,OAAOA,EAAQgE,QAAUhE,EAAQwE,QAAUxE,EAAQsE,aAKrDhD,EAAU2C,MAAQ,SAAUtC,EAAK3B,EAAS+B,GAExC/B,EAAUA,GAAW,GAErB+B,EAAOA,GAAQ,GACf,IAQI0C,EACAC,EACAc,EACAC,EACAX,EACAC,EACAH,EACAC,EACAa,EACAC,EACAC,EACA5B,EAnBA6B,EACFlE,EAAIwD,YACH7D,EAAUiE,eAAevF,MACtBsB,EAAU8B,OAAO0C,kBACnBnD,EAAQhB,EAAIe,cAAgBf,EAAIgB,MAChCG,EAASnB,EAAIkB,eAAiBlB,EAAImB,OAClCkC,EAAYrC,EACZsC,EAAanC,EAgBjB,SAASiD,IACP,IAAI9B,EAAQ+B,KAAKC,KACdT,GAAYR,GAAaA,GACzBS,GAAaR,GAAcA,GAElB,EAARhB,IACFe,GAAaf,EACbgB,GAAchB,GAMlB,SAASiC,IACP,IAAIjC,EAAQ+B,KAAKG,KACd1B,GAAYO,GAAaA,GACzBN,GAAaO,GAAcA,GAE1BhB,EAAQ,IACVe,GAAaf,EACbgB,GAAchB,GA2DlB,GAxDI4B,IAGFjB,GADA5E,EAAUsB,EAAU6C,sBAAsBxC,EAAK3B,EAAS+B,IACtCqE,MAAQ,EAC1BvB,EAAU7E,EAAQqG,KAAO,EACrBrG,EAAQ8E,aACVA,EAAc9E,EAAQ8E,YAClB9E,EAAQsG,QAAUC,WAAavG,EAAQoG,OAASG,YAClD3B,EAAUjC,EAAQmC,EAAc9E,EAAQsG,QAG1CxB,EAAcnC,EAAQiC,GAAW5E,EAAQsG,OAAS,GAEhDtG,EAAQ+E,cACVA,EAAe/E,EAAQ+E,aACnB/E,EAAQwG,SAAWD,WAAavG,EAAQqG,MAAQE,YAClD1B,EAAU/B,EAASiC,EAAe/E,EAAQwG,SAG5CzB,EAAejC,EAAS+B,GAAW7E,EAAQwG,QAAU,GAEvDxB,EAAYF,EACZG,EAAaF,GAEfN,EAAWzE,EAAQyE,SACnBC,EAAY1E,EAAQ0E,UACpBc,EAAWxF,EAAQwF,SACnBC,EAAYzF,EAAQyF,UAChBI,GAAapB,GAAYC,GAAa1E,EAAQwE,MAGhDoB,EAAMd,EAAcC,GAFpBC,EAAYP,IACZQ,EAAaP,IAEH,GACRK,EAAgBL,EAAYI,EAAeL,EACvCzE,EAAQqG,MAAQE,WAAavG,EAAQwG,SAAWD,YAClD1B,GAAW/B,EAASiC,GAAgB,IAEvB,EAANa,IACTd,EAAeL,EAAWM,EAAgBL,EACtC1E,EAAQoG,OAASG,WAAavG,EAAQsG,QAAUC,YAClD3B,GAAWjC,EAAQmC,GAAe,MAIlC9E,EAAQyG,SAAWzG,EAAQ0G,SAC7BlB,EAAWf,EAAWA,GAAYe,EAClCC,EAAYf,EAAYA,GAAae,GAEnCzF,EAAQ0G,OACVR,IACAH,MAEAA,IACAG,MAGAL,EAAW,CAsCb,GAnCe,GAFfH,EAAa1F,EAAQ0F,eAKjB/D,EAAIgF,MAAMhE,OACVqD,KAAKY,MAAMC,WAAWlF,EAAIgF,MAAMhE,MAAO,OACrCqD,KAAKY,MAAMjE,EAAQ+C,MAGvBV,GAAaU,EACbT,GAAcS,GAKdpE,EAAUwF,qBACTnF,EAAIwD,aACJP,GAAWC,GAAWC,IAAgBnC,GAASoC,IAAiBjC,KAGjE8C,EAAMjE,EAENA,EAAML,EAAUuC,aAAalB,EAAOG,GAAQ,GAC5CxB,EAAUqD,UACRiB,EACAjE,EACA,EACA,EACAgB,EACAG,EACAH,EACAG,EACA9C,IAKkB,GAFtB2F,EAAoB3F,EAAQ2F,oBAG1BA,EAAoB,GACpBX,EAAYF,GACZG,EAAaF,EAEb,KAAyCC,EAAlCF,EAAca,GACnB3B,EAAS1C,EAAUuC,aACjBiB,EAAca,EACdZ,EAAeY,GACf,GAEFrE,EAAUqD,UACRhD,EACAqC,EACAY,EACAC,EACAC,EACAC,EACAf,EAAOrB,MACPqB,EAAOlB,OACP9C,GAGF6E,EADAD,EAAU,EAEVE,EAAcd,EAAOrB,MACrBoC,EAAef,EAAOlB,OAEtBnB,EAAMqC,EAqBV,OAlBAA,EAAS1C,EAAUuC,aAAamB,EAAWC,GAC3C3D,EAAU4C,qBAAqBF,EAAQhE,EAAS+B,GAC/B,EAAb2D,IACF1B,EAAO2C,MAAMhE,MAAQqB,EAAOrB,MAAQ+C,EAAa,MAEnDpE,EACGqD,UACChD,EACAqC,EACAY,EACAC,EACAC,EACAC,EACAC,EACAC,EACAjF,GAED+G,aAAa,EAAG,EAAG,EAAG,EAAG,EAAG,GACxB/C,EAIT,OAFArC,EAAIgB,MAAQqC,EACZrD,EAAImB,OAASmC,EACNtD,KCnTV,SAAW+B,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,gBAAiBK,GACE,iBAAXH,QAAuBA,OAAOC,QAC9CE,EAAQC,QAAQ,iBAGhBD,EAAQD,OAAOnC,WATlB,CAWE,SAAUA,gBAGX,IAAI8B,EAAS9B,EAAU8B,OACnBQ,EAAoBtC,EAAUyB,UAE9BiE,EACF5D,EAAO6D,OACNA,KAAK7F,UAAUnB,OACdgH,KAAK7F,UAAU8F,aACfD,KAAK7F,UAAU+F,UAEfC,EACDhE,EAAOiE,aAAeA,YAAYjG,UAAUnB,OAC7C,SAAUqH,EAAOC,GAGfA,EAAMA,GAAO5G,KAAK6G,WAAaF,EAC/B,IAAIG,EAAO,IAAIC,WAAW/G,KAAM2G,EAAOC,GACnCI,EAAO,IAAID,WAAWH,GAE1B,OADAI,EAAKC,IAAIH,GACFE,EAAKE,QAGZC,EAAkB,CACpBC,KAAM,CACJC,MAAQ,GACRC,MAAQ,KAmBZ,SAASC,EAAc9H,EAAMmB,EAAUvB,EAAS+B,GAC9C,IAAIoG,EAAOxH,KAQX,SAASa,EAASC,EAASC,GACzB,KAEI0B,EAAOgF,UACPpB,GACA5G,GACa,IAAbA,EAAKiI,MACS,eAAdjI,EAAKa,MAIP,OAAOQ,EAAQM,GAGjB,IAAIuG,EAAkBtI,EAAQsI,iBAAmB,OAE9ChH,EAAUnB,SACT6G,EAAUtG,KAAKN,EAAM,EAAGkI,GACxB,SAAUT,GAKR,IAAIU,EAAW,IAAIH,SAASP,GAE5B,GAA8B,QAA1BU,EAASC,UAAU,GACrB,OAAO9G,EACL,IAAIM,MAAM,4CAUd,IAPA,IAGIyG,EACAC,EACAC,EACAtE,EANAuE,EAAS,EACTC,EAAYN,EAASf,WAAa,EAClCsB,EAAaF,EAKVA,EAASC,IAMI,QALlBJ,EAAcF,EAASC,UAAUI,KAKLH,GAAe,OACzB,QAAhBA,IAPuB,CAcvB,GAAIG,GADJF,EAAeH,EAASC,UAAUI,EAAS,GAAK,GACpBL,EAASf,WAAY,CAE/CpF,QAAQC,IAAI,gDACZ,MAGF,IADAsG,EAAUb,EAAgBC,KAAKU,MACfzI,EAAQ+I,uBACtB,IAAK1E,EAAI,EAAGA,EAAIsE,EAAQK,OAAQ3E,GAAK,EACnCsE,EAAQtE,GAAG3D,KACTyH,EACAI,EACAK,EACAF,EACA3G,EACA/B,GAKN8I,EADAF,GAAUF,GAUT1I,EAAQiJ,kBAAiC,EAAbH,IAC/B/G,EAAKmH,UAAY9B,EAAY1G,KAAKmH,EAAQ,EAAGiB,IAE/CrH,EAAQM,IAEVL,EACA,sBAIFD,EAAQM,GAIZ,OADA/B,EAAUA,GAAW,GACjBoD,EAAOF,SAA+B,mBAAb3B,GAE3BQ,EADA/B,EAAUuB,GAAY,GAEf,IAAI2B,QAAQ1B,KAErBO,EAAOA,GAAQ,GACRP,EAASD,EAAUA,IAW5B,SAAS4H,EAAgBvJ,EAAMwJ,EAASC,GACtC,OAAKzJ,GAASwJ,GAAYC,EACnB,IAAIpC,KAAK,CAACoC,EAASrC,EAAUtG,KAAKd,EAAMwJ,EAAQ5B,aAAc,CACnEvG,KAAM,eAFkC,KA+B5CK,EAAUyB,UAAY,SAAUpB,EAAK3B,EAASuB,EAAUnB,EAAM2B,GACxDT,EAAU0B,iBAAiBhD,GAE7BkI,EACE9H,EACA,SAAUQ,GACJA,IAAWmB,IAETqB,EAAOhB,SAASA,QAAQC,IAAIzB,GAChCA,EAASmB,GAEX6B,EAAkBlD,KAChBY,EACAK,EACA3B,EACAuB,EACAnB,EACAQ,IAGJZ,EAlBF+B,EAAOA,GAAQ,IAsBf6B,EAAkB0F,MAAMhI,EAAWiI,YAIvCjI,EAAU0F,UAAYA,EACtB1F,EAAU8F,YAAcA,EACxB9F,EAAUkI,YA9CV,SAAqB5J,EAAM6J,EAAMlI,GAC/B,IAAIvB,EAAU,CAAEsI,gBAAiB,IAAKS,wBAAwB,GAC9D,IAAKxH,GAAY6B,EAAOF,QACtB,OAAOgF,EAActI,EAAMI,GAAS0J,KAAK,SAAU3H,GACjD,OAAOoH,EAAgBvJ,EAAMmC,EAAKmH,UAAWO,KAGjDvB,EACEtI,EACA,SAAUmC,GACRR,EAAS4H,EAAgBvJ,EAAMmC,EAAKmH,UAAWO,KAEjDzJ,IAmCJsB,EAAU4G,cAAgBA,EAC1B5G,EAAUwG,gBAAkBA,ICpP7B,SAAWpE,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,gBAAiBK,GACE,iBAAXH,QAAuBA,OAAOC,QAC9CE,EAAQC,QAAQ,iBAGhBD,EAAQD,OAAOnC,WATlB,CAWE,SAAUA,gBAGX,IAAI8B,EAAS9B,EAAU8B,OAGrBA,EAAOuG,OACPvG,EAAOwG,SACPxG,EAAOyG,UACPzG,EAAOyG,SAASzI,UAAUxB,KAE1B0B,EAAU2B,UAAY,SAAUnD,EAAKyB,EAAUvB,GAO7C,SAAS8J,EAAgBC,GACvB,OAAOA,EAASnK,OAElB,GAAIwD,EAAOF,SAA+B,mBAAb3B,EAC3B,OAAOoI,MAAM,IAAIC,QAAQ9J,EAAKyB,IAAWmI,KAAKI,GAEhDH,MAAM,IAAIC,QAAQ9J,EAAKE,IACpB0J,KAAKI,GACLJ,KAAKnI,GAKN,SAAE,SAAUY,GACVZ,EAAS,KAAMY,MAIrBiB,EAAO4G,gBAE+B,MAAtC,IAAIA,gBAAiBC,eAErB3I,EAAU2B,UAAY,SAAUnD,EAAKyB,EAAUvB,GAO7C,SAASwB,EAASC,EAASC,GACzB1B,EAAUA,GAAW,GACrB,IAAIkK,EAAM,IAAIF,eACdE,EAAIC,KAAKnK,EAAQO,QAAU,MAAOT,GAC9BE,EAAQoK,SACVjJ,OAAOkJ,KAAKrK,EAAQoK,SAASE,QAAQ,SAAUC,GAC7CL,EAAIM,iBAAiBD,EAAKvK,EAAQoK,QAAQG,MAG9CL,EAAIO,gBAA0C,YAAxBzK,EAAQ0K,YAC9BR,EAAID,aAAe,OACnBC,EAAI7J,OAAS,WACXoB,EAAQyI,EAAIH,WAEdG,EAAI5J,QAAU4J,EAAIrJ,QAAUqJ,EAAIS,UAAY,SAAUxI,GAChDV,IAAYC,EAEdA,EAAO,KAAMS,GAEbT,EAAOS,IAGX+H,EAAIU,KAAK5K,EAAQ6K,MAEnB,OAAIzH,EAAOF,SAA+B,mBAAb3B,GAC3BvB,EAAUuB,EACH,IAAI2B,QAAQ1B,IAEdA,EAASD,EAAUA,OCzD/B,SAAWmC,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,eAAgB,qBAAsB,qBAAsBK,GACzC,iBAAXH,QAAuBA,OAAOC,QAC9CE,EACEC,QAAQ,gBACRA,QAAQ,sBACRA,QAAQ,sBAIVD,EAAQD,OAAOnC,WAblB,CAeE,SAAUA,gBAGX,IAMY/B,EAiBNoC,EAvBFiC,EAAoBtC,EAAUyB,UAC9B+H,EAAyBxJ,EAAUiE,eACnCwF,EAA2BzJ,EAAU0B,iBACrCgI,EAA+B1J,EAAU4C,qBACzC+G,EAAgC3J,EAAU6C,sBAgD9C,SAAS+G,EAA0BlL,EAASmL,GAC1C,IAAIC,EAAcpL,GAAWA,EAAQoL,YACrC,OAEmB,IAAhBA,IAAyB9J,EAAU8J,aAEnB,IAAhBA,GAAqB9J,EAAU8J,eAE7BD,GAAgB7J,EAAU8J,cACb,EAAdA,GACAA,EAAc,EAWpB,SAASC,EAA0BD,EAAaE,GAC9C,OACEF,IAAgBE,IACE,IAAhBF,GAAuC,EAAlBE,GAAuBA,EAAkB,GAC/C,EAAdF,GAAmBA,EAAc,GAsBxC,SAASG,EAAeH,EAAaE,GACnC,GAAsB,EAAlBA,GAAuBA,EAAkB,EAC3C,OAAQF,GACN,KAAK,EACL,KAAK,EACH,OAAyB,EAAlBE,EACT,KAAK,EACL,KAAK,EACH,OAAOA,EAAkB,GAAM,EACjC,KAAK,EACL,KAAK,EACH,OACsB,IAApBA,GACoB,IAApBA,GACoB,IAApBA,GACoB,IAApBA,IA5GE/L,EAqCT+B,GAnCM8B,OAAOxB,YAeVD,EAAMC,SAASC,cAAc,QAC7BxB,OAAS,WAGX,IAEM6E,EAHN3F,EAAE6L,YAA4B,IAAdzJ,EAAIgB,OAA8B,IAAfhB,EAAImB,OACnCvD,EAAE6L,eAEAlG,EADS3F,EAAEsE,aAAa,EAAG,GAAG,GACjBsB,WAAW,OACxBR,UAAUhD,EAAK,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,EAAG,GAQxCpC,EAAEuH,mBACiD,oBAAjD5B,EAAIsG,aAAa,EAAG,EAAG,EAAG,GAAGzJ,KAAKV,aAGxCM,EAAIY,IA3BF,mfA2GJjB,EAAUiE,eAAiB,SAAUvF,GACnC,OACEkL,EAA0BlL,IAC1B8K,EAAuBpK,KAAKY,EAAWtB,IAK3CsB,EAAU0B,iBAAmB,SAAUhD,GACrC,OACEkL,EAA0BlL,GAAS,IACnC+K,EAAyBrK,KAAKY,EAAWtB,IAI7CsB,EAAUyB,UAAY,SAAUpB,EAAK3B,EAASuB,EAAUnB,EAAM2B,GAC5D6B,EAAkBlD,KAChBY,EACAK,EACA3B,EACA,SAAU2B,EAAKI,GACb,IACMuJ,EAIE7I,EACAG,GANJb,GAGoB,GAFlBuJ,EACFhK,EAAU8J,aAAerJ,EAAK0J,MAAQ1J,EAAK0J,KAAKC,IAAI,iBAC3BJ,EAAkB,IAEvC7I,EAAgBV,EAAKU,cACrBG,EAAiBb,EAAKa,eAC1Bb,EAAKU,cAAgBG,EACrBb,EAAKa,eAAiBH,GAG1BlB,EAASI,EAAKI,IAEhB3B,EACA2B,IAMJT,EAAU6C,sBAAwB,SAAUxC,EAAKgK,EAAM5J,GACrD,IAAI/B,EAAUiL,EAA8BvK,KAAKY,EAAWK,EAAKgK,GAC7DC,EAAkB7J,EAAK0J,MAAQ1J,EAAK0J,KAAKC,IAAI,eAC7CN,EAAcpL,EAAQoL,YACtBE,EAAkBhK,EAAU8J,aAAeQ,EAE/C,IADoB,IAAhBR,IAAsBA,EAAcQ,IACnCP,EAA0BD,EAAaE,GAC1C,OAAOtL,EAET,IA2EQ6L,EACAC,EA5EJzF,EAAMrG,EAAQqG,IACdC,EAAQtG,EAAQsG,MAChBE,EAASxG,EAAQwG,OACjBJ,EAAOpG,EAAQoG,KACfhC,EAAa,GACjB,IAAK,IAAIC,KAAKrE,EACRmB,OAAOC,UAAUmD,eAAe7D,KAAKV,EAASqE,KAChDD,EAAWC,GAAKrE,EAAQqE,IAgB5B,IAXiB,GAFjBD,EAAWgH,YAAcA,MAEiB,EAAlBE,IACrBF,EAAc,GAAuB,EAAlBE,KAGpBlH,EAAWK,SAAWzE,EAAQ0E,UAC9BN,EAAWM,UAAY1E,EAAQyE,SAC/BL,EAAWoB,SAAWxF,EAAQyF,UAC9BrB,EAAWqB,UAAYzF,EAAQwF,SAC/BpB,EAAWU,YAAc9E,EAAQ+E,aACjCX,EAAWW,aAAe/E,EAAQ8E,aAEd,EAAlBwG,EAAqB,CAGvB,OAAQA,GACN,KAAK,EAEHhF,EAAQtG,EAAQoG,KAChBA,EAAOpG,EAAQsG,MACf,MACF,KAAK,EAEHD,EAAMrG,EAAQwG,OACdF,EAAQtG,EAAQoG,KAChBI,EAASxG,EAAQqG,IACjBD,EAAOpG,EAAQsG,MACf,MACF,KAAK,EAEHD,EAAMrG,EAAQwG,OACdA,EAASxG,EAAQqG,IACjB,MACF,KAAK,EAEHA,EAAMrG,EAAQoG,KACdE,EAAQtG,EAAQwG,OAChBA,EAASxG,EAAQsG,MACjBF,EAAOpG,EAAQqG,IACf,MACF,KAAK,EAEHA,EAAMrG,EAAQoG,KACdE,EAAQtG,EAAQqG,IAChBG,EAASxG,EAAQsG,MACjBF,EAAOpG,EAAQwG,OACf,MACF,KAAK,EAEHH,EAAMrG,EAAQsG,MACdA,EAAQtG,EAAQqG,IAChBG,EAASxG,EAAQoG,KACjBA,EAAOpG,EAAQwG,OACf,MACF,KAAK,EAEHH,EAAMrG,EAAQsG,MACdA,EAAQtG,EAAQwG,OAChBA,EAASxG,EAAQoG,KACjBA,EAAOpG,EAAQqG,IAIfkF,EAAeH,EAAaE,KAC1BO,EAASxF,EACTyF,EAAWxF,EACfD,EAAMG,EACNF,EAAQF,EACRI,EAASqF,EACTzF,EAAO0F,GAQX,OALA1H,EAAWiC,IAAMA,EACjBjC,EAAWkC,MAAQA,EACnBlC,EAAWoC,OAASA,EACpBpC,EAAWgC,KAAOA,EAEVgF,GACN,KAAK,EAEHhH,EAAWkC,MAAQF,EACnBhC,EAAWgC,KAAOE,EAClB,MACF,KAAK,EAEHlC,EAAWiC,IAAMG,EACjBpC,EAAWkC,MAAQF,EACnBhC,EAAWoC,OAASH,EACpBjC,EAAWgC,KAAOE,EAClB,MACF,KAAK,EAEHlC,EAAWiC,IAAMG,EACjBpC,EAAWoC,OAASH,EACpB,MACF,KAAK,EAEHjC,EAAWiC,IAAMD,EACjBhC,EAAWkC,MAAQE,EACnBpC,EAAWoC,OAASF,EACpBlC,EAAWgC,KAAOC,EAClB,MACF,KAAK,EAEHjC,EAAWiC,IAAMC,EACjBlC,EAAWkC,MAAQE,EACnBpC,EAAWoC,OAASJ,EACpBhC,EAAWgC,KAAOC,EAClB,MACF,KAAK,EAEHjC,EAAWiC,IAAMC,EACjBlC,EAAWkC,MAAQD,EACnBjC,EAAWoC,OAASJ,EACpBhC,EAAWgC,KAAOI,EAClB,MACF,KAAK,EAEHpC,EAAWiC,IAAMD,EACjBhC,EAAWkC,MAAQD,EACnBjC,EAAWoC,OAASF,EACpBlC,EAAWgC,KAAOI,EAGtB,OAAOpC,GAIT9C,EAAU4C,qBAAuB,SAAUF,EAAQhE,EAAS+B,GAC1DiJ,EAA6BtK,KAAKY,EAAW0C,EAAQhE,EAAS+B,GAC9D,IAAIqJ,EAAcpL,EAAQoL,YACtBE,EACFhK,EAAU8J,aAAerJ,EAAK0J,MAAQ1J,EAAK0J,KAAKC,IAAI,eACtD,GAAKL,EAA0BD,EAAaE,GAA5C,CAGA,IAAIpG,EAAMlB,EAAOmB,WAAW,MACxBxC,EAAQqB,EAAOrB,MACfG,EAASkB,EAAOlB,OAChBgC,EAAcnC,EACdoC,EAAejC,EAenB,QAbiB,EAAdsI,KAAuC,EAAlBE,IACrBF,EAAc,GAAuB,EAAlBE,KAGpBtH,EAAOrB,MAAQG,EACfkB,EAAOlB,OAASH,GAEA,EAAdyI,IAEFtG,EAAchC,EACdiC,EAAepC,GAGT2I,GACN,KAAK,EAEHpG,EAAI6G,UAAUjH,EAAa,GAC3BI,EAAIjB,OAAO,EAAG,GACd,MACF,KAAK,EAEHiB,EAAI6G,UAAUjH,EAAaC,GAC3BG,EAAI8G,OAAOhG,KAAKiG,IAChB,MACF,KAAK,EAEH/G,EAAI6G,UAAU,EAAGhH,GACjBG,EAAIjB,MAAM,GAAI,GACd,MACF,KAAK,EAEHiB,EAAI8G,QAAQ,GAAMhG,KAAKiG,IACvB/G,EAAIjB,OAAO,EAAG,GACd,MACF,KAAK,EAEHiB,EAAI8G,QAAQ,GAAMhG,KAAKiG,IACvB/G,EAAI6G,WAAWjH,EAAa,GAC5B,MACF,KAAK,EAEHI,EAAI8G,QAAQ,GAAMhG,KAAKiG,IACvB/G,EAAI6G,WAAWjH,EAAaC,GAC5BG,EAAIjB,MAAM,GAAI,GACd,MACF,KAAK,EAEHiB,EAAI8G,OAAO,GAAMhG,KAAKiG,IACtB/G,EAAI6G,UAAU,GAAIhH,GAQtB,OAJIwG,EAAeH,EAAaE,KAC9BpG,EAAI6G,UAAUjH,EAAaC,GAC3BG,EAAI8G,OAAOhG,KAAKiG,KAEVb,GACN,KAAK,EAEHlG,EAAI6G,UAAUpJ,EAAO,GACrBuC,EAAIjB,OAAO,EAAG,GACd,MACF,KAAK,EAEHiB,EAAI6G,UAAUpJ,EAAOG,GACrBoC,EAAI8G,OAAOhG,KAAKiG,IAChB,MACF,KAAK,EAEH/G,EAAI6G,UAAU,EAAGjJ,GACjBoC,EAAIjB,MAAM,GAAI,GACd,MACF,KAAK,EAEHiB,EAAI8G,OAAO,GAAMhG,KAAKiG,IACtB/G,EAAIjB,MAAM,GAAI,GACd,MACF,KAAK,EAEHiB,EAAI8G,OAAO,GAAMhG,KAAKiG,IACtB/G,EAAI6G,UAAU,GAAIjJ,GAClB,MACF,KAAK,EAEHoC,EAAI8G,OAAO,GAAMhG,KAAKiG,IACtB/G,EAAI6G,UAAUpJ,GAAQG,GACtBoC,EAAIjB,OAAO,EAAG,GACd,MACF,KAAK,EAEHiB,EAAI8G,QAAQ,GAAMhG,KAAKiG,IACvB/G,EAAI6G,WAAWpJ,EAAO,QC7c7B,SAAWe,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,eAAgB,qBAAsBK,GACnB,iBAAXH,QAAuBA,OAAOC,QAC9CE,EAAQC,QAAQ,gBAAiBA,QAAQ,sBAGzCD,EAAQD,OAAOnC,WATlB,CAWE,SAAUA,gBAUX,SAAS4K,EAAQC,GACXA,IACFhL,OAAOiL,eAAezL,KAAM,MAAO,CACjC0L,MAAO1L,KAAK2L,KAAKH,GAASI,MAE5BpL,OAAOiL,eAAezL,KAAM,OAAQ,CAClC0L,MAAQ1L,KAAK6L,MAAQ7L,KAAK6L,KAAKL,IAAa,MAclDD,EAAQ9K,UAAUkL,KAAO,CACvBG,KAAM,CAAEC,KAAM,YAAaH,IAV7BL,EAAQ9K,UAAUmL,IAAM,CACtBI,YAAa,IACbC,UAAW,OACX3F,KAAM,IACN4F,KAAM,MACNC,QAAS,MACTC,iBAAkB,QAKlBC,MAAQ,CAAEN,KAAM,OAAQH,IAAK,IAC7BU,MAAQ,CAAEP,KAAM,UAAWH,IAAK,IAChCW,MAAQ,CAAER,KAAM,mBAAoBH,IAAK,KAS3CL,EAAQ9K,UAAUsK,IAAM,SAAUyB,GAChC,OAAOxM,KAAKwM,IAAOxM,KAAKA,KAAK4L,IAAIY,KAyBnC,IAAIC,EAAe,CAEjBC,EAAG,CACDC,SAAU,SAAU/E,EAAUgF,GAC5B,OAAOhF,EAASiF,SAASD,IAE3BlF,KAAM,GAGRoF,EAAG,CACDH,SAAU,SAAU/E,EAAUgF,GAC5B,OAAOG,OAAOC,aAAapF,EAASiF,SAASD,KAE/ClF,KAAM,EACNuF,OAAO,GAGTC,EAAG,CACDP,SAAU,SAAU/E,EAAUgF,EAAYO,GACxC,OAAOvF,EAASC,UAAU+E,EAAYO,IAExCzF,KAAM,GAGR0F,EAAG,CACDT,SAAU,SAAU/E,EAAUgF,EAAYO,GACxC,OAAOvF,EAASyF,UAAUT,EAAYO,IAExCzF,KAAM,GAGR4F,EAAG,CACDX,SAAU,SAAU/E,EAAUgF,EAAYO,GACxC,OACEvF,EAASyF,UAAUT,EAAYO,GAC/BvF,EAASyF,UAAUT,EAAa,EAAGO,IAGvCzF,KAAM,GAGR6F,EAAG,CACDZ,SAAU,SAAU/E,EAAUgF,EAAYO,GACxC,OAAOvF,EAAS4F,SAASZ,EAAYO,IAEvCzF,KAAM,GAGR+F,GAAI,CACFd,SAAU,SAAU/E,EAAUgF,EAAYO,GACxC,OACEvF,EAAS4F,SAASZ,EAAYO,GAC9BvF,EAAS4F,SAASZ,EAAa,EAAGO,IAGtCzF,KAAM,IAkFV,SAASgG,EAAiBC,EAAaC,EAAapC,GAClD,QACImC,GAAeA,EAAYnC,OAC3BoC,IAAwC,IAAzBA,EAAYpC,IAiBjC,SAASqC,EACPjG,EACAkG,EACAC,EACAZ,EACAtB,EACAmC,EACAL,EACAC,GAEA,IAAIK,EAAYC,EAAcxK,EAAGyK,EAAWC,EAAWC,EACvD,GAAIN,EAAY,EAAInG,EAASf,WAC3BpF,QAAQC,IAAI,oDADd,CAMA,MADAwM,EAAeH,EAAY,EAAI,IAD/BE,EAAarG,EAASC,UAAUkG,EAAWZ,KAExB,EAAIvF,EAASf,YAAhC,CAIA,IAAKnD,EAAI,EAAGA,EAAIuK,EAAYvK,GAAK,EAC/ByK,EAAYJ,EAAY,EAAI,GAAKrK,EAE5BgK,EAAiBC,EAAaC,EADnCQ,EAAYxG,EAASC,UAAUsG,EAAWhB,MAE1CkB,EA9GJ,SACEzG,EACAkG,EACA7F,EACA3H,EACA+H,EACA8E,GAEA,IACImB,EACA1B,EACA2B,EACA7K,EACA8K,EACAC,EANAC,EAAUjC,EAAanM,GAO3B,GAAKoO,EAAL,CAWA,MAJA9B,EACY,GAJZ0B,EAAUI,EAAQhH,KAAOW,GAKnByF,EAAalG,EAASyF,UAAUpF,EAAS,EAAGkF,GAC5ClF,EAAS,GACEqG,EAAU1G,EAASf,YAApC,CAIA,GAAe,IAAXwB,EACF,OAAOqG,EAAQ/B,SAAS/E,EAAUgF,EAAYO,GAGhD,IADAoB,EAAS,GACJ7K,EAAI,EAAGA,EAAI2E,EAAQ3E,GAAK,EAC3B6K,EAAO7K,GAAKgL,EAAQ/B,SAClB/E,EACAgF,EAAalJ,EAAIgL,EAAQhH,KACzByF,GAGJ,GAAIuB,EAAQzB,MAAO,CAGjB,IAFAuB,EAAM,GAED9K,EAAI,EAAGA,EAAI6K,EAAOlG,QAGX,QAFVoG,EAAIF,EAAO7K,IADkBA,GAAK,EAMlC8K,GAAOC,EAET,OAAOD,EAET,OAAOD,EA3BL9M,QAAQC,IAAI,gDAXZD,QAAQC,IAAI,wCA8FDiN,CACT/G,EACAkG,EACAK,EACAvG,EAASC,UAAUsG,EAAY,EAAGhB,GAClCvF,EAASyF,UAAUc,EAAY,EAAGhB,GAClCA,GAEFtB,EAAKuC,GAAaC,EACdL,IACFA,EAAWI,GAAaD,IAI5B,OAAOvG,EAASyF,UAAUa,EAAcf,GArBtC1L,QAAQC,IAAI,+CApHhB+K,EAAa,GAAKA,EAAa,GAmL/B9L,EAAUiO,cAAgB,SAAUhH,EAAUK,EAAQI,EAAQjH,EAAM/B,GAClE,IAAIA,EAAQwP,YAAZ,CAGA,IAQI1B,EACAY,EACAe,EAVAnB,EAActO,EAAQ0P,gBACtBnB,EAAcvO,EAAQ2P,iBAAmB,CAC3C3C,MAAQ,CAEN4C,OAAQ,IAGRnB,EAAa7F,EAAS,GAK1B,GAAuC,aAAnCL,EAASyF,UAAUpF,EAAS,GAIhC,GAAI6F,EAAa,EAAIlG,EAASf,WAC5BpF,QAAQC,IAAI,iDAId,GAAuC,IAAnCkG,EAASC,UAAUI,EAAS,GAAhC,CAKA,OAAQL,EAASC,UAAUiG,IACzB,KAAK,MACHX,GAAe,EACf,MACF,KAAK,MACHA,GAAe,EACf,MACF,QAEE,YADA1L,QAAQC,IAAI,qDAIyC,KAArDkG,EAASC,UAAUiG,EAAa,EAAGX,IAKvCY,EAAYnG,EAASyF,UAAUS,EAAa,EAAGX,GAE/C/L,EAAK0J,KAAO,IAAIS,EACXlM,EAAQ6P,qBACX9N,EAAK+N,YAAc,IAAI5D,EACvBnK,EAAKgO,eAAiBtB,EACtB1M,EAAKiO,iBAAmBlC,IAI1BY,EAAYF,EACVjG,EACAkG,EACAA,EAAaC,EACbZ,EACA/L,EAAK0J,KACL1J,EAAK+N,YACLxB,EACAC,KAEeF,EAAiBC,EAAaC,EAAa,UAC1DxM,EAAK0J,KAAKgB,KAAOiC,EACb3M,EAAK+N,cACP/N,EAAK+N,YAAYrD,KAAOgC,EAAaC,IAGzCvN,OAAOkJ,KAAKtI,EAAK0J,KAAKa,MAAMhC,QAAQ,SAAU6B,GArGhD,IACEpK,EACAoK,EACA5D,EACAkG,EACAX,EACAQ,EACAC,EAEIG,EAPJvC,EAsGIA,EArGJ5D,EAsGIA,EArGJkG,EAsGIA,EArGJX,EAsGIA,EArGJQ,EAsGIA,EArGJC,EAsGIA,GApGAG,GARJ3M,EAsGIA,GA9FiB0J,KAAKU,MAExBpK,EAAK0J,KAAKU,GAAW,IAAID,EAAQC,GAC7BpK,EAAK+N,cACP/N,EAAK+N,YAAY3D,GAAW,IAAID,EAAQC,IAE1CqC,EACEjG,EACAkG,EACAA,EAAaC,EACbZ,EACA/L,EAAK0J,KAAKU,GACVpK,EAAK+N,aAAe/N,EAAK+N,YAAY3D,GACrCmC,GAAeA,EAAYnC,GAC3BoC,GAAeA,EAAYpC,QAyF/BsD,EAAe1N,EAAK0J,KAAKgB,OAELgD,EAAa,OAC/BA,EAAa,KAnVjB,SAA0BlH,EAAUK,EAAQI,GAC1C,GAAKA,EAAL,CACA,KAAIJ,EAASI,EAAST,EAASf,YAI/B,OAAO,IAAIP,KACT,CAAC3F,EAAU8F,YAAY1G,KAAK6H,EAASV,OAAQe,EAAQA,EAASI,IAC9D,CACE/H,KAAM,eANRmB,QAAQC,IAAI,+CAgVW4N,CACrB1H,EACAkG,EAAagB,EAAa,KAC1BA,EAAa,QA/CfrN,QAAQC,IAAI,gDAjBZD,QAAQC,IAAI,uDAsEhBf,EAAUwG,gBAAgBC,KAAK,OAAQmI,KAAK5O,EAAUiO,eAEtDjO,EAAU6O,YAAc,CAEtBC,IAAQ,SAAUvI,EAAQ9F,EAAMsK,GAC9B,IAAIgE,EAAoBtO,EAAK+N,YAAY,KACzC,OAAKO,GACM,IAAIjI,SAASP,EAAQwI,EAAoB,EAAG,GAClDC,UAAU,EAAGjE,EAAOtK,EAAKiO,kBACvBnI,IAIXvG,EAAUiP,cAAgB,SAAU1I,EAAQ9F,EAAMoL,EAAId,GACpD/K,EAAU6O,YAAYpO,EAAK0J,KAAKc,IAAIY,IAAKtF,EAAQ9F,EAAMsK,IAGzD/K,EAAU4K,QAAUA,IC9arB,SAAWxI,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,eAAgB,qBAAsBK,GACnB,iBAAXH,QAAuBA,OAAOC,QAC9CE,EAAQC,QAAQ,gBAAiBA,QAAQ,sBAGzCD,EAAQD,OAAOnC,WATlB,CAWE,SAAUA,gBAGX,IAAIkP,EAAelP,EAAU4K,QAAQ9K,UAErCoP,EAAahE,KAAO,CAIlBiE,IAAQ,aACRC,IAAQ,cACRC,IAAQ,gBACRC,IAAQ,cACRC,IAAQ,4BACRT,IAAQ,cACRU,IAAQ,kBACRC,IAAQ,sBACRC,IAAQ,mBACRC,IAAQ,mBACRC,IAAQ,cACRC,IAAQ,cACRC,IAAQ,iBACRC,IAAQ,eACRC,IAAQ,eACRC,IAAQ,kBACRC,IAAQ,wBACRC,IAAQ,8BACRC,IAAQ,mBACRC,IAAQ,aACRC,IAAQ,wBACRC,IAAQ,oBACRC,IAAQ,sBACRC,IAAQ,WACRC,IAAQ,mBACRC,IAAQ,OACRC,IAAQ,QACRC,IAAQ,WACRC,IAAQ,SACRC,MAAQ,YACRrF,MAAQ,CAENsF,MAAQ,cACRC,MAAQ,kBACRC,MAAQ,aACRC,MAAQ,kBACRC,MAAQ,kBACRC,MAAQ,QACRC,MAAQ,0BACRC,MAAQ,yBACRjD,MAAQ,YACRkD,MAAQ,cACRC,MAAQ,mBACRC,MAAQ,mBACRC,MAAQ,oBACRC,MAAQ,aACRC,MAAQ,qBACRC,MAAQ,sBACRC,MAAQ,aACRC,MAAQ,qBACRC,MAAQ,sBACRC,MAAQ,eACRC,MAAQ,UACRC,MAAQ,kBACRC,MAAQ,sBACRC,MAAQ,0BACRC,MAAQ,OACRC,MAAQ,kBACRC,MAAQ,4BACRC,MAAQ,2BACRC,MAAQ,WACRC,MAAQ,sBACRC,MAAQ,sBACRC,MAAQ,oBACRC,MAAQ,gBACRC,MAAQ,kBACRC,MAAQ,eACRC,MAAQ,mBACRC,MAAQ,kBACRC,MAAQ,eACRC,MAAQ,cACRC,MAAQ,QACRC,MAAQ,cACRC,MAAQ,cACRC,MAAQ,cACRC,MAAQ,2BACRC,MAAQ,wBACRC,MAAQ,wBACRC,MAAQ,2BACRC,MAAQ,kBACRC,MAAQ,gBACRC,MAAQ,gBACRC,MAAQ,aACRC,MAAQ,YACRC,MAAQ,aACRC,MAAQ,iBACRC,MAAQ,eACRC,MAAQ,eACRC,MAAQ,mBACRC,MAAQ,wBACRC,MAAQ,mBACRC,MAAQ,cACRC,MAAQ,WACRC,MAAQ,aACRC,MAAQ,YACRC,MAAQ,2BACRC,MAAQ,uBACRC,MAAQ,gBACRC,MAAQ,kBACRC,MAAQ,mBACRC,MAAQ,oBACRC,MAAQ,WACRC,MAAQ,YACRC,MAAQ,oBAEV3J,MAAQ,CAEN4J,EAAQ,eACRxJ,EAAQ,iBACRI,EAAQ,cACRI,EAAQ,kBACRE,EAAQ,eACRE,EAAQ,iBACR6I,EAAQ,cACRC,EAAQ,eACRC,EAAQ,gBACR9I,EAAQ,YACRE,GAAQ,iBACR6I,GAAQ,SACRC,GAAQ,cACRC,GAAQ,WACRC,GAAQ,cACRC,GAAQ,WACRC,GAAQ,qBACRC,GAAQ,kBACRC,GAAQ,cACRC,GAAQ,qBACRC,GAAQ,kBACRC,GAAQ,sBACRC,GAAQ,mBACRC,GAAQ,oBACRC,GAAQ,iBACRC,GAAQ,qBACRC,GAAQ,kBACRC,GAAQ,sBACRC,GAAQ,qBACRC,GAAQ,eACRC,GAAQ,kBACRC,GAAQ,wBAEVnL,MAAQ,CAENG,EAAQ,0BAKZmD,EAAahE,KAAKC,KAAO+D,EAAahE,KAEtCgE,EAAa8H,aAAe,CAC1BC,gBAAiB,CACf1B,EAAG,YACHxJ,EAAG,SACHI,EAAG,iBACHI,EAAG,oBACHE,EAAG,mBACHE,EAAG,mBACH6I,EAAG,iBACHC,EAAG,gBACHC,EAAG,kBAELwB,aAAc,CACZ3B,EAAG,UACHxJ,EAAG,UACHI,EAAG,wBACHI,EAAG,OACHE,EAAG,YACHE,EAAG,UACH6I,EAAG,UACH2B,IAAK,SAEPC,YAAa,CACX7B,EAAG,UACHxJ,EAAG,WACHI,EAAG,cACHI,EAAG,gCACHE,EAAG,QACHG,EAAG,eACHE,GAAI,iBACJ6I,GAAI,QACJC,GAAI,wCACJC,GAAI,yCACJC,GAAI,0CACJC,GAAI,sCACJE,GAAI,mBACJC,GAAI,mBACJC,GAAI,mBACJC,GAAI,MACJC,GAAI,MACJC,GAAI,MACJC,GAAI,MACJC,GAAI,sBACJW,IAAK,SAEPE,MAAO,CACL9B,EAAQ,qBACRxJ,EAAQ,cACRY,EAAQ,mCACR8I,EAAQ,+BACR7I,EAAQ,qCACRiJ,GAAQ,gEACRE,GAAQ,4DACRC,GAAQ,4CACRQ,GAAQ,gCACRC,GAAQ,yBACRI,GAAQ,oDACRE,GAAQ,gDACRO,GAAQ,oBACRC,GAAQ,sCACRC,GAAQ,iEACRC,GAAQ,6DACRC,GAAQ,6DACRC,GAAQ,wFACRC,GAAQ,oFACRC,GAAQ,iDACRC,GAAQ,4EACRC,GAAQ,yEAEVC,cAAe,CACbjM,EAAG,YACHI,EAAG,6BACHI,EAAG,6BACHE,EAAG,+BACHE,EAAG,+BACH8I,EAAG,mBACHC,EAAG,kCAELuC,iBAAkB,CAChB1C,EAAG,WACHxJ,EAAG,YACHI,EAAG,WACHI,EAAG,eAEL2L,UAAW,CACTnM,EAAG,yBAELoM,eAAgB,CACd5C,EAAG,iBACHxJ,EAAG,kBAELqM,aAAc,CACZ7C,EAAG,qBACHxJ,EAAG,wBAELsM,YAAa,CACX9C,EAAG,OACHxJ,EAAG,cACHI,EAAG,eACHI,EAAG,gBACHE,EAAG,kBAEL6L,SAAU,CACR/C,EAAG,SACHxJ,EAAG,OACHI,EAAG,QAELoM,WAAY,CACVhD,EAAG,SACHxJ,EAAG,iBACHI,EAAG,mBAELqM,UAAW,CACTjD,EAAG,SACHxJ,EAAG,OACHI,EAAG,QAELsM,qBAAsB,CACpBlD,EAAG,UACHxJ,EAAG,QACHI,EAAG,aACHI,EAAG,gBAELmM,WAAY,CACVnM,EAAG,OAELoM,wBAAyB,CACvBpD,EAAG,GACHxJ,EAAG,IACHI,EAAG,KACHI,EAAG,KACHE,EAAG,IACHE,EAAG,IACH6I,EAAG,KAELnK,YAAa,CACXU,EAAG,WACHI,EAAG,kBACHI,EAAG,kBACHE,EAAG,gBACHE,EAAG,gCACH6I,EAAG,gBACHC,EAAG,kCACHC,EAAG,mBAIPxG,EAAa0J,QAAU,SAAUxN,GAC/B,IAAIL,EAAQ1L,KAAK+K,IAAIgB,GACrB,OAAQA,GACN,IAAK,cACL,IAAK,QACL,IAAK,eACL,IAAK,kBACL,IAAK,gBACL,IAAK,mBACL,IAAK,YACL,IAAK,iBACL,IAAK,eACL,IAAK,cACL,IAAK,WACL,IAAK,aACL,IAAK,YACL,IAAK,uBACL,IAAK,aACL,IAAK,cACH,OAAO/L,KAAK2X,aAAa5L,GAAML,GACjC,IAAK,cACL,IAAK,kBACH,IAAKA,EAAO,OACZ,OAAOqB,OAAOC,aAAatB,EAAM,GAAIA,EAAM,GAAIA,EAAM,GAAIA,EAAM,IACjE,IAAK,0BACH,IAAKA,EAAO,OACZ,OACE1L,KAAK2X,aAAa5L,GAAML,EAAM,IAC9B1L,KAAK2X,aAAa5L,GAAML,EAAM,IAC9B1L,KAAK2X,aAAa5L,GAAML,EAAM,IAC9B1L,KAAK2X,aAAa5L,GAAML,EAAM,IAElC,IAAK,eACH,IAAKA,EAAO,OACZ,OAAOA,EAAM,GAAK,IAAMA,EAAM,GAAK,IAAMA,EAAM,GAAK,IAAMA,EAAM,GAEpE,OAAOqB,OAAOrB,IAGhBmE,EAAa2J,OAAS,WACpB,IACIC,EACAlZ,EACAwL,EAHAH,EAAM,GAIV,IAAK6N,KAAQzZ,KACPQ,OAAOC,UAAUmD,eAAe7D,KAAKC,KAAMyZ,MAC7ClZ,EAAMP,KAAKyZ,KACAlZ,EAAIiZ,OACb5N,EAAI5L,KAAK2L,KAAK8N,GAAM1N,MAAQxL,EAAIiZ,UAEhCzN,EAAO/L,KAAK6L,KAAK4N,MACP7N,EAAIG,GAAQ/L,KAAKuZ,QAAQxN,KAIzC,OAAOH,GAGTiE,EAAa6J,QAAU,SAAUlO,GAC/B,IAAIO,EAAO/L,KAAK6L,KAAKL,GACrB,MAAoB,iBAATO,EAA0B/L,KAAK2L,KAAKH,GAASO,KACjDA,GAIR,WACC,IACI0N,EACAE,EACAC,EAHA/N,EAAOgE,EAAahE,KAKxB,IAAK4N,KAAQ5N,EACX,GAAIrL,OAAOC,UAAUmD,eAAe7D,KAAK8L,EAAM4N,GAE7C,GADAE,EAAM9J,EAAalE,KAAK8N,GAGtB,IAAKA,KADLG,EAAU/N,EAAK4N,GAETjZ,OAAOC,UAAUmD,eAAe7D,KAAK6Z,EAASH,KAChDE,EAAI/N,IAAIgO,EAAQH,IAASI,OAAOJ,SAIpC5J,EAAajE,IAAIC,EAAK4N,IAASI,OAAOJ,GAjB7C,KC/XF,SAAW1W,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,eAAgB,qBAAsBK,GACnB,iBAAXH,QAAuBA,OAAOC,QAC9CE,EAAQC,QAAQ,gBAAiBA,QAAQ,sBAGzCD,EAAQD,OAAOnC,WATlB,CAWE,SAAUA,gBASX,SAASmZ,KAkDT,SAASC,EAAYvO,EAASI,EAAKhE,EAAUK,EAAQI,GACnD,MAA2B,WAAvBuD,EAAIoO,MAAMxO,GACL,IAAIlF,KAAK,CAACsB,EAASV,OAAO5H,MAAM2I,EAAQA,EAASI,KAE/B,WAAvBuD,EAAIoO,MAAMxO,GACL5D,EAASC,UAAUI,GAxB9B,SAAwBL,EAAUK,EAAQI,GAGxC,IAFA,IAAI4R,EAAS,GACTrT,EAAMqB,EAASI,EACV6R,EAAIjS,EAAQiS,EAAItT,EAAKsT,GAAK,EACjCD,GAAUlN,OAAOC,aAAapF,EAASiF,SAASqN,IAElD,OAAOD,EAoBAE,CAAevS,EAAUK,EAAQI,GA6B1C,SAAS+R,EACPxS,EACAyS,EACAC,EACAlZ,EACAuM,EACAC,GAKA,IAHA,IAAIlC,EAAO4C,EAAS9C,EA3BIE,EAAO6O,EA4B3BC,EAAaH,EAAgBC,EAC7BrS,EAASoS,EACNpS,EAASuS,GAEkB,KAA9B5S,EAASiF,SAAS5E,IACgB,IAAlCL,EAASiF,SAAS5E,EAAS,KAE3BuD,EAAU5D,EAASiF,SAAS5E,EAAS,GAEjC0F,IAAeA,EAAYnC,IAC3BoC,GAAgBA,EAAYpC,KAE9B8C,EAAU1G,EAAS6S,SAASxS,EAAS,GACrCyD,EAAQqO,EAAYvO,EAASpK,EAAKsZ,KAAM9S,EAAUK,EAAS,EAAGqG,GAC9DlN,EAAKsZ,KAAKlP,IA1CQE,EA0CoBtK,EAAKsZ,KAAKlP,GA1CvB+O,EA0CiC7O,EAzC5DA,IAAU9F,UAAkB2U,EAC5B7O,aAAiBiP,OACnBjP,EAAM6D,KAAKgL,GACJ7O,GAEF,CAACA,EAAO6O,IAqCLnZ,EAAKwZ,cACPxZ,EAAKwZ,YAAYpP,GAAWvD,KAIlCA,GAAU,EAjHd6R,EAAQrZ,UAAUmL,IAAM,CACtBiP,WAAY,GAGdf,EAAQrZ,UAAUuZ,MAAQ,CACxB9D,EAAG,SACH4E,IAAK,SACLC,IAAK,SACLC,IAAK,UASPlB,EAAQrZ,UAAUsK,IAAM,SAAUyB,GAChC,OAAOxM,KAAKwM,IAAOxM,KAAKA,KAAK4L,IAAIY,KAmInC7L,EAAUsa,cAAgB,SAAUrT,EAAUK,EAAQI,EAAQjH,EAAM/B,GAClE,IAAIA,EAAQ6b,YAIZ,IADA,IAfiCjT,EAC7BI,EAfkBT,EAAUK,EA6B5BF,EAAeE,EAASI,EACrBJ,EAAS,EAAIF,GAAc,CAChC,GA/B8BE,EA+BDA,EA7BE,aAFXL,EA+BDA,GA7BVyF,UAAUpF,IACgB,OAAnCL,EAASC,UAAUI,EAAS,GA4BU,CACpC,IAAIkT,GAlByBlT,EAkBgBA,EAjB7CI,OAAAA,GAAAA,EAiBmCT,EAjBjBiF,SAAS5E,EAAS,IAC3B,GAAM,IAAGI,GAAU,GAEjB,IAAXA,IAEFA,EAAS,GAEJA,GAWCgS,EAAgBpS,EAAS,EAAIkT,EACjC,GAAoBpT,EAAhBsS,EAA8B,CAEhC5Y,QAAQC,IAAI,8CACZ,MAEF,IAAI4Y,EAAgB1S,EAASC,UAAUI,EAAS,EAAIkT,GACpD,GAA6BpT,EAAzBE,EAASqS,EAA8B,CAEzC7Y,QAAQC,IAAI,4CACZ,MAeF,OAZAN,EAAKsZ,KAAO,IAAIZ,EACXza,EAAQ+b,qBACXha,EAAKwZ,YAAc,IAAId,QAEzBM,EACExS,EACAyS,EACAC,EACAlZ,EACA/B,EAAQgc,gBACRhc,EAAQic,iBAAmB,CAAEN,KAAK,IAKtC/S,GAAU,IAKdtH,EAAUwG,gBAAgBC,KAAK,OAAQmI,KAAK5O,EAAUsa,eAEtDta,EAAUmZ,QAAUA,ICnNrB,SAAW/W,gBAEY,mBAAXL,QAAyBA,OAAOC,IAEzCD,OAAO,CAAC,eAAgB,qBAAsBK,GACnB,iBAAXH,QAAuBA,OAAOC,QAC9CE,EAAQC,QAAQ,gBAAiBA,QAAQ,sBAGzCD,EAAQD,OAAOnC,WATlB,CAWE,SAAUA,gBAGX,IAAI4a,EAAe5a,EAAUmZ,QAAQrZ,UAErC8a,EAAa1P,KAAO,CAClBqK,EAAG,2BACHhJ,EAAG,sBACHE,EAAG,2BACHE,EAAG,aACH8I,EAAG,aACHC,EAAG,kBACH5I,GAAI,UACJ8I,GAAI,mBACJG,GAAI,WACJK,GAAI,yBACJE,GAAI,oBACJG,GAAI,WACJC,GAAI,sBACJC,GAAI,sBACJG,GAAI,cACJ+D,GAAI,cACJC,GAAI,iBACJC,GAAI,iBACJC,GAAI,sBACJC,GAAI,gBACJC,GAAI,mBACJC,GAAI,gBACJC,GAAI,kBACJC,GAAI,cACJC,GAAI,cACJC,GAAI,sBACJC,GAAI,sBACJjE,GAAI,qBACJkE,GAAI,iBACJC,GAAI,cACJC,GAAI,SACJC,GAAI,cACJC,GAAI,OACJC,GAAI,cACJ/D,GAAI,QACJgE,IAAK,cACLC,IAAK,UACLC,IAAK,gCACLC,IAAK,WACLC,IAAK,SACLC,IAAK,SACLC,IAAK,kBACLC,IAAK,UACLC,IAAK,UACLC,IAAK,eACLC,IAAK,SACLC,IAAK,oBACLC,IAAK,YACLC,IAAK,mBACLC,IAAK,qBACLC,IAAK,YACLC,IAAK,oBACLC,IAAK,0BACLC,IAAK,gBACLC,IAAK,cACLC,IAAK,QACLC,IAAK,mBACLC,IAAK,kBACLC,IAAK,mBACLC,IAAK,UACLpD,IAAK,0BACLC,IAAK,2BACLC,IAAK,oBACLmD,IAAK,QACLC,IAAK,gBACLC,IAAK,kBACLC,IAAK,gBACLC,IAAK,kBACLC,IAAK,iBACL1G,IAAK,eAGPyD,EAAa5D,aAAe,CAC1BlK,GAAI,CACFyI,EAAG,eACHxJ,EAAG,kBACHI,EAAG,IACHI,EAAG,IACHE,EAAG,IACHE,EAAG,qBACH6I,EAAG,IACHC,EAAG,IACHC,EAAG,mBACH9I,EAAG,6BAEL8O,GAAI,CACFoC,EAAG,UACHC,EAAG,2BACHC,EAAG,WAELpB,IAAK,CACHqB,EAAG,YACHC,EAAG,WACHC,EAAG,WAIPvD,EAAahC,QAAU,SAAU/M,GAC/B,IAAId,EAAQ1L,KAAK+K,IAAIyB,GACjBhB,EAAUxL,KAAK4L,IAAIY,GACnBuS,EAAc/e,KAAK2X,aAAanM,GACpC,OAAIuT,EAAoBA,EAAYrT,GAC7BqB,OAAOrB,IAGhB6P,EAAa/B,OAAS,WACpB,IACIC,EACA1N,EAFAH,EAAM,GAGV,IAAK6N,KAAQzZ,KACPQ,OAAOC,UAAUmD,eAAe7D,KAAKC,KAAMyZ,KAC7C1N,EAAO/L,KAAK6L,KAAK4N,MACP7N,EAAIG,GAAQ/L,KAAKuZ,QAAQxN,IAGvC,OAAOH,GAGT2P,EAAa7B,QAAU,SAAUlO,GAC/B,OAAOxL,KAAK6L,KAAKL,IAIlB,WACC,IAEIiO,EAFA5N,EAAO0P,EAAa1P,KACpBD,EAAM2P,EAAa3P,KAAO,GAG9B,IAAK6N,KAAQ5N,EACPrL,OAAOC,UAAUmD,eAAe7D,KAAK8L,EAAM4N,KAC7C7N,EAAIC,EAAK4N,IAASI,OAAOJ,IAP9B"} \ 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 034/671] 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 035/671] 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 036/671] 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 0c4bf0216d485e2eb32a6bd5e704b41b287c78da Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Thu, 11 Jun 2020 13:02:19 -0500 Subject: [PATCH 037/671] 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 038/671] 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 039/671] 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 040/671] 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 041/671] 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 042/671] 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 043/671] 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 044/671] 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 1f92e3dbf5dbafc1086002ed652a80b1f99db378 Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Mon, 22 Jun 2020 10:59:01 +0300 Subject: [PATCH 045/671] 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 80adcee1647860d788372b5c1714236fc338b244 Mon Sep 17 00:00:00 2001 From: Mark Berube Date: Mon, 22 Jun 2020 15:09:39 -0500 Subject: [PATCH 046/671] 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 047/671] 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 048/671] 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 049/671] 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 050/671] [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 051/671] 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 052/671] 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 053/671] 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 054/671] 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 055/671] 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 ad85143801deb724aed15c484d735fe83c8ec274 Mon Sep 17 00:00:00 2001 From: Viktor Sevch Date: Wed, 8 Jul 2020 12:40:15 +0300 Subject: [PATCH 056/671] 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 057/671] 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 058/671] 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 de823ab06ab0fd65d9d831ae60442beba88d81dd Mon Sep 17 00:00:00 2001 From: Pavel Bystritsky Date: Tue, 14 Jul 2020 10:54:00 +0300 Subject: [PATCH 059/671] 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 3f6d8c42a08cdf82f852d2c4e50903ad276488ee Mon Sep 17 00:00:00 2001 From: ogorkun Date: Wed, 15 Jul 2020 14:32:40 -0500 Subject: [PATCH 060/671] 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 @@ + + + + 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 061/671] 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 062/671] 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 063/671] 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 064/671] 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 065/671] 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 066/671] 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 067/671] 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 068/671] 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 069/671] 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 3dcf9110e7147beebf1529880315dfc1daccedc7 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Mon, 20 Jul 2020 13:32:27 -0500 Subject: [PATCH 070/671] 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 071/671] 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 1a9fc92f48a84dc8744e6254180e33743406de96 Mon Sep 17 00:00:00 2001 From: Sachin Admane Date: Tue, 21 Jul 2020 10:03:24 -0500 Subject: [PATCH 072/671] 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 073/671] 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 074/671] 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 075/671] 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 076/671] 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 077/671] 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: Mon, 27 Jul 2020 15:07:55 -0500 Subject: [PATCH 078/671] 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 49f7dae9013c1be18f61433973459f51567e59c4 Mon Sep 17 00:00:00 2001 From: ogorkun Date: Mon, 27 Jul 2020 15:36:03 -0500 Subject: [PATCH 079/671] 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 efc25d74e4c8f34f11115add6c0eb5f0f60bbc1c Mon Sep 17 00:00:00 2001 From: ogorkun Date: Tue, 28 Jul 2020 10:48:00 -0500 Subject: [PATCH 080/671] 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 Date: Tue, 28 Jul 2020 12:10:16 -0500 Subject: [PATCH 081/671] 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 Date: Tue, 28 Jul 2020 22:19:30 -0500 Subject: [PATCH 082/671] 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 @@ +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 @@ + + + + + Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\OrderItemTypeResolver + + + + + + + Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\InvoiceItemTypeResolver + + + 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 Date: Tue, 28 Jul 2020 23:18:47 -0500 Subject: [PATCH 083/671] 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 Date: Wed, 29 Jul 2020 14:35:24 +0300 Subject: [PATCH 084/671] 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 Date: Wed, 29 Jul 2020 11:19:21 -0500 Subject: [PATCH 085/671] 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 @@ + Date: Wed, 29 Jul 2020 12:36:06 -0500 Subject: [PATCH 086/671] 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 @@ - Date: Wed, 29 Jul 2020 14:57:12 -0500 Subject: [PATCH 087/671] 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 Date: Wed, 29 Jul 2020 15:47:11 -0500 Subject: [PATCH 088/671] 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 Date: Wed, 29 Jul 2020 19:15:39 -0500 Subject: [PATCH 089/671] 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 dae1dd8d1bbab88eb7f6ee2d12c7049f9dcb930b Mon Sep 17 00:00:00 2001 From: Hwashiang Yu Date: Fri, 31 Jul 2020 15:31:27 -0500 Subject: [PATCH 090/671] 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 @@ +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 @@ +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 @@ +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 Date: Fri, 31 Jul 2020 16:08:16 -0500 Subject: [PATCH 091/671] 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 Date: Mon, 3 Aug 2020 09:11:57 -0500 Subject: [PATCH 092/671] 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 @@ +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 = + <<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 Date: Mon, 3 Aug 2020 12:32:46 -0500 Subject: [PATCH 093/671] 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 d425596643b966a3f08229cf7dcdb6cd38c527bd Mon Sep 17 00:00:00 2001 From: Victor Rad Date: Mon, 3 Aug 2020 16:21:48 -0500 Subject: [PATCH 094/671] 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 5e3ee96efdb20ea129b82e909c9fe88c9c58a5df Mon Sep 17 00:00:00 2001 From: Victor Rad Date: Tue, 4 Aug 2020 07:34:05 -0500 Subject: [PATCH 095/671] 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 Date: Tue, 4 Aug 2020 17:16:42 +0300 Subject: [PATCH 096/671] 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 @@ +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 @@ +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 Date: Tue, 4 Aug 2020 15:27:11 -0500 Subject: [PATCH 097/671] 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 @@ +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 @@ +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 Date: Wed, 5 Aug 2020 10:52:03 -0500 Subject: [PATCH 098/671] 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 Date: Tue, 4 Aug 2020 15:44:19 -0500 Subject: [PATCH 099/671] 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 Date: Wed, 5 Aug 2020 12:52:53 -0500 Subject: [PATCH 100/671] 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 Date: Wed, 5 Aug 2020 12:54:41 -0500 Subject: [PATCH 101/671] 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 Date: Wed, 5 Aug 2020 17:36:19 -0500 Subject: [PATCH 102/671] 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 Date: Thu, 6 Aug 2020 10:15:38 -0500 Subject: [PATCH 103/671] 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 Date: Thu, 6 Aug 2020 16:38:10 -0500 Subject: [PATCH 104/671] 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 @@ - - + - - Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\OrderItemTypeResolver + + DownloadableOrderItem - + - - Magento\DownloadableGraphQl\Resolver\DownloadableOrderItem\InvoiceItemTypeResolver + + DownloadableInvoiceItem + + + + + + + DownloadableCreditMemoItem From fe4b27b6a46810fb456bcfc6202e938eb3e288bc Mon Sep 17 00:00:00 2001 From: Cristian Partica Date: Fri, 7 Aug 2020 10:12:06 -0500 Subject: [PATCH 105/671] 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 Date: Fri, 7 Aug 2020 10:16:51 -0500 Subject: [PATCH 106/671] 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 Date: Fri, 7 Aug 2020 10:32:07 -0500 Subject: [PATCH 107/671] 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 @@ +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 @@ +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?= Date: Fri, 7 Aug 2020 18:56:25 +0200 Subject: [PATCH 108/671] 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 Date: Fri, 7 Aug 2020 14:10:16 -0500 Subject: [PATCH 109/671] 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 @@ +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 @@ +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 @@ + + + @@ -48,6 +51,7 @@ 1 + 1 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 @@ + 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 @@ Magento\Checkout\Model\DefaultConfigProvider Magento\Checkout\Model\Cart\CheckoutSummaryConfigProvider + Magento\Checkout\Model\PaymentCaptchaConfigProvider @@ -99,4 +100,11 @@ + + + + payment_processing_request + + + 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 @@ + + Magento_Checkout/js/view/checkout/placeOrderCaptcha + place-order-captcha + payment_processing_request + checkoutConfig + uiComponent beforeMethods 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 @@
+ + + +
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');
+ getChildHtml('captcha') ?>