diff --git a/.gitignore b/.gitignore index 8ec1104f25535..7092a568ba2a2 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,8 @@ atlassian* /pub/media/tmp/* !/pub/media/tmp/.htaccess /pub/media/captcha/* +/pub/media/sitemap/* +!/pub/media/sitemap/.htaccess /pub/static/* !/pub/static/.htaccess diff --git a/.htaccess b/.htaccess index c5f3bf034d2fb..ae929f8bc6467 100644 --- a/.htaccess +++ b/.htaccess @@ -1,393 +1,7 @@ -############################################ -## overrides deployment configuration mode value -## use command bin/magento deploy:mode:set to switch modes - -# SetEnv MAGE_MODE developer - -############################################ -## uncomment these lines for CGI mode -## make sure to specify the correct cgi php binary file name -## it might be /cgi-bin/php-cgi - -# Action php5-cgi /cgi-bin/php5-cgi -# AddHandler php5-cgi .php - -############################################ -## GoDaddy specific options - -# Options -MultiViews - -## you might also need to add this line to php.ini -## cgi.fix_pathinfo = 1 -## if it still doesn't work, rename php.ini to php5.ini - -############################################ -## this line is specific for 1and1 hosting - - #AddType x-mapp-php5 .php - #AddHandler x-mapp-php5 .php - -############################################ -## enable usage of methods arguments in backtrace - - SetEnv MAGE_DEBUG_SHOW_ARGS 1 - -############################################ -## default index file - - DirectoryIndex index.php - - -############################################ -## adjust memory limit - - php_value memory_limit 756M - php_value max_execution_time 18000 - -############################################ -## disable automatic session start -## before autoload was initialized - - php_flag session.auto_start off - -############################################ -## enable resulting html compression - - #php_flag zlib.output_compression on - -########################################### -## disable user agent verification to not break multiple image upload - - php_flag suhosin.session.cryptua off - - -########################################### -## disable POST processing to not break multiple image upload - - SecFilterEngine Off - SecFilterScanPOST Off - - - - -############################################ -## enable apache served files compression -## http://developer.yahoo.com/performance/rules.html#gzip - - # Insert filter on all content - ###SetOutputFilter DEFLATE - # Insert filter on selected content types only - #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/x-javascript application/json image/svg+xml - - # Netscape 4.x has some problems... - #BrowserMatch ^Mozilla/4 gzip-only-text/html - - # Netscape 4.06-4.08 have some more problems - #BrowserMatch ^Mozilla/4\.0[678] no-gzip - - # MSIE masquerades as Netscape, but it is fine - #BrowserMatch \bMSIE !no-gzip !gzip-only-text/html - - # Don't compress images - #SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary - - # Make sure proxies don't deliver the wrong content - #Header append Vary User-Agent env=!dont-vary - - - - - -############################################ -## make HTTPS env vars available for CGI mode - - SSLOptions StdEnvVars - - - -############################################ -## workaround for Apache 2.4.6 CentOS build when working via ProxyPassMatch with HHVM (or any other) -## Please, set it on virtual host configuration level - -## SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 -############################################ - - - -############################################ -## enable rewrites - - Options +FollowSymLinks - RewriteEngine on - -############################################ -## you can put here your magento root folder -## path relative to web root - - #RewriteBase /magento/ - -############################################ -## workaround for HTTP authorization -## in CGI environment - - RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] - -############################################ -## TRACE and TRACK HTTP methods disabled to prevent XSS attacks - - RewriteCond %{REQUEST_METHOD} ^TRAC[EK] - RewriteRule .* - [L,R=405] - -############################################ -## redirect for mobile user agents - - #RewriteCond %{REQUEST_URI} !^/mobiledirectoryhere/.*$ - #RewriteCond %{HTTP_USER_AGENT} "android|blackberry|ipad|iphone|ipod|iemobile|opera mobile|palmos|webos|googlebot-mobile" [NC] - #RewriteRule ^(.*)$ /mobiledirectoryhere/ [L,R=302] - -############################################ -## never rewrite for existing files, directories and links - - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-l - -############################################ -## rewrite everything else to index.php - - RewriteRule .* index.php [L] - - - - -############################################ -## Prevent character encoding issues from server overrides -## If you still have problems, use the second line instead - - AddDefaultCharset Off - #AddDefaultCharset UTF-8 - AddType 'text/html; charset=UTF-8' html - - - -############################################ -## Add default Expires header -## http://developer.yahoo.com/performance/rules.html#expires - - ExpiresDefault "access plus 1 year" - ExpiresByType text/html A0 - ExpiresByType text/plain A0 - - - -########################################### -## Deny access to root files to hide sensitive application information - RedirectMatch 403 /\.git - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - - -# For 404s and 403s that aren't handled by the application, show plain 404 response -ErrorDocument 404 /pub/errors/404.php -ErrorDocument 403 /pub/errors/404.php - -################################ -## If running in cluster environment, uncomment this -## http://developer.yahoo.com/performance/rules.html#etags - - #FileETag none - -# ###################################################################### -# # INTERNET EXPLORER # -# ###################################################################### - -# ---------------------------------------------------------------------- -# | Document modes | -# ---------------------------------------------------------------------- - -# Force Internet Explorer 8/9/10 to render pages in the highest mode -# available in the various cases when it may not. -# -# https://hsivonen.fi/doctype/#ie8 -# -# (!) Starting with Internet Explorer 11, document modes are deprecated. -# If your business still relies on older web apps and services that were -# designed for older versions of Internet Explorer, you might want to -# consider enabling `Enterprise Mode` throughout your company. -# -# https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode -# http://blogs.msdn.com/b/ie/archive/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11.aspx - - - - Header set X-UA-Compatible "IE=edge" - - # `mod_headers` cannot match based on the content-type, however, - # the `X-UA-Compatible` response header should be send only for - # HTML documents and not for the other resources. - - - Header unset X-UA-Compatible - - - +RewriteEngine on +RewriteCond %{REQUEST_URI} !^/pub/ +RewriteCond %{REQUEST_URI} !^/setup/ +RewriteCond %{REQUEST_URI} !^/update/ +RewriteCond %{REQUEST_URI} !^/dev/ +RewriteRule .* /pub/$0 [L] +DirectoryIndex index.php diff --git a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php index e888c38c4e817..50081d6ae1f17 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/LinkProviderTest.php @@ -90,7 +90,7 @@ protected function setUp(): void public function testGet() { - $baseUrl = 'http://magento.local/pub/media/'; + $baseUrl = 'http://magento.local/media/'; $fileInfoPath = 'analytics/data.tgz'; $fileInitializationVector = 'er312esq23eqq'; $this->fileInfoManagerMock->expects($this->once()) diff --git a/app/code/Magento/AwsS3/Driver/AwsS3.php b/app/code/Magento/AwsS3/Driver/AwsS3.php new file mode 100644 index 0000000000000..dcf52b3188404 --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/AwsS3.php @@ -0,0 +1,880 @@ + 'private']; + + /** + * @var AdapterInterface + */ + private $adapter; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var array + */ + private $streams = []; + + /** + * @var string + */ + private $objectUrl; + + /** + * @param AdapterInterface $adapter + * @param LoggerInterface $logger + * @param string $objectUrl + */ + public function __construct( + AdapterInterface $adapter, + LoggerInterface $logger, + string $objectUrl + ) { + $this->adapter = $adapter; + $this->logger = $logger; + $this->objectUrl = $objectUrl; + } + + /** + * Destroy opened streams. + */ + public function __destruct() + { + try { + foreach ($this->streams as $stream) { + $this->fileClose($stream); + } + } catch (\Exception $e) { + // log exception as throwing an exception from a destructor causes a fatal error + $this->logger->critical($e); + } + } + + /** + * @inheritDoc + */ + public function test(): void + { + try { + $this->adapter->write(self::TEST_FLAG, '', new Config(self::CONFIG)); + } catch (Exception $exception) { + throw new DriverException(__($exception->getMessage()), $exception); + } + } + + /** + * @inheritDoc + */ + public function fileGetContents($path, $flag = null, $context = null): string + { + $path = $this->normalizeRelativePath($path); + + if (isset($this->streams[$path])) { + //phpcs:disable + return file_get_contents(stream_get_meta_data($this->streams[$path])['uri']); + //phpcs:enable + } + + return $this->adapter->read($path)['contents'] ?? ''; + } + + /** + * @inheritDoc + */ + public function isExists($path): bool + { + if ($path === '/') { + return true; + } + + $path = $this->normalizeRelativePath($path); + + if (!$path || $path === '/') { + return true; + } + + return $this->adapter->has($path); + } + + /** + * @inheritDoc + */ + public function isWritable($path): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function createDirectory($path, $permissions = 0777): bool + { + if ($path === '/') { + return true; + } + + return $this->createDirectoryRecursively( + $this->normalizeRelativePath($path) + ); + } + + /** + * Created directory recursively. + * + * @param string $path + * @return bool + * @throws FileSystemException + */ + private function createDirectoryRecursively(string $path): bool + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $parentDir = dirname($path); + + while (!$this->isDirectory($parentDir)) { + $this->createDirectoryRecursively($parentDir); + } + + return (bool)$this->adapter->createDir(rtrim($path, '/'), new Config(self::CONFIG)); + } + + /** + * @inheritDoc + */ + public function copy($source, $destination, DriverInterface $targetDriver = null): bool + { + return $this->adapter->copy( + $this->normalizeRelativePath($source), + $this->normalizeRelativePath($destination) + ); + } + + /** + * @inheritDoc + */ + public function deleteFile($path): bool + { + return $this->adapter->delete( + $this->normalizeRelativePath($path) + ); + } + + /** + * @inheritDoc + */ + public function deleteDirectory($path): bool + { + return $this->adapter->deleteDir( + $this->normalizeRelativePath($path) + ); + } + + /** + * @inheritDoc + */ + public function filePutContents($path, $content, $mode = null): int + { + $path = $this->normalizeRelativePath($path); + $config = self::CONFIG; + + if (false !== ($imageSize = @getimagesizefromstring($content))) { + $config['Metadata'] = [ + 'image-width' => $imageSize[0], + 'image-height' => $imageSize[1] + ]; + } + + return $this->adapter->write($path, $content, new Config($config))['size']; + } + + /** + * @inheritDoc + */ + public function readDirectoryRecursively($path = null): array + { + return $this->readPath($path, true); + } + + /** + * @inheritDoc + */ + public function readDirectory($path): array + { + return $this->readPath($path, false); + } + + /** + * @inheritDoc + */ + public function getRealPathSafety($path) + { + if (strpos($path, '/.') === false) { + return $path; + } + + $isAbsolute = strpos($path, $this->normalizeAbsolutePath()) === 0; + $path = $this->normalizeRelativePath($path); + + //Removing redundant directory separators. + $path = preg_replace( + '/\\/\\/+/', + '/', + $path + ); + $pathParts = explode('/', $path); + if (end($pathParts) === '.') { + $pathParts[count($pathParts) - 1] = ''; + } + $realPath = []; + foreach ($pathParts as $pathPart) { + if ($pathPart === '.') { + continue; + } + if ($pathPart === '..') { + array_pop($realPath); + continue; + } + $realPath[] = $pathPart; + } + + if ($isAbsolute) { + return $this->normalizeAbsolutePath(implode('/', $realPath)); + } + + return implode('/', $realPath); + } + + /** + * @inheritDoc + */ + public function getAbsolutePath($basePath, $path, $scheme = null) + { + $basePath = $this->normalizeRelativePath((string)$basePath); + $path = $this->normalizeRelativePath((string)$path); + if ($basePath && $path && 0 === strpos($path, $basePath)) { + return $this->normalizeAbsolutePath($path); + } + + if ($basePath && $basePath !== '/') { + $path = $basePath . ltrim((string)$path, '/'); + } + + return $this->normalizeAbsolutePath($path); + } + + /** + * Resolves absolute path. + * + * @param string $path Relative path + * @return string Absolute path + */ + private function normalizeAbsolutePath(string $path = '/'): string + { + $path = ltrim($path, '/'); + $path = str_replace($this->getObjectUrl(''), '', $path); + + if (!$path) { + $path = '/'; + } + + return $this->getObjectUrl($path); + } + + /** + * Retrieves object URL from cache. + * + * @param string $path + * @return string + */ + private function getObjectUrl(string $path): string + { + return $this->objectUrl . ltrim($path, '/'); + } + + /** + * Resolves relative path. + * + * @param string $path Absolute path + * @return string Relative path + */ + private function normalizeRelativePath(string $path): string + { + return str_replace($this->normalizeAbsolutePath(), '', $path); + } + + /** + * @inheritDoc + */ + public function isReadable($path): bool + { + return $this->isExists($path); + } + + /** + * @inheritDoc + */ + public function isFile($path): bool + { + if (!$path || $path === '/') { + return false; + } + + $path = $this->normalizeRelativePath($path); + $path = rtrim($path, '/'); + + if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { + return ($meta['type'] ?? null) === self::TYPE_FILE; + } + + return false; + } + + /** + * @inheritDoc + */ + public function isDirectory($path): bool + { + if (in_array($path, ['.', '/'], true)) { + return true; + } + + $path = $this->normalizeRelativePath($path); + + if (!$path || $path === '/') { + return true; + } + + $path = rtrim($path, '/') . '/'; + + if ($this->adapter->has($path) && ($meta = $this->adapter->getMetadata($path))) { + return ($meta['type'] ?? null) === self::TYPE_DIR; + } + + return false; + } + + /** + * @inheritDoc + */ + public function getRelativePath($basePath, $path = null): string + { + $basePath = $this->normalizeAbsolutePath($basePath); + $absolutePath = $this->normalizeAbsolutePath((string)$path); + + if ($basePath === $absolutePath . '/' || strpos($absolutePath, $basePath) === 0) { + return ltrim(substr($absolutePath, strlen($basePath)), '/'); + } + + return ltrim($path, '/'); + } + + /** + * @inheritDoc + */ + public function getParentDirectory($path): string + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return rtrim(dirname($this->normalizeAbsolutePath($path)), '/') . '/'; + } + + /** + * @inheritDoc + */ + public function getRealPath($path) + { + return $this->normalizeAbsolutePath($path); + } + + /** + * @inheritDoc + */ + public function rename($oldPath, $newPath, DriverInterface $targetDriver = null): bool + { + return $this->adapter->rename( + $this->normalizeRelativePath($oldPath), + $this->normalizeRelativePath($newPath) + ); + } + + /** + * @inheritDoc + */ + public function stat($path): array + { + $path = $this->normalizeRelativePath($path); + $metaInfo = $this->adapter->getMetadata($path); + + if (!$metaInfo) { + throw new FileSystemException(__('Cannot gather stats! %1', [$this->getWarningMessage()])); + } + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'atime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + 'size' => $metaInfo['size'] ?? 0, + 'type' => $metaInfo['type'] ?? '', + 'mtime' => $metaInfo['timestamp'] ?? 0, + 'disposition' => null + ]; + } + + /** + * @inheritDoc + */ + public function getMetadata(string $path): array + { + $path = $this->normalizeRelativePath($path); + $metaInfo = $this->adapter->getMetadata($path); + + if (!$metaInfo) { + throw new FileSystemException(__('Cannot gather meta info! %1', [$this->getWarningMessage()])); + } + + return [ + 'path' => $metaInfo['path'], + 'dirname' => $metaInfo['dirname'], + 'basename' => $metaInfo['basename'], + 'extension' => $metaInfo['extension'], + 'filename' => $metaInfo['filename'], + 'timestamp' => $metaInfo['timestamp'], + 'size' => $metaInfo['size'], + 'mimetype' => $metaInfo['mimetype'], + 'extra' => [ + 'image-width' => $metaInfo['metadata']['image-width'] ?? 0, + 'image-height' => $metaInfo['metadata']['image-height'] ?? 0 + ] + ]; + } + + /** + * @inheritDoc + */ + public function search($pattern, $path): array + { + return iterator_to_array( + $this->glob(rtrim($path, '/') . '/' . ltrim($pattern, '/')), + false + ); + } + + /** + * Emulate php glob function for AWS S3 storage + * + * @param string $pattern + * @return Generator + * @throws FileSystemException + */ + private function glob(string $pattern): Generator + { + $patternFound = preg_match('(\*|\?|\[.+\])', $pattern, $parentPattern, PREG_OFFSET_CAPTURE); + + if ($patternFound) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $parentDirectory = \dirname(substr($pattern, 0, $parentPattern[0][1] + 1)); + $leftover = substr($pattern, $parentPattern[0][1]); + $index = strpos($leftover, '/'); + $searchPattern = $this->getSearchPattern($pattern, $parentPattern, $parentDirectory, $index); + + if ($this->isDirectory($parentDirectory . '/')) { + yield from $this->getDirectoryContent($parentDirectory, $searchPattern, $leftover, $index); + } + } elseif ($this->isDirectory($pattern) || $this->isFile($pattern)) { + yield $pattern; + } + } + + /** + * @inheritDoc + */ + public function symlink($source, $destination, DriverInterface $targetDriver = null): bool + { + return $this->copy($source, $destination, $targetDriver); + } + + /** + * @inheritDoc + */ + public function changePermissions($path, $permissions): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function changePermissionsRecursively($path, $dirPermissions, $filePermissions): bool + { + return true; + } + + /** + * @inheritDoc + */ + public function touch($path, $modificationTime = null) + { + $path = $this->normalizeRelativePath($path); + + $content = $this->adapter->has($path) ? + $this->adapter->read($path)['contents'] + : ''; + + return (bool)$this->adapter->write($path, $content, new Config([])); + } + + /** + * @inheritDoc + */ + public function fileReadLine($resource, $length, $ending = null): string + { + // phpcs:disable + $result = @stream_get_line($resource, $length, $ending); + // phpcs:enable + if (false === $result) { + throw new FileSystemException( + new Phrase('File cannot be read %1', [$this->getWarningMessage()]) + ); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function fileRead($resource, $length): string + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = fread($resource, $length); + if ($result === false) { + throw new FileSystemException(__('File cannot be read %1', [$this->getWarningMessage()])); + } + + return $result; + } + + /** + * @inheritDoc + */ + public function fileGetCsv($resource, $length = 0, $delimiter = ',', $enclosure = '"', $escape = '\\') + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $result = fgetcsv($resource, $length, $delimiter, $enclosure, $escape); + if ($result === null) { + throw new FileSystemException( + new Phrase( + 'The "%1" CSV handle is incorrect. Verify the handle and try again.', + [$this->getWarningMessage()] + ) + ); + } + return $result; + } + + /** + * @inheritDoc + */ + public function fileTell($resource): int + { + $result = @ftell($resource); + if ($result === null) { + throw new FileSystemException( + new Phrase('An error occurred during "%1" execution.', [$this->getWarningMessage()]) + ); + } + return $result; + } + + /** + * @inheritDoc + */ + public function fileSeek($resource, $offset, $whence = SEEK_SET): int + { + $result = @fseek($resource, $offset, $whence); + if ($result === -1) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileSeek execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; + } + + /** + * @inheritDoc + */ + public function endOfFile($resource): bool + { + return feof($resource); + } + + /** + * @inheritDoc + */ + public function filePutCsv($resource, array $data, $delimiter = ',', $enclosure = '"') + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + return fputcsv($resource, $data, $delimiter, $enclosure); + } + + /** + * @inheritDoc + */ + public function fileFlush($resource): bool + { + $result = @fflush($resource); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileFlush execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; + } + + /** + * @inheritDoc + */ + public function fileLock($resource, $lockMode = LOCK_EX): bool + { + $result = @flock($resource, $lockMode); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileLock execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; + } + + /** + * @inheritDoc + */ + public function fileUnlock($resource): bool + { + $result = @flock($resource, LOCK_UN); + if (!$result) { + throw new FileSystemException( + new Phrase( + 'An error occurred during "%1" fileUnlock execution.', + [$this->getWarningMessage()] + ) + ); + } + return $result; + } + + /** + * @inheritDoc + */ + public function fileWrite($resource, $data) + { + //phpcs:disable + $resourcePath = stream_get_meta_data($resource)['uri']; + //phpcs:enable + + foreach ($this->streams as $stream) { + //phpcs:disable + if (stream_get_meta_data($stream)['uri'] === $resourcePath) { + return fwrite($stream, $data); + } + //phpcs:enable + } + + return false; + } + + /** + * @inheritDoc + */ + public function fileClose($resource): bool + { + //phpcs:disable + $resourcePath = stream_get_meta_data($resource)['uri']; + //phpcs:enable + + foreach ($this->streams as $path => $stream) { + //phpcs:disable + if (stream_get_meta_data($stream)['uri'] === $resourcePath) { + $this->adapter->writeStream($path, $resource, new Config(self::CONFIG)); + + // Remove path from streams after + unset($this->streams[$path]); + + return fclose($stream); + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function fileOpen($path, $mode) + { + $path = $this->normalizeRelativePath($path); + + if (!isset($this->streams[$path])) { + $this->streams[$path] = tmpfile(); + if ($this->adapter->has($path)) { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + fwrite($this->streams[$path], $this->adapter->read($path)['contents']); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + rewind($this->streams[$path]); + } + } + + return $this->streams[$path]; + } + + /** + * Returns last warning message string + * + * @return string|null + */ + private function getWarningMessage(): ?string + { + $warning = error_get_last(); + if ($warning && $warning['type'] === E_WARNING) { + return 'Warning!' . $warning['message']; + } + + return null; + } + + /** + * Read directory by path and is recursive flag + * + * @param string $path + * @param bool $isRecursive + * @return array + */ + private function readPath(string $path, $isRecursive = false): array + { + $relativePath = $this->normalizeRelativePath($path); + $contentsList = $this->adapter->listContents( + $relativePath, + $isRecursive + ); + $itemsList = []; + foreach ($contentsList as $item) { + if (isset($item['path']) + && $item['path'] !== $relativePath + && strpos($item['path'], $relativePath) === 0) { + $itemsList[] = $item['path']; + } + } + + return $itemsList; + } + + /** + * Get search pattern for directory + * + * @param string $pattern + * @param array $parentPattern + * @param string $parentDirectory + * @param int|bool $index + * @return string + */ + private function getSearchPattern(string $pattern, array $parentPattern, string $parentDirectory, $index): string + { + $parentLength = \strlen($parentDirectory); + if ($index !== false) { + $searchPattern = substr( + $pattern, + $parentLength + 1, + $parentPattern[0][1] - $parentLength + $index - 1 + ); + } else { + $searchPattern = substr($pattern, $parentLength + 1); + } + + $replacement = [ + '/\*/' => '.*', + '/\?/' => '.', + '/\//' => '\/' + ]; + + return preg_replace(array_keys($replacement), array_values($replacement), $searchPattern); + } + + /** + * Get directory content by given search pattern + * + * @param string $parentDirectory + * @param string $searchPattern + * @param string $leftover + * @param int|bool $index + * @return Generator + * @throws FileSystemException + */ + private function getDirectoryContent( + string $parentDirectory, + string $searchPattern, + string $leftover, + $index + ): Generator { + $items = $this->readDirectory($parentDirectory . '/'); + $directoryContent = []; + foreach ($items as $item) { + if (preg_match('/' . $searchPattern . '$/', $item) + // phpcs:ignore Magento2.Functions.DiscouragedFunction + && strpos(basename($item), '.') !== 0) { + if ($index === false || \strlen($leftover) === $index + 1) { + yield $this->isDirectory($item) ? rtrim($item, '/') . '/' : $item; + } elseif (strlen($leftover) > $index + 1) { + yield from $this->glob("{$parentDirectory}/{$item}" . substr($leftover, $index)); + } + } + } + + return $directoryContent; + } +} diff --git a/app/code/Magento/AwsS3/Driver/AwsS3Factory.php b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php new file mode 100644 index 0000000000000..87efd7c13f398 --- /dev/null +++ b/app/code/Magento/AwsS3/Driver/AwsS3Factory.php @@ -0,0 +1,61 @@ +objectManager = $objectManager; + } + + /** + * @inheritDoc + */ + public function create(array $config, string $prefix): RemoteDriverInterface + { + $config['version'] = 'latest'; + + if (empty($config['credentials']['key']) || empty($config['credentials']['secret'])) { + unset($config['credentials']); + } + + if (empty($config['bucket']) || empty($config['region'])) { + throw new DriverException(__('Bucket and region are required values')); + } + + $client = new S3Client($config); + $adapter = new AwsS3Adapter($client, $config['bucket'], $prefix); + + return $this->objectManager->create( + AwsS3::class, + [ + 'adapter' => $adapter, + 'objectUrl' => $client->getObjectUrl($adapter->getBucket(), $adapter->applyPathPrefix('.')) + ] + ); + } +} diff --git a/app/code/Magento/AwsS3/LICENSE.txt b/app/code/Magento/AwsS3/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/AwsS3/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/AwsS3/LICENSE_AFL.txt b/app/code/Magento/AwsS3/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/AwsS3/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/AwsS3/Model/Config.php b/app/code/Magento/AwsS3/Model/Config.php new file mode 100644 index 0000000000000..f4e19edd4eec3 --- /dev/null +++ b/app/code/Magento/AwsS3/Model/Config.php @@ -0,0 +1,85 @@ +config = $config; + } + + /** + * Retrieves region. + * + * @return string + */ + public function getRegion(): string + { + return (string)$this->config->get(self::PATH_REGION); + } + + /** + * Retrieves bucket. + * + * @return string + */ + public function getBucket(): string + { + return (string)$this->config->get(self::PATH_BUCKET); + } + + /** + * Retrieves access key. + * + * @return string + */ + public function getAccessKey(): string + { + return (string)$this->config->get(self::PATH_ACCESS_KEY); + } + + /** + * Retrieves secret key. + * + * @return string + */ + public function getSecretKey(): string + { + return (string)$this->config->get(self::PATH_SECRET_KEY); + } + + /** + * Retrieves prefix. + * + * @return string + */ + public function getPrefix(): string + { + return (string)$this->config->get(self::PATH_PREFIX, ''); + } +} diff --git a/app/code/Magento/AwsS3/README.md b/app/code/Magento/AwsS3/README.md new file mode 100644 index 0000000000000..fc07df1717136 --- /dev/null +++ b/app/code/Magento/AwsS3/README.md @@ -0,0 +1,3 @@ +# Magento_AwsS3 module + +The Magento_AwsS3 module integrates your Magento with the [AWS S3](https://aws.amazon.com/s3) storage. diff --git a/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..23be7918106ee --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,19 @@ + + + + + {{_ENV.REMOTE_STORAGE_AWSS3_DRIVER}} + {{_ENV.REMOTE_STORAGE_AWSS3_REGION}} + {{_ENV.REMOTE_STORAGE_AWSS3_PREFIX}} + {{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}} + {{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}} + {{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}} + --remote-storage-driver={{_ENV.REMOTE_STORAGE_AWSS3_DRIVER}} --remote-storage-bucket={{_ENV.REMOTE_STORAGE_AWSS3_BUCKET}} --remote-storage-region={{_ENV.REMOTE_STORAGE_AWSS3_REGION}} --remote-storage-prefix={{_ENV.REMOTE_STORAGE_AWSS3_PREFIX}} --remote-storage-key={{_ENV.REMOTE_STORAGE_AWSS3_ACCESS_KEY}} --remote-storage-secret={{_ENV.REMOTE_STORAGE_AWSS3_SECRET_KEY}} -n + --remote-storage-driver=file -n + + diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml new file mode 100644 index 0000000000000..a8f0d4da9e338 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageForCategoryTest.xml @@ -0,0 +1,27 @@ + + + + + + + + <stories value="Add/remove images and videos for all product types and category"/> + <description value="Admin should be able to add image to a Category"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38688"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml new file mode 100644 index 0000000000000..13e0dcbf41c01 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGBlockTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminAddImageToWYSIWYGBlockTest" extends="AdminAddImageToWYSIWYGBlockTest"> + <annotations> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of Block with remote filesystem enabled"/> + <stories value="Default WYSIWYG toolbar configuration with Magento Media Gallery"/> + <description value="Admin should be able to add image to WYSIWYG content of Block"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38302"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml new file mode 100644 index 0000000000000..a56d5d0710d3a --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGCMSTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminAddImageToWYSIWYGCMSTest" extends="AdminAddImageToWYSIWYGCMSTest"> + <annotations> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of CMS Page with remote filesystem enabled"/> + <stories value="Default WYSIWYG toolbar configuration with Magento Media Gallery"/> + <description value="Admin should be able to add image to WYSIWYG content of CMS Page"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38295"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml new file mode 100644 index 0000000000000..adc4eea8acf2e --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddImageToWYSIWYGNewsletterTest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminAddImageToWYSIWYGNewsletterTest" extends="AdminAddImageToWYSIWYGNewsletterTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Apply new WYSIWYG in Newsletter"/> + <group value="Newsletter"/> + <title value="AWS S3 Admin should be able to add image to WYSIWYG content of Newsletter"/> + <description value="Admin should be able to add image to WYSIWYG content Newsletter"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38716"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml new file mode 100644 index 0000000000000..2b46ddcacb94c --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminAddRemoveDefaultVideoSimpleProductTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminAddRemoveDefaultVideoSimpleProductTest" extends="AdminAddRemoveDefaultVideoSimpleProductTest"> + <annotations> + <title value="AWS S3Admin should be able to add/remove default product video for a Simple Product"/> + <stories value="Add/remove images and videos for all product types and category"/> + <description value="Admin should be able to add/remove default product video for a Simple Product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38693"/> + <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MC-33903"/> + </skip> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml new file mode 100644 index 0000000000000..dd0fe36f44dde --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminCreateDownloadableProductWithLinkTest.xml @@ -0,0 +1,189 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminCreateDownloadableProductWithLinkTest"> + <annotations> + <features value="Catalog"/> + <stories value="Support remote file storage by downloadable products"/> + <title value="Create, view and check out downloadable product with remote filesystem configured. "/> + <description value="Admin should be able to create downloadable product with remote filesystem enabled"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38036"/> + <testCaseId value="MC-38037"/> + <testCaseId value="MC-38039"/> + <group value="Downloadable"/> + <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MQE-2288" /> + </skip> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add static.magento.com"/> + <!-- Create category --> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete created downloadable product --> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Create downloadable product --> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> + <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> + <argument name="productType" value="downloadable"/> + </actionGroup> + + <!-- Fill downloadable product values --> + <actionGroup ref="FillMainProductFormNoWeightActionGroup" stepKey="fillDownloadableProductForm"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Add downloadable product to category --> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[$$createCategory.name$$]" stepKey="fillCategory"/> + + <!-- Add downloadable links --> + <click selector="{{AdminProductDownloadableSection.sectionHeader}}" stepKey="openDownloadableSection"/> + <checkOption selector="{{AdminProductDownloadableSection.isDownloadableProduct}}" stepKey="checkIsDownloadable"/> + <fillField userInput="{{downloadableData.link_title}}" selector="{{AdminProductDownloadableSection.linksTitleInput}}" stepKey="fillDownloadableLinkTitle"/> + <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkLinksPurchasedSeparately"/> + <fillField userInput="{{downloadableData.sample_title}}" selector="{{AdminProductDownloadableSection.samplesTitleInput}}" stepKey="fillDownloadableSampleTitle"/> + <actionGroup ref="AddDownloadableProductLinkWithMaxDownloadsActionGroup" stepKey="addDownloadableLinkWithMaxDownloads"> + <argument name="link" value="downloadableLinkWithMaxDownloads"/> + </actionGroup> + <actionGroup ref="AddDownloadableProductLinkActionGroup" stepKey="addDownloadableLink"> + <argument name="link" value="downloadableLink"/> + </actionGroup> + <!-- Add downloadable sample--> + <actionGroup ref="AddDownloadableSampleFileActionGroup" stepKey="addDownloadableProductSample"/> + + <!-- Save product --> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> + + <!-- Login to frontend --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="signIn"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <!-- Assert product in storefront category page --> + <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <actionGroup ref="StorefrontCheckProductPriceInCategoryActionGroup" stepKey="StorefrontCheckCategorySimpleProduct"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Assert product in storefront product page --> + <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageActionGroup" stepKey="AssertProductInStorefrontProductPage"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + + <!-- Assert product price in storefront product page --> + <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{DownloadableProduct.price}}" stepKey="assertProductPrice"/> + + <!-- Assert link sample urls are accessible --> + <!-- Click on the link sample --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableLinkSampleWithMaxDownloads"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLinkWithMaxDownloads.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLinkWithMaxDownloads.title)}}" stepKey="clickDownloadableLinkSampleWithMaxDownloads"/> + <waitForPageLoad stepKey="waitForLinkSampleWithMaxDownloadsPage"/> + <!-- Grab Link Sample id --> + <switchToNextTab stepKey="switchToLinkSampleWithMaxDownloadsTab"/> + <grabFromCurrentUrl regex="~/link_id/(\d+)/~" stepKey="grabDownloadableLinkWithMaxDownloadsId"/> + <!-- Check is svg --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedSvg('Logo')}}" stepKey="assertDownloadableLinkWithMaxDownloadsIsSvg"/> + <closeTab stepKey="closeLinkSampleWithMaxDownloadsTab"/> + <!-- Click on the link sample --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableLinkSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLink.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkSampleByTitle(downloadableLink.title)}}" stepKey="clickDownloadableLinkSample"/> + <waitForPageLoad stepKey="waitForLinkSamplePage"/> + <!-- Grab Link Sample id --> + <switchToNextTab stepKey="switchToLinkSampleTab"/> + <grabFromCurrentUrl regex="~/link_id/(\d+)/~" stepKey="grabDownloadableLinkSampleId"/> + <!-- Check is image --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedImage}}" stepKey="assertDownloadableLinkSampleIsImage"/> + <closeTab stepKey="closeLinkSampleTab"/> + + <!-- Assert sample file is accessible --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableSampleLabel(downloadableSampleFile.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableSampleLabel(downloadableSampleFile.title)}}" stepKey="clickDownloadableSample"/> + <waitForPageLoad stepKey="waitForSamplePage"/> + <!-- Grab Sample id --> + <switchToNextTab stepKey="switchToSampleTab"/> + <grabFromCurrentUrl regex="~/sample_id/(\d+)/~" stepKey="grabDownloadableSampleId"/> + <!-- Check is image --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedImage}}" stepKey="assertDownloadableSampleIsImage"/> + <closeTab stepKey="closeSampleTab"/> + + <!-- Select product link in storefront product page--> + <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkBlock}}" stepKey="scrollToLinks"/> + <click selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="selectProductLink"/> + + <!-- Add product with selected link to the cart --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="DownloadableProduct"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Assert product price in cart --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$52.99" stepKey="assertProductPriceInCart"/> + + <!-- Perform checkout --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> + <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Open created order --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchOrder"> + <argument name="keyword" value="$grabOrderNumber"/> + </actionGroup> + <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> + + <!-- Open Create invoice --> + <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createCreditMemo"/> + + <!-- Check downloadable product link on frontend --> + <actionGroup ref="StorefrontAssertDownloadableProductIsPresentInCustomerAccount" stepKey="seeStorefrontMyAccountDownloadableProductsLink"> + <argument name="product" value="DownloadableProduct"/> + </actionGroup> + <click selector="{{StorefrontCustomerDownloadableProductsSection.downloadableLink}}" stepKey="clickDownloadLink" /> + <waitForPageLoad stepKey="waitForDownloadedLinkPage"/> + <!-- Grab downloadable URL --> + <switchToNextTab stepKey="switchToDownloadedLinkTab"/> + <grabFromCurrentUrl regex="~/link/id/(.+)/~" stepKey="grabDownloadLinkUrl"/> + <!-- Check is svg --> + <seeElement selector="{{StorefrontDownloadableLinkSection.downloadedSvg('Logo')}}" stepKey="assertDownloadedLinkIsSvg"/> + <closeTab stepKey="closeDownloadedLinkTab"/> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml new file mode 100644 index 0000000000000..d9dc75c18ad4b --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingCreateSitemapEntityTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminMarketingCreateSitemapEntityTest" extends="AdminMarketingCreateSitemapEntityTest"> + <annotations> + <stories value="Admin Creates Sitemap Entity"/> + <description value="Sitemap Entity Creation"/> + <severity value="MAJOR"/> + <title value="AWS S3 Sitemap Creation"/> + <testCaseId value="MC-38319"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml new file mode 100644 index 0000000000000..bbdeb7ff1155a --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3AdminMarketingSiteMapCreateNewTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3AdminMarketingSiteMapCreateNewTest" extends="AdminMarketingSiteMapCreateNewTest"> + <annotations> + <title value="AWS S3 Create New Site Map with valid data"/> + <stories value="Create Site Map"/> + <description value="Create New Site Map with valid data"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38320" /> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml new file mode 100644 index 0000000000000..6d9d89fd29be5 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3CheckingRMAPrintTest.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3CheckingRMAPrintTest" extends="CheckingRMAPrintTest"> + <annotations> + <title value="AWS S3 Checking Returns Print"/> + <stories value="Exception when try to print RMA"/> + <description value="RMA file should be downloaded"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38694"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml new file mode 100644 index 0000000000000..049caa2180d69 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3ConfigurableProductChildImageShouldBeShownOnWishListTest" extends="ConfigurableProductChildImageShouldBeShownOnWishListTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Configurable product child image should be Shown on wishlist"/> + <group value="wishlist"/> + <title value="AWS S3 when user add Configurable child product to WIshlist then child product image should be shown in Wishlist"/> + <description value="When user add Configurable child product to WIshlist then child product image should be shown in Wishlist"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38708"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml new file mode 100644 index 0000000000000..c8d2947632b59 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3StorefrontPrintOrderGuestTest.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3StorefrontPrintOrderGuestTest" extends="StorefrontPrintOrderGuestTest"> + <annotations> + <title value="AWS S3 Print Order from Guest on Frontend"/> + <stories value="Print Order"/> + <description value="Print Order from Guest on Frontend"/> + <severity value="BLOCKER"/> + <testCaseId value="MC-38689"/> + <group value="remote_storage_aws_s3"/> + <skip> + <issueId value="MQE-2288" /> + </skip> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml new file mode 100644 index 0000000000000..8e2ec348d4f41 --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Mftf/Test/AwsS3UpdateImageFileCustomerAttributeTest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AwsS3UpdateImageFileCustomerAttributeTest" extends="UpdateImageFileCustomerAttributeTest"> + <annotations> + <title value="AWS S3 Update image file customer attribute test"/> + <stories value="Update Customer Custom Attributes"/> + <description value="Update image file customer attribute"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38692"/> + <group value="remote_storage_aws_s3"/> + </annotations> + <before> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.enable_options}}" stepKey="enableRemoteStorage"/> + </before> + <after> + <magentoCLI command="setup:config:set {{RemoteStorageAwsS3ConfigData.disable_options}}" stepKey="disableRemoteStorage"/> + </after> + </test> +</tests> diff --git a/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php new file mode 100644 index 0000000000000..20bc28be4583c --- /dev/null +++ b/app/code/Magento/AwsS3/Test/Unit/Driver/AwsS3Test.php @@ -0,0 +1,438 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\AwsS3\Test\Unit\Driver; + +use League\Flysystem\AwsS3v3\AwsS3Adapter; +use League\Flysystem\Cached\CachedAdapter; +use Magento\AwsS3\Driver\AwsS3; +use Magento\Framework\Exception\FileSystemException; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * @see AwsS3 + */ +class AwsS3Test extends TestCase +{ + private const URL = 'https://test.s3.amazonaws.com/'; + + /** + * @var AwsS3 + */ + private $driver; + + /** + * @var AwsS3Adapter|MockObject + */ + private $adapterMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->adapterMock = $this->createMock(CachedAdapter::class); + $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + + $this->driver = new AwsS3($this->adapterMock, $loggerMock, self::URL); + } + + /** + * @param string|null $basePath + * @param string|null $path + * @param string $expected + * + * @dataProvider getAbsolutePathDataProvider + */ + public function testGetAbsolutePath($basePath, $path, string $expected): void + { + self::assertSame($expected, $this->driver->getAbsolutePath($basePath, $path)); + } + + /** + * @return array + */ + public function getAbsolutePathDataProvider(): array + { + return [ + [ + null, + 'test.png', + self::URL . 'test.png' + ], + [ + self::URL . 'test/test.png', + null, + self::URL . 'test/test.png' + ], + [ + '', + 'test.png', + self::URL . 'test.png' + ], + [ + '', + '/test/test.png', + self::URL . 'test/test.png' + ], + [ + self::URL . 'test/test.png', + self::URL . 'test/test.png', + self::URL . 'test/test.png' + ], + [ + self::URL, + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], + [ + '', + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], + [ + self::URL . 'test/', + 'test.txt', + self::URL . 'test/test.txt' + ], + [ + self::URL . 'media/', + 'media/image.jpg', + self::URL . 'media/image.jpg' + ], + [ + self::URL . 'media/', + '/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], + [ + self::URL, + 'var/import/images', + self::URL . 'var/import/images' + ], + [ + self::URL . 'export/', + null, + self::URL . 'export/' + ], + [ + self::URL . 'var/import/images/product_images/', + self::URL . 'var/import/images/product_images/1.png', + self::URL . 'var/import/images/product_images/1.png' + ], + [ + '', + self::URL . 'media/catalog/test.png', + self::URL . 'media/catalog/test.png' + ], + [ + self::URL, + 'var/import/images', + self::URL . 'var/import/images' + ], + [ + self::URL . 'var/import/images/product_images/', + self::URL . 'var/import/images/product_images/1.png', + self::URL . 'var/import/images/product_images/1.png' + ] + ]; + } + + /** + * @param string $basePath + * @param string $path + * @param string $expected + * + * @dataProvider getRelativePathDataProvider + */ + public function testGetRelativePath(string $basePath, string $path, string $expected): void + { + self::assertSame($expected, $this->driver->getRelativePath($basePath, $path)); + } + + /** + * @return array + */ + public function getRelativePathDataProvider(): array + { + return [ + [ + '', + 'test/test.txt', + 'test/test.txt' + ], + [ + '', + '/test/test.txt', + 'test/test.txt' + ], + [ + self::URL, + self::URL . 'test/test.txt', + 'test/test.txt' + ], + + ]; + } + + /** + * @param string $path + * @param string $normalizedPath + * @param bool $has + * @param array $metadata + * @param bool $expected + * @throws FileSystemException + * + * @dataProvider isDirectoryDataProvider + */ + public function testIsDirectory( + string $path, + string $normalizedPath, + bool $has, + array $metadata, + bool $expected + ): void { + $this->adapterMock->method('has') + ->with($normalizedPath) + ->willReturn($has); + $this->adapterMock->method('getMetadata') + ->with($normalizedPath) + ->willReturn($metadata); + + self::assertSame($expected, $this->driver->isDirectory($path)); + } + + /** + * @return array + */ + public function isDirectoryDataProvider(): array + { + return [ + [ + 'some_directory/', + 'some_directory/', + false, + [], + false + ], + [ + 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + true + ], + [ + self::URL . 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + true + ], + [ + self::URL . 'some_directory', + 'some_directory/', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + false + ], + [ + '', + '', + true, + [], + true + ], + [ + '/', + '', + true, + [], + true + ], + ]; + } + + /** + * @param string $path + * @param string $normalizedPath + * @param bool $has + * @param array $metadata + * @param bool $expected + * @throws FileSystemException + * + * @dataProvider isFileDataProvider + */ + public function testIsFile( + string $path, + string $normalizedPath, + bool $has, + array $metadata, + bool $expected + ): void { + $this->adapterMock->method('has') + ->with($normalizedPath) + ->willReturn($has); + $this->adapterMock->method('getMetadata') + ->with($normalizedPath) + ->willReturn($metadata); + + self::assertSame($expected, $this->driver->isFile($path)); + } + + /** + * @return array + */ + public function isFileDataProvider(): array + { + return [ + [ + 'some_file.txt', + 'some_file.txt', + false, + [], + false + ], + [ + 'some_file.txt/', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + true + ], + [ + self::URL . 'some_file.txt', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_FILE + ], + true + ], + [ + self::URL . 'some_file.txt/', + 'some_file.txt', + true, + [ + 'type' => AwsS3::TYPE_DIR + ], + false + ], + [ + '', + '', + false, + [], + false + ], + [ + '/', + '', + false, + [], + false + ] + ]; + } + + /** + * @param string $path + * @param string $expected + * + * @dataProvider getRealPathSafetyDataProvider + */ + public function testGetRealPathSafety(string $path, string $expected): void + { + self::assertSame($expected, $this->driver->getRealPathSafety($path)); + } + + /** + * @return array + */ + public function getRealPathSafetyDataProvider(): array + { + return [ + [ + self::URL, + self::URL + ], + [ + 'test.txt', + 'test.txt' + ], + [ + self::URL . 'test/test/../test.txt', + self::URL . 'test/test.txt' + ], + [ + 'test/test/../test.txt', + 'test/test.txt' + ] + ]; + } + + /** + * @throws FileSystemException + */ + public function testSearchDirectory(): void + { + $expression = '/*'; + $path = 'path/'; + $subPaths = [ + ['path' => 'path/1'], + ['path' => 'path/2'] + ]; + $expectedResult = ['path/1', 'path/2']; + $this->adapterMock->expects(self::atLeastOnce())->method('has') + ->willReturnMap([ + [$path, true] + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('getMetadata') + ->willReturnMap([ + [$path, ['type' => AwsS3::TYPE_DIR]] + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('listContents')->with($path, false) + ->willReturn($subPaths); + self::assertEquals($expectedResult, $this->driver->search($expression, $path)); + } + + /** + * @throws FileSystemException + */ + public function testSearchFiles(): void + { + $expression = "/*"; + $path = 'path/'; + $subPaths = [ + ['path' => 'path/1.jpg'], + ['path' => 'path/2.png'] + ]; + $expectedResult = ['path/1.jpg', 'path/2.png']; + + $this->adapterMock->expects(self::atLeastOnce())->method('has') + ->willReturnMap([ + [$path, true], + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('getMetadata') + ->willReturnMap([ + [$path, ['type' => AwsS3::TYPE_DIR]], + ]); + $this->adapterMock->expects(self::atLeastOnce())->method('listContents')->with($path, false) + ->willReturn($subPaths); + self::assertEquals($expectedResult, $this->driver->search($expression, $path)); + } +} diff --git a/app/code/Magento/AwsS3/composer.json b/app/code/Magento/AwsS3/composer.json new file mode 100644 index 0000000000000..6e72ac37f8ba6 --- /dev/null +++ b/app/code/Magento/AwsS3/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-aws-s-3", + "description": "N/A", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "^100.0.2", + "magento/module-remote-storage": "*", + "league/flysystem": "^1.0", + "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-cached-adapter": "^1.0" + }, + "type": "magento2-module", + "license": [ + "proprietary" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\AwsS3\\": "" + } + } +} diff --git a/app/code/Magento/AwsS3/etc/adminhtml/di.xml b/app/code/Magento/AwsS3/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..4d3dcd601047f --- /dev/null +++ b/app/code/Magento/AwsS3/etc/adminhtml/di.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\RemoteStorage\Model\Config\Source\FileStorage"> + <arguments> + <argument name="options" xsi:type="array"> + <item name="aws-s3" xsi:type="array"> + <item name="value" xsi:type="string">aws-s3</item> + <item name="label" xsi:type="string" translate="true">AWS S3</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/AwsS3/etc/di.xml b/app/code/Magento/AwsS3/etc/di.xml new file mode 100644 index 0000000000000..94df51fcd6856 --- /dev/null +++ b/app/code/Magento/AwsS3/etc/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\RemoteStorage\Driver\DriverFactoryPool"> + <arguments> + <argument name="pool" xsi:type="array"> + <item name="aws-s3" xsi:type="object">Magento\AwsS3\Driver\AwsS3Factory</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/AwsS3/etc/module.xml b/app/code/Magento/AwsS3/etc/module.xml new file mode 100644 index 0000000000000..ab99195d45ab5 --- /dev/null +++ b/app/code/Magento/AwsS3/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_AwsS3"> + <sequence> + <module name="Magento_RemoteStorage"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/AwsS3/registration.php b/app/code/Magento/AwsS3/registration.php new file mode 100644 index 0000000000000..496fbad1d3371 --- /dev/null +++ b/app/code/Magento/AwsS3/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_AwsS3', __DIR__); diff --git a/setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php b/app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php similarity index 85% rename from setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php rename to app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php index 85ae008adf366..eb452f62e91ce 100644 --- a/setup/src/Magento/Setup/Console/Command/AbstractMaintenanceCommand.php +++ b/app/code/Magento/Backend/Console/Command/AbstractMaintenanceCommand.php @@ -3,14 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Validator\IpValidator; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; +use Magento\Backend\Model\Validator\IpValidator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +/** + * General maintenance command. + */ abstract class AbstractMaintenanceCommand extends AbstractSetupCommand { /** @@ -38,6 +43,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal { $this->maintenanceMode = $maintenanceMode; $this->ipValidator = $ipValidator; + parent::__construct(); } @@ -57,6 +63,7 @@ protected function configure() ), ]; $this->setDefinition($options); + parent::configure(); } @@ -75,16 +82,18 @@ abstract protected function isEnable(); abstract protected function getDisplayString(); /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $addresses = $input->getOption(self::INPUT_KEY_IP); $messages = $this->validate($addresses); + if (!empty($messages)) { $output->writeln('<error>' . implode('</error>' . PHP_EOL . '<error>', $messages)); - // we must have an exit code higher than zero to indicate something was wrong - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + + // We must have an exit code higher than zero to indicate something was wrong + return Cli::RETURN_FAILURE; } $this->maintenanceMode->set($this->isEnable()); @@ -92,14 +101,15 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!empty($addresses)) { $addresses = implode(',', $addresses); - $addresses = ('none' == $addresses) ? '' : $addresses; + $addresses = ('none' === $addresses) ? '' : $addresses; $this->maintenanceMode->setAddresses($addresses); $output->writeln( '<info>Set exempt IP-addresses: ' . (implode(', ', $this->maintenanceMode->getAddressInfo()) ?: 'none') . '</info>' ); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php similarity index 90% rename from setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php index 09f33cf85062c..230c6a6814ebc 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceAllowIpsCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceAllowIpsCommand.php @@ -3,12 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Framework\Module\ModuleList; -use Magento\Setup\Validator\IpValidator; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; +use Magento\Backend\Model\Validator\IpValidator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -37,8 +37,6 @@ class MaintenanceAllowIpsCommand extends AbstractSetupCommand private $ipValidator; /** - * Constructor - * * @param MaintenanceMode $maintenanceMode * @param IpValidator $ipValidator */ @@ -46,6 +44,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal { $this->maintenanceMode = $maintenanceMode; $this->ipValidator = $ipValidator; + parent::__construct(); } @@ -54,7 +53,7 @@ public function __construct(MaintenanceMode $maintenanceMode, IpValidator $ipVal * * @return void */ - protected function configure() + protected function configure(): void { $arguments = [ new InputArgument( @@ -80,19 +79,21 @@ protected function configure() $this->setName('maintenance:allow-ips') ->setDescription('Sets maintenance mode exempt IPs') ->setDefinition(array_merge($arguments, $options)); + parent::configure(); } /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { if (!$input->getOption(self::INPUT_KEY_NONE)) { $addresses = $input->getArgument(self::INPUT_KEY_IP); $messages = $this->validate($addresses); if (!empty($messages)) { $output->writeln('<error>' . implode('</error>' . PHP_EOL . '<error>', $messages)); + // we must have an exit code higher than zero to indicate something was wrong return \Magento\Framework\Console\Cli::RETURN_FAILURE; } @@ -111,7 +112,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->maintenanceMode->setAddresses(''); $output->writeln('<info>Set exempt IP-addresses: none</info>'); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** @@ -120,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output) * @param string[] $addresses * @return string[] */ - protected function validate(array $addresses) + protected function validate(array $addresses): array { return $this->ipValidator->validateIps($addresses, false); } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php similarity index 84% rename from setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php index abebbdb76346b..5108866fbe65c 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceDisableCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceDisableCommand.php @@ -3,8 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; /** * Command for disabling maintenance mode @@ -19,6 +18,7 @@ class MaintenanceDisableCommand extends AbstractMaintenanceCommand protected function configure() { $this->setName('maintenance:disable')->setDescription('Disables maintenance mode'); + parent::configure(); } @@ -27,7 +27,7 @@ protected function configure() * * @return bool */ - protected function isEnable() + protected function isEnable(): bool { return false; } @@ -37,7 +37,7 @@ protected function isEnable() * * @return string */ - protected function getDisplayString() + protected function getDisplayString(): string { return '<info>Disabled maintenance mode</info>'; } @@ -47,7 +47,7 @@ protected function getDisplayString() * * @return bool */ - public function isSetAddressInfo() + public function isSetAddressInfo(): bool { return count($this->maintenanceMode->getAddressInfo()) > 0; } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php similarity index 80% rename from setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php index 94ab312b60811..7e5e034483d20 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceEnableCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceEnableCommand.php @@ -3,8 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; /** * Command for enabling maintenance mode @@ -16,9 +15,10 @@ class MaintenanceEnableCommand extends AbstractMaintenanceCommand * * @return void */ - protected function configure() + protected function configure(): void { $this->setName('maintenance:enable')->setDescription('Enables maintenance mode'); + parent::configure(); } @@ -27,7 +27,7 @@ protected function configure() * * @return bool */ - protected function isEnable() + protected function isEnable(): bool { return true; } @@ -37,7 +37,7 @@ protected function isEnable() * * @return string */ - protected function getDisplayString() + protected function getDisplayString(): string { return '<info>Enabled maintenance mode</info>'; } diff --git a/setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php b/app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php similarity index 85% rename from setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php rename to app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php index f2d3d2bf30caa..e7feae32cf8b0 100644 --- a/setup/src/Magento/Setup/Console/Command/MaintenanceStatusCommand.php +++ b/app/code/Magento/Backend/Console/Command/MaintenanceStatusCommand.php @@ -3,11 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -namespace Magento\Setup\Console\Command; +namespace Magento\Backend\Console\Command; use Magento\Framework\App\MaintenanceMode; -use Magento\Framework\Module\ModuleList; +use Magento\Framework\Console\Cli; +use Magento\Setup\Console\Command\AbstractSetupCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -29,6 +29,7 @@ class MaintenanceStatusCommand extends AbstractSetupCommand public function __construct(MaintenanceMode $maintenanceMode) { $this->maintenanceMode = $maintenanceMode; + parent::__construct(); } @@ -37,17 +38,18 @@ public function __construct(MaintenanceMode $maintenanceMode) * * @return void */ - protected function configure() + protected function configure(): void { $this->setName('maintenance:status') ->setDescription('Displays maintenance mode status'); + parent::configure(); } /** - * {@inheritdoc} + * @inheritDoc */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln( '<info>Status: maintenance mode is ' . @@ -56,6 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $addressInfo = $this->maintenanceMode->getAddressInfo(); $addresses = implode(' ', $addressInfo); $output->writeln('<info>List of exempt IP-addresses: ' . ($addresses ? $addresses : 'none') . '</info>'); - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } } diff --git a/app/code/Magento/Backend/Console/CommandList.php b/app/code/Magento/Backend/Console/CommandList.php new file mode 100644 index 0000000000000..563ef964812ab --- /dev/null +++ b/app/code/Magento/Backend/Console/CommandList.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Backend\Console; + +use Magento\Backend\Console\Command\MaintenanceAllowIpsCommand; +use Magento\Backend\Console\Command\MaintenanceDisableCommand; +use Magento\Backend\Console\Command\MaintenanceEnableCommand; +use Magento\Backend\Console\Command\MaintenanceStatusCommand; +use Magento\Framework\Console\CommandListInterface; +use Magento\Framework\ObjectManagerInterface; + +/** + * Provides list of commands to be available for uninstalled application + */ +class CommandList implements CommandListInterface +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @param ObjectManagerInterface $objectManager + */ + public function __construct(ObjectManagerInterface $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * Gets list of command classes + * + * @return string[] + */ + private function getCommandsClasses(): array + { + return [ + MaintenanceAllowIpsCommand::class, + MaintenanceDisableCommand::class, + MaintenanceEnableCommand::class, + MaintenanceStatusCommand::class + ]; + } + + /** + * @inheritdoc + */ + public function getCommands(): array + { + $commands = []; + foreach ($this->getCommandsClasses() as $class) { + if (class_exists($class)) { + $commands[] = $this->objectManager->get($class); + } else { + throw new \RuntimeException('Class ' . $class . ' does not exist'); + } + } + + return $commands; + } +} diff --git a/setup/src/Magento/Setup/Validator/IpValidator.php b/app/code/Magento/Backend/Model/Validator/IpValidator.php similarity index 97% rename from setup/src/Magento/Setup/Validator/IpValidator.php rename to app/code/Magento/Backend/Model/Validator/IpValidator.php index 5d1e83021e34b..f208d02ee140a 100644 --- a/setup/src/Magento/Setup/Validator/IpValidator.php +++ b/app/code/Magento/Backend/Model/Validator/IpValidator.php @@ -3,7 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -namespace Magento\Setup\Validator; +namespace Magento\Backend\Model\Validator; /** * Class to validate list of IPs for maintenance commands diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml index 186bb183d68d6..4ebb3316a0245 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminHeaderSection.xml @@ -11,6 +11,8 @@ <section name="AdminHeaderSection"> <element name="pageTitle" type="text" selector=".page-header h1.page-title"/> <element name="adminUserAccountText" type="text" selector=".page-header .admin-user-account-text" /> + <element name="globalSearchInput" type="text" selector="#search-global" /> + <element name="globalSearchInputVisible" type="text" selector=".search-global-field._active #search-global" /> <!-- Legacy heading section. Mostly used for admin 404 and 403 pages --> <element name="pageHeading" type="text" selector=".page-content .page-heading"/> <!-- Used for page not found error --> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml index 2469151337bfe..b87b92e86528c 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml @@ -29,7 +29,7 @@ <!-- 2. Wait for session to expire. --> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <wait time="60" stepKey="waitForSessionLifetime"/> - <reloadPage stepKey="reloadPage"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> <!-- 3. Perform asserts. --> <seeElement selector="{{AdminLoginFormSection.loginBlock}}" stepKey="assertAdminLoginPageIsAvailable"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml index 0e3bf07d32441..b2b71c4ad3eca 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml @@ -40,7 +40,7 @@ <argument name="Customer" value="$$createCustomer$$" /> </actionGroup> <wait time="60" stepKey="waitForCookieLifetime"/> - <reloadPage stepKey="reloadPage"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> <!-- 5. Perform asserts. --> <seeElement selector="{{StorefrontPanelHeaderSection.customerLoginLink}}" stepKey="assertAuthorizationLinkIsVisibleOnStoreFront"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml new file mode 100644 index 0000000000000..89e8668fa3c23 --- /dev/null +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminSearchHotkeyTest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminSearchHotkeyTest"> + <annotations> + <features value="Backend"/> + <stories value="Search form hotkey in backend"/> + <title value="Admin should be able focus on the search field with a hotkey"/> + <description value="Admin should be able focus on the search field with a hotkey - forwardslash"/> + <severity value="MINOR"/> + <group value="backend"/> + <group value="search"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <pressKey selector="body" parameterArray="[/]" stepKey="pressForwardslashKey"/> + <seeElement selector="{{AdminHeaderSection.globalSearchInputVisible}}" stepKey="seeActiveGlobalSearchInput"/> + <seeInField userInput="" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeEmptyGlobalSearchInput"/> + <pressKey selector="{{AdminHeaderSection.globalSearchInput}}" parameterArray="[/]" stepKey="pressForwardslashKeyAgain"/> + <seeInField userInput="/" selector="{{AdminHeaderSection.globalSearchInput}}" stepKey="seeForwardSlashInGlobalSearchInput"/> + </test> +</tests> diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php similarity index 96% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php index 2a18a892ed06d..281065c51337d 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceAllowIpsCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceAllowIpsCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceAllowIpsCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php similarity index 95% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php index 73afa22f3ebcd..6663a7f9f6504 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceDisableCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceDisableCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceDisableCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php similarity index 93% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php index 0b1afb7310c08..c4a2e35d37d49 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceEnableCommandTest.php @@ -5,11 +5,11 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceEnableCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceEnableCommand; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php similarity index 95% rename from setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php rename to app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php index 731eff370b00f..8e3970aa5529e 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php +++ b/app/code/Magento/Backend/Test/Unit/Console/Command/MaintenanceStatusCommandTest.php @@ -5,10 +5,10 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Console\Command; +namespace Magento\Backend\Test\Unit\Console\Command; +use Magento\Backend\Console\Command\MaintenanceStatusCommand; use Magento\Framework\App\MaintenanceMode; -use Magento\Setup\Console\Command\MaintenanceStatusCommand; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; diff --git a/setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php b/app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php similarity index 79% rename from setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php rename to app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php index b6f9f01c80ee5..ccffc58d79780 100644 --- a/setup/src/Magento/Setup/Test/Unit/Validator/IpValidatorTest.php +++ b/app/code/Magento/Backend/Test/Unit/Model/Validator/IpValidatorTest.php @@ -5,11 +5,14 @@ */ declare(strict_types=1); -namespace Magento\Setup\Test\Unit\Validator; +namespace Magento\Backend\Test\Unit\Model\Validator; -use Magento\Setup\Validator\IpValidator; +use Magento\Backend\Model\Validator\IpValidator; use PHPUnit\Framework\TestCase; +/** + * @see IpValidator + */ class IpValidatorTest extends TestCase { /** @@ -17,6 +20,9 @@ class IpValidatorTest extends TestCase */ private $ipValidator; + /** + * @inheritDoc + */ protected function setUp(): void { $this->ipValidator = new IpValidator(); @@ -27,15 +33,15 @@ protected function setUp(): void * @param string[] $ips * @param string[] $expectedMessages */ - public function testValidateIpsNoneAllowed($ips, $expectedMessages) + public function testValidateIpsNoneAllowed(array $ips, array $expectedMessages): void { - $this->assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, true)); + self::assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, true)); } /** * @return array */ - public function validateIpsNoneAllowedDataProvider() + public function validateIpsNoneAllowedDataProvider(): array { return [ [['127.0.0.1', '127.0.0.2'], []], @@ -54,9 +60,9 @@ public function validateIpsNoneAllowedDataProvider() * @param string[] $ips * @param string[] $expectedMessages */ - public function testValidateIpsNoneNotAllowed($ips, $expectedMessages) + public function testValidateIpsNoneNotAllowed($ips, $expectedMessages): void { - $this->assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, false)); + self::assertEquals($expectedMessages, $this->ipValidator->validateIps($ips, false)); } /** diff --git a/app/code/Magento/Backend/cli_commands.php b/app/code/Magento/Backend/cli_commands.php new file mode 100644 index 0000000000000..3c4140b40a993 --- /dev/null +++ b/app/code/Magento/Backend/cli_commands.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +if (PHP_SAPI === 'cli') { + \Magento\Framework\Console\CommandLocator::register(\Magento\Backend\Console\CommandList::class); +} diff --git a/app/code/Magento/Backend/composer.json b/app/code/Magento/Backend/composer.json index ee5491057d861..29a78d9ae0ab0 100644 --- a/app/code/Magento/Backend/composer.json +++ b/app/code/Magento/Backend/composer.json @@ -10,6 +10,7 @@ "magento/module-backup": "*", "magento/module-catalog": "*", "magento/module-config": "*", + "magento/module-cms": "*", "magento/module-customer": "*", "magento/module-developer": "*", "magento/module-directory": "*", @@ -34,7 +35,8 @@ ], "autoload": { "files": [ - "registration.php" + "registration.php", + "cli_commands.php" ], "psr-4": { "Magento\\Backend\\": "" diff --git a/app/code/Magento/Backend/etc/di.xml b/app/code/Magento/Backend/etc/di.xml index 65f73f028eb20..1297bd9603a1f 100644 --- a/app/code/Magento/Backend/etc/di.xml +++ b/app/code/Magento/Backend/etc/di.xml @@ -150,6 +150,10 @@ <item name="cacheFlushCommand" xsi:type="object">Magento\Backend\Console\Command\CacheFlushCommand</item> <item name="cacheCleanCommand" xsi:type="object">Magento\Backend\Console\Command\CacheCleanCommand</item> <item name="cacheStatusCommand" xsi:type="object">Magento\Backend\Console\Command\CacheStatusCommand</item> + <item name="maintenanceAllowIps" xsi:type="object">Magento\Backend\Console\Command\MaintenanceAllowIpsCommand</item> + <item name="maintenanceDisable" xsi:type="object">Magento\Backend\Console\Command\MaintenanceDisableCommand</item> + <item name="maintenanceEnableCommand" xsi:type="object">Magento\Backend\Console\Command\MaintenanceDisableCommand</item> + <item name="maintenanceStatusCommand" xsi:type="object">Magento\Backend\Console\Command\MaintenanceStatusCommand</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Backup/Model/Fs/Collection.php b/app/code/Magento/Backup/Model/Fs/Collection.php index b17c17f7074fb..41a497495f687 100644 --- a/app/code/Magento/Backup/Model/Fs/Collection.php +++ b/app/code/Magento/Backup/Model/Fs/Collection.php @@ -45,6 +45,7 @@ class Collection extends \Magento\Framework\Data\Collection\Filesystem * @param \Magento\Backup\Helper\Data $backupData * @param \Magento\Framework\Filesystem $filesystem * @param \Magento\Backup\Model\Backup $backup + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, @@ -53,7 +54,7 @@ public function __construct( \Magento\Backup\Model\Backup $backup ) { $this->_backupData = $backupData; - parent::__construct($entityFactory); + parent::__construct($entityFactory, $filesystem); $this->_filesystem = $filesystem; $this->_backup = $backup; diff --git a/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php b/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php index 69e2fcb6e1f25..cec0ccff70ce6 100644 --- a/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php +++ b/app/code/Magento/Backup/Test/Unit/Model/Fs/CollectionTest.php @@ -10,6 +10,7 @@ use Magento\Backup\Helper\Data; use Magento\Backup\Model\Fs\Collection; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; @@ -36,10 +37,23 @@ public function testConstructor() $directoryWrite->expects($this->any())->method('create')->with('backups'); $directoryWrite->expects($this->any())->method('getAbsolutePath')->with('backups'); - + $directoryWrite->expects($this->any())->method('isDirectory')->willReturn(true); + $targetDirectory = $this->getMockBuilder(TargetDirectory::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDirectoryWrite->expects($this->any())->method('isDirectory')->willReturn(true); + $targetDirectory->expects($this->any())->method('getDirectoryWrite')->willReturn($targetDirectoryWrite); $classObject = $helper->getObject( Collection::class, - ['filesystem' => $filesystem, 'backupData' => $backupData] + [ + 'filesystem' => $filesystem, + 'backupData' => $backupData, + 'directoryWrite' => $directoryWrite, + 'targetDirectory' => $targetDirectory + ] ); $this->assertNotNull($classObject); } diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index fe120e9a179dd..6ee67859db015 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -13,6 +13,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\File\UploaderFactory; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Serialize\Serializer\Json; use Magento\Framework\Stdlib\ArrayUtils; @@ -190,11 +191,11 @@ class Type extends \Magento\Catalog\Model\Product\Type\AbstractType * @param PriceCurrencyInterface $priceCurrency * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry * @param \Magento\CatalogInventory\Api\StockStateInterface $stockState - * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param Json|null $serializer * @param MetadataPool|null $metadataPool * @param SelectionCollectionFilterApplier|null $selectionCollectionFilterApplier * @param ArrayUtils|null $arrayUtility - * + * @param UploaderFactory $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -222,7 +223,8 @@ public function __construct( Json $serializer = null, MetadataPool $metadataPool = null, SelectionCollectionFilterApplier $selectionCollectionFilterApplier = null, - ArrayUtils $arrayUtility = null + ArrayUtils $arrayUtility = null, + UploaderFactory $uploaderFactory = null ) { $this->_catalogProduct = $catalogProduct; $this->_catalogData = $catalogData; @@ -254,7 +256,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php index 96d68d7e74117..55bef4980098b 100644 --- a/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php +++ b/app/code/Magento/Bundle/Model/ResourceModel/Indexer/Price.php @@ -17,6 +17,7 @@ use Magento\Customer\Model\Indexer\CustomerGroupDimensionProvider; use Magento\Store\Model\Indexer\WebsiteDimensionProvider; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogInventory\Model\Stock; /** * Bundle products Price indexer resource model @@ -624,6 +625,13 @@ private function calculateDynamicBundleSelectionPrice($dimensions) 'tier_price' => $tierExpr, ] ); + $select->join( + ['si' => $this->getTable('cataloginventory_stock_status')], + 'si.product_id = bs.product_id', + [] + ); + $select->where('si.stock_status = ?', Stock::STOCK_IN_STOCK); + $this->tableMaintainer->insertFromSelect($select, $this->getBundleSelectionTable(), []); } diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml new file mode 100644 index 0000000000000..fe4faed29d144 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontBundlePlaceOrderWithVirtualAndSimpleChildrenTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product placing order"/> + <title value="Admin should be able to invoice order for the bundle product with virtual and simple products in options"/> + <description value="Place order for bundle product and create invoice"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38683"/> + <useCaseId value="MC-37663"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="CustomerEntityOne" stepKey="createCustomer"/> + <!--Create bundle product with fixed price with simple and virtual products in options--> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">100.00</field> + </createData> + <createData entity="VirtualProduct" stepKey="createVirtualProduct"> + <field key="price">50.00</field> + </createData> + <createData entity="ApiFixedBundleProduct" stepKey="createFixedBundleProduct"/> + <createData entity="DropDownBundleOption" stepKey="createFirstBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="DropDownBundleOption" stepKey="createSecondBundleOption"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="firstLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createFirstBundleOption"/> + <requiredEntity createDataKey="createSimpleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="secondLinkOptionToFixedProduct"> + <requiredEntity createDataKey="createFixedBundleProduct"/> + <requiredEntity createDataKey="createSecondBundleOption"/> + <requiredEntity createDataKey="createVirtualProduct"/> + </createData> + <actionGroup stepKey="loginToAdminPanel" ref="AdminLoginActionGroup"/> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$createFixedBundleProduct.id$"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <!--Perform reindex and flush cache--> + <actionGroup ref="AdminReindexAndFlushCache" stepKey="reindexAndFlushCache"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProductForBundleItem"/> + <deleteData createDataKey="createVirtualProduct" stepKey="deleteVirtualProductForBundleItem"/> + <deleteData createDataKey="createFixedBundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> + </after> + <!--Login customer on storefront--> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginCustomer"> + <argument name="Customer" value="$$createCustomer$$" /> + </actionGroup> + <!--Open Product Page--> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openBundleProductPage"> + <argument name="product" value="$createFixedBundleProduct$"/> + </actionGroup> + <!--Add bundle to cart--> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickAddToCart"> + <argument name="productUrl" value="$createFixedBundleProduct.name$"/> + </actionGroup> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + <!--Navigate to checkout--> + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="openCheckoutPage"/> + <!--Click next button to open payment section--> + <actionGroup ref="StorefrontCheckoutClickNextButtonActionGroup" stepKey="clickNext"/> + <!--Click place order--> + <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + <!--Order review page has address that was created during checkout--> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <!--Create Invoice for this Order--> + <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="createInvoice"/> + <actionGroup ref="SubmitInvoiceActionGroup" stepKey="submitInvoice"/> + </test> +</tests> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml index 9e99fa96ee766..66183cb31aebc 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -46,8 +46,10 @@ <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaField}}" stepKey="seeCaptchaField"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaImg}}" stepKey="seeCaptchaImage"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.captchaReload}}" stepKey="seeCaptchaReloadButton"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> + + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad2" /> + <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickCart2"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout2"/> <waitForElementVisible selector="{{StorefrontCustomerSignInPopupFormSection.email}}" stepKey="waitEmailFieldVisible2"/> diff --git a/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php b/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php index ec9f6f03134cc..4b9286f69cce5 100644 --- a/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Helper/DataTest.php @@ -197,7 +197,7 @@ public function testGetImgDir() */ public function testGetImgUrl() { - $this->assertEquals($this->helper->getImgUrl(), 'http://localhost/pub/media/captcha/base/'); + $this->assertEquals($this->helper->getImgUrl(), 'http://localhost/media/captcha/base/'); } /** @@ -223,7 +223,7 @@ protected function _getStoreStub() { $store = $this->createMock(Store::class); - $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/pub/media/'); + $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/media/'); return $store; } diff --git a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php index 59399cde99ea8..1d222e273dc1f 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/DefaultTest.php @@ -243,7 +243,7 @@ public function testGetImgSrc() { $this->assertEquals( $this->_object->getImgSrc(), - 'http://localhost/pub/media/captcha/base/' . $this->_object->getId() . '.png' + 'http://localhost/media/captcha/base/' . $this->_object->getId() . '.png' ); } @@ -336,7 +336,7 @@ protected function _getHelperStub() )->method( 'getImgUrl' )->willReturn( - 'http://localhost/pub/media/captcha/base/' + 'http://localhost/media/captcha/base/' ); return $helper; @@ -391,7 +391,7 @@ protected function _getStoreStub() ->onlyMethods(['getBaseUrl']) ->disableOriginalConstructor() ->getMock(); - $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/pub/media/'); + $store->expects($this->any())->method('getBaseUrl')->willReturn('http://localhost/media/'); $store->expects($this->any())->method('isAdmin')->willReturn(false); return $store; } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php index 49165c85f85d7..1d6939acacfd0 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper/AttributeFilter.php @@ -80,7 +80,7 @@ private function prepareDefaultData(array $attributeList, string $attributeCode, // For non-numeric types set the attributeValue to 'false' to trigger their removal from the db if ($attributeType === 'varchar' || $attributeType === 'text' || $attributeType === 'datetime') { $attribute->setIsRequired(false); - $productData[$attributeCode] = false; + $productData[$attributeCode] = $attribute->getDefaultValue() ?: false; } else { $productData[$attributeCode] = null; } diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index ab74b5694ce9f..de32f6b7637d4 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -5,7 +5,10 @@ */ namespace Magento\Catalog\Helper; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Framework\App\Helper\AbstractHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Element\Block\ArgumentInterface; /** @@ -133,27 +136,34 @@ class Image extends AbstractHelper implements ArgumentInterface */ private $viewAssetPlaceholderFactory; + /** + * @var CatalogMediaConfig + */ + private $mediaConfig; + /** * @param \Magento\Framework\App\Helper\Context $context * @param \Magento\Catalog\Model\Product\ImageFactory $productImageFactory * @param \Magento\Framework\View\Asset\Repository $assetRepo * @param \Magento\Framework\View\ConfigInterface $viewConfig * @param \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory + * @param CatalogMediaConfig $mediaConfig */ public function __construct( \Magento\Framework\App\Helper\Context $context, \Magento\Catalog\Model\Product\ImageFactory $productImageFactory, \Magento\Framework\View\Asset\Repository $assetRepo, \Magento\Framework\View\ConfigInterface $viewConfig, - \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory = null + \Magento\Catalog\Model\View\Asset\PlaceholderFactory $placeholderFactory = null, + CatalogMediaConfig $mediaConfig = null ) { $this->_productImageFactory = $productImageFactory; parent::__construct($context); $this->_assetRepo = $assetRepo; $this->viewConfig = $viewConfig; $this->viewAssetPlaceholderFactory = $placeholderFactory - ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class); + ?: ObjectManager::getInstance()->get(\Magento\Catalog\Model\View\Asset\PlaceholderFactory::class); + $this->mediaConfig = $mediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); } /** @@ -532,7 +542,16 @@ protected function isScheduledActionsAllowed() public function getUrl() { try { - $this->applyScheduledActions(); + switch ($this->mediaConfig->getMediaUrlFormat()) { + case CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS: + $this->initBaseFile(); + break; + case CatalogMediaConfig::HASH: + $this->applyScheduledActions(); + break; + default: + throw new LocalizedException(__("The specified Catalog media URL format is not supported.")); + } return $this->_getModel()->getUrl(); } catch (\Exception $e) { return $this->getDefaultPlaceholderUrl(); diff --git a/app/code/Magento/Catalog/Model/Category/FileInfo.php b/app/code/Magento/Catalog/Model/Category/FileInfo.php index 7d679f2645be1..f5aec60b2fcc0 100644 --- a/app/code/Magento/Catalog/Model/Category/FileInfo.php +++ b/app/code/Magento/Catalog/Model/Category/FileInfo.php @@ -239,7 +239,8 @@ private function getMediaDirectoryPathRelativeToBaseDirectoryPath(string $filePa $mediaDirectoryRelativeSubpath = substr($mediaDirectoryPath, strlen($baseDirectoryPath)); $pubDirectory = $baseDirectory->getRelativePath($pubDirectoryPath); - if (strpos($mediaDirectoryRelativeSubpath, $pubDirectory) === 0 && strpos($filePath, $pubDirectory) !== 0) { + if ($pubDirectory && strpos($mediaDirectoryRelativeSubpath, $pubDirectory) === 0 + && strpos($filePath, $pubDirectory) !== 0) { $mediaDirectoryRelativeSubpath = substr($mediaDirectoryRelativeSubpath, strlen($pubDirectory)); } diff --git a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php index 44bf153f83697..320a253a9a1dd 100644 --- a/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php +++ b/app/code/Magento/Catalog/Model/Category/Product/PositionResolver.php @@ -49,4 +49,30 @@ public function getPositions(int $categoryId): array return array_flip($connection->fetchCol($select)); } + + /** + * Get category product minimum position + * + * @param int $categoryId + * @return int + */ + public function getMinPosition(int $categoryId): int + { + $connection = $this->getConnection(); + + $select = $connection->select()->from( + ['cpe' => $this->getTable('catalog_product_entity')], + ['position' => new \Zend_Db_Expr('MIN(position)')] + )->joinLeft( + ['ccp' => $this->getTable('catalog_category_product')], + 'ccp.product_id=cpe.entity_id' + )->where( + 'ccp.category_id = ?', + $categoryId + )->order( + 'ccp.product_id ' . \Magento\Framework\DB\Select::SQL_DESC + ); + + return (int)$connection->fetchOne($select); + } } diff --git a/app/code/Magento/Catalog/Model/CategoryLinkManagement.php b/app/code/Magento/Catalog/Model/CategoryLinkManagement.php index 8966848a6d036..591cbc32a0d86 100644 --- a/app/code/Magento/Catalog/Model/CategoryLinkManagement.php +++ b/app/code/Magento/Catalog/Model/CategoryLinkManagement.php @@ -3,11 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model; /** - * Class CategoryLinkManagement + * Represents Category Product Link Management class */ class CategoryLinkManagement implements \Magento\Catalog\Api\CategoryLinkManagementInterface { @@ -56,7 +57,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getAssignedProducts($categoryId) { @@ -65,6 +66,7 @@ public function getAssignedProducts($categoryId) /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $products */ $products = $category->getProductCollection(); $products->addFieldToSelect('position'); + $products->groupByAttribute($products->getProductEntityMetadata()->getIdentifierField()); /** @var \Magento\Catalog\Api\Data\CategoryProductLinkInterface[] $links */ $links = []; diff --git a/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php new file mode 100644 index 0000000000000..0ae128b34d348 --- /dev/null +++ b/app/code/Magento/Catalog/Model/Config/CatalogMediaConfig.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Config; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Config for catalog media + */ +class CatalogMediaConfig +{ + private const XML_PATH_CATALOG_MEDIA_URL_FORMAT = 'web/url/catalog_media_url_format'; + + const IMAGE_OPTIMIZATION_PARAMETERS = 'image_optimization_parameters'; + const HASH = 'hash'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Get media URL format for catalog images + * + * @param string $scopeType + * @param null|int|string $scopeCode + * @return string + */ + public function getMediaUrlFormat($scopeType = ScopeConfigInterface::SCOPE_TYPE_DEFAULT, $scopeCode = null): string + { + return $this->scopeConfig->getValue( + CatalogMediaConfig::XML_PATH_CATALOG_MEDIA_URL_FORMAT, + $scopeType, + $scopeCode + ); + } +} diff --git a/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php b/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php new file mode 100644 index 0000000000000..bab2d5ccb3f1f --- /dev/null +++ b/app/code/Magento/Catalog/Model/Config/Source/Web/CatalogMediaUrlFormat.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Config\Source\Web; + +use Magento\Catalog\Model\Config\CatalogMediaConfig; + +/** + * Option provider for catalog media URL format system setting. + */ +class CatalogMediaUrlFormat implements \Magento\Framework\Data\OptionSourceInterface +{ + /** + * Get a list of supported catalog media URL formats. + * + * @codeCoverageIgnore + * @return array + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS, + 'label' => __('Image optimization based on query parameters') + ], + ['value' => CatalogMediaConfig::HASH, 'label' => __('Unique hash per image variant (Legacy mode)')] + ]; + } +} diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php index 5d81c1405efe0..c53277a58157d 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/Action/Rows.php @@ -13,15 +13,17 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\DB\Query\Generator as QueryGenerator; use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Config; use Magento\Catalog\Model\Category; -use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Indexer\Model\WorkingStateProvider; /** * Reindex multiple rows action. * - * @package Magento\Catalog\Model\Indexer\Category\Product\Action * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractAction @@ -48,15 +50,23 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ private $indexerRegistry; + /** + * @var WorkingStateProvider + */ + private $workingStateProvider; + /** * @param ResourceConnection $resource * @param StoreManagerInterface $storeManager * @param Config $config * @param QueryGenerator|null $queryGenerator * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer * @param CacheContext|null $cacheContext * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry + * @param WorkingStateProvider|null $workingStateProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Preserve compatibility with the parent class */ public function __construct( ResourceConnection $resource, @@ -64,14 +74,18 @@ public function __construct( Config $config, QueryGenerator $queryGenerator = null, MetadataPool $metadataPool = null, + ?TableMaintainer $tableMaintainer = null, CacheContext $cacheContext = null, EventManagerInterface $eventManager = null, - IndexerRegistry $indexerRegistry = null + IndexerRegistry $indexerRegistry = null, + ?WorkingStateProvider $workingStateProvider = null ) { - parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); + parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool, $tableMaintainer); $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); + $this->workingStateProvider = $workingStateProvider ?: + ObjectManager::getInstance()->get(WorkingStateProvider::class); } /** @@ -97,44 +111,64 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByCategories = array_unique($this->limitationByCategories); $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); - $workingState = $indexer->isWorking(); + $workingState = $this->isWorkingState(); - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->getIndexTable($store->getId())); + if (!$indexer->isScheduled() + || ($indexer->isScheduled() && !$useTempTable) + || ($indexer->isScheduled() && $useTempTable && !$workingState)) { + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->truncateTable($this->getIndexTable($store->getId())); + } + } else { + $this->removeEntries(); } - } else { - $this->removeEntries(); - } - $this->reindex(); - - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]); - $this->connection->delete( - $this->tableMaintainer->getMainTable($store->getId()), - ['category_id IN (?)' => $removalCategoryIds] - ); - $select = $this->connection->select() - ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); - $this->connection->query( - $this->connection->insertFromSelect( - $select, + $this->reindex(); + + // get actual state + $workingState = $this->isWorkingState(); + + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $removalCategoryIds = array_diff($this->limitationByCategories, [$this->getRootCategoryId($store)]); + $this->connection->delete( $this->tableMaintainer->getMainTable($store->getId()), - [], - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); + ['category_id IN (?)' => $removalCategoryIds] + ); + $select = $this->connection->select() + ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->query( + $this->connection->insertFromSelect( + $select, + $this->tableMaintainer->getMainTable($store->getId()), + [], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } } - } - $this->registerCategories($entityIds); - $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + $this->registerCategories($entityIds); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + } return $this; } + /** + * Get state for current and shared indexer + * + * @return bool + */ + private function isWorkingState() : bool + { + $indexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); + $sharedIndexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); + return $this->workingStateProvider->isWorking($indexer->getId()) + || $this->workingStateProvider->isWorking($sharedIndexer->getId()); + } + /** * Register categories assigned to products * diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php index 861f7c9c1c50e..ab04f7c56c3db 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Category/Action/Rows.php @@ -17,7 +17,10 @@ use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Indexer\Model\WorkingStateProvider; /** * Category rows indexer. @@ -48,15 +51,23 @@ class Rows extends \Magento\Catalog\Model\Indexer\Category\Product\AbstractActio */ private $indexerRegistry; + /** + * @var WorkingStateProvider + */ + private $workingStateProvider; + /** * @param ResourceConnection $resource * @param StoreManagerInterface $storeManager * @param Config $config * @param QueryGenerator|null $queryGenerator * @param MetadataPool|null $metadataPool + * @param TableMaintainer|null $tableMaintainer * @param CacheContext|null $cacheContext * @param EventManagerInterface|null $eventManager * @param IndexerRegistry|null $indexerRegistry + * @param WorkingStateProvider|null $workingStateProvider + * @SuppressWarnings(PHPMD.ExcessiveParameterList) Preserve compatibility with the parent class */ public function __construct( ResourceConnection $resource, @@ -64,14 +75,18 @@ public function __construct( Config $config, QueryGenerator $queryGenerator = null, MetadataPool $metadataPool = null, + ?TableMaintainer $tableMaintainer = null, CacheContext $cacheContext = null, EventManagerInterface $eventManager = null, - IndexerRegistry $indexerRegistry = null + IndexerRegistry $indexerRegistry = null, + ?WorkingStateProvider $workingStateProvider = null ) { - parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool); + parent::__construct($resource, $storeManager, $config, $queryGenerator, $metadataPool, $tableMaintainer); $this->cacheContext = $cacheContext ?: ObjectManager::getInstance()->get(CacheContext::class); $this->eventManager = $eventManager ?: ObjectManager::getInstance()->get(EventManagerInterface::class); $this->indexerRegistry = $indexerRegistry ?: ObjectManager::getInstance()->get(IndexerRegistry::class); + $this->workingStateProvider = $workingStateProvider ?: + ObjectManager::getInstance()->get(WorkingStateProvider::class); } /** @@ -82,6 +97,7 @@ public function __construct( * @return $this * @throws \Exception if metadataPool doesn't contain metadata for ProductInterface * @throws \DomainException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function execute(array $entityIds = [], $useTempTable = false) { @@ -90,46 +106,68 @@ public function execute(array $entityIds = [], $useTempTable = false) $this->limitationByProducts = $idsToBeReIndexed; $this->useTempTable = $useTempTable; $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); - $workingState = $indexer->isWorking(); + $workingState = $this->isWorkingState(); - $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); + if (!$indexer->isScheduled() + || ($indexer->isScheduled() && !$useTempTable) + || ($indexer->isScheduled() && $useTempTable && !$workingState)) { - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $this->connection->truncateTable($this->getIndexTable($store->getId())); + $affectedCategories = $this->getCategoryIdsFromIndex($idsToBeReIndexed); + + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->truncateTable($this->getIndexTable($store->getId())); + } + } else { + $this->removeEntries(); } - } else { - $this->removeEntries(); - } - $this->reindex(); - if ($useTempTable && !$workingState && $indexer->isScheduled()) { - foreach ($this->storeManager->getStores() as $store) { - $this->connection->delete( - $this->tableMaintainer->getMainTable($store->getId()), - ['product_id IN (?)' => $this->limitationByProducts] - ); - $select = $this->connection->select() - ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); - $this->connection->query( - $this->connection->insertFromSelect( - $select, + $this->reindex(); + + // get actual state + $workingState = $this->isWorkingState(); + + if ($useTempTable && !$workingState && $indexer->isScheduled()) { + foreach ($this->storeManager->getStores() as $store) { + $this->connection->delete( $this->tableMaintainer->getMainTable($store->getId()), - [], - AdapterInterface::INSERT_ON_DUPLICATE - ) - ); + ['product_id IN (?)' => $this->limitationByProducts] + ); + $select = $this->connection->select() + ->from($this->tableMaintainer->getMainReplicaTable($store->getId())); + $this->connection->query( + $this->connection->insertFromSelect( + $select, + $this->tableMaintainer->getMainTable($store->getId()), + [], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } } - } - $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed)); + $affectedCategories = array_merge($affectedCategories, $this->getCategoryIdsFromIndex($idsToBeReIndexed)); - $this->registerProducts($idsToBeReIndexed); - $this->registerCategories($affectedCategories); - $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + $this->registerProducts($idsToBeReIndexed); + $this->registerCategories($affectedCategories); + $this->eventManager->dispatch('clean_cache_by_tags', ['object' => $this->cacheContext]); + } return $this; } + /** + * Get state for current and shared indexer + * + * @return bool + */ + private function isWorkingState() : bool + { + $indexer = $this->indexerRegistry->get(CategoryProductIndexer::INDEXER_ID); + $sharedIndexer = $this->indexerRegistry->get(ProductCategoryIndexer::INDEXER_ID); + return $this->workingStateProvider->isWorking($indexer->getId()) + || $this->workingStateProvider->isWorking($sharedIndexer->getId()); + } + /** * Get IDs of parent products by their child IDs. * diff --git a/app/code/Magento/Catalog/Model/Product/Authorization.php b/app/code/Magento/Catalog/Model/Product/Authorization.php index b8aa8f70ba70f..4022eb34e65e3 100644 --- a/app/code/Magento/Catalog/Model/Product/Authorization.php +++ b/app/code/Magento/Catalog/Model/Product/Authorization.php @@ -159,7 +159,7 @@ public function authorizeSavingOf(ProductInterface $product): void if (!$savedProduct->getSku()) { throw NoSuchEntityException::singleField('id', $product->getId()); } - $oldData = $product->getOrigData(); + $oldData = $savedProduct->getData(); } } if ($this->hasProductChanged($product, $oldData)) { diff --git a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php index 8061422d84288..6a1392d776d31 100644 --- a/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php +++ b/app/code/Magento/Catalog/Model/Product/Gallery/UpdateHandler.php @@ -77,26 +77,26 @@ protected function processDeletedImages($product, array &$images) { $filesToDelete = []; $recordsToDelete = []; - $picturesInOtherStores = []; $imagesToDelete = []; - - foreach ($this->resourceModel->getProductImages($product, $this->extractStoreIds($product)) as $image) { - $picturesInOtherStores[$image['filepath']] = true; + $imagesToNotDelete = []; + foreach ($images as $image) { + if (empty($image['removed'])) { + $imagesToNotDelete[] = $image['file']; + } } - foreach ($images as &$image) { + foreach ($images as $image) { if (!empty($image['removed'])) { if (!empty($image['value_id'])) { if (preg_match('/\.\.(\\\|\/)/', $image['file'])) { continue; } $recordsToDelete[] = $image['value_id']; - $imagesToDelete[] = $image['file']; - $catalogPath = $this->mediaConfig->getBaseMediaPath(); - $isFile = $this->mediaDirectory->isFile($catalogPath . $image['file']); - // only delete physical files if they are not used by any other products and if this file exist - if ($isFile && !($this->resourceModel->countImageUses($image['file']) > 1)) { - $filesToDelete[] = ltrim($image['file'], '/'); + if (!in_array($image['file'], $imagesToNotDelete)) { + $imagesToDelete[] = $image['file']; + if ($this->canDeleteImage($image['file'])) { + $filesToDelete[] = ltrim($image['file'], '/'); + } } } } @@ -107,6 +107,19 @@ protected function processDeletedImages($product, array &$images) $this->removeDeletedImages($filesToDelete); } + /** + * Check if image exists and is not used by any other products + * + * @param string $file + * @return bool + */ + private function canDeleteImage(string $file): bool + { + $catalogPath = $this->mediaConfig->getBaseMediaPath(); + return $this->mediaDirectory->isFile($catalogPath . $file) + && $this->resourceModel->countImageUses($file) <= 1; + } + /** * @inheritdoc * diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index 3c60d81e9a4d8..842ee197f83fe 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -10,9 +10,11 @@ use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Image as MagentoImage; use Magento\Framework\Serialize\SerializerInterface; use Magento\Catalog\Model\Product\Image\ParamsBuilder; +use Magento\Framework\Filesystem\Driver\File as FilesystemDriver; /** * Image operations @@ -101,6 +103,7 @@ class Image extends \Magento\Framework\Model\AbstractModel /** * @var int + * @deprecated unused */ protected $_angle; @@ -199,6 +202,11 @@ class Image extends \Magento\Framework\Model\AbstractModel */ private $serializer; + /** + * @var FilesystemDriver + */ + private $filesystemDriver; + /** * Constructor * @@ -219,6 +227,8 @@ class Image extends \Magento\Framework\Model\AbstractModel * @param array $data * @param SerializerInterface $serializer * @param ParamsBuilder $paramsBuilder + * @param FilesystemDriver $filesystemDriver + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -239,7 +249,8 @@ public function __construct( \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], SerializerInterface $serializer = null, - ParamsBuilder $paramsBuilder = null + ParamsBuilder $paramsBuilder = null, + FilesystemDriver $filesystemDriver = null ) { $this->_storeManager = $storeManager; $this->_catalogProductMediaConfig = $catalogProductMediaConfig; @@ -254,6 +265,7 @@ public function __construct( $this->viewAssetPlaceholderFactory = $viewAssetPlaceholderFactory; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->paramsBuilder = $paramsBuilder ?: ObjectManager::getInstance()->get(ParamsBuilder::class); + $this->filesystemDriver = $filesystemDriver ?: ObjectManager::getInstance()->get(FilesystemDriver::class); } /** @@ -663,7 +675,12 @@ public function getDestinationSubdir() public function isCached() { $path = $this->imageAsset->getPath(); - return is_array($this->loadImageInfoFromCache($path)) || file_exists($path); + try { + $isCached = is_array($this->loadImageInfoFromCache($path)) || $this->filesystemDriver->isExists($path); + } catch (FileSystemException $e) { + $isCached = false; + } + return $isCached; } /** diff --git a/app/code/Magento/Catalog/Model/Product/Media/Config.php b/app/code/Magento/Catalog/Model/Product/Media/Config.php index 33af93db13b4c..71e29515791a7 100644 --- a/app/code/Magento/Catalog/Model/Product/Media/Config.php +++ b/app/code/Magento/Catalog/Model/Product/Media/Config.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\Product\Media; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\UrlInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -76,7 +77,7 @@ public function getBaseMediaPath() public function getBaseMediaUrl() { return $this->storeManager->getStore() - ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . 'catalog/product'; + ->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $this->getBaseMediaUrlAddition(); } /** @@ -97,7 +98,7 @@ public function getBaseTmpMediaPath() public function getBaseTmpMediaUrl() { return $this->storeManager->getStore()->getBaseUrl( - \Magento\Framework\UrlInterface::URL_TYPE_MEDIA + UrlInterface::URL_TYPE_MEDIA ) . 'tmp/' . $this->getBaseMediaUrlAddition(); } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php index 8001d692c011b..725635bf4fc45 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Date.php @@ -72,6 +72,9 @@ public function validateUserValue($values) $dateValid = true; if ($this->_dateExists()) { if ($this->useCalendar()) { + if (is_array($value) && $this->checkDateWithoutJSCalendar($value)) { + $value['date'] = sprintf("%s/%s/%s", $value['day'], $value['month'], $value['year']); + } /* Fixed validation if the date was not saved correctly after re-saved the order for example: "09\/24\/2020,2020-09-24 00:00:00" */ if (is_string($value) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4},+(\w|\W)*$/', $value)) { @@ -81,6 +84,9 @@ public function validateUserValue($values) } $dateValid = isset($value['date']) && preg_match('/^\d{1,4}.+\d{1,4}.+\d{1,4}$/', $value['date']); } else { + if (is_array($value)) { + $value = $this->prepareDateByDateInternal($value); + } $dateValid = isset( $value['day'] ) && isset( @@ -411,4 +417,38 @@ protected function _timeExists() ] ); } + + /** + * Check is date without JS Calendar + * + * @param array $value + * + * @return bool + */ + private function checkDateWithoutJSCalendar(array $value): bool + { + return empty($value['date']) + && !empty($value['day']) + && !empty($value['month']) + && !empty($value['year']); + } + + /** + * Prepare date by date internal + * + * @param array $value + * @return array + */ + private function prepareDateByDateInternal(array $value): array + { + if (!empty($value['date']) && !empty($value['date_internal'])) { + $formatDate = explode(' ', $value['date_internal']); + $date = explode('-', $formatDate[0]); + $value['year'] = $date[0]; + $value['month'] = $date[1]; + $value['day'] = $date[2]; + } + + return $value; + } } diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php index fef4999a1174a..934ff48045097 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFile.php @@ -12,6 +12,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Math\Random; use Magento\Framework\App\ObjectManager; +use Magento\MediaStorage\Model\File\Uploader; /** * Validator class. Represents logic for validation file given from product option @@ -173,15 +174,11 @@ public function validate($processingParams, $option) $userValue = []; if ($upload->isUploaded($file) && $upload->isValid($file)) { - $fileName = \Magento\MediaStorage\Model\File\Uploader::getCorrectFileName($fileInfo['name']); - $dispersion = \Magento\MediaStorage\Model\File\Uploader::getDispersionPath($fileName); - - $filePath = $dispersion; - $tmpDirectory = $this->filesystem->getDirectoryRead(DirectoryList::SYS_TMP); - $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); $fileRandomName = $this->random->getRandomString(32); - $filePath .= '/' .$fileRandomName; + $fileName = Uploader::getCorrectFileName($fileRandomName); + $dispersion = Uploader::getDispersionPath($fileName); + $filePath = $dispersion . '/' . $fileName; $fileFullPath = $this->mediaDirectory->getAbsolutePath($this->quotePath . $filePath); $upload->addFilter(new \Zend_Filter_File_Rename(['target' => $fileFullPath, 'overwrite' => true])); @@ -216,6 +213,8 @@ public function validate($processingParams, $option) } } + $fileHash = md5($tmpDirectory->readFile($tmpDirectory->getRelativePath($fileInfo['tmp_name']))); + $userValue = [ 'type' => $fileInfo['type'], 'title' => $fileInfo['name'], diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index eb4a71cb90a8c..f90b097415661 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -3,13 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\UploaderFactory; /** * Abstract model for product type implementation @@ -113,6 +114,11 @@ abstract class AbstractType */ protected $_cacheProductSetAttributes = '_cache_instance_product_set_attributes'; + /** + * @var UploaderFactory + */ + private $uploaderFactory; + /** * Delete data specific for this product type * @@ -175,8 +181,6 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ protected $serializer; /** - * Construct - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -187,6 +191,7 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ * @param \Psr\Log\LoggerInterface $logger * @param ProductRepositoryInterface $productRepository * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -199,7 +204,8 @@ public function __construct( \Magento\Framework\Registry $coreRegistry, \Psr\Log\LoggerInterface $logger, ProductRepositoryInterface $productRepository, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->_catalogProductOption = $catalogProductOption; $this->_eavConfig = $eavConfig; @@ -212,6 +218,7 @@ public function __construct( $this->productRepository = $productRepository; $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->uploaderFactory = $uploaderFactory ?: ObjectManager::getInstance()->get(UploaderFactory::class); } /** @@ -493,28 +500,20 @@ public function processFileQueue() if (isset($queueOptions['operation']) && ($operation = $queueOptions['operation'])) { switch ($operation) { case 'receive_uploaded_file': - $src = isset($queueOptions['src_name']) ? $queueOptions['src_name'] : ''; - $dst = isset($queueOptions['dst_name']) ? $queueOptions['dst_name'] : ''; + $src = $queueOptions['src_name'] ?? ''; + $dst = $queueOptions['dst_name'] ?? ''; /** @var $uploader \Zend_File_Transfer_Adapter_Http */ - $uploader = isset($queueOptions['uploader']) ? $queueOptions['uploader'] : null; - - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $path = dirname($dst); - - try { - $rootDir = $this->_filesystem->getDirectoryWrite( - DirectoryList::ROOT - ); - $rootDir->create($rootDir->getRelativePath($path)); - } catch (\Magento\Framework\Exception\FileSystemException $e) { - throw new \Magento\Framework\Exception\LocalizedException( - __('We can\'t create the "%1" writeable directory.', $path) - ); + $uploader = $queueOptions['uploader'] ?? null; + $isUploaded = false; + if ($uploader && $uploader->isValid()) { + $path = pathinfo($dst, PATHINFO_DIRNAME); + $uploader = $this->uploaderFactory->create(['fileId' => $src]); + $uploader->setFilesDispersion(false); + $uploader->setAllowRenameFiles(true); + $isUploaded = $uploader->save($path, pathinfo($dst, PATHINFO_FILENAME)); } - $uploader->setDestination($path); - - if (empty($src) || empty($dst) || !$uploader->receive($src)) { + if (empty($src) || empty($dst) || !$isUploaded) { /** * @todo: show invalid option */ @@ -620,7 +619,7 @@ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $p } } if (count($results) > 0) { - throw new LocalizedException(__(implode("\n", $results))); + throw new LocalizedException(__(implode("\n", array_unique($results)))); } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category.php b/app/code/Magento/Catalog/Model/ResourceModel/Category.php index 917aafb643b47..e19286efc38c0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category.php @@ -13,7 +13,7 @@ namespace Magento\Catalog\Model\ResourceModel; -use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Indexer\Category\Product\Processor; use Magento\Catalog\Setup\CategorySetup; use Magento\Framework\App\ObjectManager; @@ -1172,11 +1172,11 @@ public function getCategoryWithChildren(int $categoryId): array return []; } - $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $linkField = $this->metadataPool->getMetadata(CategoryInterface::class)->getLinkField(); $select = $connection->select() ->from( ['cce' => $this->getTable('catalog_category_entity')], - [$linkField, 'parent_id', 'path'] + [$linkField, 'entity_id', 'parent_id', 'path'] )->join( ['cce_int' => $this->getTable('catalog_category_entity_int')], 'cce.' . $linkField . ' = cce_int.' . $linkField, diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 7dbfe0d5fccea..3f908663c8e5e 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -7,6 +7,7 @@ namespace Magento\Catalog\Model\ResourceModel\Product; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; use Magento\Catalog\Model\Indexer\Product\Price\PriceTableResolver; @@ -2130,16 +2131,17 @@ private function getChildrenCategories(int $categoryId): array $firstCategory = array_shift($categories); if ($firstCategory['is_anchor'] == 1) { - $linkField = $this->getProductEntityMetadata()->getLinkField(); - $anchorCategory[] = (int)$firstCategory[$linkField]; + //category hierarchy can not be modified by staging updates + $entityField = $this->metadataPool->getMetadata(CategoryInterface::class)->getIdentifierField(); + $anchorCategory[] = (int)$firstCategory[$entityField]; foreach ($categories as $category) { if (in_array($category['parent_id'], $categoryIds) && in_array($category['parent_id'], $anchorCategory)) { - $categoryIds[] = (int)$category[$linkField]; + $categoryIds[] = (int)$category[$entityField]; // Storefront approach is to treat non-anchor children of anchor category as anchors. - // Adding their's IDs to $anchorCategory for consistency. + // Adding theirs IDs to $anchorCategory for consistency. if ($category['is_anchor'] == 1 || in_array($category['parent_id'], $anchorCategory)) { - $anchorCategory[] = (int)$category[$linkField]; + $anchorCategory[] = (int)$category[$entityField]; } } } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php index 5ea71176429fc..58e6290a820cd 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Option/Value/Collection.php @@ -14,6 +14,20 @@ */ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection { + /** + * Name prefix of events that are dispatched by model + * + * @var string + */ + protected $_eventPrefix = 'catalog_product_option_value_collection'; + + /** + * Name of event parameter + * + * @var string + */ + protected $_eventObject = 'product_option_value_collection'; + /** * Resource initialization * diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index 0a46af3ef021d..bf624c3435103 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -108,7 +108,7 @@ public function viewDirective($construction) * The original intent of _absolute parameter was to simply append specified path to a base URL * bypassing any kind of processing. * For example, normally you would use {{view url="css/styles.css"}} directive which would automatically resolve - * into something like http://example.com/pub/static/area/theme/en_US/css/styles.css + * into something like http://example.com/static/area/theme/en_US/css/styles.css * But with _absolute, the expected behavior is this: {{view url="favicon.ico" _absolute=true}} should resolve * into something like http://example.com/favicon.ico * diff --git a/app/code/Magento/Catalog/Model/View/Asset/Image.php b/app/code/Magento/Catalog/Model/View/Asset/Image.php index c547ec612bb94..0f7082f9df154 100644 --- a/app/code/Magento/Catalog/Model/View/Asset/Image.php +++ b/app/code/Magento/Catalog/Model/View/Asset/Image.php @@ -6,11 +6,16 @@ namespace Magento\Catalog\Model\View\Asset; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product\Media\ConfigInterface; use Magento\Framework\Encryption\Encryptor; use Magento\Framework\Encryption\EncryptorInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\View\Asset\ContextInterface; use Magento\Framework\View\Asset\LocalInterface; +use Magento\Catalog\Helper\Image as ImageHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Store\Model\StoreManagerInterface; /** * A locally available image file asset that can be referred with a file path @@ -58,6 +63,21 @@ class Image implements LocalInterface */ private $encryptor; + /** + * @var ImageHelper + */ + private $imageHelper; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var string + */ + private $mediaFormatUrl; + /** * Image constructor. * @@ -66,13 +86,19 @@ class Image implements LocalInterface * @param EncryptorInterface $encryptor * @param string $filePath * @param array $miscParams + * @param ImageHelper $imageHelper + * @param CatalogMediaConfig $catalogMediaConfig + * @param StoreManagerInterface $storeManager */ public function __construct( ConfigInterface $mediaConfig, ContextInterface $context, EncryptorInterface $encryptor, $filePath, - array $miscParams + array $miscParams, + ImageHelper $imageHelper = null, + CatalogMediaConfig $catalogMediaConfig = null, + StoreManagerInterface $storeManager = null ) { if (isset($miscParams['image_type'])) { $this->sourceContentType = $miscParams['image_type']; @@ -85,14 +111,73 @@ public function __construct( $this->filePath = $filePath; $this->miscParams = $miscParams; $this->encryptor = $encryptor; + $this->imageHelper = $imageHelper ?: ObjectManager::getInstance()->get(ImageHelper::class); + $this->storeManager = $storeManager ?: ObjectManager::getInstance()->get(StoreManagerInterface::class); + + $catalogMediaConfig = $catalogMediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaFormatUrl = $catalogMediaConfig->getMediaUrlFormat(); } /** - * @inheritdoc + * Get catalog image URL. + * + * @return string + * @throws LocalizedException */ public function getUrl() { - return $this->context->getBaseUrl() . DIRECTORY_SEPARATOR . $this->getImageInfo(); + switch ($this->mediaFormatUrl) { + case CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS: + return $this->getUrlWithTransformationParameters(); + case CatalogMediaConfig::HASH: + return $this->context->getBaseUrl() . DIRECTORY_SEPARATOR . $this->getImageInfo(); + default: + throw new LocalizedException( + __("The specified Catalog media URL format '$this->mediaFormatUrl' is not supported.") + ); + } + } + + /** + * Get image URL with transformation parameters + * + * @return string + */ + private function getUrlWithTransformationParameters() + { + return $this->getOriginalImageUrl() . '?' . http_build_query($this->getImageTransformationParameters()); + } + + /** + * The list of parameters to be used during image transformations (e.g. resizing or applying watermarks). + * + * This method can be used as an extension point. + * + * @return string[] + */ + public function getImageTransformationParameters() + { + return [ + 'width' => $this->miscParams['image_width'], + 'height' => $this->miscParams['image_height'], + 'store' => $this->storeManager->getStore()->getCode(), + 'image-type' => $this->sourceContentType + ]; + } + + /** + * Get URL to the original version of the product image. + * + * @return string + */ + private function getOriginalImageUrl() + { + $originalImageFile = $this->getSourceFile(); + if (!$originalImageFile) { + return $this->imageHelper->getDefaultPlaceholderUrl(); + } else { + return $this->context->getBaseUrl() . $this->getFilePath(); + } } /** diff --git a/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php b/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php index 91d2868afab8c..54b655a217a08 100644 --- a/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php +++ b/app/code/Magento/Catalog/Observer/ImageResizeAfterProductSave.php @@ -10,7 +10,11 @@ use Magento\Framework\Event\ObserverInterface; use Magento\Framework\App\State; use Magento\MediaStorage\Service\ImageResize; +use Magento\Catalog\Model\Config\CatalogMediaConfig; +/** + * Resize product images after the product is saved + */ class ImageResizeAfterProductSave implements ObserverInterface { /** @@ -23,17 +27,26 @@ class ImageResizeAfterProductSave implements ObserverInterface */ private $state; + /** + * @var CatalogMediaConfig + */ + private $catalogMediaConfig; + /** * Product constructor. + * * @param ImageResize $imageResize * @param State $state + * @param CatalogMediaConfig $catalogMediaConfig */ public function __construct( ImageResize $imageResize, - State $state + State $state, + CatalogMediaConfig $catalogMediaConfig ) { $this->imageResize = $imageResize; $this->state = $state; + $this->catalogMediaConfig = $catalogMediaConfig; } /** @@ -44,6 +57,12 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + $catalogMediaUrlFormat = $this->catalogMediaConfig->getMediaUrlFormat(); + if ($catalogMediaUrlFormat == CatalogMediaConfig::IMAGE_OPTIMIZATION_PARAMETERS) { + // Skip image resizing on the Magento side when it is offloaded to a web server or CDN + return; + } + /** @var $product \Magento\Catalog\Model\Product */ $product = $observer->getEvent()->getProduct(); diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml new file mode 100644 index 0000000000000..4b5aca5050858 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminMassUpdateProductQtyAndStockStatusActionGroup.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Update Product Name and Description attribute--> + <actionGroup name="AdminMassUpdateProductQtyAndStockStatusActionGroup"> + <arguments> + <argument name="attributes"/> + <argument name="product"/> + </arguments> + <!--Filter product in product grid--> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageFirstTime"/> + <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> + <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="openProductFilters"/> + <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{product.name}}" stepKey="fillProductNameFilter"/> + <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillProductSkuFilter"/> + <selectOption selector="{{AdminProductGridFilterSection.typeFilter}}" userInput="{{product.type_id}}" stepKey="selectionProductType"/> + <click selector="{{AdminProductGridFilterSection.applyFilters}}" stepKey="clickApplyFilters"/> + <!--Select first product from grid and open mass action--> + <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckbox"/> + <click selector="{{AdminProductGridSection.bulkActionDropdown}}" stepKey="clickDropdown"/> + <click selector="{{AdminProductGridSection.bulkActionOption('Update attributes')}}" stepKey="clickOption"/> + <waitForPageLoad stepKey="waitForUploadPage"/> + <seeInCurrentUrl url="{{ProductAttributesEditPage.url}}" stepKey="seeAttributePageEditUrl"/> + <!--Update inventory attributes and save--> + <click selector="{{AdminUpdateAttributesAdvancedInventorySection.inventory}}" stepKey="openInvetoryTab"/> + <click selector="{{AdminUpdateAttributesAdvancedInventorySection.changeQty}}" stepKey="uncheckChangeQty"/> + <fillField selector="{{AdminUpdateAttributesAdvancedInventorySection.qty}}" userInput="{{attributes.qty}}" stepKey="fillFieldName"/> + <click selector="{{AdminUpdateAttributesAdvancedInventorySection.changeStockAvailability}}" stepKey="uncheckChangeStockAvailability"/> + <selectOption selector="{{AdminUpdateAttributesAdvancedInventorySection.stockAvailability}}" userInput="{{attributes.stockAvailability}}" stepKey="selectStatus"/> + <click selector="{{AdminUpdateAttributesSection.saveButton}}" stepKey="save"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitVisibleSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="Message is added to queue" stepKey="seeSuccessMessage"/> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageSecondTime"/> + <waitForPageLoad stepKey="waitForProductGridPage"/> + <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersAfterMassAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetEnableQtyIncrementsActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetEnableQtyIncrementsActionGroup.xml new file mode 100644 index 0000000000000..2e211dad6dc81 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetEnableQtyIncrementsActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetEnableQtyIncrementsActionGroup"> + <annotations> + <description>Set "Enable Qty Increments" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="scrollToEnableQtyIncrements"/> + <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="clickOnEnableQtyIncrementsUseConfigSettingsCheckbox"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" userInput="{{value}}" + stepKey="setEnableQtyIncrements"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyIncrementsForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyIncrementsForProductActionGroup.xml new file mode 100644 index 0000000000000..6ea82a2f2a490 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyIncrementsForProductActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetQtyIncrementsForProductActionGroup"> + <annotations> + <description>Fills in the "Qty Increments" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="scrollToQtyIncrementsUseConfigSettings"/> + <click selector="{{AdminProductFormAdvancedInventorySection.qtyIncrementsUseConfigSettings}}" stepKey="clickOnQtyIncrementsUseConfigSettings"/> + <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" stepKey="scrollToQtyIncrements"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput="{{qty}}" stepKey="fillQtyIncrements"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml index 99908f1c9df5f..22557972b991f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductAttributeMassUpdateData.xml @@ -12,4 +12,12 @@ <data key="name" unique="suffix">New Bundle Product Name</data> <data key="description" unique="suffix">This is the description</data> </entity> + <entity name="UpdateAttributeQtyAndStockToInStock" type="productAttributeMassUpdate"> + <data key="qty">10</data> + <data key="stockAvailability">In Stock</data> + </entity> + <entity name="UpdateAttributeQtyAndStockToOutOfStock" type="productAttributeMassUpdate"> + <data key="qty">0</data> + <data key="stockAvailability">Out of Stock</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml new file mode 100644 index 0000000000000..92dadbdd26c2d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminUpdateAttributesSection/AdminUpdateAttributesAdvancedInventorySection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminUpdateAttributesAdvancedInventorySection"> + <element name="inventory" type="button" selector="#attributes_update_tabs_inventory"/> + <element name="changeQty" type="checkbox" selector="#inventory_qty_checkbox"/> + <element name="qty" type="input" selector="#inventory_qty"/> + <element name="changeStockAvailability" type="checkbox" selector="#inventory_stock_availability_checkbox"/> + <element name="stockAvailability" type="select" selector="//select[@name='inventory[is_in_stock]']"/> + </section> +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml index 3008e89fd9dd1..51e267a7c166b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCategoryIndexerInUpdateOnScheduleModeTest.xml @@ -97,7 +97,7 @@ <magentoCLI command="cron:run" stepKey="runCron"/> <!-- 5. Open category A on Storefront again --> - <reloadPage stepKey="reloadCategoryA"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadCategoryA"/> <!-- Category A displays product A1 now --> <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeTitleCategoryA1"/> @@ -126,7 +126,7 @@ <magentoCLI command="cron:run" stepKey="runCron1"/> <!-- 9. Open category A on Storefront again --> - <reloadPage stepKey="refreshCategoryAPage"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshCategoryAPage"/> <!-- Category A is empty now --> <see userInput="$$createCategoryA.name$$" selector="{{StorefrontCategoryMainSection.CategoryTitle}}" stepKey="seeOnPageCategoryAName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml index 2a59be6306a30..fb4bd4d1dcb74 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml @@ -40,8 +40,10 @@ <!-- Assert single row - no hover state --> <createData entity="ApiCategoryA" stepKey="createFirstCategoryBlank"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForBlankSingleRowAppear"/> + + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForBlankSingleRowAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFirstCategoryBlank.name$$)}}" stepKey="hoverFirstCategoryBlank"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.subItemLevelHover('level0')}}" stepKey="assertNoHoverState"/> @@ -87,8 +89,9 @@ </createData> <!-- Several rows. Hover on category without children --> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForBlankSeveralRowsAppear"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForBlankSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategoryWithoutChildrenBlank.name$$)}}" stepKey="hoverCategoryWithoutChildren"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createCategoryWithoutChildrenBlank.name$$, 'level0')}}" stepKey="dontSeeChildrenInCategory"/> @@ -167,8 +170,9 @@ <createData entity="ApiCategory" stepKey="createFourthCategoryLuma"/> <!-- Single row. No hover state --> - <reloadPage stepKey="reload"/> - <waitForPageLoad stepKey="waitForLumaSingleRowAppear"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reload"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLumaSingleRowAppear"/> + <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFirstCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInFirstCategory"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createSecondCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateInSecondCategory"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createThirdCategoryLuma.name$$, 'level0')}}" stepKey="noHoverStateThirdCategory"/> @@ -203,8 +207,9 @@ <createData entity="ApiCategory" stepKey="createEighthCategoryLuma"/> <!-- Several rows. Hover on Category without children --> - <reloadPage stepKey="refresh"/> - <waitForPageLoad stepKey="waitForLumaSeveralRowsAppear"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refresh"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLumaSeveralRowsAppear"/> + <moveMouseOver selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createFifthCategoryLuma.name$$)}}" stepKey="hoverOnCategoryWithoutChildren"/> <dontSeeElement selector="{{StorefrontNavigationMenuSection.itemByNameAndLevel($$createFifthCategoryLuma.name$$, 'level0')}}" stepKey="dontSeeSubcategoriesInCategory"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php deleted file mode 100644 index 572dbc4ca2732..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ /dev/null @@ -1,454 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Block\Adminhtml\Product\Helper\Form\Gallery; - -use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; -use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content; -use Magento\Catalog\Helper\Image; -use Magento\Catalog\Model\Entity\Attribute; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Media\Config; -use Magento\Framework\Exception\FileSystemException; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\Read; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Json\EncoderInterface; -use Magento\Framework\Phrase; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\MediaStorage\Helper\File\Storage\Database; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class ContentTest extends TestCase -{ - /** - * @var Filesystem|MockObject - */ - protected $fileSystemMock; - - /** - * @var Read|MockObject - */ - protected $readMock; - - /** - * @var Content|MockObject - */ - protected $content; - - /** - * @var Config|MockObject - */ - protected $mediaConfigMock; - - /** - * @var EncoderInterface|MockObject - */ - protected $jsonEncoderMock; - - /** - * @var Gallery|MockObject - */ - protected $galleryMock; - - /** - * @var Image|MockObject - */ - protected $imageHelper; - - /** - * @var Database|MockObject - */ - protected $databaseMock; - - /** - * @var ObjectManager - */ - protected $objectManager; - - protected function setUp(): void - { - $this->fileSystemMock = $this->getMockBuilder(Filesystem::class) - ->addMethods(['stat']) - ->onlyMethods(['getDirectoryRead']) - ->disableOriginalConstructor() - ->getMock(); - $this->readMock = $this->getMockForAbstractClass(ReadInterface::class); - $this->galleryMock = $this->createMock(Gallery::class); - $this->mediaConfigMock = $this->createPartialMock( - Config::class, - ['getMediaUrl', 'getMediaPath'] - ); - $this->jsonEncoderMock = $this->getMockBuilder(EncoderInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->databaseMock = $this->getMockBuilder(Database::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->objectManager = new ObjectManager($this); - $this->content = $this->objectManager->getObject( - Content::class, - [ - 'mediaConfig' => $this->mediaConfigMock, - 'jsonEncoder' => $this->jsonEncoderMock, - 'filesystem' => $this->fileSystemMock, - 'fileStorageDatabase' => $this->databaseMock - ] - ); - } - - public function testGetImagesJson() - { - $url = [ - ['file_1.jpg', 'url_to_the_image/image_1.jpg'], - ['file_2.jpg', 'url_to_the_image/image_2.jpg'] - ]; - $mediaPath = [ - ['file_1.jpg', 'catalog/product/image_1.jpg'], - ['file_2.jpg', 'catalog/product/image_2.jpg'] - ]; - - $sizeMap = [ - ['catalog/product/image_1.jpg', ['size' => 399659]], - ['catalog/product/image_2.jpg', ['size' => 879394]] - ]; - - $imagesResult = [ - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0', - 'url' => 'url_to_the_image/image_2.jpg', - 'size' => 879394 - ], - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1', - 'url' => 'url_to_the_image/image_1.jpg', - 'size' => 399659 - ] - ]; - - $images = [ - 'images' => [ - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1' - ] , - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn($images); - $this->fileSystemMock->expects($this->once())->method('getDirectoryRead')->willReturn($this->readMock); - - $this->mediaConfigMock->method('getMediaUrl')->willReturnMap($url); - $this->mediaConfigMock->method('getMediaPath')->willReturnMap($mediaPath); - $this->readMock->method('stat')->willReturnMap($sizeMap); - $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); - - $this->readMock->method('isFile')->willReturn(true); - $this->databaseMock->method('checkDbUsage')->willReturn(false); - - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } - - public function testGetImagesJsonWithoutImages() - { - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn(null); - - $this->assertSame('[]', $this->content->getImagesJson()); - } - - public function testGetImagesJsonWithException() - { - $this->imageHelper = $this->getMockBuilder(Image::class) - ->disableOriginalConstructor() - ->setMethods(['getDefaultPlaceholderUrl']) - ->getMock(); - - $this->objectManager->setBackwardCompatibleProperty( - $this->content, - 'imageHelper', - $this->imageHelper - ); - - $placeholderUrl = 'url_to_the_placeholder/placeholder.jpg'; - - $imagesResult = [ - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0', - 'url' => 'url_to_the_placeholder/placeholder.jpg', - 'size' => 0 - ], - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1', - 'url' => 'url_to_the_placeholder/placeholder.jpg', - 'size' => 0 - ] - ]; - - $images = [ - 'images' => [ - [ - 'value_id' => '1', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '1' - ], - [ - 'value_id' => '2', - 'file' => 'file_2.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $this->content->setElement($this->galleryMock); - $this->galleryMock->expects($this->once())->method('getImages')->willReturn($images); - $this->fileSystemMock->method('getDirectoryRead')->willReturn($this->readMock); - $this->mediaConfigMock->method('getMediaUrl'); - $this->mediaConfigMock->method('getMediaPath'); - - $this->readMock - ->method('isFile') - ->willReturn(true); - $this->databaseMock - ->method('checkDbUsage') - ->willReturn(false); - - $this->readMock->method('stat')->willReturnOnConsecutiveCalls( - $this->throwException( - new FileSystemException(new Phrase('test')) - ), - $this->throwException( - new FileSystemException(new Phrase('test')) - ) - ); - $this->imageHelper->method('getDefaultPlaceholderUrl')->willReturn($placeholderUrl); - $this->jsonEncoderMock->expects($this->once())->method('encode')->willReturnCallback('json_encode'); - - $this->assertSame(json_encode($imagesResult), $this->content->getImagesJson()); - } - - /** - * Test GetImageTypes() will return value for given attribute from data persistor. - * - * @return void - */ - public function testGetImageTypesFromDataPersistor() - { - $attributeCode = 'thumbnail'; - $value = 'testImageValue'; - $scopeLabel = 'testScopeLabel'; - $label = 'testLabel'; - $name = 'testName'; - $expectedTypes = [ - $attributeCode => [ - 'code' => $attributeCode, - 'value' => $value, - 'label' => $label, - 'name' => $name, - ], - ]; - $product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - $product->expects($this->once()) - ->method('getData') - ->with($this->identicalTo($attributeCode)) - ->willReturn(null); - $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); - $product->expects($this->once()) - ->method('getMediaAttributes') - ->willReturn([$mediaAttribute]); - $this->galleryMock->expects($this->exactly(2)) - ->method('getDataObject') - ->willReturn($product); - $this->galleryMock->expects($this->once()) - ->method('getImageValue') - ->with($this->identicalTo($attributeCode)) - ->willReturn($value); - $this->galleryMock->expects($this->once()) - ->method('getScopeLabel') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($scopeLabel); - $this->galleryMock->expects($this->once()) - ->method('getAttributeFieldName') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($name); - $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); - } - - /** - * Test GetImageTypes() will return value for given attribute from product. - * - * @return void - */ - public function testGetImageTypesFromProduct() - { - $attributeCode = 'thumbnail'; - $value = 'testImageValue'; - $scopeLabel = 'testScopeLabel'; - $label = 'testLabel'; - $name = 'testName'; - $expectedTypes = [ - $attributeCode => [ - 'code' => $attributeCode, - 'value' => $value, - 'label' => $label, - 'name' => $name, - ], - ]; - $product = $this->getMockBuilder(Product::class) - ->disableOriginalConstructor() - ->getMock(); - $product->expects($this->once()) - ->method('getData') - ->with($this->identicalTo($attributeCode)) - ->willReturn($value); - $mediaAttribute = $this->getMediaAttribute($label, $attributeCode); - $product->expects($this->once()) - ->method('getMediaAttributes') - ->willReturn([$mediaAttribute]); - $this->galleryMock->expects($this->exactly(2)) - ->method('getDataObject') - ->willReturn($product); - $this->galleryMock->expects($this->never()) - ->method('getImageValue'); - $this->galleryMock->expects($this->once()) - ->method('getScopeLabel') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($scopeLabel); - $this->galleryMock->expects($this->once()) - ->method('getAttributeFieldName') - ->with($this->identicalTo($mediaAttribute)) - ->willReturn($name); - $this->getImageTypesAssertions($attributeCode, $scopeLabel, $expectedTypes); - } - - /** - * Perform assertions. - * - * @param string $attributeCode - * @param string $scopeLabel - * @param array $expectedTypes - * @return void - */ - private function getImageTypesAssertions(string $attributeCode, string $scopeLabel, array $expectedTypes) - { - $this->content->setElement($this->galleryMock); - $result = $this->content->getImageTypes(); - $scope = $result[$attributeCode]['scope']; - $this->assertSame($scopeLabel, $scope->getText()); - unset($result[$attributeCode]['scope']); - $this->assertSame($expectedTypes, $result); - } - - /** - * Get media attribute mock. - * - * @param string $label - * @param string $attributeCode - * @return MockObject - */ - private function getMediaAttribute(string $label, string $attributeCode) - { - $frontend = $this->getMockBuilder(Product\Attribute\Frontend\Image::class) - ->disableOriginalConstructor() - ->getMock(); - $frontend->expects($this->once()) - ->method('getLabel') - ->willReturn($label); - $mediaAttribute = $this->getMockBuilder(Attribute::class) - ->disableOriginalConstructor() - ->getMock(); - $mediaAttribute - ->method('getAttributeCode') - ->willReturn($attributeCode); - $mediaAttribute->expects($this->once()) - ->method('getFrontend') - ->willReturn($frontend); - - return $mediaAttribute; - } - - /** - * Test GetImagesJson() calls MediaStorage functions to obtain image from DB prior to stat call - * - * @return void - */ - public function testGetImagesJsonMediaStorageMode() - { - $images = [ - 'images' => [ - [ - 'value_id' => '0', - 'file' => 'file_1.jpg', - 'media_type' => 'image', - 'position' => '0' - ] - ] - ]; - - $mediaPath = [ - ['file_1.jpg', 'catalog/product/image_1.jpg'] - ]; - - $this->content->setElement($this->galleryMock); - - $this->galleryMock->expects($this->once()) - ->method('getImages') - ->willReturn($images); - $this->fileSystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->willReturn($this->readMock); - $this->mediaConfigMock - ->method('getMediaPath') - ->willReturnMap($mediaPath); - - $this->readMock - ->method('isFile') - ->willReturn(false); - $this->databaseMock - ->method('checkDbUsage') - ->willReturn(true); - - $this->databaseMock->expects($this->once()) - ->method('saveFileToFilesystem') - ->with('catalog/product/image_1.jpg'); - - $this->readMock->method('stat')->willReturn(['size' => 123]); - - $this->content->getImagesJson(); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php index c606b7537cc44..125fd287cd4ce 100644 --- a/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Helper/ImageTest.php @@ -8,6 +8,7 @@ namespace Magento\Catalog\Test\Unit\Helper; use Magento\Catalog\Helper\Image; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\ImageFactory as ProductImageFactory; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; @@ -70,6 +71,11 @@ class ImageTest extends TestCase */ protected $placeholderFactory; + /** + * @var CatalogMediaConfig|MockObject + */ + private $catalogMediaConfigMock; + protected function setUp(): void { $this->mockContext(); @@ -90,12 +96,17 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); + $this->catalogMediaConfigMock = $this->createPartialMock(CatalogMediaConfig::class, ['getMediaUrlFormat']); + $this->catalogMediaConfigMock->method('getMediaUrlFormat')->willReturn(CatalogMediaConfig::HASH); + + $this->helper = new Image( $this->context, $this->imageFactory, $this->assetRepository, $this->viewConfig, - $this->placeholderFactory + $this->placeholderFactory, + $this->catalogMediaConfigMock ); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php index 16771214026f0..23136e55a2307 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/Attribute/Backend/ImageTest.php @@ -14,7 +14,9 @@ use Magento\Framework\DataObject; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; @@ -186,6 +188,7 @@ public function testBeforeSaveValueInvalid($value) */ public function testBeforeSaveAttributeFileName() { + $this->setupObjectManagerForCheckImageExist(false); $this->attribute->expects($this->once()) ->method('getName') ->willReturn('test_attribute'); @@ -253,11 +256,23 @@ public function testBeforeSaveAttributeFileNameOutsideOfCategoryDir() ); } + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + /** * Test beforeSaveTemporaryAttribute. */ public function testBeforeSaveTemporaryAttribute() { + $this->setupObjectManagerForCheckImageExist(false); $this->attribute->expects($this->once()) ->method('getName') ->willReturn('test_attribute'); @@ -268,7 +283,7 @@ public function testBeforeSaveTemporaryAttribute() $this->storeMock->expects($this->once()) ->method('getBaseMediaDir') - ->willReturn('pub/media'); + ->willReturn('media'); $model = $this->setUpModelForTests(); $model->setAttribute($this->attribute); @@ -279,7 +294,9 @@ public function testBeforeSaveTemporaryAttribute() ->with(DirectoryList::MEDIA) ->willReturn($mediaDirectoryMock); - $this->imageUploader->expects($this->any())->method('moveFileFromTmp')->willReturn('test123.jpg'); + $mediaDirectoryMock->method('getAbsolutePath')->willReturn('/media/test123.jpg'); + + $this->imageUploader->method('moveFileFromTmp')->willReturn('test123.jpg'); $object = new DataObject( [ @@ -287,7 +304,7 @@ public function testBeforeSaveTemporaryAttribute() [ 'name' => 'test123.jpg', 'tmp_name' => 'abc123', - 'url' => 'http://www.example.com/pub/media/temp/test123.jpg' + 'url' => 'http://www.example.com/media/temp/test123.jpg' ], ], ] @@ -297,7 +314,7 @@ public function testBeforeSaveTemporaryAttribute() $this->assertEquals( [ - ['name' => '/pub/media/test123.jpg', 'tmp_name' => 'abc123', 'url' => '/pub/media/test123.jpg'], + ['name' => '/media/test123.jpg', 'tmp_name' => 'abc123', 'url' => '/media/test123.jpg'], ], $object->getData('_additional_data_test_attribute') ); @@ -418,6 +435,7 @@ public function testBeforeSaveWithoutAdditionalData($value) */ public function testBeforeSaveWithExceptions() { + $this->setupObjectManagerForCheckImageExist(false); $model = $this->setUpModelForTests(); $this->storeManagerInterfaceMock->expects($this->once()) diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php index 42a3031ae27e0..676cf07912f1d 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Category/ImageTest.php @@ -84,8 +84,8 @@ public function getUrlDataProvider() ], [ 'testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/catalog/category/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/catalog/category/testimage' ], [ 'testimage', @@ -94,8 +94,8 @@ public function getUrlDataProvider() ], [ '/pub/media/catalog/category/testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/catalog/category/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/catalog/category/testimage' ], [ '/pub/media/catalog/category/testimage', @@ -104,8 +104,8 @@ public function getUrlDataProvider() ], [ '/pub/media/posters/testimage', - 'http://www.example.com/pub/media/', - 'http://www.example.com/pub/media/posters/testimage' + 'http://www.example.com/media/', + 'http://www.example.com/media/posters/testimage' ], [ '/pub/media/posters/testimage', diff --git a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php index be79b11cdf2b8..7cb2064d34d20 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/CategoryLinkManagementTest.php @@ -15,6 +15,7 @@ use Magento\Catalog\Model\CategoryRepository; use Magento\Catalog\Model\ResourceModel\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Framework\DataObject; use Magento\Framework\Indexer\IndexerRegistry; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -85,7 +86,11 @@ public function testGetAssignedProducts() $categoryMock->expects($this->once())->method('getProductCollection')->willReturn($productsMock); $categoryMock->expects($this->once())->method('getId')->willReturn($categoryId); $productsMock->expects($this->once())->method('addFieldToSelect')->with('position')->willReturnSelf(); + $productsMock->expects($this->once())->method('groupByAttribute')->with('entity_id')->willReturnSelf(); $productsMock->expects($this->once())->method('getItems')->willReturn($items); + $productsMock->expects($this->once()) + ->method('getProductEntityMetadata') + ->willReturn(new DataObject(['identifier_field' => 'entity_id'])); $this->productLinkFactoryMock->expects($this->once())->method('create')->willReturn($categoryProductLinkMock); $categoryProductLinkMock->expects($this->once()) ->method('setSku') diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php deleted file mode 100644 index 93bb85abced75..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/ImageUploaderTest.php +++ /dev/null @@ -1,168 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model; - -use Magento\Catalog\Model\ImageUploader; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use Magento\MediaStorage\Helper\File\Storage\Database; -use Magento\MediaStorage\Model\File\Uploader; -use Magento\MediaStorage\Model\File\UploaderFactory; -use Magento\Store\Model\Store; -use Magento\Store\Model\StoreManagerInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; - -class ImageUploaderTest extends TestCase -{ - /** - * @var ImageUploader - */ - private $imageUploader; - - /** - * Core file storage database - * - * @var Database|MockObject - */ - private $coreFileStorageDatabaseMock; - - /** - * Media directory object (writable). - * - * @var Filesystem|MockObject - */ - private $mediaDirectoryMock; - - /** - * Media directory object (writable). - * - * @var WriteInterface|MockObject - */ - private $mediaWriteDirectoryMock; - - /** - * Uploader factory - * - * @var UploaderFactory|MockObject - */ - private $uploaderFactoryMock; - - /** - * Store manager - * - * @var StoreManagerInterface|MockObject - */ - private $storeManagerMock; - - /** - * @var LoggerInterface|MockObject - */ - private $loggerMock; - - /** - * Base tmp path - * - * @var string - */ - private $baseTmpPath; - - /** - * Base path - * - * @var string - */ - private $basePath; - - /** - * Allowed extensions - * - * @var array - */ - private $allowedExtensions; - - /** - * Allowed mime types - * - * @var array - */ - private $allowedMimeTypes; - - protected function setUp(): void - { - $this->coreFileStorageDatabaseMock = $this->createMock( - Database::class - ); - $this->mediaDirectoryMock = $this->createMock( - Filesystem::class - ); - $this->mediaWriteDirectoryMock = $this->createMock( - WriteInterface::class - ); - $this->mediaDirectoryMock->expects($this->any())->method('getDirectoryWrite')->willReturn( - $this->mediaWriteDirectoryMock - ); - $this->uploaderFactoryMock = $this->createMock( - UploaderFactory::class - ); - $this->storeManagerMock = $this->createMock( - StoreManagerInterface::class - ); - $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $this->baseTmpPath = 'base/tmp/'; - $this->basePath = 'base/real/'; - $this->allowedExtensions = ['.jpg']; - $this->allowedMimeTypes = ['image/jpg', 'image/jpeg', 'image/gif', 'image/png']; - - $this->imageUploader = - new ImageUploader( - $this->coreFileStorageDatabaseMock, - $this->mediaDirectoryMock, - $this->uploaderFactoryMock, - $this->storeManagerMock, - $this->loggerMock, - $this->baseTmpPath, - $this->basePath, - $this->allowedExtensions, - $this->allowedMimeTypes - ); - } - - public function testSaveFileToTmpDir() - { - $fileId = 'file.jpg'; - $allowedMimeTypes = [ - 'image/jpg', - 'image/jpeg', - 'image/gif', - 'image/png', - ]; - /** @var \Magento\MediaStorage\Model\File\Uploader|MockObject $uploader */ - $uploader = $this->createMock(Uploader::class); - $this->uploaderFactoryMock->expects($this->once())->method('create')->willReturn($uploader); - $uploader->expects($this->once())->method('setAllowedExtensions')->with($this->allowedExtensions); - $uploader->expects($this->once())->method('setAllowRenameFiles')->with(true); - $this->mediaWriteDirectoryMock->expects($this->once())->method('getAbsolutePath')->with($this->baseTmpPath) - ->willReturn($this->basePath); - $uploader->expects($this->once())->method('save')->with($this->basePath) - ->willReturn(['tmp_name' => $this->baseTmpPath, 'file' => $fileId, 'path' => $this->basePath]); - $uploader->expects($this->atLeastOnce())->method('checkMimeType')->with($allowedMimeTypes)->willReturn(true); - $storeMock = $this->createPartialMock( - Store::class, - ['getBaseUrl'] - ); - $this->storeManagerMock->expects($this->once())->method('getStore')->willReturn($storeMock); - $storeMock->expects($this->once())->method('getBaseUrl'); - $this->coreFileStorageDatabaseMock->expects($this->once())->method('saveFile'); - - $result = $this->imageUploader->saveFileToTmpDir($fileId); - - $this->assertArrayNotHasKey('path', $result); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php new file mode 100644 index 0000000000000..f53b05a88c54f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/Product/Action/RowsTest.php @@ -0,0 +1,260 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Category\Product\Action; + +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Category\Product\Action\Rows; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Indexer\Model\WorkingStateProvider; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Indexer\IndexerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for Rows action + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) to preserve compatibility with tested class + */ +class RowsTest extends TestCase +{ + /** + * @var WorkingStateProvider|MockObject + */ + private $workingStateProvider; + + /** + * @var ResourceConnection|MockObject + */ + private $resource; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var Config|MockObject + */ + private $config; + + /** + * @var QueryGenerator|MockObject + */ + private $queryGenerator; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var CacheContext|MockObject + */ + private $cacheContext; + + /** + * @var EventManagerInterface|MockObject + */ + private $eventManager; + + /** + * @var IndexerRegistry|MockObject + */ + private $indexerRegistry; + + /** + * @var TableMaintainer|MockObject + */ + private $tableMaintainer; + + /** + * @var IndexerInterface|MockObject + */ + private $indexer; + + /** + * @var AdapterInterface|MockObject + */ + private $connection; + + /** + * @var Select|MockObject + */ + private $select; + + /** + * @var Rows + */ + private $rowsModel; + + protected function setUp() : void + { + $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resource = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->resource->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connection); + $this->select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $this->select->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinLeft') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('columns') + ->willReturnSelf(); + $this->connection->expects($this->any()) + ->method('select') + ->willReturn($this->select); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->queryGenerator = $this->getMockBuilder(QueryGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cacheContext = $this->getMockBuilder(CacheContext::class) + ->disableOriginalConstructor() + ->getMock(); + $this->eventManager = $this->getMockBuilder(EventManagerInterface::class) + ->getMockForAbstractClass(); + $this->indexerRegistry = $this->getMockBuilder(IndexerRegistry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexer = $this->getMockBuilder(IndexerInterface::class) + ->getMockForAbstractClass(); + $this->tableMaintainer = $this->getMockBuilder(TableMaintainer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->rowsModel = new Rows( + $this->resource, + $this->storeManager, + $this->config, + $this->queryGenerator, + $this->metadataPool, + $this->tableMaintainer, + $this->cacheContext, + $this->eventManager, + $this->indexerRegistry, + $this->workingStateProvider + ); + } + + public function testExecuteWithIndexerWorking() : void + { + $categoryId = '1'; + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any()) + ->method('getRootCategoryId') + ->willReturn($categoryId); + $store->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $attribute = $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->config->expects($this->any()) + ->method('getAttribute') + ->willReturn($attribute); + + $table = $this->getMockBuilder(Table::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection->expects($this->any()) + ->method('newTable') + ->willReturn($table); + + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + + $this->connection->expects($this->any()) + ->method('fetchAll') + ->willReturn([]); + + $this->connection->expects($this->any()) + ->method('fetchOne') + ->willReturn($categoryId); + $this->indexerRegistry->expects($this->at(0)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(1)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(2)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(3)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(4)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexer->expects($this->any()) + ->method('getId') + ->willReturn(ProductCategoryIndexer::INDEXER_ID); + $this->workingStateProvider->expects($this->any()) + ->method('isWorking') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn(true); + $this->storeManager->expects($this->any()) + ->method('getStores') + ->willReturn([$store]); + + $this->connection->expects($this->once()) + ->method('delete'); + + $result = $this->rowsModel->execute([1, 2, 3]); + $this->assertInstanceOf(Rows::class, $result); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php new file mode 100644 index 0000000000000..66eb058c7b0a4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/Category/Action/RowsTest.php @@ -0,0 +1,269 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\Indexer\Product\Category\Action; + +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Store\Model\Store; +use Magento\Catalog\Model\Config; +use Magento\Catalog\Model\Indexer\Product\Category\Action\Rows; +use Magento\Catalog\Model\Indexer\Product\Category as ProductCategoryIndexer; +use Magento\Catalog\Model\Indexer\Category\Product as CategoryProductIndexer; +use Magento\Catalog\Model\Indexer\Category\Product\TableMaintainer; +use Magento\Indexer\Model\WorkingStateProvider; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Query\Generator as QueryGenerator; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Select; +use Magento\Framework\DB\Ddl\Table; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface as EventManagerInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Framework\Indexer\CacheContext; +use Magento\Framework\Indexer\IndexerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for Rows action + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) to preserve compatibility with tested class + */ +class RowsTest extends TestCase +{ + /** + * @var WorkingStateProvider|MockObject + */ + private $workingStateProvider; + + /** + * @var ResourceConnection|MockObject + */ + private $resource; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManager; + + /** + * @var Config|MockObject + */ + private $config; + + /** + * @var QueryGenerator|MockObject + */ + private $queryGenerator; + + /** + * @var MetadataPool|MockObject + */ + private $metadataPool; + + /** + * @var CacheContext|MockObject + */ + private $cacheContext; + + /** + * @var EventManagerInterface|MockObject + */ + private $eventManager; + + /** + * @var IndexerRegistry|MockObject + */ + private $indexerRegistry; + + /** + * @var TableMaintainer|MockObject + */ + private $tableMaintainer; + + /** + * @var IndexerInterface|MockObject + */ + private $indexer; + + /** + * @var AdapterInterface|MockObject + */ + private $connection; + + /** + * @var Select|MockObject + */ + private $select; + + /** + * @var Rows + */ + private $rowsModel; + + protected function setUp() : void + { + $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) + ->disableOriginalConstructor() + ->getMock(); + $this->resource = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->resource->expects($this->any()) + ->method('getConnection') + ->willReturn($this->connection); + $this->select = $this->getMockBuilder(Select::class) + ->disableOriginalConstructor() + ->getMock(); + $this->select->expects($this->any()) + ->method('from') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('where') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('distinct') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinInner') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('group') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('joinLeft') + ->willReturnSelf(); + $this->select->expects($this->any()) + ->method('columns') + ->willReturnSelf(); + $this->connection->expects($this->any()) + ->method('select') + ->willReturn($this->select); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) + ->getMockForAbstractClass(); + $this->config = $this->getMockBuilder(Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->queryGenerator = $this->getMockBuilder(QueryGenerator::class) + ->disableOriginalConstructor() + ->getMock(); + $this->metadataPool = $this->getMockBuilder(MetadataPool::class) + ->disableOriginalConstructor() + ->getMock(); + $this->cacheContext = $this->getMockBuilder(CacheContext::class) + ->disableOriginalConstructor() + ->getMock(); + $this->eventManager = $this->getMockBuilder(EventManagerInterface::class) + ->getMockForAbstractClass(); + $this->indexerRegistry = $this->getMockBuilder(IndexerRegistry::class) + ->disableOriginalConstructor() + ->getMock(); + $this->indexer = $this->getMockBuilder(IndexerInterface::class) + ->getMockForAbstractClass(); + $this->tableMaintainer = $this->getMockBuilder(TableMaintainer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->rowsModel = new Rows( + $this->resource, + $this->storeManager, + $this->config, + $this->queryGenerator, + $this->metadataPool, + $this->tableMaintainer, + $this->cacheContext, + $this->eventManager, + $this->indexerRegistry, + $this->workingStateProvider + ); + } + + public function testExecuteWithIndexerWorking() : void + { + $categoryId = '1'; + $store = $this->getMockBuilder(Store::class) + ->disableOriginalConstructor() + ->getMock(); + $store->expects($this->any()) + ->method('getRootCategoryId') + ->willReturn($categoryId); + $store->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $attribute = $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->config->expects($this->any()) + ->method('getAttribute') + ->willReturn($attribute); + + $table = $this->getMockBuilder(Table::class) + ->disableOriginalConstructor() + ->getMock(); + $this->connection->expects($this->any()) + ->method('newTable') + ->willReturn($table); + + $metadata = $this->getMockBuilder(EntityMetadataInterface::class) + ->getMockForAbstractClass(); + $this->metadataPool->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + + $this->connection->expects($this->any()) + ->method('fetchAll') + ->willReturn([]); + $this->connection->expects($this->any()) + ->method('fetchCol') + ->willReturn([]); + + $this->connection->expects($this->any()) + ->method('fetchOne') + ->willReturn($categoryId); + $this->indexerRegistry->expects($this->at(0)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(1)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(2)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(3)) + ->method('get') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexerRegistry->expects($this->at(4)) + ->method('get') + ->with(ProductCategoryIndexer::INDEXER_ID) + ->willReturn($this->indexer); + $this->indexer->expects($this->any()) + ->method('getId') + ->willReturn(CategoryProductIndexer::INDEXER_ID); + $this->workingStateProvider->expects($this->any()) + ->method('isWorking') + ->with(CategoryProductIndexer::INDEXER_ID) + ->willReturn(true); + $this->storeManager->expects($this->any()) + ->method('getStores') + ->willReturn([$store]); + + $this->connection->expects($this->once()) + ->method('delete'); + + $result = $this->rowsModel->execute([1, 2, 3]); + $this->assertInstanceOf(Rows::class, $result); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php deleted file mode 100644 index af8245de3525d..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/Image/ContextTest.php +++ /dev/null @@ -1,79 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\View\Asset\Image; - -use Magento\Catalog\Model\Product\Media\ConfigInterface; -use Magento\Catalog\Model\View\Asset\Image\Context; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\WriteInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ContextTest extends TestCase -{ - /** - * @var Context - */ - protected $model; - - /** - * @var WriteInterface|MockObject - */ - protected $mediaDirectory; - - /** - * @var ContextInterface|MockObject - */ - protected $mediaConfig; - - /** - * @var Filesystem|MockObject - */ - protected $filesystem; - - protected function setUp(): void - { - $this->mediaConfig = $this->getMockBuilder(ConfigInterface::class) - ->getMockForAbstractClass(); - $this->mediaConfig->expects($this->any())->method('getBaseMediaPath')->willReturn('catalog/product'); - $this->mediaDirectory = $this->getMockBuilder(WriteInterface::class) - ->getMockForAbstractClass(); - $this->mediaDirectory->expects($this->once())->method('create')->with('catalog/product'); - $this->filesystem = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - $this->filesystem->expects($this->once()) - ->method('getDirectoryWrite') - ->with(DirectoryList::MEDIA) - ->willReturn($this->mediaDirectory); - $this->model = new Context( - $this->mediaConfig, - $this->filesystem - ); - } - - public function testGetPath() - { - $path = '/var/www/html/magento2ce/pub/media/catalog/product'; - $this->mediaDirectory->expects($this->once()) - ->method('getAbsolutePath') - ->with('catalog/product') - ->willReturn($path); - - $this->assertEquals($path, $this->model->getPath()); - } - - public function testGetUrl() - { - $baseUrl = 'http://localhost/pub/media/catalog/product'; - $this->mediaConfig->expects($this->once())->method('getBaseMediaUrl')->willReturn($baseUrl); - - $this->assertEquals($baseUrl, $this->model->getBaseUrl()); - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php deleted file mode 100644 index 1a61cd4d4eea8..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/ImageTest.php +++ /dev/null @@ -1,213 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Model\View\Asset; - -use Magento\Catalog\Model\Product\Media\ConfigInterface; -use Magento\Catalog\Model\View\Asset\Image; -use Magento\Framework\Encryption\EncryptorInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Framework\View\Asset\ContextInterface; -use Magento\Framework\View\Asset\Repository; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class ImageTest extends TestCase -{ - /** - * @var Image - */ - protected $model; - - /** - * @var ContextInterface|MockObject - */ - protected $mediaConfig; - - /** - * @var EncryptorInterface|MockObject - */ - protected $encryptor; - - /** - * @var ContextInterface|MockObject - */ - protected $context; - - /** - * @var Repository|MockObject - */ - private $assetRepo; - - private $objectManager; - - protected function setUp(): void - { - $this->mediaConfig = $this->getMockForAbstractClass(ConfigInterface::class); - $this->encryptor = $this->getMockForAbstractClass(EncryptorInterface::class); - $this->context = $this->getMockForAbstractClass(ContextInterface::class); - $this->assetRepo = $this->createMock(Repository::class); - $this->objectManager = new ObjectManager($this); - $this->model = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'imageContext' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => '/somefile.png', - 'assetRepo' => $this->assetRepo, - 'miscParams' => [ - 'image_width' => 100, - 'image_height' => 50, - 'constrain_only' => false, - 'keep_aspect_ratio' => false, - 'keep_frame' => true, - 'keep_transparency' => false, - 'background' => '255,255,255', - 'image_type' => 'image', //thumbnail,small_image,image,swatch_image,swatch_thumb - 'quality' => 80, - 'angle' => null - ] - ] - ); - } - - public function testModuleAndContentAndContentType() - { - $contentType = 'image'; - $this->assertEquals($contentType, $this->model->getContentType()); - $this->assertEquals($contentType, $this->model->getSourceContentType()); - $this->assertNull($this->model->getContent()); - $this->assertEquals('cache', $this->model->getModule()); - } - - public function testGetFilePath() - { - $this->assertEquals('/somefile.png', $this->model->getFilePath()); - } - - public function testGetSoureFile() - { - $this->mediaConfig->expects($this->once())->method('getBaseMediaPath')->willReturn('catalog/product'); - $this->assertEquals('catalog/product/somefile.png', $this->model->getSourceFile()); - } - - public function testGetContext() - { - $this->assertInstanceOf(ContextInterface::class, $this->model->getContext()); - } - - /** - * @param string $filePath - * @param array $miscParams - * @param string $readableParams - * @dataProvider getPathDataProvider - */ - public function testGetPath($filePath, $miscParams, $readableParams) - { - $imageModel = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'context' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => $filePath, - 'assetRepo' => $this->assetRepo, - 'miscParams' => $miscParams - ] - ); - $absolutePath = '/var/www/html/magento2ce/pub/media/catalog/product'; - $hashPath = 'somehash'; - $this->context->method('getPath')->willReturn($absolutePath); - $this->encryptor->expects(static::once()) - ->method('hash') - ->with($readableParams, $this->anything()) - ->willReturn($hashPath); - static::assertEquals( - $absolutePath . '/cache/' . $hashPath . $filePath, - $imageModel->getPath() - ); - } - - /** - * @param string $filePath - * @param array $miscParams - * @param string $readableParams - * @dataProvider getPathDataProvider - */ - public function testGetUrl($filePath, $miscParams, $readableParams) - { - $imageModel = $this->objectManager->getObject( - Image::class, - [ - 'mediaConfig' => $this->mediaConfig, - 'context' => $this->context, - 'encryptor' => $this->encryptor, - 'filePath' => $filePath, - 'assetRepo' => $this->assetRepo, - 'miscParams' => $miscParams - ] - ); - $absolutePath = 'http://localhost/pub/media/catalog/product'; - $hashPath = 'somehash'; - $this->context->expects(static::once())->method('getBaseUrl')->willReturn($absolutePath); - $this->encryptor->expects(static::once()) - ->method('hash') - ->with($readableParams, $this->anything()) - ->willReturn($hashPath); - static::assertEquals( - $absolutePath . '/cache/' . $hashPath . $filePath, - $imageModel->getUrl() - ); - } - - /** - * @return array - */ - public function getPathDataProvider() - { - return [ - [ - '/some_file.png', - [], //default value for miscParams, - 'h:empty_w:empty_q:empty_r:empty_nonproportional_noframe_notransparency_notconstrainonly_nobackground', - ], - [ - '/some_file_2.png', - [ - 'image_type' => 'thumbnail', - 'image_height' => 75, - 'image_width' => 75, - 'keep_aspect_ratio' => true, - 'keep_frame' => true, - 'keep_transparency' => true, - 'constrain_only' => true, - 'background' => [233,1,0], - 'angle' => null, - 'quality' => 80, - ], - 'h:75_w:75_proportional_frame_transparency_doconstrainonly_rgb233,1,0_r:empty_q:80', - ], - [ - '/some_file_3.png', - [ - 'image_type' => 'thumbnail', - 'image_height' => 75, - 'image_width' => 75, - 'keep_aspect_ratio' => false, - 'keep_frame' => false, - 'keep_transparency' => false, - 'constrain_only' => false, - 'background' => [233,1,0], - 'angle' => 90, - 'quality' => 80, - ], - 'h:75_w:75_nonproportional_noframe_notransparency_notconstrainonly_rgb233,1,0_r:90_q:80', - ], - ]; - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php index f32a7513f236b..401f16831e75a 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/View/Asset/PlaceholderTest.php @@ -141,10 +141,10 @@ public function testGetUrl($imageType, $placeholderPath) if ($placeholderPath == null) { $this->imageContext->expects($this->never())->method('getBaseUrl'); - $expectedResult = 'http://localhost/pub/media/catalog/product/to_default/placeholder/by_type'; + $expectedResult = 'http://localhost/media/catalog/product/to_default/placeholder/by_type'; $this->repository->expects($this->any())->method('getUrl')->willReturn($expectedResult); } else { - $baseUrl = 'http://localhost/pub/media/catalog/product'; + $baseUrl = 'http://localhost/media/catalog/product'; $this->imageContext->expects($this->any())->method('getBaseUrl')->willReturn($baseUrl); $expectedResult = $baseUrl . DIRECTORY_SEPARATOR . $imageModel->getModule() diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProviderTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProviderTest.php new file mode 100644 index 0000000000000..07b3de40c31f8 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProviderTest.php @@ -0,0 +1,304 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\CurrencySymbolProvider; +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Catalog\Model\Product; +use Magento\Directory\Model\Currency as CurrencyModel; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Locale\CurrencyInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Zend_Currency; + +/** + * Test class for Website Currency Symbol provider + */ +class CurrencySymbolProviderTest extends TestCase +{ + /** + * @var CurrencySymbolProvider|MockObject + */ + private $currencySymbolProvider; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var LocatorInterface|MockObject + */ + private $locatorMock; + + /** + * @var CurrencyInterface|MockObject + */ + private $localeCurrencyMock; + + /** + * @var StoreInterface|MockObject + */ + private $currentStoreMock; + + /** + * @var CurrencyModel|MockObject + */ + private $currencyMock; + + /** + * @var Zend_Currency|MockObject + */ + private $websiteCurrencyMock; + + /** + * @var Product|MockObject + */ + private $productMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->scopeConfigMock = $this->getMockForAbstractClass( + ScopeConfigInterface::class, + [], + '', + true, + true, + true, + ['getValue'] + ); + $this->storeManagerMock = $this->getMockForAbstractClass( + StoreManagerInterface::class, + [], + '', + true, + true, + true, + ['getWebsites'] + ); + $this->currentStoreMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + true, + true, + true, + ['getBaseCurrency'] + ); + $this->currencyMock = $this->createMock(CurrencyModel::class); + $this->websiteCurrencyMock = $this->createMock(Zend_Currency::class); + $this->productMock = $this->createMock(Product::class); + $this->locatorMock = $this->getMockForAbstractClass( + LocatorInterface::class, + [], + '', + true, + true, + true, + ['getStore', 'getProduct'] + ); + $this->localeCurrencyMock = $this->getMockForAbstractClass( + CurrencyInterface::class, + [], + '', + true, + true, + true, + ['getWebsites', 'getCurrency'] + ); + $this->currencySymbolProvider = $objectManager->getObject( + CurrencySymbolProvider::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'storeManager' => $this->storeManagerMock, + 'locator' => $this->locatorMock, + 'localeCurrency' => $this->localeCurrencyMock + ] + ); + } + + /** + * Test for Get option array of currency symbol prefixes. + * + * @param int $catalogPriceScope + * @param string $defaultStoreCurrencySymbol + * @param array $listOfWebsites + * @param array $productWebsiteIds + * @param array $currencySymbols + * @param array $actualResult + * @dataProvider getWebsiteCurrencySymbolDataProvider + */ + public function testGetCurrenciesPerWebsite( + int $catalogPriceScope, + string $defaultStoreCurrencySymbol, + array $listOfWebsites, + array $productWebsiteIds, + array $currencySymbols, + array $actualResult + ): void { + $this->locatorMock->expects($this->any()) + ->method('getStore') + ->willReturn($this->currentStoreMock); + $this->currentStoreMock->expects($this->any()) + ->method('getBaseCurrency') + ->willReturn($this->currencyMock); + $this->currencyMock->expects($this->any()) + ->method('getCurrencySymbol') + ->willReturn($defaultStoreCurrencySymbol); + $this->scopeConfigMock + ->expects($this->any()) + ->method('getValue') + ->willReturn($catalogPriceScope); + $this->locatorMock->expects($this->any()) + ->method('getProduct') + ->willReturn($this->productMock); + $this->storeManagerMock->expects($this->any()) + ->method('getWebsites') + ->willReturn($listOfWebsites); + $this->productMock->expects($this->any()) + ->method('getWebsiteIds') + ->willReturn($productWebsiteIds); + $this->localeCurrencyMock->expects($this->any()) + ->method('getCurrency') + ->willReturn($this->websiteCurrencyMock); + foreach ($currencySymbols as $currencySymbol) { + $this->websiteCurrencyMock->expects($this->any()) + ->method('getSymbol') + ->willReturn($currencySymbol); + } + $expectedResult = $this->currencySymbolProvider + ->getCurrenciesPerWebsite(); + $this->assertEquals($expectedResult, $actualResult); + } + + /** + * DataProvider for getCurrenciesPerWebsite. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getWebsiteCurrencySymbolDataProvider(): array + { + return [ + 'verify website currency with default website and global price scope' => [ + 'catalogPriceScope' => 0, + 'defaultStoreCurrencySymbol' => '$', + 'listOfWebsites' => $this->getWebsitesMock( + [ + [ + 'id' => '1', + 'name' => 'Main Website', + 'code' => 'main_website', + 'base_currency_code' => 'USD', + 'currency_symbol' => '$' + ] + ] + ), + 'productWebsiteIds' => ['1'], + 'currencySymbols' => ['$'], + 'actualResult' => ['$'] + ], + 'verify website currency with default website and website price scope' => [ + 'catalogPriceScope' => 1, + 'defaultStoreCurrencySymbol' => '$', + 'listOfWebsites' => $this->getWebsitesMock( + [ + [ + 'id' => '1', + 'name' => 'Main Website', + 'code' => 'main_website', + 'base_currency_code' => 'USD', + 'currency_symbol' => '$' + ] + ] + ), + 'productWebsiteIds' => ['1'], + 'currencySymbols' => ['$'], + 'actualResult' => ['$', '$'] + ], + 'verify website currency with two website and website price scope' => [ + 'catalogPriceScope' => 1, + 'defaultStoreCurrencySymbol' => '$', + 'listOfWebsites' => $this->getWebsitesMock( + [ + [ + 'id' => '1', + 'name' => 'Main Website', + 'code' => 'main_website', + 'base_currency_code' => 'USD', + 'currency_symbol' => '$' + ], + [ + 'id' => '2', + 'name' => 'Indian Website', + 'code' => 'indian_website', + 'base_currency_code' => 'INR', + 'currency_symbol' => '₹' + ] + ] + ), + 'productWebsiteIds' => ['1', '2'], + 'currencySymbols' => ['$', '₹'], + 'actualResult' => ['$', '$', '$'] + ] + ]; + } + + /** + * Get list of websites mock + * + * @param array $websites + * @return array + */ + private function getWebsitesMock(array $websites): array + { + $websitesMock = []; + foreach ($websites as $key => $website) { + $websitesMock[$key] = $this->getMockForAbstractClass( + WebsiteInterface::class, + [], + '', + true, + true, + true, + ['getId', 'getBaseCurrencyCode'] + ); + $websitesMock[$key]->expects($this->any()) + ->method('getId') + ->willReturn($website['id']); + $websitesMock[$key]->expects($this->any()) + ->method('getBaseCurrencyCode') + ->willReturn($website['base_currency_code']); + } + return $websitesMock; + } + + protected function tearDown(): void + { + unset($this->scopeConfigMock); + unset($this->storeManagerMock); + unset($this->currentStoreMock); + unset($this->currencyMock); + unset($this->websiteCurrencyMock); + unset($this->productMock); + unset($this->locatorMock); + unset($this->localeCurrencyMock); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php index 605a5e4fd5e3b..457408e0934af 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Listing/Collector/ImageTest.php @@ -104,9 +104,6 @@ public function testGet() ->method('create') ->willReturn($image); - $imageHelper->expects($this->once()) - ->method('getResizedImageInfo') - ->willReturn([11, 11]); $this->state->expects($this->once()) ->method('emulateAreaCode') ->with( @@ -116,12 +113,14 @@ public function testGet() ) ->willReturn($imageHelper); + $width = 5; + $height = 10; $imageHelper->expects($this->once()) ->method('getHeight') - ->willReturn(10); + ->willReturn($height); $imageHelper->expects($this->once()) ->method('getWidth') - ->willReturn(10); + ->willReturn($width); $imageHelper->expects($this->once()) ->method('getLabel') ->willReturn('Label'); @@ -137,10 +136,10 @@ public function testGet() ->with(); $image->expects($this->once()) ->method('setResizedHeight') - ->with(11); + ->with($height); $image->expects($this->once()) ->method('setResizedWidth') - ->with(11); + ->with($width); $productRenderInfoDto->expects($this->once()) ->method('setImages') diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php index 174a01b72a109..8c9421b073394 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AdvancedPricing.php @@ -100,6 +100,11 @@ class AdvancedPricing extends AbstractModifier */ private $customerGroupSource; + /** + * @var CurrencySymbolProvider + */ + private $currencySymbolProvider; + /** * @param LocatorInterface $locator * @param StoreManagerInterface $storeManager @@ -110,7 +115,8 @@ class AdvancedPricing extends AbstractModifier * @param Data $directoryHelper * @param ArrayManager $arrayManager * @param string $scopeName - * @param GroupSourceInterface $customerGroupSource + * @param GroupSourceInterface|null $customerGroupSource + * @param CurrencySymbolProvider|null $currencySymbolProvider * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -123,7 +129,8 @@ public function __construct( Data $directoryHelper, ArrayManager $arrayManager, $scopeName = '', - GroupSourceInterface $customerGroupSource = null + GroupSourceInterface $customerGroupSource = null, + ?CurrencySymbolProvider $currencySymbolProvider = null ) { $this->locator = $locator; $this->storeManager = $storeManager; @@ -136,6 +143,8 @@ public function __construct( $this->scopeName = $scopeName; $this->customerGroupSource = $customerGroupSource ?: ObjectManager::getInstance()->get(GroupSourceInterface::class); + $this->currencySymbolProvider = $currencySymbolProvider + ?: ObjectManager::getInstance()->get(CurrencySymbolProvider::class); } /** @@ -488,6 +497,7 @@ private function getTierPriceStructure($tierPricePath) 'arguments' => [ 'data' => [ 'config' => [ + 'component' => 'Magento_Catalog/js/components/website-currency-symbol', 'dataType' => Text::NAME, 'formElement' => Select::NAME, 'componentType' => Field::NAME, @@ -498,6 +508,10 @@ private function getTierPriceStructure($tierPricePath) 'visible' => $this->isMultiWebsites(), 'disabled' => ($this->isShowWebsiteColumn() && !$this->isAllowChangeWebsite()), 'sortOrder' => 10, + 'currenciesForWebsites' => $this->currencySymbolProvider + ->getCurrenciesPerWebsite(), + 'currency' => $this->currencySymbolProvider + ->getDefaultCurrency(), ], ], ], @@ -548,9 +562,6 @@ private function getTierPriceStructure($tierPricePath) 'label' => __('Price'), 'enableLabel' => true, 'dataScope' => 'price', - 'addbefore' => $this->locator->getStore() - ->getBaseCurrency() - ->getCurrencySymbol(), 'sortOrder' => 40, 'validation' => [ 'required-entry' => true, @@ -559,8 +570,12 @@ private function getTierPriceStructure($tierPricePath) ], 'imports' => [ 'priceValue' => '${ $.provider }:data.product.price', - '__disableTmpl' => ['priceValue' => false], + '__disableTmpl' => ['priceValue' => false, 'addbefore' => false], + 'addbefore' => '${ $.parentName }:currency' ], + 'tracks' => [ + 'addbefore' => true + ] ], ], ], diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProvider.php new file mode 100644 index 0000000000000..b46ca682e576a --- /dev/null +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/CurrencySymbolProvider.php @@ -0,0 +1,139 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Ui\DataProvider\Product\Form\Modifier; + +use Magento\Catalog\Model\Locator\LocatorInterface; +use Magento\Framework\Locale\CurrencyInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Website; + +/** + * Website Currency Symbol provider + */ +class CurrencySymbolProvider +{ + /** + * Scope Config Details + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Store Information + * + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * Store locator + * + * @var LocatorInterface + */ + private $locator; + + /** + * Locale Currency + * + * @var CurrencyInterface + */ + private $localeCurrency; + + /** + * Initialize objects for website currency scope + * + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager + * @param LocatorInterface $locator + * @param CurrencyInterface $localeCurrency + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, + LocatorInterface $locator, + CurrencyInterface $localeCurrency + ) { + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; + $this->locator = $locator; + $this->localeCurrency = $localeCurrency; + } + + /** + * Get option array of currency symbol prefixes. + * + * @return array + */ + public function getCurrenciesPerWebsite(): array + { + $baseCurrency = $this->locator->getStore() + ->getBaseCurrency(); + $websitesCurrencySymbol[0] = $baseCurrency->getCurrencySymbol() ?? + $baseCurrency->getCurrencyCode(); + $catalogPriceScope = $this->getCatalogPriceScope(); + $product = $this->locator->getProduct(); + $websitesList = $this->storeManager->getWebsites(); + $productWebsiteIds = $product->getWebsiteIds(); + if ($catalogPriceScope!=0) { + foreach ($websitesList as $website) { + /** @var Website $website */ + if (!in_array($website->getId(), $productWebsiteIds)) { + continue; + } + $websitesCurrencySymbol[$website->getId()] = $this + ->getCurrencySymbol( + $website->getBaseCurrencyCode() + ); + } + } + return $websitesCurrencySymbol; + } + + /** + * Get default store currency symbol + * + * @return string + */ + public function getDefaultCurrency(): string + { + $baseCurrency = $this->locator->getStore() + ->getBaseCurrency(); + return $baseCurrency->getCurrencySymbol() ?? + $baseCurrency->getCurrencyCode(); + } + + /** + * Get catalog price scope from the admin config + * + * @return int + */ + public function getCatalogPriceScope(): int + { + return (int) $this->scopeConfig->getValue( + Store::XML_PATH_PRICE_SCOPE, + ScopeInterface::SCOPE_WEBSITE + ); + } + + /** + * Retrieve currency name by code + * + * @param string $code + * @return string + */ + private function getCurrencySymbol(string $code): string + { + $currency = $this->localeCurrency->getCurrency($code); + return $currency->getSymbol() ? + $currency->getSymbol() : $currency->getShortName(); + } +} diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index e9e8229e581ba..25e04302bd33c 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -118,6 +118,10 @@ private function getUpdatedTierPriceStructure(array $priceMeta) 'showLabel' => false, 'dataScope' => '', 'additionalClasses' => 'control-grouped', + 'imports' => [ + 'currency' => '${ $.parentName }.website_id:currency', + '__disableTmpl' => ['currency' => false], + ], 'sortOrder' => isset($priceMeta['arguments']['data']['config']['sortOrder']) ? $priceMeta['arguments']['data']['config']['sortOrder'] : 40, ], diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index 2324ca27ffaaf..2d4f1566a5b6e 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -118,18 +118,14 @@ public function collect(ProductInterface $product, ProductRenderInterface $produ [$product, $imageCode, (int) $productRender->getStoreId(), $image] ); - try { - $resizedInfo = $helper->getResizedImageInfo(); - } catch (NotLoadInfoImageException $exception) { - $resizedInfo = [$helper->getWidth(), $helper->getHeight()]; - } - $image->setCode($imageCode); - $image->setHeight($helper->getHeight()); - $image->setWidth($helper->getWidth()); + $height = $helper->getHeight(); + $image->setHeight($height); + $width = $helper->getWidth(); + $image->setWidth($width); $image->setLabel($helper->getLabel()); - $image->setResizedHeight($resizedInfo[1]); - $image->setResizedWidth($resizedInfo[0]); + $image->setResizedHeight($height); + $image->setResizedWidth($width); $images[] = $image; } diff --git a/app/code/Magento/Catalog/etc/adminhtml/system.xml b/app/code/Magento/Catalog/etc/adminhtml/system.xml index 8f8a5f36e516c..4e10453f542bb 100644 --- a/app/code/Magento/Catalog/etc/adminhtml/system.xml +++ b/app/code/Magento/Catalog/etc/adminhtml/system.xml @@ -214,6 +214,13 @@ <source_model>Magento\Catalog\Model\Config\Source\LayoutList</source_model> </field> </group> + <group id="url"> + <field id="catalog_media_url_format" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0" canRestore="1"> + <label>Catalog media URL format</label> + <source_model>Magento\Catalog\Model\Config\Source\Web\CatalogMediaUrlFormat</source_model> + <comment><![CDATA[Images should be optimized based on query parameters by your CDN or web server. Use the legacy mode for backward compatibility. <a href="https://docs.magento.com/m2/ee/user_guide/configuration/general/web.html#url-options">Learn more</a> about catalog URL formats.<br/><br/><strong style="color:red">Warning!</strong> If you switch back to legacy mode, you must <a href="https://devdocs.magento.com/guides/v2.3/frontend-dev-guide/themes/theme-images.html#resize-catalog-images">use the CLI to regenerate images</a>.]]></comment> + </field> + </group> </section> <section id="system" translate="label" type="text" sortOrder="900" showInDefault="1" showInWebsite="1" showInStore="1"> <class>separator-top</class> diff --git a/app/code/Magento/Catalog/etc/config.xml b/app/code/Magento/Catalog/etc/config.xml index aa689c7dd35b2..f5546a06dd235 100644 --- a/app/code/Magento/Catalog/etc/config.xml +++ b/app/code/Magento/Catalog/etc/config.xml @@ -67,7 +67,6 @@ <media_storage_configuration> <allowed_resources> <tmp_images_folder>tmp</tmp_images_folder> - <catalog_product_images>media/catalog/product/cache/</catalog_product_images> <catalog_images_folder>catalog</catalog_images_folder> <product_custom_options_fodler>custom_options</product_custom_options_fodler> </allowed_resources> @@ -83,6 +82,11 @@ <thumbnail_position>stretch</thumbnail_position> </watermark> </design> + <web> + <url> + <catalog_media_url_format>hash</catalog_media_url_format> + </url> + </web> <general> <validator_data> <input_types> diff --git a/app/code/Magento/Catalog/view/adminhtml/web/js/components/website-currency-symbol.js b/app/code/Magento/Catalog/view/adminhtml/web/js/components/website-currency-symbol.js new file mode 100644 index 0000000000000..069bd9baed86f --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/web/js/components/website-currency-symbol.js @@ -0,0 +1,30 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/form/element/select' +], function (Select) { + 'use strict'; + + return Select.extend({ + defaults: { + currenciesForWebsites: {}, + tracks: { + currency: true + } + }, + + /** + * Set currency symbol per website + * + * @param {String} value - currency symbol + */ + setDifferedFromDefault: function (value) { + this.currency = this.currenciesForWebsites[value]; + + return this._super(); + } + }); +}); diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml index 98d17045a1b2d..86b332679bcb4 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image.phtml @@ -11,7 +11,7 @@ <!--deprecated template as image_with_borders is a primary one--> <img class="photo image <?= $escaper->escapeHtmlAttr($block->getClass()) ?>" <?php foreach ($block->getCustomAttributes() as $name => $value): ?> - <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtmlAttr($value) ?>" + <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtml($value) ?>" <?php endforeach; ?> src="<?= $escaper->escapeUrl($block->getImageUrl()) ?>" loading="lazy" diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index 8abfe368909e4..cc1a7276c70b8 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -15,7 +15,7 @@ $paddingBottom = $block->getRatio() * 100; <span class="product-image-wrapper"> <img class="<?= $escaper->escapeHtmlAttr($block->getClass()) ?>" <?php foreach ($block->getCustomAttributes() as $name => $value): ?> - <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtmlAttr($value) ?>" + <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtml($value) ?>" <?php endforeach; ?> src="<?= $escaper->escapeUrl($block->getImageUrl()) ?>" loading="lazy" diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index f59bc338ced69..428961aa6ddf6 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -9,7 +9,6 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Config as CatalogConfig; use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Model\ResourceModel\Product\Link; use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; use Magento\CatalogImportExport\Model\Import\Product\LinkProcessor; use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; @@ -2209,6 +2208,11 @@ protected function _getUploader() $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + // make media folder a primary folder for media in external storages + if (!is_a($this->_mediaDirectory->getDriver(), File::class)) { + $dirAddon = DirectoryList::MEDIA; + } + $tmpPath = $this->getImportDir(); if (!$fileUploader->setTmpDir($tmpPath)) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index 6571b16c87565..bd17cfd2cd7f1 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -533,6 +533,7 @@ public function prepareAttributesWithDefaultValueForSave(array $rowData, $withDe if ($attrParams['is_static']) { continue; } + $attrCode = mb_strtolower($attrCode); if (isset($rowData[$attrCode]) && strlen(trim($rowData[$attrCode]))) { if (in_array($attrParams['type'], ['select', 'boolean'])) { $resultAttrs[$attrCode] = $attrParams['options'][strtolower($rowData[$attrCode])]; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php index 5b90ced62b0eb..d2a0019349ef2 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Uploader.php @@ -9,6 +9,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverPool; /** @@ -116,6 +117,11 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader */ private $maxFilenameLength = 255; + /** + * @var TargetDirectory + */ + private $targetDirectory; + /** * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb * @param \Magento\MediaStorage\Helper\File\Storage $coreFileStorage @@ -125,6 +131,7 @@ class Uploader extends \Magento\MediaStorage\Model\File\Uploader * @param Filesystem\File\ReadFactory $readFactory * @param string|null $filePath * @param \Magento\Framework\Math\Random|null $random + * @param TargetDirectory|null $targetDirectory * @throws \Magento\Framework\Exception\FileSystemException * @throws \Magento\Framework\Exception\LocalizedException */ @@ -136,7 +143,8 @@ public function __construct( Filesystem $filesystem, Filesystem\File\ReadFactory $readFactory, $filePath = null, - \Magento\Framework\Math\Random $random = null + \Magento\Framework\Math\Random $random = null, + TargetDirectory $targetDirectory = null ) { $this->_imageFactory = $imageFactory; $this->_coreFileStorageDb = $coreFileStorageDb; @@ -149,6 +157,7 @@ public function __construct( $this->_setUploadFile($filePath); } $this->random = $random ?: ObjectManager::getInstance()->get(\Magento\Framework\Math\Random::class); + $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); } /** @@ -188,7 +197,8 @@ public function move($fileName, $renameFileOff = false) } $this->_setUploadFile($tmpFilePath); - $destDir = $this->_directory->getAbsolutePath($this->getDestDir()); + $rootDirectory = $this->getTargetDirectory()->getDirectoryRead(DirectoryList::ROOT); + $destDir = $rootDirectory->getAbsolutePath($this->getDestDir()); $result = $this->save($destDir); unset($result['path']); $result['name'] = self::getCorrectFileName($result['name']); @@ -243,6 +253,20 @@ private function downloadFileFromUrl($url, $driver) return $tmpFilePath; } + /** + * Retrieves target directory. + * + * @return TargetDirectory + */ + private function getTargetDirectory(): TargetDirectory + { + if (!isset($this->targetDirectory)) { + $this->targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); + } + + return $this->targetDirectory; + } + /** * Prepare information about the file for moving * @@ -381,7 +405,8 @@ public function getDestDir() */ public function setDestDir($path) { - if (is_string($path) && $this->_directory->isWritable($path)) { + $directoryRoot = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + if (is_string($path) && $directoryRoot->isWritable($path)) { $this->_destDir = $path; return true; } @@ -404,7 +429,8 @@ protected function _moveFile($tmpPath, $destPath) $destinationRealPath = $this->_directory->getDriver()->getRealPath($destPath); $relativeDestPath = $this->_directory->getRelativePath($destPath); $isSameFile = $tmpRealPath === $destinationRealPath; - return $isSameFile ?: $this->_directory->copyFile($tmpPath, $relativeDestPath); + $rootDirectory = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + return $isSameFile ?: $this->_directory->copyFile($tmpPath, $relativeDestPath, $rootDirectory); } else { return false; } diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php index 08915fb31a8aa..9453075f99e7c 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/Product/Type/AbstractTypeTest.php @@ -6,6 +6,7 @@ */ namespace Magento\CatalogImportExport\Test\Unit\Model\Import\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory as AttributeCollectionFactory; use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; use Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType as AbstractType; @@ -13,6 +14,7 @@ use Magento\Eav\Model\Entity\Attribute; use Magento\Eav\Model\Entity\Attribute\Set; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection; +use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory as AttributeSetCollectionFactory; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; @@ -68,12 +70,12 @@ protected function setUp(): void { $this->entityModel = $this->createMock(Product::class); $attrSetColFactory = $this->createPartialMock( - \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory::class, + AttributeSetCollectionFactory::class, ['create'] ); $attrSetCollection = $this->createMock(Collection::class); $attrColFactory = $this->createPartialMock( - \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory::class, + AttributeCollectionFactory::class, ['create'] ); $attributeSet = $this->createMock(Set::class); @@ -100,14 +102,22 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMock(); - $attribute->expects($this->any())->method('getIsVisible')->willReturn(true); - $attribute->expects($this->any())->method('getIsGlobal')->willReturn(true); - $attribute->expects($this->any())->method('getIsRequired')->willReturn(true); - $attribute->expects($this->any())->method('getIsUnique')->willReturn(true); - $attribute->expects($this->any())->method('getFrontendLabel')->willReturn('frontend_label'); - $attribute->expects($this->any())->method('getApplyTo')->willReturn(['simple']); - $attribute->expects($this->any())->method('getDefaultValue')->willReturn('default_value'); - $attribute->expects($this->any())->method('usesSource')->willReturn(true); + $attribute->method('getIsVisible') + ->willReturn(true); + $attribute->method('getIsGlobal') + ->willReturn(true); + $attribute->method('getIsRequired') + ->willReturn(true); + $attribute->method('getIsUnique') + ->willReturn(true); + $attribute->method('getFrontendLabel') + ->willReturn('frontend_label'); + $attribute->method('getApplyTo') + ->willReturn(['simple']); + $attribute->method('getDefaultValue') + ->willReturn('default_value'); + $attribute->method('usesSource') + ->willReturn(true); $entityAttributes = [ [ @@ -123,38 +133,54 @@ protected function setUp(): void $attribute2 = clone $attribute; $attribute3 = clone $attribute; - $attribute1->expects($this->any())->method('getId')->willReturn('1'); - $attribute1->expects($this->any())->method('getAttributeCode')->willReturn('attr_code'); - $attribute1->expects($this->any())->method('getFrontendInput')->willReturn('multiselect'); - $attribute1->expects($this->any())->method('isStatic')->willReturn(true); - - $attribute2->expects($this->any())->method('getId')->willReturn('2'); - $attribute2->expects($this->any())->method('getAttributeCode')->willReturn('boolean_attribute'); - $attribute2->expects($this->any())->method('getFrontendInput')->willReturn('boolean'); - $attribute2->expects($this->any())->method('isStatic')->willReturn(false); - - $attribute3->expects($this->any())->method('getId')->willReturn('3'); - $attribute3->expects($this->any())->method('getAttributeCode')->willReturn('text_attribute'); - $attribute3->expects($this->any())->method('getFrontendInput')->willReturn('text'); - $attribute3->expects($this->any())->method('isStatic')->willReturn(false); - - $this->entityModel->expects($this->any())->method('getEntityTypeId')->willReturn(3); - $this->entityModel->expects($this->any())->method('getAttributeOptions')->willReturnOnConsecutiveCalls( - ['option1', 'option2'], - ['yes' => 1, 'no' => 0] - ); - $attrSetColFactory->expects($this->any())->method('create')->willReturn($attrSetCollection); - $attrSetCollection->expects($this->any())->method('setEntityTypeFilter')->willReturn([$attributeSet]); - $attrColFactory->expects($this->any())->method('create')->willReturn($attrCollection); - $attrCollection->expects($this->any()) - ->method('setAttributeSetFilter') + $attribute1->method('getId') + ->willReturn('1'); + $attribute1->method('getAttributeCode') + ->willReturn('attr_code'); + $attribute1->method('getFrontendInput') + ->willReturn('multiselect'); + $attribute1->method('isStatic') + ->willReturn(true); + + $attribute2->method('getId') + ->willReturn('2'); + $attribute2->method('getAttributeCode') + ->willReturn('boolean_attribute'); + $attribute2->method('getFrontendInput') + ->willReturn('boolean'); + $attribute2->method('isStatic') + ->willReturn(false); + + $attribute3->method('getId') + ->willReturn('3'); + $attribute3->method('getAttributeCode') + ->willReturn('Text_attribute'); + $attribute3->method('getFrontendInput') + ->willReturn('text'); + $attribute3->method('isStatic') + ->willReturn(false); + + $this->entityModel->method('getEntityTypeId') + ->willReturn(3); + $this->entityModel->method('getAttributeOptions') + ->willReturnOnConsecutiveCalls( + ['option1', 'option2'], + ['yes' => 1, 'no' => 0] + ); + $attrSetColFactory->method('create') + ->willReturn($attrSetCollection); + $attrSetCollection->method('setEntityTypeFilter') + ->willReturn([$attributeSet]); + $attrColFactory->method('create') + ->willReturn($attrCollection); + $attrCollection->method('setAttributeSetFilter') ->willReturn([$attribute1, $attribute2, $attribute3]); - $attributeSet->expects($this->any())->method('getId')->willReturn(1); - $attributeSet->expects($this->any())->method('getAttributeSetName')->willReturn('attribute_set_name'); + $attributeSet->method('getId') + ->willReturn(1); + $attributeSet->method('getAttributeSetName') + ->willReturn('attribute_set_name'); - $attrCollection - ->expects($this->any()) - ->method('addFieldToFilter') + $attrCollection->method('addFieldToFilter') ->with( ['main_table.attribute_id', 'main_table.attribute_code'], [ @@ -193,19 +219,26 @@ protected function setUp(): void 'getConnection', ] ); - $this->select->expects($this->any())->method('from')->willReturnSelf(); - $this->select->expects($this->any())->method('where')->willReturnSelf(); - $this->select->expects($this->any())->method('joinLeft')->willReturnSelf(); - $this->connection->expects($this->any())->method('select')->willReturn($this->select); + $this->select->method('from') + ->willReturnSelf(); + $this->select->method('where') + ->willReturnSelf(); + $this->select->method('joinLeft') + ->willReturnSelf(); + $this->connection->method('select') + ->willReturn($this->select); $connection = $this->createMock(Mysql::class); - $connection->expects($this->any())->method('quoteInto')->willReturn('query'); - $this->select->expects($this->any())->method('getConnection')->willReturn($connection); - $this->connection->expects($this->any())->method('insertOnDuplicate')->willReturnSelf(); - $this->connection->expects($this->any())->method('delete')->willReturnSelf(); - $this->connection->expects($this->any())->method('quoteInto')->willReturn(''); - $this->connection - ->expects($this->any()) - ->method('fetchAll') + $connection->method('quoteInto') + ->willReturn('query'); + $this->select->method('getConnection') + ->willReturn($connection); + $this->connection->method('insertOnDuplicate') + ->willReturnSelf(); + $this->connection->method('delete') + ->willReturnSelf(); + $this->connection->method('quoteInto') + ->willReturn(''); + $this->connection->method('fetchAll') ->willReturn($entityAttributes); $this->resource = $this->createPartialMock( @@ -215,12 +248,10 @@ protected function setUp(): void 'getTableName', ] ); - $this->resource->expects($this->any())->method('getConnection')->willReturn( - $this->connection - ); - $this->resource->expects($this->any())->method('getTableName')->willReturn( - 'tableName' - ); + $this->resource->method('getConnection') + ->willReturn($this->connection); + $this->resource->method('getTableName') + ->willReturn('tableName'); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->simpleType = $this->objectManagerHelper->getObject( @@ -233,9 +264,7 @@ protected function setUp(): void ] ); - $this->abstractType = $this->getMockBuilder( - \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class - ) + $this->abstractType = $this->getMockBuilder(AbstractType::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); } @@ -277,8 +306,10 @@ public function testIsRowValidSuccess() { $rowData = ['_attribute_set' => 'attribute_set_name']; $rowNum = 1; - $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(null); - $this->entityModel->expects($this->never())->method('addRowError'); + $this->entityModel->method('getRowScope') + ->willReturn(null); + $this->entityModel->expects($this->never()) + ->method('addRowError'); $this->setPropertyValue( $this->simpleType, '_attributes', @@ -296,8 +327,9 @@ public function testIsRowValidError() 'sku' => 'sku' ]; $rowNum = 1; - $this->entityModel->expects($this->any())->method('getRowScope')->willReturn(1); - $this->entityModel->expects($this->once())->method('addRowError') + $this->entityModel->method('getRowScope') + ->willReturn(1); + $this->entityModel->method('addRowError') ->with( RowValidatorInterface::ERROR_VALUE_IS_REQUIRED, 1, diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php index 52769859a74ac..2eb8c86a34686 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/ProductTest.php @@ -39,6 +39,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File as DriverFile; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Json\Helper\Data; use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; @@ -207,6 +208,9 @@ class ProductTest extends AbstractImportTestCase /** @var ImageTypeProcessor|MockObject */ protected $imageTypeProcessor; + /** @var DriverFile|MockObject */ + private $driverFile; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -374,6 +378,10 @@ protected function setUp(): void $this->errorAggregator = $this->getErrorAggregatorObject(); + $this->driverFile = $this->getMockBuilder(DriverFile::class) + ->disableOriginalConstructor() + ->getMock(); + $this->data = []; $this->imageTypeProcessor = $this->getMockBuilder(ImageTypeProcessor::class) @@ -1336,6 +1344,10 @@ public function testFillUploaderObject($isRead, $isWrite, $message) ->with('pub/media/catalog/product') ->willReturn($isWrite); + $this->_mediaDirectory + ->method('getDriver') + ->willReturn($this->driverFile); + $this->_mediaDirectory ->method('getRelativePath') ->willReturnMap( diff --git a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php index 2d482938949bc..bc8fba5e2b919 100644 --- a/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php +++ b/app/code/Magento/CatalogImportExport/Test/Unit/Model/Import/UploaderTest.php @@ -9,6 +9,7 @@ use Magento\CatalogImportExport\Model\Import\Uploader; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Driver\Http; use Magento\Framework\Filesystem\Driver\Https; @@ -73,6 +74,11 @@ class UploaderTest extends TestCase */ protected $uploader; + /** + * @var TargetDirectory|MockObject + */ + private $targetDirectory; + protected function setUp(): void { $this->coreFileStorageDb = $this->getMockBuilder(Database::class) @@ -115,6 +121,13 @@ protected function setUp(): void ->setMethods(['getRandomString']) ->getMock(); + $this->targetDirectory = $this->getMockBuilder(TargetDirectory::class) + ->disableOriginalConstructor() + ->setMethods(['getDirectoryWrite', 'getDirectoryRead']) + ->getMock(); + $this->targetDirectory->method('getDirectoryWrite')->willReturn($this->directoryMock); + $this->targetDirectory->method('getDirectoryRead')->willReturn($this->directoryMock); + $this->uploader = $this->getMockBuilder(Uploader::class) ->setConstructorArgs( [ @@ -125,7 +138,8 @@ protected function setUp(): void $this->filesystem, $this->readFactory, null, - $this->random + $this->random, + $this->targetDirectory ] ) ->setMethods(['_setUploadFile', 'save', 'getTmpDir', 'checkAllowedExtension']) @@ -274,9 +288,9 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive ->addMethods(['readAll']) ->onlyMethods(['isExists']) ->getMock(); - $driverMock->expects($this->any())->method('isExists')->willReturn(true); - $driverMock->expects($this->any())->method('readAll')->willReturn(null); - $driverPool->expects($this->any())->method('getDriver')->willReturn($driverMock); + $driverMock->method('isExists')->willReturn(true); + $driverMock->method('readAll')->willReturn(null); + $driverPool->method('getDriver')->willReturn($driverMock); $readFactory = $this->getMockBuilder(ReadFactory::class) ->setConstructorArgs( @@ -287,10 +301,11 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive ->setMethods(['create']) ->getMock(); - $readFactory->expects($this->any())->method('create') + $readFactory->method('create') ->with($expectedHost, $expectedScheme) ->willReturn($driverMock); + /** @var Uploader $uploaderMock */ $uploaderMock = $this->getMockBuilder(Uploader::class) ->setConstructorArgs( [ @@ -300,6 +315,9 @@ public function testMoveFileUrlDrivePool($fileUrl, $expectedHost, $expectedDrive $this->validator, $this->filesystem, $readFactory, + null, + $this->random, + $this->targetDirectory ] ) ->getMock(); diff --git a/app/code/Magento/CatalogInventory/Model/StockRegistryPreloader.php b/app/code/Magento/CatalogInventory/Model/StockRegistryPreloader.php new file mode 100644 index 0000000000000..2d0aef46a4ebd --- /dev/null +++ b/app/code/Magento/CatalogInventory/Model/StockRegistryPreloader.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Model; + +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; + +/** + * Preload stock data into stock registry + */ +class StockRegistryPreloader +{ + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + /** + * @var StockRegistryStorage + */ + private $stockRegistryStorage; + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $stockItemCriteriaFactory; + /** + * @var StockStatusCriteriaInterfaceFactory + */ + private $stockStatusCriteriaFactory; + /** + * @var StockStatusRepositoryInterface + */ + private $stockStatusRepository; + + /** + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockStatusRepositoryInterface $stockStatusRepository + * @param StockItemCriteriaInterfaceFactory $stockItemCriteriaFactory + * @param StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory + * @param StockConfigurationInterface $stockConfiguration + * @param StockRegistryStorage $stockRegistryStorage + */ + public function __construct( + StockItemRepositoryInterface $stockItemRepository, + StockStatusRepositoryInterface $stockStatusRepository, + StockItemCriteriaInterfaceFactory $stockItemCriteriaFactory, + StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory, + StockConfigurationInterface $stockConfiguration, + StockRegistryStorage $stockRegistryStorage + ) { + $this->stockItemRepository = $stockItemRepository; + $this->stockStatusRepository = $stockStatusRepository; + $this->stockItemCriteriaFactory = $stockItemCriteriaFactory; + $this->stockStatusCriteriaFactory = $stockStatusCriteriaFactory; + $this->stockConfiguration = $stockConfiguration; + $this->stockRegistryStorage = $stockRegistryStorage; + } + + /** + * Preload stock item into stock registry + * + * @param array $productIds + * @param int|null $scopeId + * @return \Magento\CatalogInventory\Api\Data\StockItemInterface[] + */ + public function preloadStockItems(array $productIds, ?int $scopeId = null): array + { + $scopeId = $scopeId ?? $this->stockConfiguration->getDefaultScopeId(); + $criteria = $this->stockItemCriteriaFactory->create(); + $criteria->setProductsFilter($productIds); + $criteria->setScopeFilter($scopeId); + $collection = $this->stockItemRepository->getList($criteria); + $this->setStockItems($collection->getItems(), $scopeId); + return $collection->getItems(); + } + + /** + * Saves stock items into registry + * + * @param \Magento\CatalogInventory\Api\Data\StockItemInterface[] $stockItems + * @param int $scopeId + */ + public function setStockItems(array $stockItems, int $scopeId): void + { + foreach ($stockItems as $item) { + $this->stockRegistryStorage->setStockItem($item->getProductId(), $scopeId, $item); + } + } + + /** + * Preload stock status into stock registry + * + * @param array $productIds + * @param int|null $scopeId + * @return \Magento\CatalogInventory\Api\Data\StockStatusInterface[] + */ + public function preloadStockStatuses(array $productIds, ?int $scopeId = null): array + { + $scopeId = $scopeId ?? $this->stockConfiguration->getDefaultScopeId(); + $criteria = $this->stockStatusCriteriaFactory->create(); + $criteria->setProductsFilter($productIds); + $criteria->setScopeFilter($scopeId); + $collection = $this->stockStatusRepository->getList($criteria); + $this->setStockStatuses($collection->getItems(), $scopeId); + return $collection->getItems(); + } + + /** + * Saves stock statuses into registry + * + * @param \Magento\CatalogInventory\Api\Data\StockStatusInterface[] $stockStatuses + * @param int $scopeId + */ + public function setStockStatuses(array $stockStatuses, int $scopeId): void + { + foreach ($stockStatuses as $item) { + $this->stockRegistryStorage->setStockStatus($item->getProductId(), $scopeId, $item); + } + } +} diff --git a/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php index 8fa90cf6531c4..68924c635de9d 100644 --- a/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php +++ b/app/code/Magento/CatalogInventory/Observer/AddStockItemsObserver.php @@ -10,7 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; @@ -19,36 +19,27 @@ */ class AddStockItemsObserver implements ObserverInterface { - /** - * @var StockItemCriteriaInterfaceFactory - */ - private $criteriaInterfaceFactory; - - /** - * @var StockItemRepositoryInterface - */ - private $stockItemRepository; - /** * @var StockConfigurationInterface */ private $stockConfiguration; + /** + * @var StockRegistryPreloader + */ + private $stockRegistryPreloader; /** * AddStockItemsObserver constructor. * - * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory - * @param StockItemRepositoryInterface $stockItemRepository * @param StockConfigurationInterface $stockConfiguration + * @param StockRegistryPreloader $stockRegistryPreloader */ public function __construct( - StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, - StockItemRepositoryInterface $stockItemRepository, - StockConfigurationInterface $stockConfiguration + StockConfigurationInterface $stockConfiguration, + StockRegistryPreloader $stockRegistryPreloader ) { - $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; - $this->stockItemRepository = $stockItemRepository; $this->stockConfiguration = $stockConfiguration; + $this->stockRegistryPreloader = $stockRegistryPreloader; } /** @@ -62,11 +53,13 @@ public function execute(Observer $observer) /** @var Collection $productCollection */ $productCollection = $observer->getData('collection'); $productIds = array_keys($productCollection->getItems()); - $criteria = $this->criteriaInterfaceFactory->create(); - $criteria->setProductsFilter($productIds); - $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - foreach ($stockItemCollection->getItems() as $item) { + $scopeId = $this->stockConfiguration->getDefaultScopeId(); + $stockItems = []; + if ($productIds) { + $stockItems = $this->stockRegistryPreloader->preloadStockItems($productIds, $scopeId); + $this->stockRegistryPreloader->preloadStockStatuses($productIds, $scopeId); + } + foreach ($stockItems as $item) { /** @var Product $product */ $product = $productCollection->getItemById($item->getProductId()); $productExtension = $product->getExtensionAttributes(); diff --git a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php index 334d2b22edbfa..2dd47eae16959 100644 --- a/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php +++ b/app/code/Magento/CatalogInventory/Plugin/MassUpdateProductAttribute.php @@ -3,13 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\CatalogInventory\Plugin; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute\Save; use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; /** - * MassUpdate product attribute. + * Around plugin for MassUpdate product attribute via product grid. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class MassUpdateProductAttribute @@ -49,6 +55,15 @@ class MassUpdateProductAttribute */ private $messageManager; + /** + * @var ParentItemProcessorInterface[] + */ + private $parentItemProcessorPool; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; /** * @param \Magento\CatalogInventory\Model\Indexer\Stock\Processor $stockIndexerProcessor * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper @@ -57,6 +72,8 @@ class MassUpdateProductAttribute * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper * @param \Magento\Framework\Message\ManagerInterface $messageManager + * @param ProductRepositoryInterface $productRepository + * @param ParentItemProcessorInterface[] $parentItemProcessorPool * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -66,7 +83,9 @@ public function __construct( \Magento\CatalogInventory\Api\StockItemRepositoryInterface $stockItemRepository, \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper, - \Magento\Framework\Message\ManagerInterface $messageManager + \Magento\Framework\Message\ManagerInterface $messageManager, + ProductRepositoryInterface $productRepository, + array $parentItemProcessorPool = [] ) { $this->stockIndexerProcessor = $stockIndexerProcessor; $this->dataObjectHelper = $dataObjectHelper; @@ -75,6 +94,8 @@ public function __construct( $this->stockConfiguration = $stockConfiguration; $this->attributeHelper = $attributeHelper; $this->messageManager = $messageManager; + $this->productRepository = $productRepository; + $this->parentItemProcessorPool = $parentItemProcessorPool; } /** @@ -145,6 +166,7 @@ private function addConfigSettings($inventoryData) private function updateInventoryInProducts($productIds, $websiteId, $inventoryData): void { foreach ($productIds as $productId) { + $product = $this->productRepository->getById($productId); $stockItemDo = $this->stockRegistry->getStockItem($productId, $websiteId); if (!$stockItemDo->getProductId()) { $inventoryData['product_id'] = $productId; @@ -153,7 +175,21 @@ private function updateInventoryInProducts($productIds, $websiteId, $inventoryDa $this->dataObjectHelper->populateWithArray($stockItemDo, $inventoryData, StockItemInterface::class); $stockItemDo->setItemId($stockItemId); $this->stockItemRepository->save($stockItemDo); + $this->processParents($product); } $this->stockIndexerProcessor->reindexList($productIds); } + + /** + * Process stock data for parent products + * + * @param ProductInterface $product + * @return void + */ + private function processParents(ProductInterface $product): void + { + foreach ($this->parentItemProcessorPool as $processor) { + $processor->process($product); + } + } } diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml new file mode 100644 index 0000000000000..e17c8fe65d4cf --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/StorefrontValidateQuantityIncrementsWithDecimalInventoryTest.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontValidateQuantityIncrementsWithDecimalInventoryTest"> + <annotations> + <features value="CatalogInventory"/> + <stories value="Qty increments wrong calculation for decimal fraction quantity"/> + <title value="Validate qty increments for decimal fraction quantity works"/> + <description value="Validate qty increments for decimal fraction quantity works"/> + <severity value="MAJOR"/> + <useCaseId value="MC-38242"/> + <testCaseId value="MC-38883"/> + <group value="catalogInventory"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createPreReqCategory"/> + <createData entity="SimpleProduct" stepKey="createPreReqSimpleProduct"> + <requiredEntity createDataKey="createPreReqCategory"/> + </createData> + </before> + <after> + <!--Clear Filters--> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> + <deleteData createDataKey="createPreReqCategory" stepKey="deletePreReqCategory"/> + <deleteData createDataKey="createPreReqSimpleProduct" stepKey="deletePreReqSimpleProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!--Step1. Login as admin. Go to Catalog > Products page. Filtering *prod1*. Open *prod1* to edit--> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin" /> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> + <argument name="product" value="SimpleProduct"/> + </actionGroup> + <!-- Step2. Update product Advanced Inventory Setting. + Set *Qty Uses Decimals* to *Yes* and *Enable Qty Increments* to *Yes* and *Qty Increments* to *3.33*. --> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetEnableQtyIncrementsActionGroup" stepKey="setEnableQtyIncrements"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetQtyIncrementsForProductActionGroup" stepKey="setQtyIncrementsValue"> + <argument name="qty" value="3.33"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + + <!--Step3. Save the product--> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton2"/> + <!--Step4. Open *Customer view* (Go to *Store Front*). Open *prod1* page (Find via search and click on product name) --> + <amOnPage url="{{StorefrontHomePage.url}}$$createPreReqSimpleProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> + <!--Step5. Fill *23.31* in *Qty*. Click on button *Add to Cart*--> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="23.31" stepKey="fillQty"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="clickOnAddToCart"/> + <waitForElementVisible selector="{{StorefrontProductPageSection.successMsg}}" time="30" stepKey="waitForProductAdded"/> + <!--Step6. Verify the product is successfully added to the cart with success message--> + <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added $$createPreReqSimpleProduct.name$$ to your shopping cart." stepKey="seeAddedToCartMessage"/> + </test> +</tests> diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryPreloaderTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryPreloaderTest.php new file mode 100644 index 0000000000000..037d491c8b5bf --- /dev/null +++ b/app/code/Magento/CatalogInventory/Test/Unit/Model/StockRegistryPreloaderTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogInventory\Test\Unit\Model; + +use Magento\CatalogInventory\Api\Data\StockItemCollectionInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\Data\StockStatusCollectionInterface; +use Magento\CatalogInventory\Api\Data\StockStatusInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterface; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterface; +use Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockStatusRepositoryInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; +use Magento\CatalogInventory\Model\StockRegistryStorage; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for StockRegistryStorage + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class StockRegistryPreloaderTest extends TestCase +{ + /** + * @var StockItemRepositoryInterface|MockObject + */ + private $stockItemRepository; + /** + * @var StockStatusRepositoryInterface|MockObject + */ + private $stockStatusRepository; + /** + * @var MockObject + */ + private $stockItemCriteriaFactory; + /** + * @var MockObject + */ + private $stockStatusCriteriaFactory; + /** + * @var StockConfigurationInterface|MockObject + */ + private $stockConfiguration; + /** + * @var StockRegistryStorage + */ + private $stockRegistryStorage; + /** + * @var StockRegistryPreloader + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->stockItemRepository = $this->createMock(StockItemRepositoryInterface::class); + $this->stockStatusRepository = $this->createMock(StockStatusRepositoryInterface::class); + $this->stockItemCriteriaFactory = $this->createMock(StockItemCriteriaInterfaceFactory::class); + $this->stockStatusCriteriaFactory = $this->createMock(StockStatusCriteriaInterfaceFactory::class); + $this->stockConfiguration = $this->createMock(StockConfigurationInterface::class); + $this->stockRegistryStorage = new StockRegistryStorage(); + $this->model = new StockRegistryPreloader( + $this->stockItemRepository, + $this->stockStatusRepository, + $this->stockItemCriteriaFactory, + $this->stockStatusCriteriaFactory, + $this->stockConfiguration, + $this->stockRegistryStorage, + ); + } + + public function testPreloadStockItems(): void + { + $productIds = [10, 20]; + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 20]), + ]; + $collection = $this->createConfiguredMock(StockItemCollectionInterface::class, ['getItems' => $stockItems]); + $criteria = $this->createMock(StockItemCriteriaInterface::class); + $criteria->expects($this->once()) + ->method('setProductsFilter') + ->with($productIds) + ->willReturnSelf(); + $criteria->expects($this->once()) + ->method('setScopeFilter') + ->with($scopeId) + ->willReturnSelf(); + $this->stockItemRepository->method('getList') + ->willReturn($collection); + $this->stockItemCriteriaFactory->method('create') + ->willReturn($criteria); + $this->assertEquals($stockItems, $this->model->preloadStockItems($productIds, $scopeId)); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockItem(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockItem(20, $scopeId)); + } + + public function testPreloadStockStatuses(): void + { + $productIds = [10, 20]; + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 20]), + ]; + $collection = $this->createConfiguredMock(StockStatusCollectionInterface::class, ['getItems' => $stockItems]); + $criteria = $this->createMock(StockStatusCriteriaInterface::class); + $criteria->expects($this->once()) + ->method('setProductsFilter') + ->with($productIds) + ->willReturnSelf(); + $criteria->expects($this->once()) + ->method('setScopeFilter') + ->with($scopeId) + ->willReturnSelf(); + $this->stockStatusRepository->method('getList') + ->willReturn($collection); + $this->stockStatusCriteriaFactory->method('create') + ->willReturn($criteria); + $this->assertEquals($stockItems, $this->model->preloadStockStatuses($productIds, $scopeId)); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockStatus(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockStatus(20, $scopeId)); + } + + public function testSetStockItems(): void + { + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockItemInterface::class, ['getProductId' => 20]), + ]; + $this->model->setStockItems($stockItems, $scopeId); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockItem(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockItem(20, $scopeId)); + } + + public function testSetStockStatuses(): void + { + $scopeId = 1; + $stockItems = [ + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 10]), + $this->createConfiguredMock(StockStatusInterface::class, ['getProductId' => 20]), + ]; + $this->model->setStockStatuses($stockItems, $scopeId); + $this->assertSame($stockItems[0], $this->stockRegistryStorage->getStockStatus(10, $scopeId)); + $this->assertSame($stockItems[1], $this->stockRegistryStorage->getStockStatus(20, $scopeId)); + } +} diff --git a/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php index bba44ef436fd6..fce232821b67d 100644 --- a/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php +++ b/app/code/Magento/CatalogInventory/Test/Unit/Observer/AddStockItemsObserverTest.php @@ -10,15 +10,12 @@ use Magento\Catalog\Api\Data\ProductExtensionInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; -use Magento\CatalogInventory\Api\Data\StockItemCollectionInterface; use Magento\CatalogInventory\Api\Data\StockItemInterface; use Magento\CatalogInventory\Api\StockConfigurationInterface; -use Magento\CatalogInventory\Api\StockItemCriteriaInterface; use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\CatalogInventory\Model\StockRegistryPreloader; use Magento\CatalogInventory\Observer\AddStockItemsObserver; use Magento\Framework\Event\Observer; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -33,46 +30,29 @@ class AddStockItemsObserverTest extends TestCase * @var AddStockItemsObserver */ private $subject; - /** - * @var StockItemCriteriaInterfaceFactory|MockObject - */ - private $criteriaInterfaceFactoryMock; - - /** - * @var StockItemRepositoryInterface|MockObject - */ - private $stockItemRepositoryMock; /** * @var StockConfigurationInterface|MockObject */ private $stockConfigurationMock; + /** + * @var StockRegistryPreloader|MockObject + */ + private $stockRegistryPreloader; /** * @inheritdoc */ protected function setUp(): void { - $objectManager = new ObjectManager($this); - $this->criteriaInterfaceFactoryMock = $this->getMockBuilder(StockItemCriteriaInterfaceFactory::class) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->stockItemRepositoryMock = $this->getMockBuilder(StockItemRepositoryInterface::class) - ->setMethods(['getList']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); $this->stockConfigurationMock = $this->getMockBuilder(StockConfigurationInterface::class) ->setMethods(['getDefaultScopeId']) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->subject = $objectManager->getObject( - AddStockItemsObserver::class, - [ - 'criteriaInterfaceFactory' => $this->criteriaInterfaceFactoryMock, - 'stockItemRepository' => $this->stockItemRepositoryMock, - 'stockConfiguration' => $this->stockConfigurationMock - ] + $this->stockRegistryPreloader = $this->createMock(StockRegistryPreloader::class); + $this->subject = new AddStockItemsObserver( + $this->stockConfigurationMock, + $this->stockRegistryPreloader, ); } @@ -84,26 +64,6 @@ public function testExecute() $productId = 1; $defaultScopeId = 0; - $criteria = $this->getMockBuilder(StockItemCriteriaInterface::class) - ->setMethods(['setProductsFilter', 'setScopeFilter']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $criteria->expects(self::once()) - ->method('setProductsFilter') - ->with(self::identicalTo([$productId])) - ->willReturn(true); - $criteria->expects(self::once()) - ->method('setScopeFilter') - ->with(self::identicalTo($defaultScopeId)) - ->willReturn(true); - - $this->criteriaInterfaceFactoryMock->expects(self::once()) - ->method('create') - ->willReturn($criteria); - $stockItemCollection = $this->getMockBuilder(StockItemCollectionInterface::class) - ->setMethods(['getItems']) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); $stockItem = $this->getMockBuilder(StockItemInterface::class) ->setMethods(['getProductId']) ->disableOriginalConstructor() @@ -112,14 +72,19 @@ public function testExecute() ->method('getProductId') ->willReturn($productId); - $stockItemCollection->expects(self::once()) - ->method('getItems') + $this->stockRegistryPreloader->expects(self::once()) + ->method('preloadStockItems') + ->with([$productId]) ->willReturn([$stockItem]); - $this->stockItemRepositoryMock->expects(self::once()) - ->method('getList') - ->with(self::identicalTo($criteria)) - ->willReturn($stockItemCollection); + $this->stockRegistryPreloader->expects(self::once()) + ->method('preloadStockStatuses') + ->with([$productId]) + ->willReturn([]); + + $this->stockRegistryPreloader->expects(self::once()) + ->method('preloadStockItems') + ->willReturn([$stockItem]); $this->stockConfigurationMock->expects(self::once()) ->method('getDefaultScopeId') diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php index 8c4690f044764..3a67025230430 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -6,10 +6,24 @@ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Action; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\CatalogSearch\Model\ResourceModel\EngineInterface; +use Magento\CatalogSearch\Model\ResourceModel\EngineProvider; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DataObject; +use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Select; +use Magento\Framework\EntityManager\EntityMetadata; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Event\ManagerInterface; use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Zend_Db; /** * Catalog search full test search data provider. @@ -24,7 +38,7 @@ class DataProvider /** * Searchable attributes cache * - * @var \Magento\Eav\Model\Entity\Attribute[] + * @var Attribute[] */ private $searchableAttributes; @@ -50,40 +64,40 @@ class DataProvider private $productEmulators = []; /** - * @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory + * @var CollectionFactory */ private $productAttributeCollectionFactory; /** * Eav config * - * @var \Magento\Eav\Model\Config + * @var Config */ private $eavConfig; /** * Catalog product type * - * @var \Magento\Catalog\Model\Product\Type + * @var Type */ private $catalogProductType; /** * Core event manager proxy * - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ private $eventManager; /** * Store manager * - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ private $storeManager; /** - * @var \Magento\CatalogSearch\Model\ResourceModel\EngineInterface + * @var EngineInterface */ private $engine; @@ -93,12 +107,12 @@ class DataProvider private $resource; /** - * @var \Magento\Framework\DB\Adapter\AdapterInterface + * @var AdapterInterface */ private $connection; /** - * @var \Magento\Framework\EntityManager\EntityMetadata + * @var EntityMetadata */ private $metadata; @@ -126,24 +140,24 @@ class DataProvider /** * @param ResourceConnection $resource - * @param \Magento\Catalog\Model\Product\Type $catalogProductType - * @param \Magento\Eav\Model\Config $eavConfig - * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttributeCollectionFactory - * @param \Magento\CatalogSearch\Model\ResourceModel\EngineProvider $engineProvider - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param Type $catalogProductType + * @param Config $eavConfig + * @param CollectionFactory $prodAttributeCollectionFactory + * @param EngineProvider $engineProvider + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param MetadataPool $metadataPool * @param int $antiGapMultiplier */ public function __construct( ResourceConnection $resource, - \Magento\Catalog\Model\Product\Type $catalogProductType, - \Magento\Eav\Model\Config $eavConfig, - \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $prodAttributeCollectionFactory, - \Magento\CatalogSearch\Model\ResourceModel\EngineProvider $engineProvider, - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\EntityManager\MetadataPool $metadataPool, + Type $catalogProductType, + Config $eavConfig, + CollectionFactory $prodAttributeCollectionFactory, + EngineProvider $engineProvider, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + MetadataPool $metadataPool, int $antiGapMultiplier = 5 ) { $this->resource = $resource; @@ -224,7 +238,7 @@ private function getSelectForSearchableProducts( $batch ) { $websiteId = (int)$this->storeManager->getStore($storeId)->getWebsiteId(); - $lastProductId = (int) $lastProductId; + $lastProductId = (int)$lastProductId; $select = $this->connection->select() ->useStraightJoin(true) @@ -242,7 +256,7 @@ private function getSelectForSearchableProducts( $this->joinAttribute($select, 'status', $storeId, [Status::STATUS_ENABLED]); if ($productIds !== null) { - $select->where('e.entity_id IN (?)', $productIds, \Zend_Db::INT_TYPE); + $select->where('e.entity_id IN (?)', $productIds, Zend_Db::INT_TYPE); } $select->where('e.entity_id > ?', $lastProductId); $select->order('e.entity_id'); @@ -308,14 +322,17 @@ private function joinAttribute(Select $select, $attributeCode, $storeId, array $ */ public function getSearchableAttributes($backendType = null) { + /** TODO: Remove this block in the next minor release and add a new public method instead */ + if ($this->eavConfig->getEntityType(Product::ENTITY)->getNeedRefreshSearchAttributesList()) { + $this->clearSearchableAttributesList(); + } if (null === $this->searchableAttributes) { $this->searchableAttributes = []; - /** @var \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection $productAttributes */ $productAttributes = $this->productAttributeCollectionFactory->create(); $productAttributes->addToIndexFilter(true); - /** @var \Magento\Eav\Model\Entity\Attribute[] $attributes */ + /** @var Attribute[] $attributes */ $attributes = $productAttributes->getItems(); /** @deprecated */ @@ -329,7 +346,7 @@ public function getSearchableAttributes($backendType = null) ['engine' => $this->engine, 'attributes' => $attributes] ); - $entity = $this->eavConfig->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getEntity(); + $entity = $this->eavConfig->getEntityType(Product::ENTITY)->getEntity(); foreach ($attributes as $attribute) { $attribute->setEntity($entity); @@ -355,6 +372,18 @@ public function getSearchableAttributes($backendType = null) return $this->searchableAttributes; } + /** + * Remove searchable attributes list. + * + * @return void + */ + private function clearSearchableAttributesList(): void + { + $this->searchableAttributes = null; + $this->searchableAttributesByBackendType = []; + $this->eavConfig->getEntityType(Product::ENTITY)->unsNeedRefreshSearchAttributesList(); + } + /** * Retrieve searchable attribute by Id or code * @@ -369,7 +398,7 @@ public function getSearchableAttribute($attribute) return $attributes[$attribute]; } - return $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attribute); + return $this->eavConfig->getAttribute(Product::ENTITY, $attribute); } /** @@ -386,6 +415,7 @@ private function unifyField($field, $backendType = 'varchar') } else { $expr = $field; } + return $expr; } @@ -411,7 +441,7 @@ public function getProductAttributes($storeId, array $productIds, array $attribu )->where( 'cpe.entity_id IN (?)', $productIds, - \Zend_Db::INT_TYPE + Zend_Db::INT_TYPE ) ); foreach ($attributeTypes as $backendType => $attributeIds) { @@ -479,6 +509,7 @@ private function getProductTypeInstance($typeId) $this->productTypes[$typeId] = $this->catalogProductType->factory($productEmulator); } + return $this->productTypes[$typeId]; } @@ -513,6 +544,7 @@ public function getProductChildIds($productId, $typeId) if ($relation->getWhere() !== null) { $select->where($relation->getWhere()); } + return $this->connection->fetchCol($select); } @@ -528,10 +560,11 @@ public function getProductChildIds($productId, $typeId) private function getProductEmulator($typeId) { if (!isset($this->productEmulators[$typeId])) { - $productEmulator = new \Magento\Framework\DataObject(); + $productEmulator = new DataObject(); $productEmulator->setTypeId($typeId); $this->productEmulators[$typeId] = $productEmulator; } + return $this->productEmulators[$typeId]; } @@ -660,6 +693,7 @@ function ($value) { $attributeOptionValue .= $this->attributeOptions[$optionKey][$attrValueId] . ' '; } } + return empty($attributeOptionValue) ? null : trim($attributeOptionValue); } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php index 7b5d43ece922d..3f0046b918f28 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Plugin/Attribute.php @@ -5,12 +5,15 @@ */ namespace Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin; +use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; use Magento\Framework\Model\AbstractModel; use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; use Magento\Framework\Search\Request\Config; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Catalog\Api\Data\EavAttributeInterface; +use Magento\Eav\Model\Config as EavConfig; /** * Catalog search indexer plugin for catalog attribute. @@ -37,16 +40,24 @@ class Attribute extends AbstractPlugin */ private $saveIsNew; + /** + * @var EavConfig + */ + private $eavConfig; + /** * @param IndexerRegistry $indexerRegistry * @param Config $config + * @param EavConfig $eavConfig */ public function __construct( IndexerRegistry $indexerRegistry, - Config $config + Config $config, + EavConfig $eavConfig ) { parent::__construct($indexerRegistry); $this->config = $config; + $this->eavConfig = $eavConfig; } /** @@ -84,6 +95,11 @@ public function afterSave( } if ($this->saveIsNew || $this->saveNeedInvalidation) { $this->config->reset(); + /** + * TODO: Remove this in next minor release and use public method instead. + * @see DataProvider::getSearchableAttributes + */ + $this->eavConfig->getEntityType(Product::ENTITY)->setNeedRefreshSearchAttributesList(true); } return $result; diff --git a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php index befe462184af6..4d8a7de391356 100644 --- a/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php +++ b/app/code/Magento/CatalogSearch/Test/Unit/Model/Indexer/Fulltext/Plugin/AttributeTest.php @@ -7,8 +7,10 @@ namespace Magento\CatalogSearch\Test\Unit\Model\Indexer\Fulltext\Plugin; +use Magento\Catalog\Model\Product; use Magento\CatalogSearch\Model\Indexer\Fulltext; use Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Attribute; +use Magento\Eav\Model\Config as EavConfig; use Magento\Framework\Indexer\IndexerInterface; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Search\Request\Config; @@ -16,6 +18,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Unit tests for @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Plugin\Attribute. + */ class AttributeTest extends TestCase { /** @@ -53,6 +58,14 @@ class AttributeTest extends TestCase */ private $config; + /** + * @var EavConfig + */ + private $eavConfig; + + /** + * @inheridoc + */ protected function setUp(): void { $this->objectManager = new ObjectManager($this); @@ -78,11 +91,16 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['reset']) ->getMock(); + $this->eavConfig = $this->createPartialMock( + EavConfig::class, + ['getEntityType'] + ); $this->model = $this->objectManager->getObject( Attribute::class, [ 'indexerRegistry' => $this->indexerRegistryMock, - 'config' => $this->config + 'config' => $this->config, + 'eavConfig' => $this->eavConfig ] ); } @@ -123,21 +141,26 @@ public function testAfterSaveWithInvalidation(bool $saveNeedInvalidation, bool $ [ 'indexerRegistry' => $this->indexerRegistryMock, 'config' => $this->config, + 'eavConfig' => $this->eavConfig, 'saveNeedInvalidation' => $saveNeedInvalidation, 'saveIsNew' => $saveIsNew, ] ); + if ($saveIsNew || $saveNeedInvalidation) { + $this->config->expects($this->once()) + ->method('reset'); + $catalogProductEntity = $this->createMock(Product::class); + $this->eavConfig->expects($this->once()) + ->method('getEntityType') + ->with(Product::ENTITY) + ->willReturn($catalogProductEntity); + } if ($saveNeedInvalidation) { $this->indexerMock->expects($this->once())->method('invalidate'); $this->prepareIndexer(); } - if ($saveIsNew || $saveNeedInvalidation) { - $this->config->expects($this->once()) - ->method('reset'); - } - $this->assertEquals( $this->subjectMock, $model->afterSave($this->subjectMock, $this->subjectMock) diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index e7e8f9f0ef699..f578a9c02caca 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -42,8 +42,8 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="guestGoToCheckoutFromMinicart" /> <selectOption stepKey="selectCounty" selector="{{CheckoutShippingSection.country}}" userInput="{{UK_Address.country_id}}"/> <waitForPageLoad stepKey="waitFormToReload"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitFormToReload1"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1" /> <fillField selector="{{CheckoutShippingSection.email}}" userInput="{{CustomerEntityOne.email}}" stepKey="enterEmail"/> <fillField selector="{{CheckoutShippingSection.firstName}}" userInput="{{CustomerEntityOne.firstname}}" stepKey="enterFirstName"/> <fillField selector="{{CheckoutShippingSection.lastName}}" userInput="{{CustomerEntityOne.lastname}}" stepKey="enterLastName"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index a1065daedd4f8..df229c4b6ed78 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -90,8 +90,8 @@ <!-- Back to the Checkout and refresh the page --> <switchToPreviousTab stepKey="switchToPreviousTab"/> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitPageReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitPageReload"/> <!-- Payment step is opened after refreshing --> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSection"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index e5897501cc067..033898bb90557 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -172,8 +172,9 @@ <actionGroup ref="CheckoutFillEstimateShippingAndTaxActionGroup" stepKey="fillEstimateShippingAndTaxFields"> <argument name="address" value="US_Address_NY_Default_Shipping"/> </actionGroup> - <reloadPage stepKey="reloadThePage"/> - <waitForPageLoad stepKey="waitForPageToReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadThePage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageToReload"/> + <waitForText selector="{{CheckoutCartSummarySection.taxAmount}}" userInput="$9.60" time="90" stepKey="waitForTaxAmount"/> <!--Select Free Shipping and proceed to checkout --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index e42d5e1bae956..a3c093d005371 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -49,8 +49,8 @@ <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappearAfterFlatRateSelection"/> <see selector="{{CheckoutCartSummarySection.total}}" userInput="15" stepKey="assertOrderTotalField"/> <!-- 5. Refresh browser page (F5) --> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad"/> <actionGroup ref="StorefrontAssertCartEstimateShippingAndTaxActionGroup" stepKey="assertCartEstimateShippingAndTaxAfterPageReload"/> <actionGroup ref="StorefrontAssertCartShippingMethodSelectedActionGroup" stepKey="assertFlatRateShippingMethodIsChecked"> <argument name="carrierCode" value="flatrate"/> @@ -71,8 +71,9 @@ <!-- 9. Fill other fields --> <actionGroup ref="StorefrontFillGuestShippingInfoActionGroup" stepKey="fillOtherFieldsInCheckoutShippingSection"/> <!-- 10. Refresh browser page(F5) --> - <reloadPage stepKey="reloadCheckoutPage"/> - <waitForPageLoad stepKey="waitForCheckoutPageLoad"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadCheckoutPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageLoad"/> + <actionGroup ref="StorefrontAssertGuestShippingInfoActionGroup" stepKey="assertGuestShippingPersistedInfoAfterReloadingCheckoutShippingPage"/> <actionGroup ref="StorefrontAssertCheckoutShippingMethodSelectedActionGroup" stepKey="assertFreeShippingShippingMethodIsChecked"> <argument name="shippingMethod" value="Free Shipping"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index a7a0917532dcb..f014a7a5bd1ee 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -62,8 +62,8 @@ <closeTab stepKey="closeTab"/> <!--Check price--> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForCheckoutPageReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload"/> <conditionalClick selector="{{CheckoutPaymentSection.cartItemsArea}}" dependentSelector="{{CheckoutPaymentSection.cartItemsAreaActive}}" visible="false" stepKey="openItemProductBlock1"/> <see userInput="$120.00" selector="{{CheckoutPaymentSection.orderSummarySubtotal}}" stepKey="checkSummarySubtotal1"/> <see userInput="$120.00" selector="{{CheckoutPaymentSection.productItemPriceByName($$createSimpleProduct.name$$)}}" stepKey="checkItemPrice1"/> diff --git a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php index 41e9358e160cf..c033e09ca8db0 100644 --- a/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php +++ b/app/code/Magento/Cms/Block/Adminhtml/Wysiwyg/Images/Tree.php @@ -58,6 +58,7 @@ public function __construct( * Json tree builder * * @return string + * @throws \Magento\Framework\Exception\ValidatorException */ public function getTreeJson() { @@ -75,8 +76,8 @@ public function getTreeJson() 'path' => substr($item->getFilename(), strlen($storageRoot)), 'cls' => 'folder', ]; - - $hasNestedDirectories = count(glob($item->getFilename() . '/*', GLOB_ONLYDIR)) > 0; + $nestedDirectories = $this->getMediaDirectory()->readRecursively($item->getFilename()); + $hasNestedDirectories = count($nestedDirectories) > 0; // if no nested directories inside dir, add 'leaf' state so that jstree hides dropdown arrow next to dir if (!$hasNestedDirectories) { diff --git a/app/code/Magento/Cms/Block/Block.php b/app/code/Magento/Cms/Block/Block.php index 86cf059525e1e..afc95d369f67d 100644 --- a/app/code/Magento/Cms/Block/Block.php +++ b/app/code/Magento/Cms/Block/Block.php @@ -10,6 +10,8 @@ /** * Cms block content block + * @deprecated This class introduces caching issues and should no longer be used + * @see \Magento\Cms\Block\BlockByIdentifier */ class Block extends AbstractBlock implements \Magento\Framework\DataObject\IdentityInterface { diff --git a/app/code/Magento/Cms/Block/BlockByIdentifier.php b/app/code/Magento/Cms/Block/BlockByIdentifier.php new file mode 100644 index 0000000000000..eb8bf3d5fe352 --- /dev/null +++ b/app/code/Magento/Cms/Block/BlockByIdentifier.php @@ -0,0 +1,175 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Block; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Cms\Model\Block as BlockModel; +use Magento\Cms\Model\Template\FilterProvider; +use Magento\Framework\DataObject\IdentityInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\AbstractBlock; +use Magento\Framework\View\Element\Context; +use Magento\Store\Model\StoreManagerInterface; + +/** + * This class is replacement of \Magento\Cms\Block\Block, that accepts only `string` identifier of CMS Block + */ +class BlockByIdentifier extends AbstractBlock implements IdentityInterface +{ + public const CACHE_KEY_PREFIX = 'CMS_BLOCK'; + + /** + * @var GetBlockByIdentifierInterface + */ + private $blockByIdentifier; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var FilterProvider + */ + private $filterProvider; + + /** + * @var BlockInterface + */ + private $cmsBlock; + + /** + * @param GetBlockByIdentifierInterface $blockByIdentifier + * @param StoreManagerInterface $storeManager + * @param FilterProvider $filterProvider + * @param Context $context + * @param array $data + */ + public function __construct( + GetBlockByIdentifierInterface $blockByIdentifier, + StoreManagerInterface $storeManager, + FilterProvider $filterProvider, + Context $context, + array $data = [] + ) { + parent::__construct($context, $data); + $this->blockByIdentifier = $blockByIdentifier; + $this->storeManager = $storeManager; + $this->filterProvider = $filterProvider; + } + + /** + * @inheritDoc + */ + protected function _toHtml(): string + { + try { + return $this->filterOutput( + $this->getCmsBlock()->getContent() + ); + } catch (NoSuchEntityException $e) { + return ''; + } + } + + /** + * Returns the value of `identifier` injected in `<block>` definition + * + * @return string|null + */ + private function getIdentifier(): ?string + { + return $this->getData('identifier') ?: null; + } + + /** + * Filters the Content + * + * @param string $content + * @return string + * @throws NoSuchEntityException + */ + private function filterOutput(string $content): string + { + return $this->filterProvider->getBlockFilter() + ->setStoreId($this->getCurrentStoreId()) + ->filter($content); + } + + /** + * Loads the CMS block by `identifier` provided as an argument + * + * @return BlockInterface|BlockModel + * @throws \InvalidArgumentException + * @throws NoSuchEntityException + */ + private function getCmsBlock(): BlockInterface + { + if (!$this->getIdentifier()) { + throw new \InvalidArgumentException('Expected value of `identifier` was not provided'); + } + + if (null === $this->cmsBlock) { + $this->cmsBlock = $this->blockByIdentifier->execute( + (string)$this->getIdentifier(), + $this->getCurrentStoreId() + ); + + if (!$this->cmsBlock->isActive()) { + throw new NoSuchEntityException( + __('The CMS block with identifier "%identifier" is not enabled.', $this->getIdentifier()) + ); + } + } + + return $this->cmsBlock; + } + + /** + * Returns the current Store ID + * + * @return int + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function getCurrentStoreId(): int + { + return (int)$this->storeManager->getStore()->getId(); + } + + /** + * Returns array of Block Identifiers used to determine Cache Tags + * + * This implementation supports different CMS blocks caching having the same identifier, + * resolving the bug introduced in scope of \Magento\Cms\Block\Block + * + * @return string[] + */ + public function getIdentities(): array + { + if (!$this->getIdentifier()) { + return []; + } + + $identities = [ + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier(), + self::CACHE_KEY_PREFIX . '_' . $this->getIdentifier() . '_' . $this->getCurrentStoreId() + ]; + + try { + $cmsBlock = $this->getCmsBlock(); + if ($cmsBlock instanceof IdentityInterface) { + $identities = array_merge($identities, $cmsBlock->getIdentities()); + } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + } catch (NoSuchEntityException $e) { + } + + return $identities; + } +} diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php index a3370b2666264..5172ff8088bf8 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Directive.php @@ -13,6 +13,8 @@ use Magento\Cms\Model\Template\Filter; use Magento\Cms\Model\Wysiwyg\Config; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; use Magento\Framework\Image\Adapter\AdapterInterface; use Magento\Framework\Image\AdapterFactory; use Psr\Log\LoggerInterface; @@ -27,6 +29,7 @@ * Process template text for wysiwyg editor. * * Class Directive + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) usage of $this->file eliminated, but it's still there due to BC */ class Directive extends Action implements HttpGetActionInterface { @@ -70,8 +73,13 @@ class Directive extends Action implements HttpGetActionInterface /** * @var File + * @deprecated use $filesystem instead */ private $file; + /** + * @var Filesystem|null + */ + private $filesystem; /** * Constructor @@ -84,6 +92,7 @@ class Directive extends Action implements HttpGetActionInterface * @param Config|null $config * @param Filter|null $filter * @param File|null $file + * @param Filesystem|null $filesystem */ public function __construct( Context $context, @@ -93,7 +102,8 @@ public function __construct( LoggerInterface $logger = null, Config $config = null, Filter $filter = null, - File $file = null + File $file = null, + Filesystem $filesystem = null ) { parent::__construct($context); $this->urlDecoder = $urlDecoder; @@ -103,17 +113,21 @@ public function __construct( $this->config = $config ?: ObjectManager::getInstance()->get(Config::class); $this->filter = $filter ?: ObjectManager::getInstance()->get(Filter::class); $this->file = $file ?: ObjectManager::getInstance()->get(File::class); + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); } /** * Template directives callback * * @return Raw + * @throws \Magento\Framework\Exception\FileSystemException */ public function execute() { $directive = $this->getRequest()->getParam('___directive'); $directive = $this->urlDecoder->decode($directive); + $image = null; + $resultRaw = null; try { /** @var Filter $filter */ $imagePath = $this->filter->filter($directive); @@ -141,7 +155,8 @@ public function execute() // To avoid issues with PNG images with alpha blending we return raw file // after validation as an image source instead of generating the new PNG image // with image adapter - $content = $this->file->fileGetContents($imagePath); + $content = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->getDriver() + ->fileGetContents($imagePath); $resultRaw->setHeader('Content-Type', $mimeType); $resultRaw->setContents($content); diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index 0cc108e5bed8b..2c94e2e76914f 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -363,15 +363,16 @@ public function getFilesCollection($path, $type = null) $collection->setFilesFilter('/\.(' . implode('|', $allowed) . ')$/i'); } - // prepare items foreach ($collection as $item) { $item->setId($this->_cmsWysiwygImages->idEncode($item->getBasename())); $item->setName($item->getBasename()); $item->setShortName($this->_cmsWysiwygImages->getShortFilename($item->getBasename())); $item->setUrl($this->_cmsWysiwygImages->getCurrentUrl() . $item->getBasename()); - $itemStats = $this->file->stat($item->getFilename()); + $driver = $this->_directory->getDriver(); + $itemStats = $driver->stat($item->getFilename()); $item->setSize($itemStats['size']); - $item->setMimeType($this->mime->getMimeType($item->getFilename())); + $mimeType = $itemStats['mimetype'] ?? $this->mime->getMimeType($item->getFilename()); + $item->setMimeType($mimeType); if ($this->isImage($item->getBasename())) { $thumbUrl = $this->getThumbnailUrl($item->getFilename(), true); @@ -381,7 +382,9 @@ public function getFilesCollection($path, $type = null) } try { - $size = getimagesize($item->getFilename()); + $size = getimagesizefromstring( + $driver->fileGetContents($item->getFilename()) + ); if (is_array($size)) { $item->setWidth($size[0]); @@ -438,7 +441,7 @@ public function createDirectory($name, $path) $path = $this->_cmsWysiwygImages->getStorageRoot(); } - $newPath = $path . '/' . $name; + $newPath = rtrim($path, '/') . '/' . $name; $relativeNewPath = $this->_directory->getRelativePath($newPath); if ($this->_directory->isDirectory($relativeNewPath)) { throw new \Magento\Framework\Exception\LocalizedException( @@ -571,7 +574,7 @@ public function uploadFile($targetPath, $type = null) } // create thumbnail - $this->resizeFile($targetPath . '/' . $uploader->getUploadedFileName(), true); + $this->resizeFile($targetPath . '/' . ltrim($uploader->getUploadedFileName(), '/'), true); return $result; } @@ -655,7 +658,7 @@ public function resizeFile($source, $keepRatio = true) $image->keepAspectRatio($keepRatio); - list($imageWidth, $imageHeight) = $this->getResizedParams($source); + [$imageWidth, $imageHeight] = $this->getResizedParams($source); $image->resize($imageWidth, $imageHeight); $dest = $targetDir . '/' . $this->ioFile->getPathInfo($source)['basename']; @@ -678,7 +681,7 @@ private function getResizedParams(string $source): array $configHeight = $this->_resizeParameters['height']; //phpcs:ignore Generic.PHP.NoSilencedErrors - list($imageWidth, $imageHeight) = @getimagesize($source); + [$imageWidth, $imageHeight] = @getimagesize($source); if ($imageWidth && $imageHeight) { $imageWidth = $configWidth > $imageWidth ? $imageWidth : $configWidth; @@ -759,7 +762,7 @@ public function getAllowedExtensions($type = null) */ public function getThumbnailRoot() { - return $this->_cmsWysiwygImages->getStorageRoot() . '/' . self::THUMBS_DIRECTORY_NAME; + return rtrim($this->_cmsWysiwygImages->getStorageRoot(), '/') . '/' . self::THUMBS_DIRECTORY_NAME; } /** @@ -844,7 +847,7 @@ protected function _sanitizePath($path) { return rtrim( preg_replace( - '~[/\\\]+~', + '~[/\\\]+(?<![htps?]://)~', '/', $this->_directory->getDriver()->getRealPathSafety( $this->_directory->getAbsolutePath($path) diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php index f66c0f6b06d91..617c8663d6f80 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage/Collection.php @@ -30,7 +30,7 @@ public function __construct( \Magento\Framework\Filesystem $filesystem ) { $this->_filesystem = $filesystem; - parent::__construct($entityFactory); + parent::__construct($entityFactory, $filesystem); } /** @@ -41,10 +41,11 @@ public function __construct( */ protected function _generateRow($filename) { - $filename = preg_replace('~[/\\\]+~', '/', $filename); + $filename = preg_replace('~[/\\\]+(?<![htps?]://)~', '/', $filename); $path = $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA); return [ - 'filename' => $filename, + 'filename' => rtrim($filename, '/'), + // phpcs:ignore Magento2.Functions.DiscouragedFunction 'basename' => basename($filename), 'mtime' => $path->stat($path->getRelativePath($filename))['mtime'] ]; diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml new file mode 100644 index 0000000000000..a61f565bac2bc --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSBlockFromGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteCMSBlockFromGridActionGroup"> + <arguments> + <argument name="identifier" type="entity"/> + </arguments> + <click selector="{{BlockPageActionsSection.select(identifier)}}" stepKey="clickSelect"/> + <click selector="{{BlockPageActionsSection.delete(identifier)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{BlockPageActionsSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{BlockPageActionsSection.deleteConfirm}}" stepKey="clickOkButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml new file mode 100644 index 0000000000000..18e7e5fb52615 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminOpenCMSBlocksGridActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenCMSBlocksGridActionGroup"> + <annotations> + <description>Navigate to the Admin Blocks Grid page.</description> + </annotations> + + <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSBlocksGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml new file mode 100644 index 0000000000000..1099cd7e753c9 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminSearchCMSBlockInGridByIdentifierActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSearchCMSBlockInGridByIdentifierActionGroup"> + <arguments> + <argument name="identifier" type="string"/> + </arguments> + <click selector="{{BlockPageActionsSection.FilterBtn}}" stepKey="clickFilterButton"/> + <fillField selector="{{BlockPageActionsSection.URLKey}}" userInput="{{identifier}}" stepKey="fillIdentifierField"/> + <click selector="{{BlockPageActionsSection.ApplyFiltersBtn}}" stepKey="clickApplyFiltersButton"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml new file mode 100644 index 0000000000000..1b5a5301eda1b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AssertAdminCMSBlockIsNotInGridActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminCMSBlockIsNotInGridActionGroup"> + <arguments> + <argument name="identifier" type="entity"/> + </arguments> + <dontSee userInput="{{identifier}}" selector="{{AdminBlockGridSection.gridDataRow}}" stepKey="dontSeeCmsBlockInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml index c3d84fafd071c..f3cf259842e1b 100644 --- a/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/NavigateToMediaFolderActionGroup.xml @@ -15,8 +15,9 @@ <arguments> <argument name="FolderName" type="string"/> </arguments> - + <conditionalClick selector="{{MediaGallerySection.StorageRootArrow}}" dependentSelector="{{MediaGallerySection.checkIfArrowExpand}}" stepKey="clickArrowIfClosed" visible="true"/> + <waitForPageLoad time="10" stepKey="waitForDirectoriesTreeBuilding"/> <waitForText userInput="{{FolderName}}" stepKey="waitForNewFolder"/> <click userInput="{{FolderName}}" stepKey="clickOnCreatedFolder"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml index dea047ec43568..bf9f199634078 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/BlockData.xml @@ -15,4 +15,11 @@ <data key="content">sales25off everything!</data> <data key="is_active">0</data> </entity> + <entity name="ActiveTestBlock" type="block"> + <data key="title" unique="suffix">Test Block</data> + <data key="identifier" unique="suffix">ActiveTestBlock</data> + <data key="store_id">All Store Views</data> + <data key="content">Test Block content</data> + <data key="is_active">1</data> + </entity> </entities> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml index ab15570a01f40..f558619fa49ac 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/AdminBlockGridSection.xml @@ -14,5 +14,6 @@ <element name="checkbox" type="checkbox" selector="//label[@class='data-grid-checkbox-cell-inner']//input[@class='admin__control-checkbox']"/> <element name="select" type="select" selector="//tr[@class='data-row']//button[@class='action-select']"/> <element name="editInSelect" type="text" selector="//a[contains(text(), 'Edit')]"/> + <element name="gridDataRow" type="input" selector="//table[@data-role='grid']//tr/td"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml index ac9c66fe82c74..38281d4d6d1d6 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml @@ -20,5 +20,7 @@ <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> <element name="blockGridRowByTitle" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> + <element name="delete" type="button" selector="//a[@data-action='item-delete']"/> + <element name="deleteConfirm" type="button" selector="//button[@data-role='action']//span[text()='OK']" timeout="60"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml new file mode 100644 index 0000000000000..4274973796b64 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminDeleteCmsBlockTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminDeleteCmsBlockTest"> + <annotations> + <features value="Cms"/> + <stories value="CMS Blocks Deleting"/> + <title value="Admin should be able to delete CMS block from grid"/> + <description value="Admin should be able to delete CMS block from grid"/> + <group value="Cms"/> + <severity value="MINOR"/> + </annotations> + <before> + <createData entity="_defaultBlock" stepKey="createCMSBlock"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenCMSBlocksGridActionGroup" stepKey="navigateToCmsBlocksGrid"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridSearchFilters"/> + <actionGroup ref="AdminSearchCMSBlockInGridByIdentifierActionGroup" stepKey="findCreatedCmsBlock"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AdminDeleteCMSBlockFromGridActionGroup" stepKey="deleteCmsBlockFromGrid"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="You deleted the block."/> + </actionGroup> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridSearchFiltersAfterBlockDeleting"/> + <actionGroup ref="AdminSearchCMSBlockInGridByIdentifierActionGroup" stepKey="searchDeletedCmsBlock"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + <actionGroup ref="AssertAdminCMSBlockIsNotInGridActionGroup" stepKey="assertDeletedCMSBlockIsNotInGrid"> + <argument name="identifier" value="$$createCMSBlock.identifier$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml new file mode 100644 index 0000000000000..245b1486058b8 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminUseQuickSearchInAdminDataGridsTest.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUseQuickSearchInAdminDataGridsTest"> + <annotations> + <features value="Cms"/> + <stories value="Create CMS Page"/> + <title value="[CMS Grids] Use quick search in Admin data grids"/> + <description value="Verify that Merchant can use quick search in order to simplify the data grid filtering in Admin"/> + <testCaseId value="MC-27559" /> + <severity value="MAJOR"/> + <group value="cms"/> + <group value="ui"/> + </annotations> + <before> + <createData entity="simpleCmsPage" stepKey="createFirstCMSPage" /> + <createData entity="_newDefaultCmsPage" stepKey="createSecondCMSPage" /> + <createData entity="_emptyCmsPage" stepKey="createThirdCMSPage" /> + <createData entity="Sales25offBlock" stepKey="createFirstCmsBlock"/> + <createData entity="ActiveTestBlock" stepKey="createSecondCmsBlock"/> + <createData entity="_emptyCmsBlock" stepKey="createThirdCmsBlock"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createFirstCMSPage" stepKey="deleteFirstCMSPage" /> + <deleteData createDataKey="createSecondCMSPage" stepKey="deleteSecondCMSPage" /> + <deleteData createDataKey="createThirdCMSPage" stepKey="deleteThirdCMSPage" /> + <deleteData createDataKey="createFirstCmsBlock" stepKey="deleteFirstCmsBlock" /> + <deleteData createDataKey="createSecondCmsBlock" stepKey="deleteSecondCmsBlock" /> + <deleteData createDataKey="createThirdCmsBlock" stepKey="deleteThirdCmsBlock" /> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCMSPageGrid"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearCmsPagesGridFilters"/> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="navigateToCmsBlockGrid"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearCmsBlockGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + <!--Go to "Cms Pages Grid" page and filter by title--> + <actionGroup ref="AdminOpenCMSPagesGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstCmsPage"> + <argument name="keyword" value="$createFirstCMSPage.title$"/> + </actionGroup> + <see userInput="$createFirstCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeFirstCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsInCmsPageGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchSecondCmsPage"> + <argument name="keyword" value="$createSecondCMSPage.title$"/> + </actionGroup> + <see userInput="$createSecondCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeSecondCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringSecondCmsPage"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFilters"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsCmsPagesBeforeClickSearchButton"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSearchMagnifierButton"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsCmsPagesAfterClickSearchButton"/> + <assertEquals stepKey="assertTotalRecordsCmsPages"> + <expectedResult type="string">$grabTotalRecordsCmsPagesBeforeClickSearchButton</expectedResult> + <actualResult type="string">$grabTotalRecordsCmsPagesAfterClickSearchButton</actualResult> + </assertEquals> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="enterNonExistentEntityInQuickSearch"> + <argument name="keyword" value="TestQueryNonExistentEntity"/> + </actionGroup> + <dontSeeElement selector="{{AdminDataGridTableSection.rows}}" stepKey="dontSeeResultRows"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringNonExistentCmsPage"> + <argument name="number" value="0"/> + </actionGroup> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchThirdCmsPage"> + <argument name="keyword" value="$createThirdCMSPage.title$"/> + </actionGroup> + <see userInput="$createThirdCMSPage.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeThirdCmsPageAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringThirdCmsPage"/> + + <!--Go to "Cms Blocks Grid" page and filter by title--> + <actionGroup ref="AdminOpenCmsBlocksGridActionGroup" stepKey="navigateToCmsBlockGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstCmsBlock"> + <argument name="keyword" value="$createFirstCmsBlock.title$"/> + </actionGroup> + <see userInput="$createFirstCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeFirstCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsInBlockGrid"/> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchSecondCmsBlock"> + <argument name="keyword" value="$createSecondCmsBlock.title$"/> + </actionGroup> + <see userInput="$createSecondCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeSecondCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringSecondBlock"/> + <actionGroup ref="AdminGridFilterResetActionGroup" stepKey="clearGridFiltersOnBlocksGridPage"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsBlocksBeforeClickSearchButton"/> + <click selector="{{AdminDataGridHeaderSection.submitSearch}}" stepKey="clickSearchMagnifierButtonOnBlocksGridPage"/> + <grabTextFrom selector="{{AdminGridHeaders.totalRecords}}" stepKey="grabTotalRecordsBlocksAfterClickSearchButton"/> + <assertEquals stepKey="assertTotalRecordsBlocks"> + <expectedResult type="string">$grabTotalRecordsBlocksBeforeClickSearchButton</expectedResult> + <actualResult type="string">$grabTotalRecordsBlocksAfterClickSearchButton</actualResult> + </assertEquals> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="enterNonExistentEntityInQuickSearchOnBlocksGridPage"> + <argument name="keyword" value="TestQueryNonExistentEntity"/> + </actionGroup> + <dontSeeElement selector="{{AdminDataGridTableSection.rows}}" stepKey="dontSeeResultRowsOnBlocksGrid"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringNonExistentCmsBlock"> + <argument name="number" value="0"/> + </actionGroup> + <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchThirdCmsBlock"> + <argument name="keyword" value="$createThirdCmsBlock.title$"/> + </actionGroup> + <see userInput="$createThirdCmsBlock.title$" selector="{{AdminGridRow.rowOne}}" stepKey="seeThirdCmsBlockAfterFiltering"/> + <actionGroup ref="AdminAssertNumberOfRecordsInUiGridActionGroup" stepKey="assertNumberOfRecordsAfterFilteringThirdBlock"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php new file mode 100644 index 0000000000000..44b94b059cb6d --- /dev/null +++ b/app/code/Magento/Cms/Test/Unit/Block/BlockByIdentifierTest.php @@ -0,0 +1,225 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Test\Unit\Block; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\GetBlockByIdentifierInterface; +use Magento\Cms\Block\BlockByIdentifier; +use Magento\Cms\Model\Block; +use Magento\Cms\Model\Template\FilterProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Filter\Template; +use Magento\Framework\View\Element\Context; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class BlockByIdentifierTest extends TestCase +{ + private const STUB_MODULE_OUTPUT_DISABLED = false; + private const STUB_EXISTING_IDENTIFIER = 'existingOne'; + private const STUB_UNAVAILABLE_IDENTIFIER = 'notExists'; + private const STUB_DEFAULT_STORE = 1; + private const STUB_CMS_BLOCK_ID = 1; + private const STUB_CONTENT = 'Content'; + + private const ASSERT_EMPTY_BLOCK_HTML = ''; + private const ASSERT_CONTENT_HTML = self::STUB_CONTENT; + private const ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES = [ + BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER, + BlockByIdentifier::CACHE_KEY_PREFIX . '_' . self::STUB_UNAVAILABLE_IDENTIFIER . '_' . self::STUB_DEFAULT_STORE + ]; + private const STUB_CMS_BLOCK_IDENTITY_BY_ID = 'CMS_BLOCK_' . self::STUB_CMS_BLOCK_ID; + private const STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER = 'CMS_BLOCK_' . self::STUB_EXISTING_IDENTIFIER; + + /** @var MockObject|GetBlockByIdentifierInterface */ + private $getBlockByIdentifierMock; + + /** @var MockObject|StoreManagerInterface */ + private $storeManagerMock; + + /** @var MockObject|FilterProvider */ + private $filterProviderMock; + + /** @var MockObject|StoreInterface */ + private $storeMock; + + protected function setUp(): void + { + $this->storeMock = $this->createMock(StoreInterface::class); + $this->storeManagerMock = $this->createMock(StoreManagerInterface::class); + $this->storeManagerMock->method('getStore')->willReturn($this->storeMock); + + $this->getBlockByIdentifierMock = $this->createMock(GetBlockByIdentifierInterface::class); + + $this->filterProviderMock = $this->createMock(FilterProvider::class); + $this->filterProviderMock->method('getBlockFilter')->willReturn($this->getPassthroughFilterMock()); + } + + public function testBlockThrowsInvalidArgumentExceptionWhenNoIdentifierProvided(): void + { + // Given + $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(null); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + + // Expect + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Expected value of `identifier` was not provided'); + + // When + $missingIdentifierBlock->toHtml(); + } + + public function testBlockReturnsEmptyStringWhenIdentifierProvidedNotFound(): void + { + // Given + $this->getBlockByIdentifierMock->method('execute')->willThrowException( + new NoSuchEntityException(__('NoSuchEntityException')) + ); + $missingIdentifierBlock = $this->getTestedBlockUsingIdentifier(self::STUB_UNAVAILABLE_IDENTIFIER); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + + // Expect + $this->assertSame(self::ASSERT_EMPTY_BLOCK_HTML, $missingIdentifierBlock->toHtml()); + $this->assertSame( + self::ASSERT_UNAVAILABLE_IDENTIFIER_BASED_IDENTITIES, + $missingIdentifierBlock->getIdentities() + ); + } + + public function testBlockReturnsCmsContentsWhenIdentifierFound(): void + { + // Given + $cmsBlockMock = $this->getCmsBlockMock( + self::STUB_CMS_BLOCK_ID, + self::STUB_EXISTING_IDENTIFIER, + self::STUB_CONTENT + ); + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + $this->getBlockByIdentifierMock->method('execute') + ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) + ->willReturn($cmsBlockMock); + $block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER); + + // Expect + $this->assertSame(self::ASSERT_CONTENT_HTML, $block->toHtml()); + } + + public function testBlockCacheIdentitiesContainCmsBlockIdentities(): void + { + // Given + $cmsBlockMock = $this->createMock(Block::class); + $cmsBlockMock->method('getId')->willReturn(self::STUB_CMS_BLOCK_ID); + $cmsBlockMock->method('isActive')->willReturn(true); + $cmsBlockMock->method('getIdentifier')->willReturn(self::STUB_EXISTING_IDENTIFIER); + $cmsBlockMock->method('getIdentities')->willReturn( + [ + self::STUB_CMS_BLOCK_IDENTITY_BY_ID, + self::STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER + ] + ); + + $this->storeMock->method('getId')->willReturn(self::STUB_DEFAULT_STORE); + $this->getBlockByIdentifierMock->method('execute') + ->with(self::STUB_EXISTING_IDENTIFIER, self::STUB_DEFAULT_STORE) + ->willReturn($cmsBlockMock); + $block = $this->getTestedBlockUsingIdentifier(self::STUB_EXISTING_IDENTIFIER); + + // When + $identities = $block->getIdentities(); + + // Then + $this->assertContains($this->getIdentityStubById(self::STUB_CMS_BLOCK_ID), $identities); + $this->assertContains(self::STUB_CMS_BLOCK_IDENTITY_BY_ID, $identities); + $this->assertContains(self::STUB_CMS_BLOCK_IDENTITY_BY_IDENTIFIER, $identities); + } + + /** + * Initializes the tested block with injecting the references required by parent classes. + * + * @param string|null $identifier + * @return BlockByIdentifier + */ + private function getTestedBlockUsingIdentifier(?string $identifier): BlockByIdentifier + { + $eventManagerMock = $this->createMock(ManagerInterface::class); + $scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $scopeConfigMock->method('getValue')->willReturn(self::STUB_MODULE_OUTPUT_DISABLED); + + $contextMock = $this->createMock(Context::class); + $contextMock->method('getEventManager')->willReturn($eventManagerMock); + $contextMock->method('getScopeConfig')->willReturn($scopeConfigMock); + + return new BlockByIdentifier( + $this->getBlockByIdentifierMock, + $this->storeManagerMock, + $this->filterProviderMock, + $contextMock, + ['identifier' => $identifier] + ); + } + + /** + * Mocks the CMS Block object for further play + * + * @param int $entityId + * @param string $identifier + * @param string $content + * @param bool $isActive + * @return MockObject|BlockInterface + */ + private function getCmsBlockMock( + int $entityId, + string $identifier, + string $content, + bool $isActive = true + ): BlockInterface { + $cmsBlock = $this->createMock(BlockInterface::class); + + $cmsBlock->method('getId')->willReturn($entityId); + $cmsBlock->method('getIdentifier')->willReturn($identifier); + $cmsBlock->method('getContent')->willReturn($content); + $cmsBlock->method('isActive')->willReturn($isActive); + + return $cmsBlock; + } + + /** + * Creates mock of the Filter that actually is doing nothing + * + * @return MockObject|Template + */ + private function getPassthroughFilterMock(): Template + { + $filterMock = $this->getMockBuilder(Template::class) + ->disableOriginalConstructor() + ->setMethods(['setStoreId', 'filter']) + ->getMock(); + $filterMock->method('setStoreId')->willReturnSelf(); + $filterMock->method('filter')->willReturnArgument(0); + + return $filterMock; + } + + /** + * Returns stub of Identity based on `$cmsBlockId` + * + * @param int $cmsBlockId + * @return string + */ + private function getIdentityStubById(int $cmsBlockId): string + { + return BlockByIdentifier::CACHE_KEY_PREFIX . '_' . $cmsBlockId; + } +} diff --git a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php index 5791ecea4e4e3..70dd95521f040 100644 --- a/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php +++ b/app/code/Magento/Cms/Test/Unit/Controller/Adminhtml/Wysiwyg/DirectiveTest.php @@ -15,7 +15,10 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\Result\Raw; use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Image\Adapter\AdapterInterface; use Magento\Framework\Image\AdapterFactory; use Magento\Framework\ObjectManagerInterface; @@ -78,11 +81,6 @@ class DirectiveTest extends TestCase */ protected $responseMock; - /** - * @var File|MockObject - */ - protected $fileMock; - /** * @var Config|MockObject */ @@ -103,6 +101,11 @@ class DirectiveTest extends TestCase */ protected $rawMock; + /** + * @var DriverInterface|MockObject + */ + private $driverMock; + protected function setUp(): void { $this->actionContextMock = $this->getMockBuilder(Context::class) @@ -146,10 +149,6 @@ protected function setUp(): void ->disableOriginalConstructor() ->setMethods(['setHeader', 'setBody', 'sendResponse']) ->getMockForAbstractClass(); - $this->fileMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['fileGetContents']) - ->getMock(); $this->wysiwygConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->getMock(); @@ -173,6 +172,17 @@ protected function setUp(): void $this->actionContextMock->expects($this->any()) ->method('getObjectManager') ->willReturn($this->objectManagerMock); + $this->driverMock = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $directoryWrite->expects($this->any())->method('getDriver')->willReturn($this->driverMock); + $filesystemMock = $this->getMockBuilder(Filesystem::class) + ->disableOriginalConstructor() + ->getMock(); + $filesystemMock->expects($this->any())->method('getDirectoryWrite')->willReturn($directoryWrite); $objectManager = new ObjectManager($this); $this->wysiwygDirective = $objectManager->getObject( @@ -185,7 +195,7 @@ protected function setUp(): void 'logger' => $this->loggerMock, 'config' => $this->wysiwygConfigMock, 'filter' => $this->templateFilterMock, - 'file' => $this->fileMock, + 'filesystem' => $filesystemMock ] ); } @@ -216,7 +226,7 @@ public function testExecute() $this->imageAdapterMock->expects($this->once()) ->method('getImage') ->willReturn($imageBody); - $this->fileMock->expects($this->once()) + $this->driverMock->expects($this->once()) ->method('fileGetContents') ->willReturn($imageBody); $this->rawFactoryMock->expects($this->any()) @@ -267,7 +277,7 @@ public function testExecuteException() $this->imageAdapterMock->expects($this->any()) ->method('getImage') ->willReturn($imageBody); - $this->fileMock->expects($this->once()) + $this->driverMock->expects($this->once()) ->method('fileGetContents') ->willReturn($imageBody); $this->loggerMock->expects($this->once()) diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php index 33bf352adf6c5..0ba3fada2a072 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/ConfigTest.php @@ -192,12 +192,12 @@ public function testGetConfig($data, $isAuthorizationAllowed, $expectedResults) ->willReturn('localhost/index.php/'); $this->filesystemMock->expects($this->once()) ->method('getUri') - ->willReturn('pub/static'); + ->willReturn('static'); /** @var ContextInterface|MockObject $contextMock */ $contextMock = $this->getMockForAbstractClass(ContextInterface::class); $contextMock->expects($this->once()) ->method('getBaseUrl') - ->willReturn('localhost/pub/static/'); + ->willReturn('localhost/static/'); $this->assetRepoMock->expects($this->once()) ->method('getStaticViewFileContext') ->willReturn($contextMock); @@ -217,8 +217,8 @@ public function testGetConfig($data, $isAuthorizationAllowed, $expectedResults) $config = $this->wysiwygConfig->getConfig($data); $this->assertInstanceOf(DataObject::class, $config); $this->assertEquals($expectedResults[0], $config->getData('someData')); - $this->assertEquals('localhost/pub/static/', $config->getData('baseStaticUrl')); - $this->assertEquals('localhost/pub/static/', $config->getData('baseStaticDefaultUrl')); + $this->assertEquals('localhost/static/', $config->getData('baseStaticUrl')); + $this->assertEquals('localhost/static/', $config->getData('baseStaticDefaultUrl')); } /** diff --git a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php index c2c748dcc7633..b03dbb8f0c888 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Wysiwyg/Images/StorageTest.php @@ -494,7 +494,7 @@ public function testUploadFile() $targetPath = self::STORAGE_ROOT_DIR . $path; $fileName = 'image.gif'; $realPath = $targetPath . '/' . $fileName; - $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '/.thumbs' . $path; + $thumbnailTargetPath = self::STORAGE_ROOT_DIR . '.thumbs' . $path; $thumbnailDestination = $thumbnailTargetPath . '/' . $fileName; $type = 'image'; $result = [ diff --git a/app/code/Magento/Cms/etc/config.xml b/app/code/Magento/Cms/etc/config.xml index d7a9e172f59a6..c1b3717386454 100644 --- a/app/code/Magento/Cms/etc/config.xml +++ b/app/code/Magento/Cms/etc/config.xml @@ -31,6 +31,7 @@ <media_storage_configuration> <allowed_resources> <wysiwyg_image_folder>wysiwyg</wysiwyg_image_folder> + <preview_folder>.thumbs</preview_folder> </allowed_resources> </media_storage_configuration> </system> diff --git a/app/code/Magento/Cms/etc/di.xml b/app/code/Magento/Cms/etc/di.xml index 2c265f881acf8..61cf33f88abd6 100644 --- a/app/code/Magento/Cms/etc/di.xml +++ b/app/code/Magento/Cms/etc/di.xml @@ -57,35 +57,35 @@ <item name="exclude" xsi:type="array"> <item name="captcha" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+captcha[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+captcha[/\\]*$</item> </item> <item name="catalog/product" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+catalog[/\\]+product[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+catalog[/\\]+product[/\\]*$</item> </item> <item name="customer" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+customer[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+customer[/\\]*$</item> </item> <item name="downloadable" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+downloadable[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+downloadable[/\\]*$</item> </item> <item name="import" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+import[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+import[/\\]*$</item> </item> <item name="theme" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+theme[/\\]*$</item> </item> <item name="theme_customization" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+theme_customization[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+theme_customization[/\\]*$</item> </item> <item name="tmp" xsi:type="array"> <item name="regexp" xsi:type="boolean">true</item> - <item name="name" xsi:type="string">pub[/\\]+media[/\\]+tmp[/\\]*$</item> + <item name="name" xsi:type="string">media[/\\]+tmp[/\\]*$</item> </item> </item> <item name="include" xsi:type="array"/> diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php index e6acd431be3d5..1763a6d1800a1 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php @@ -10,6 +10,7 @@ namespace Magento\Config\Model\Config\Backend\Admin; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; /** @@ -36,7 +37,6 @@ class Robots extends \Magento\Framework\App\Config\Value * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data - * @param DocumentRoot $documentRoot */ public function __construct( \Magento\Framework\Model\Context $context, @@ -46,13 +46,11 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, - array $data = [], - \Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot $documentRoot = null + array $data = [] ) { parent::__construct($context, $registry, $config, $cacheTypeList, $resource, $resourceCollection, $data); - $documentRoot = $documentRoot ?: ObjectManager::getInstance()->get(DocumentRoot::class); - $this->_directory = $filesystem->getDirectoryWrite($documentRoot->getPath()); + $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::PUB); $this->_file = 'robots.txt'; } diff --git a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php index bf59c729790a7..fb78de35569ac 100644 --- a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +++ b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php @@ -3,57 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Config\Model\Config\Reader\Source\Deployed; -use Magento\Framework\Config\ConfigOptionsListConstants; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\App\Filesystem\DirectoryList; /** - * Class DocumentRoot - * @package Magento\Config\Model\Config\Reader\Source\Deployed + * Document root detector. + * * @api * @since 101.0.0 + * + * @deprecated Magento always uses the pub directory + * @see DirectoryList::PUB */ class DocumentRoot { /** - * @var DeploymentConfig - */ - private $config; - - /** - * DocumentRoot constructor. * @param DeploymentConfig $config + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct(DeploymentConfig $config) { - $this->config = $config; } /** - * A shortcut to load the document root path from the DirectoryList based on the - * deployment configuration. + * A shortcut to load the document root path from the DirectoryList. * * @return string * @since 101.0.0 */ public function getPath() { - return $this->isPub() ? DirectoryList::PUB : DirectoryList::ROOT; + return DirectoryList::PUB; } /** - * Returns whether the deployment configuration specifies that the document root is - * in the pub/ folder. This affects ares such as sitemaps and robots.txt (and will - * likely be extended to control other areas). + * Checks if root folder is /pub. * * @return bool * @since 101.0.0 */ public function isPub() { - return (bool)$this->config->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB); + return true; } } diff --git a/app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php b/app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php deleted file mode 100644 index 6f1758f3d2b92..0000000000000 --- a/app/code/Magento/Config/Test/Unit/Model/Config/Reader/Source/Deployed/DocumentRootTest.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Config\Test\Unit\Model\Config\Reader\Source\Deployed; - -use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; -use Magento\Framework\App\Config; -use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Config\ConfigOptionsListConstants; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Test class for checking settings that defined in config file - */ -class DocumentRootTest extends TestCase -{ - /** - * @var Config|MockObject - */ - private $configMock; - - /** - * @var DocumentRoot - */ - private $documentRoot; - - protected function setUp(): void - { - $this->configMock = $this->getMockBuilder(DeploymentConfig::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->documentRoot = new DocumentRoot($this->configMock); - } - - /** - * Ensures that the path returned matches the pub/ path. - */ - public function testGetPath() - { - $this->configMockSetForDocumentRootIsPub(); - - $this->assertSame(DirectoryList::PUB, $this->documentRoot->getPath()); - } - - /** - * Ensures that the deployment configuration returns the mocked value for - * the pub/ folder. - */ - public function testIsPub() - { - $this->configMockSetForDocumentRootIsPub(); - - $this->assertTrue($this->documentRoot->isPub()); - } - - private function configMockSetForDocumentRootIsPub() - { - $this->configMock->expects($this->any()) - ->method('get') - ->willReturnMap([ - [ - ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, - null, - true - ], - ]); - } -} diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index c2ae381b345c6..79f6d1e47f1a2 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -17,6 +17,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\File\UploaderFactory; /** * Configurable product type implementation @@ -235,11 +236,12 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor * @param \Magento\Framework\Cache\FrontendInterface|null $cache * @param \Magento\Customer\Model\Session|null $customerSession - * @param \Magento\Framework\Serialize\Serializer\Json $serializer - * @param ProductInterfaceFactory $productFactory - * @param SalableProcessor $salableProcessor + * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param ProductInterfaceFactory|null $productFactory + * @param SalableProcessor|null $salableProcessor * @param ProductAttributeRepositoryInterface|null $productAttributeRepository * @param SearchCriteriaBuilder|null $searchCriteriaBuilder + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -266,7 +268,8 @@ public function __construct( ProductInterfaceFactory $productFactory = null, SalableProcessor $salableProcessor = null, ProductAttributeRepositoryInterface $productAttributeRepository = null, - SearchCriteriaBuilder $searchCriteriaBuilder = null + SearchCriteriaBuilder $searchCriteriaBuilder = null, + UploaderFactory $uploaderFactory = null ) { $this->typeConfigurableFactory = $typeConfigurableFactory; $this->_eavAttributeFactory = $eavAttributeFactory; @@ -295,7 +298,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php new file mode 100644 index 0000000000000..cbeaf2cea90e0 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/BaseStockStatusSelectProcessor.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; + +use Magento\CatalogInventory\Model\Stock; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; + +/** + * A Select object processor. + * + * Adds stock status limitations to a given Select object. + */ +class BaseStockStatusSelectProcessor implements BaseSelectProcessorInterface +{ + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var StockConfigurationInterface + */ + private $stockConfig; + + /** + * @param ResourceConnection $resource + * @param StockConfigurationInterface $stockConfig + */ + public function __construct( + ResourceConnection $resource, + StockConfigurationInterface $stockConfig + ) { + $this->resource = $resource; + $this->stockConfig = $stockConfig; + } + + /** + * @inheritdoc + */ + public function process(Select $select) + { + // Does not make sense to extend query if out of stock products won't appear in tables for indexing + if ($this->stockConfig->isShowOutOfStock()) { + $select->join( + ['si' => $this->resource->getTableName('cataloginventory_stock_item')], + 'si.product_id = l.product_id', + [] + ); + $select->join( + ['si_parent' => $this->resource->getTableName('cataloginventory_stock_item')], + 'si_parent.product_id = l.parent_id', + [] + ); + $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); + $select->orWhere('si_parent.is_in_stock = ?', Stock::STOCK_OUT_OF_STOCK); + } + + return $select; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php index 6031ab6f8f8ae..d00e5c72a4622 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/Configurable.php @@ -3,9 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; +use Magento\Catalog\Model\ResourceModel\Product\BaseSelectProcessorInterface; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\BasePriceModifier; +use Magento\Framework\DB\Select; use Magento\Framework\Indexer\DimensionalIndexerInterface; use Magento\Framework\EntityManager\MetadataPool; use Magento\Catalog\Model\Indexer\Product\Price\TableMaintainer; @@ -13,10 +17,8 @@ use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructureFactory; use Magento\Catalog\Model\ResourceModel\Product\Indexer\Price\IndexTableStructure; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Store\Model\ScopeInterface; use Magento\Framework\App\ObjectManager; use Magento\CatalogInventory\Model\Stock; -use Magento\CatalogInventory\Model\Configuration; /** * Configurable Products Price Indexer Resource model @@ -75,6 +77,11 @@ class Configurable implements DimensionalIndexerInterface */ private $scopeConfig; + /** + * @var BaseSelectProcessorInterface + */ + private $baseSelectProcessor; + /** * @param BaseFinalPrice $baseFinalPrice * @param IndexTableStructureFactory $indexTableStructureFactory @@ -85,6 +92,9 @@ class Configurable implements DimensionalIndexerInterface * @param bool $fullReindexAction * @param string $connectionName * @param ScopeConfigInterface $scopeConfig + * @param BaseSelectProcessorInterface|null $baseSelectProcessor + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( BaseFinalPrice $baseFinalPrice, @@ -95,7 +105,8 @@ public function __construct( BasePriceModifier $basePriceModifier, $fullReindexAction = false, $connectionName = 'indexer', - ScopeConfigInterface $scopeConfig = null + ScopeConfigInterface $scopeConfig = null, + ?BaseSelectProcessorInterface $baseSelectProcessor = null ) { $this->baseFinalPrice = $baseFinalPrice; $this->indexTableStructureFactory = $indexTableStructureFactory; @@ -106,6 +117,8 @@ public function __construct( $this->fullReindexAction = $fullReindexAction; $this->basePriceModifier = $basePriceModifier; $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); + $this->baseSelectProcessor = $baseSelectProcessor ?: + ObjectManager::getInstance()->get(BaseSelectProcessorInterface::class); } /** @@ -198,15 +211,7 @@ private function fillTemporaryOptionsTable(string $temporaryOptionsTableName, ar [] ); - // Does not make sense to extend query if out of stock products won't appear in tables for indexing - if ($this->isConfigShowOutOfStock()) { - $select->join( - ['si' => $this->getTable('cataloginventory_stock_item')], - 'si.product_id = l.product_id', - [] - ); - $select->where('si.is_in_stock = ?', Stock::STOCK_IN_STOCK); - } + $this->baseSelectProcessor->process($select); $select->columns( [ @@ -295,17 +300,4 @@ private function getTable($tableName) { return $this->resource->getTableName($tableName, $this->connectionName); } - - /** - * Is flag Show Out Of Stock setted - * - * @return bool - */ - private function isConfigShowOutOfStock(): bool - { - return $this->scopeConfig->isSetFlag( - Configuration::XML_PATH_SHOW_OUT_OF_STOCK, - ScopeInterface::SCOPE_STORE - ); - } } diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQty.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQty.php new file mode 100644 index 0000000000000..28237ca71b07a --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQty.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Plugin\Model\Order\Invoice; + +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Sales\Model\Order\Invoice; + +/** + * Update total quantity for configurable product invoice + */ +class UpdateConfigurableProductTotalQty +{ + /** + * Set total quantity for configurable product invoice + * + * @param Invoice $invoice + * @param float $totalQty + * @return float + */ + public function beforeSetTotalQty( + Invoice $invoice, + float $totalQty + ): float { + $order = $invoice->getOrder(); + $productTotalQty = 0; + $hasConfigurableProduct = false; + foreach ($order->getAllItems() as $orderItem) { + if ($orderItem->getParentItemId() === null && + $orderItem->getProductType() == Configurable::TYPE_CODE + ) { + $hasConfigurableProduct = true; + continue; + } + $productTotalQty += (float) $orderItem->getQtyOrdered(); + } + return $hasConfigurableProduct ? $productTotalQty : $totalQty; + } +} diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 976be77122547..79705e679fb78 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -62,8 +62,9 @@ <argument name="attributeType" value="{{colorProductAttribute.input_type}}"/> <argument name="scope" value="Global"/> </actionGroup> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForProductPageReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForProductPageReload"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> <waitForPageLoad stepKey="waitForFilters"/> <actionGroup ref="CreateOptionsForAttributeActionGroup" stepKey="createOptions"> @@ -142,8 +143,9 @@ <argument name="attributeType" value="{{productAttributeColor.input_type}}"/> <argument name="scope" value="Global"/> </actionGroup> - <reloadPage stepKey="reloadDuplicatedProductPage"/> - <waitForPageLoad stepKey="waitForDuplicatedProductReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadDuplicatedProductPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForDuplicatedProductReload"/> + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="createConfigurationsDuplicatedProduct"/> <waitForElementVisible selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="waitForCreateConfigurationsPageLoad"/> <click selector="{{AdminGridSelectRows.multicheckDropdown}}" stepKey="openMulticheckDropdown"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQtyTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQtyTest.php new file mode 100644 index 0000000000000..bff629fd94ac2 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/Order/Invoice/UpdateConfigurableProductTotalQtyTest.php @@ -0,0 +1,182 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\Order\Invoice; + +use Magento\Bundle\Model\Product\Type as Bundle; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\ConfigurableProduct\Plugin\Model\Order\Invoice\UpdateConfigurableProductTotalQty; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Item; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for class UpdateConfigurableProductTotalQty. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class UpdateConfigurableProductTotalQtyTest extends TestCase +{ + /** + * @var UpdateConfigurableProductTotalQty + */ + private $model; + + /** + * @var ObjectManagerHelper|null + */ + private $objectManagerHelper; + + /** + * @var Invoice|MockObject + */ + private $invoiceMock; + + /** + * @var Order|MockObject + */ + private $orderMock; + + /** + * @var Item[]|MockObject + */ + private $orderItemsMock; + + protected function setUp(): void + { + $this->invoiceMock = $this->createMock(Invoice::class); + $this->orderMock = $this->createMock(Order::class); + $this->orderItemsMock = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->getMock(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + UpdateConfigurableProductTotalQty::class, + [] + ); + } + + /** + * Test Set total quantity for configurable product invoice + * + * @param array $orderItems + * @param float $totalQty + * @param float $productTotalQty + * @dataProvider getOrdersForConfigurableProducts + */ + public function testBeforeSetTotalQty( + array $orderItems, + float $totalQty, + float $productTotalQty + ): void { + $this->invoiceMock->expects($this->any()) + ->method('getOrder') + ->willReturn($this->orderMock); + $this->orderMock->expects($this->any()) + ->method('getAllItems') + ->willReturn($orderItems); + $expectedQty= $this->model->beforeSetTotalQty($this->invoiceMock, $totalQty); + $this->assertEquals($expectedQty, $productTotalQty); + } + + /** + * DataProvider for beforeSetTotalQty. + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @return array + */ + public function getOrdersForConfigurableProducts(): array + { + + return [ + 'verify productQty for simple products' => [ + 'orderItems' => $this->getOrderItems( + [ + [ + 'parent_item_id' => null, + 'product_type' => 'simple', + 'qty_ordered' => 10 + ] + ] + ), + 'totalQty' => 10.00, + 'productTotalQty' => 10.00 + ], + 'verify productQty for configurable products' => [ + 'orderItems' => $this->getOrderItems( + [ + [ + 'parent_item_id' => '2', + 'product_type' => Configurable::TYPE_CODE, + 'qty_ordered' => 10 + ] + ] + ), + 'totalQty' => 10.00, + 'productTotalQty' => 10.00 + ], + 'verify productQty for simple configurable products' => [ + 'orderItems' => $this->getOrderItems( + [ + [ + 'parent_item_id' => null, + 'product_type' => 'simple', + 'qty_ordered' => 10 + ], + [ + 'parent_item_id' => '2', + 'product_type' => Configurable::TYPE_CODE, + 'qty_ordered' => 10 + ], + [ + 'parent_item_id' => '2', + 'product_type' => Bundle::TYPE_CODE, + 'qty_ordered' => 10 + ] + ] + ), + 'totalQty' => 30.00, + 'productTotalQty' => 30.00 + ] + ]; + } + + /** + * Get Order Items. + * + * @param array $orderItems + * @return array + */ + public function getOrderItems(array $orderItems): array + { + $orderItemsMock = []; + foreach ($orderItems as $key => $orderItem) { + $orderItemsMock[$key] = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->getMock(); + $orderItemsMock[$key]->expects($this->any()) + ->method('getParentItemId') + ->willReturn($orderItem['parent_item_id']); + $orderItemsMock[$key]->expects($this->any()) + ->method('getProductType') + ->willReturn($orderItem['product_type']); + $orderItemsMock[$key]->expects($this->any()) + ->method('getQtyOrdered') + ->willReturn($orderItem['qty_ordered']); + } + return $orderItemsMock; + } + + protected function tearDown(): void + { + unset($this->invoiceMock); + unset($this->orderMock); + unset($this->orderItemsMock); + } +} diff --git a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml index de6765138fce6..60ad9e03fc17e 100644 --- a/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/adminhtml/di.xml @@ -78,4 +78,7 @@ </argument> </arguments> </virtualType> + <type name="Magento\Sales\Model\Order\Invoice"> + <plugin name="update_configurable_product_total_qty" type="Magento\ConfigurableProduct\Plugin\Model\Order\Invoice\UpdateConfigurableProductTotalQty"/> + </type> </config> diff --git a/app/code/Magento/ConfigurableProduct/etc/di.xml b/app/code/Magento/ConfigurableProduct/etc/di.xml index c8a278df92dc6..c7f67a69d669f 100644 --- a/app/code/Magento/ConfigurableProduct/etc/di.xml +++ b/app/code/Magento/ConfigurableProduct/etc/di.xml @@ -198,6 +198,7 @@ <arguments> <argument name="tableStrategy" xsi:type="object">Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy</argument> <argument name="connectionName" xsi:type="string">indexer</argument> + <argument name="baseSelectProcessor" xsi:type="object">Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\BaseStockStatusSelectProcessor</argument> </arguments> </type> <type name="Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product"> diff --git a/app/code/Magento/Customer/Model/FileProcessor.php b/app/code/Magento/Customer/Model/FileProcessor.php index c16faea284296..c596f8c313ab3 100644 --- a/app/code/Magento/Customer/Model/FileProcessor.php +++ b/app/code/Magento/Customer/Model/FileProcessor.php @@ -233,7 +233,8 @@ public function moveTemporaryFile($fileName) ); } catch (\Exception $e) { throw new \Magento\Framework\Exception\LocalizedException( - __('Something went wrong while saving the file.') + __('Something went wrong while saving the file.'), + $e ); } diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php index 6aadc814a4b9b..1000575805018 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByToken.php @@ -8,6 +8,8 @@ namespace Magento\Customer\Model\ForgotPasswordToken; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Exception\LocalizedException; /** * Confirm customer by reset password token @@ -25,15 +27,11 @@ class ConfirmCustomerByToken private $customerRepository; /** - * ConfirmByToken constructor. - * * @param GetCustomerByToken $getByToken * @param CustomerRepositoryInterface $customerRepository */ - public function __construct( - GetCustomerByToken $getByToken, - CustomerRepositoryInterface $customerRepository - ) { + public function __construct(GetCustomerByToken $getByToken, CustomerRepositoryInterface $customerRepository) + { $this->getByToken = $getByToken; $this->customerRepository = $customerRepository; } @@ -42,17 +40,29 @@ public function __construct( * Confirm customer account my rp_token * * @param string $resetPasswordToken - * * @return void - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException */ public function execute(string $resetPasswordToken): void { $customer = $this->getByToken->execute($resetPasswordToken); if ($customer->getConfirmation()) { - $this->customerRepository->save( - $customer->setConfirmation(null) - ); + $this->resetConfirmation($customer); } } + + /** + * Reset customer confirmation + * + * @param CustomerInterface $customer + * @return void + */ + private function resetConfirmation(CustomerInterface $customer): void + { + // skip unnecessary address and customer validation + $customer->setData('ignore_validation_flag', true); + $customer->setConfirmation(null); + + $this->customerRepository->save($customer); + } } diff --git a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php index 211a71d827f7e..6d2274351faee 100644 --- a/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php +++ b/app/code/Magento/Customer/Model/ForgotPasswordToken/GetCustomerByToken.php @@ -54,7 +54,7 @@ public function __construct( * @throws NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException */ - public function execute(string $resetPasswordToken):CustomerInterface + public function execute(string $resetPasswordToken): CustomerInterface { $this->searchCriteriaBuilder->addFilter( 'rp_token', diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index cb003ed837294..7442a32d58b2d 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -40,8 +40,9 @@ <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> <argument name="indices" value=""/> </actionGroup> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForLoad2"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForLoad2"/> + <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerFiltersSection.emailInput}}" stepKey="filterEmail"/> <click selector="{{AdminCustomerFiltersSection.apply}}" stepKey="applyFilter"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml new file mode 100644 index 0000000000000..ef610831a721d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithInvalidDataTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCreateCustomerWithInvalidDataTest"> + <annotations> + <stories value="Create a Customer via the Storefront"/> + <features value="Customer"/> + <title value="Register customer on storefront after customer form validation failed."/> + <description value="Customer should be able to re-submit register form after correcting invalid form data on storefront."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-38532"/> + <useCaseId value="MC-38509"/> + <group value="customer"/> + </annotations> + + <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> + <!--Try to submit register form with wrong password.--> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountFormWithWrongData"> + <argument name="customer" value="Simple_Customer_With_Password_Length_Is_Below_Eight_Characters"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="tryToSubmitFormWithWrongPassword"/> + <actionGroup ref="AssertMessageCustomerCreateAccountPasswordComplexityActionGroup" stepKey="seeTheErrorPasswordLength"> + <argument name="message" value="Minimum length of this field must be equal or greater than 8 symbols. Leading and trailing spaces will be ignored."/> + </actionGroup> + <!--Re-submit customer register form with correct data.--> + <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountFormWithCorrectData"> + <argument name="customer" value="Simple_US_Customer"/> + </actionGroup> + <actionGroup ref="StorefrontClickCreateAnAccountCustomerAccountCreationFormActionGroup" stepKey="submitCreateAccountForm"/> + <actionGroup ref="AssertMessageCustomerCreateAccountActionGroup" stepKey="seeSuccessMessage"/> + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php index 62964a311af42..e1c771d79694e 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/FileProcessorTest.php @@ -11,10 +11,13 @@ use Magento\Customer\Api\CustomerMetadataInterface; use Magento\Customer\Model\FileProcessor; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Url\EncoderInterface; use Magento\Framework\UrlInterface; use Magento\MediaStorage\Model\File\Uploader; @@ -363,17 +366,73 @@ public function testMoveTemporaryFile() $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; $newPath = $destinationPath . $filePath; + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method('get')->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn(false); + ObjectManager::setInstance($objectManagerMock); + $this->mediaDirectory->expects($this->once()) ->method('renameFile') ->with($path, $newPath) ->willReturn(true); + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); $this->assertEquals('/f/i' . $filePath, $model->moveTemporaryFile($filePath)); } + public function testMoveTemporaryFileNewFileName() + { + $filePath = '/filename.ext1'; + + $destinationPath = 'customer/f/i'; + + $this->mediaDirectory->expects($this->once()) + ->method('create') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('isWritable') + ->with($destinationPath) + ->willReturn(true); + $this->mediaDirectory->expects($this->once()) + ->method('getAbsolutePath') + ->with($destinationPath) + ->willReturn('/' . $destinationPath); + + $path = CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER . '/' . FileProcessor::TMP_DIR . $filePath; + + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method('get')->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturnOnConsecutiveCalls(true, true, false); + ObjectManager::setInstance($objectManagerMock); + + $this->mediaDirectory->expects($this->once()) + ->method('renameFile') + ->with($path, 'customer/f/i/filename_2.ext1') + ->willReturn(true); + + + $model = $this->getModel(CustomerMetadataInterface::ENTITY_TYPE_CUSTOMER); + $this->assertEquals('/f/i/filename_2.ext1', $model->moveTemporaryFile($filePath)); + } + public function testMoveTemporaryFileWithException() { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn(false); + ObjectManager::setInstance($objectManagerMock); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('Something went wrong while saving the file'); diff --git a/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php new file mode 100644 index 0000000000000..4a6769e0653ad --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Model\ForgotPasswordToken; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken; +use Magento\Customer\Model\ForgotPasswordToken\GetCustomerByToken; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken. + */ +class ConfirmCustomerByTokenTest extends TestCase +{ + private const STUB_RESET_PASSWORD_TOKEN = 'resetPassword'; + + /** + * @var ConfirmCustomerByToken; + */ + private $model; + + /** + * @var CustomerInterface|MockObject + */ + private $customerMock; + + /** + * @var CustomerRepositoryInterface|MockObject + */ + private $customerRepositoryMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->customerMock = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->addMethods(['setData']) + ->getMockForAbstractClass(); + + $this->customerRepositoryMock = $this->createMock(CustomerRepositoryInterface::class); + + $getCustomerByTokenMock = $this->createMock(GetCustomerByToken::class); + $getCustomerByTokenMock->method('execute')->willReturn($this->customerMock); + + $this->model = new ConfirmCustomerByToken($getCustomerByTokenMock, $this->customerRepositoryMock); + } + + /** + * Confirm customer with confirmation + * + * @return void + */ + public function testExecuteWithConfirmation(): void + { + $this->customerMock->expects($this->once()) + ->method('getConfirmation') + ->willReturn('GWz2ik7Kts517MXAgrm4DzfcxKayGCm4'); + $this->customerMock->expects($this->once()) + ->method('setData') + ->with('ignore_validation_flag', true); + $this->customerMock->expects($this->once()) + ->method('setConfirmation') + ->with(null); + $this->customerRepositoryMock->expects($this->once()) + ->method('save') + ->with($this->customerMock); + + $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); + } + + /** + * Confirm customer without confirmation + * + * @return void + */ + public function testExecuteWithoutConfirmation(): void + { + $this->customerMock->expects($this->once()) + ->method('getConfirmation') + ->willReturn(null); + $this->customerRepositoryMock->expects($this->never()) + ->method('save'); + + $this->model->execute(self::STUB_RESET_PASSWORD_TOKEN); + } +} diff --git a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js index b941ec7a254d8..75f4ee6097685 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js +++ b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js @@ -14,9 +14,15 @@ define([ dataForm.submit(function () { $(this).find(':submit').attr('disabled', 'disabled'); + + if (this.isValid === false) { + $(this).find(':submit').prop('disabled', false); + } + this.isValid = true; }); dataForm.bind('invalid-form.validate', function () { $(this).find(':submit').prop('disabled', false); + this.isValid = false; }); }; }); diff --git a/app/code/Magento/Directory/etc/zip_codes.xml b/app/code/Magento/Directory/etc/zip_codes.xml index 14d250656d28c..634d4abe06763 100644 --- a/app/code/Magento/Directory/etc/zip_codes.xml +++ b/app/code/Magento/Directory/etc/zip_codes.xml @@ -19,6 +19,7 @@ <zip countryCode="AR"> <codes> <code id="pattern_1" active="true" example="1234">^[0-9]{4}$</code> + <code id="pattern_2" active="true" example="A1234BCD">^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$</code> </codes> </zip> <zip countryCode="AM"> @@ -228,6 +229,7 @@ <zip countryCode="KR"> <codes> <code id="pattern_1" active="true" example="123-456">^[0-9]{3}-[0-9]{3}$</code> + <code id="pattern_2" active="true" example="12345">^[0-9]{5}$</code> </codes> </zip> <zip countryCode="KG"> diff --git a/app/code/Magento/Downloadable/Helper/Download.php b/app/code/Magento/Downloadable/Helper/Download.php index 6b7db3af51195..1425f71f2fd8a 100644 --- a/app/code/Magento/Downloadable/Helper/Download.php +++ b/app/code/Magento/Downloadable/Helper/Download.php @@ -7,6 +7,8 @@ namespace Magento\Downloadable\Helper; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Exception\LocalizedException as CoreException; @@ -18,12 +20,12 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper { /** - * Link type url + * Link type for url */ const LINK_TYPE_URL = 'url'; /** - * Link type file + * Link type for file */ const LINK_TYPE_FILE = 'file'; @@ -109,6 +111,11 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper */ protected $_session; + /** + * @var Mime + */ + private $mime; + /** * @param \Magento\Framework\App\Helper\Context $context * @param File $downloadableFile @@ -116,6 +123,7 @@ class Download extends \Magento\Framework\App\Helper\AbstractHelper * @param Filesystem $filesystem * @param \Magento\Framework\Session\SessionManagerInterface $session * @param Filesystem\File\ReadFactory $fileReadFactory + * @param Mime|null $mime */ public function __construct( \Magento\Framework\App\Helper\Context $context, @@ -123,7 +131,8 @@ public function __construct( \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb, \Magento\Framework\Filesystem $filesystem, \Magento\Framework\Session\SessionManagerInterface $session, - \Magento\Framework\Filesystem\File\ReadFactory $fileReadFactory + \Magento\Framework\Filesystem\File\ReadFactory $fileReadFactory, + Mime $mime = null ) { parent::__construct($context); $this->_downloadableFile = $downloadableFile; @@ -131,6 +140,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->_session = $session; $this->fileReadFactory = $fileReadFactory; + $this->mime = $mime ?? ObjectManager::getInstance()->get(Mime::class); } /** @@ -148,6 +158,7 @@ protected function _getHandle() if ($this->_handle === null) { if ($this->_linkType == self::LINK_TYPE_URL) { $path = $this->_resourceFile; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $protocol = strtolower(parse_url($path, PHP_URL_SCHEME)); if ($protocol) { // Strip down protocol from path @@ -188,14 +199,8 @@ public function getContentType() { $this->_getHandle(); if ($this->_linkType === self::LINK_TYPE_FILE) { - if (function_exists('mime_content_type') - && ($contentType = mime_content_type( - $this->_workingDirectory->getAbsolutePath($this->_resourceFile) - )) - ) { - return $contentType; - } - return $this->_downloadableFile->getFileType($this->_resourceFile); + $absolutePath = $this->_workingDirectory->getAbsolutePath($this->_resourceFile); + return $this->mime->getMimeType($absolutePath); } if ($this->_linkType === self::LINK_TYPE_URL) { return (is_array($this->_handle->stat($this->_resourceFile)['type']) @@ -209,6 +214,8 @@ public function getContentType() * Return name of the file * * @return string + * phpcs:disable Magento2.Functions.DiscouragedFunction + * phpcs:disable Generic.PHP.NoSilencedErrors */ public function getFilename() { @@ -254,20 +261,21 @@ public function setResource($resourceFile, $linkType = self::LINK_TYPE_FILE) ); } } - + $this->_resourceFile = $resourceFile; - + /** * check header for urls */ if ($linkType === self::LINK_TYPE_URL) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $headers = array_change_key_case(get_headers($this->_resourceFile, 1), CASE_LOWER); if (isset($headers['location'])) { $this->_resourceFile = is_array($headers['location']) ? current($headers['location']) : $headers['location']; } } - + $this->_linkType = $linkType; return $this; } @@ -282,6 +290,7 @@ public function output() $handle = $this->_getHandle(); $this->_session->writeClose(); while (true == ($buffer = $handle->read(1024))) { + // phpcs:ignore Magento2.Security.LanguageConstruct echo $buffer; //@codingStandardsIgnoreLine } } diff --git a/app/code/Magento/Downloadable/Model/Product/Type.php b/app/code/Magento/Downloadable/Model/Product/Type.php index cb79dda3baccb..45a03b50d78b8 100644 --- a/app/code/Magento/Downloadable/Model/Product/Type.php +++ b/app/code/Magento/Downloadable/Model/Product/Type.php @@ -7,6 +7,7 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\File\UploaderFactory; /** * Downloadable product type model @@ -67,8 +68,6 @@ class Type extends \Magento\Catalog\Model\Product\Type\Virtual private $extensionAttributesJoinProcessor; /** - * Construct - * * @param \Magento\Catalog\Model\Product\Option $catalogProductOption * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\Catalog\Model\Product\Type $catalogProductType @@ -87,6 +86,7 @@ class Type extends \Magento\Catalog\Model\Product\Type\Virtual * @param TypeHandler\TypeHandlerInterface $typeHandler * @param JoinProcessorInterface $extensionAttributesJoinProcessor * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -107,7 +107,8 @@ public function __construct( \Magento\Downloadable\Model\LinkFactory $linkFactory, \Magento\Downloadable\Model\Product\TypeHandler\TypeHandlerInterface $typeHandler, JoinProcessorInterface $extensionAttributesJoinProcessor, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->_sampleResFactory = $sampleResFactory; $this->_linkResource = $linkResource; @@ -127,7 +128,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml new file mode 100644 index 0000000000000..6364600faee30 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Section/StorefrontDownloadableLinkSection.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontDownloadableLinkSection"> + <element name="downloadedImage" type="text" selector="//img[contains(@style, '-webkit-user-select')]"/> + <element name="downloadedSvg" type="text" selector="//*[@id='{{id}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php index 59de5b0139ff6..da89efac59fa8 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Helper/DownloadTest.php @@ -10,6 +10,7 @@ use Magento\Downloadable\Helper\Download as DownloadHelper; use Magento\Downloadable\Helper\File as DownloadableFile; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\ReadInterface as DirReadInterface; use Magento\Framework\Filesystem\File\ReadFactory; @@ -62,6 +63,11 @@ class DownloadTest extends TestCase const URL = 'http://example.com'; + /** + * @var Mime|MockObject + */ + private $mime; + protected function setUp(): void { require_once __DIR__ . '/../_files/download_mock.php'; @@ -77,6 +83,7 @@ protected function setUp(): void SessionManagerInterface::class ); $this->fileReadFactory = $this->createMock(ReadFactory::class); + $this->mime = $this->createMock(Mime::class); $this->_helper = (new ObjectManager($this))->getObject( \Magento\Downloadable\Helper\Download::class, @@ -85,6 +92,7 @@ protected function setUp(): void 'filesystem' => $this->_filesystemMock, 'session' => $this->sessionManager, 'fileReadFactory' => $this->fileReadFactory, + 'mime' => $this->mime ] ); } @@ -132,8 +140,17 @@ public function testGetFileSizeNoFile() public function testGetContentType() { + $this->mime->expects( + self::once() + )->method( + 'getMimeType' + )->willReturn( + self::MIME_TYPE + ); $this->_setupFileMocks(); $this->_downloadableFileMock->expects($this->never())->method('getFileType'); + $this->_workingDirectoryMock->expects($this->once())->method('getAbsolutePath') + ->willReturn('/path/to/file.txt'); $this->assertEquals(self::MIME_TYPE, $this->_helper->getContentType()); } @@ -146,10 +163,10 @@ public function testGetContentTypeThroughHelper($functionExistsResult, $mimeCont self::$functionExists = $functionExistsResult; self::$mimeContentType = $mimeContentTypeResult; - $this->_downloadableFileMock->expects( - $this->once() + $this->mime->expects( + self::once() )->method( - 'getFileType' + 'getMimeType' )->willReturn( self::MIME_TYPE ); diff --git a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php index e6ead5d5cc021..3450376365cd0 100644 --- a/app/code/Magento/DownloadableImportExport/Helper/Uploader.php +++ b/app/code/Magento/DownloadableImportExport/Helper/Uploader.php @@ -6,6 +6,7 @@ namespace Magento\DownloadableImportExport\Helper; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Driver\File; /** * Uploader helper for downloadable products @@ -82,6 +83,11 @@ public function getUploader($type, $parameters) $dirConfig = DirectoryList::getDefaultConfig(); $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; + // make media folder a primary folder for media in external storages + if (!is_a($this->mediaDirectory->getDriver(), File::class)) { + $dirAddon = DirectoryList::MEDIA; + } + if (!empty($parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR])) { $tmpPath = $parameters[\Magento\ImportExport\Model\Import::FIELD_NAME_IMG_FILE_DIR]; } else { @@ -113,7 +119,9 @@ public function getUploader($type, $parameters) */ public function isFileExist(string $fileName): bool { - return $this->mediaDirectory->isExist($this->fileUploader->getDestDir().$fileName); + $fileName = '/' . ltrim($fileName, '/'); + + return $this->mediaDirectory->isExist($this->fileUploader->getDestDir() . $fileName); } /** diff --git a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php index daa874e829e54..5dc98f2d150f4 100644 --- a/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php +++ b/app/code/Magento/DownloadableImportExport/Model/Export/RowCustomizer.php @@ -82,7 +82,9 @@ public function prepareData($collection, $productIds): void ->addAttributeToSelect('samples_title'); // set global scope during export $this->storeManager->setCurrentStore(Store::DEFAULT_STORE_ID); - foreach ($collection as $product) { + + while ($product = $productCollection->fetchItem()) { + /** @var $product \Magento\Catalog\Api\Data\ProductInterface */ $productLinks = $this->linkRepository->getLinksByProduct($product); $productSamples = $this->sampleRepository->getSamplesByProduct($product); $this->downloadableData[$product->getId()] = []; diff --git a/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php new file mode 100644 index 0000000000000..f36676c1a8749 --- /dev/null +++ b/app/code/Magento/DownloadableImportExport/Test/Unit/Model/Export/RowCustomizerTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableImportExport\Test\Unit\Model\Export; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Downloadable\Model\LinkRepository; +use Magento\Downloadable\Model\SampleRepository; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class RowCustomizerTest for export RowCustomizer + */ +class RowCustomizerTest extends TestCase +{ + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + /** + * @var LinkRepository|MockObject + */ + private $linkRepositoryMock; + + /** + * @var SampleRepository|MockObject + */ + private $sampleRepositoryMock; + + /** + * @var \Magento\DownloadableImportExport\Model\Export\RowCustomizer + */ + private $model; + + /** + * Setup + * + * @return void + */ + protected function setUp(): void + { + $this->storeManagerMock = $this->getMockBuilder(StoreManagerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->linkRepositoryMock = $this->getMockBuilder(LinkRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->sampleRepositoryMock = $this->getMockBuilder(SampleRepository::class) + ->disableOriginalConstructor() + ->getMock(); + + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + \Magento\DownloadableImportExport\Model\Export\RowCustomizer::class, + [ + 'storeManager' => $this->storeManagerMock, + 'linkRepository' => $this->linkRepositoryMock, + 'sampleRepository' => $this->sampleRepositoryMock, + ] + ); + } + + /** + * Test Prepare configurable data for export + */ + public function testPrepareData() + { + $product1 = $this->getMockBuilder(ProductInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $product1->expects($this->any()) + ->method('getId') + ->willReturn(1); + $product2 = $this->getMockBuilder(ProductInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $product2->expects($this->any()) + ->method('getId') + ->willReturn(2); + $collection = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + $collection->expects($this->atLeastOnce()) + ->method('fetchItem') + ->willReturn($product1, $product2); + + $collection->expects($this->exactly(2)) + ->method('addAttributeToFilter') + ->willReturnSelf(); + $collection->expects($this->exactly(2)) + ->method('addAttributeToSelect') + ->willReturnSelf(); + $this->linkRepositoryMock->expects($this->exactly(2)) + ->method('getLinksByProduct') + ->will($this->returnValue([])); + $this->sampleRepositoryMock->expects($this->exactly(2)) + ->method('getSamplesByProduct') + ->will($this->returnValue([])); + + $this->model->prepareData($collection, []); + } +} diff --git a/app/code/Magento/Eav/Model/AttributeRepository.php b/app/code/Magento/Eav/Model/AttributeRepository.php index bb307d5581121..ee2db9a9b6b35 100644 --- a/app/code/Magento/Eav/Model/AttributeRepository.php +++ b/app/code/Magento/Eav/Model/AttributeRepository.php @@ -88,7 +88,7 @@ public function save(\Magento\Eav\Api\Data\AttributeInterface $attribute) try { $this->eavResource->save($attribute); } catch (\Exception $e) { - throw new StateException(__("The attribute can't be saved.")); + throw new StateException(__("The attribute can't be saved."), $e); } return $attribute; } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 9fa001097df87..0edc63b10f9ab 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -90,6 +90,13 @@ class ProductDataMapper implements BatchDataMapperInterface */ private $filterableAttributeTypes; + /** + * @var string[] + */ + private $sortableCaseSensitiveAttributes = [ + 'name', + ]; + /** * @param Builder $builder * @param FieldMapperInterface $fieldMapper @@ -99,6 +106,7 @@ class ProductDataMapper implements BatchDataMapperInterface * @param array $excludedAttributes * @param array $sortableAttributesValuesToImplode * @param array $filterableAttributeTypes + * @param array $sortableCaseSensitiveAttributes */ public function __construct( Builder $builder, @@ -108,7 +116,8 @@ public function __construct( DataProvider $dataProvider, array $excludedAttributes = [], array $sortableAttributesValuesToImplode = [], - array $filterableAttributeTypes = [] + array $filterableAttributeTypes = [], + array $sortableCaseSensitiveAttributes = [] ) { $this->builder = $builder; $this->fieldMapper = $fieldMapper; @@ -122,6 +131,10 @@ public function __construct( $this->dataProvider = $dataProvider; $this->attributeOptionsCache = []; $this->filterableAttributeTypes = $filterableAttributeTypes; + $this->sortableCaseSensitiveAttributes = array_merge( + $this->sortableCaseSensitiveAttributes, + $sortableCaseSensitiveAttributes + ); } /** @@ -259,6 +272,9 @@ private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool * @param array $attributeValues * @param int $storeId * @return array + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ private function prepareAttributeValues( int $productId, @@ -298,6 +314,12 @@ function (string $valueId) { $attributeValues = [$productId => implode(' ', $attributeValues)]; } + if (in_array($attribute->getAttributeCode(), $this->sortableCaseSensitiveAttributes)) { + foreach ($attributeValues as $key => $attributeValue) { + $attributeValues[$key] = strtolower($attributeValue); + } + } + return $attributeValues; } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php index 89c98d29ae03e..75636991e7ee6 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/AttributeProvider.php @@ -10,6 +10,7 @@ use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeAdapter\DummyAttribute; +use Magento\Framework\ObjectManagerInterface; use Psr\Log\LoggerInterface; /** @@ -20,7 +21,7 @@ class AttributeProvider /** * Object Manager instance * - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; @@ -49,13 +50,13 @@ class AttributeProvider /** * Factory constructor * - * @param \Magento\Framework\ObjectManagerInterface $objectManager + * @param ObjectManagerInterface $objectManager * @param Config $eavConfig * @param LoggerInterface $logger * @param string $instanceName */ public function __construct( - \Magento\Framework\ObjectManagerInterface $objectManager, + ObjectManagerInterface $objectManager, Config $eavConfig, LoggerInterface $logger, $instanceName = AttributeAdapter::class @@ -87,4 +88,17 @@ public function getByAttributeCode(string $attributeCode): AttributeAdapter return $this->cachedPool[$attributeCode]; } + + /** + * Remove attribute from cache by code. + * + * @param string $attributeCode + * @return void + */ + public function removeAttributeCacheByCode(string $attributeCode): void + { + if (isset($this->cachedPool[$attributeCode])) { + unset($this->cachedPool[$attributeCode]); + } + } } diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php index 53f036a3b8e38..e15d91148b8ce 100644 --- a/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php +++ b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Indexer\IndexerHandler as ElasticsearchIndexerHandler; use Magento\Framework\Indexer\DimensionProviderInterface; use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; @@ -41,6 +42,11 @@ class Attribute */ private $indexerHandlerFactory; + /** + * @var AttributeProvider + */ + private $attributeProvider; + /** * @var bool */ @@ -56,17 +62,20 @@ class Attribute * @param Processor $indexerProcessor * @param DimensionProviderInterface $dimensionProvider * @param IndexerHandlerFactory $indexerHandlerFactory + * @param AttributeProvider $attributeProvider */ public function __construct( Config $config, Processor $indexerProcessor, DimensionProviderInterface $dimensionProvider, - IndexerHandlerFactory $indexerHandlerFactory + IndexerHandlerFactory $indexerHandlerFactory, + AttributeProvider $attributeProvider ) { $this->config = $config; $this->indexerProcessor = $indexerProcessor; $this->dimensionProvider = $dimensionProvider; $this->indexerHandlerFactory = $indexerHandlerFactory; + $this->attributeProvider = $attributeProvider; } /** @@ -82,6 +91,7 @@ public function afterSave( AttributeResourceModel $subject, AttributeResourceModel $result ): AttributeResourceModel { + $this->attributeProvider->removeAttributeCacheByCode($this->attributeCode); $indexer = $this->indexerProcessor->getIndexer(); if ($this->isNewObject && !$indexer->isScheduled() diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php index 2d0018ff81ee5..816565ff7a905 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/Css/ProcessorTest.php @@ -44,7 +44,7 @@ protected function setUp(): void public function testProcess() { - $url = 'http://magento.local/pub/static/'; + $url = 'http://magento.local/static/'; $locale = 'en_US'; $css = '@import url("{{base_url_path}}frontend/_view/{{locale}}/css/email.css");'; $expectedCss = '@import url("' . $url . 'frontend/_view/' . $locale . '/css/email.css");'; diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php b/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php new file mode 100644 index 0000000000000..bf1f6c1a5cf1a --- /dev/null +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ChangeParentStockStatus.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GroupedProduct\Model\Inventory; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\CatalogInventory\Api\Data\StockItemInterface; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; +use Magento\CatalogInventory\Api\StockItemRepositoryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\GroupedProduct\Model\Product\Type\Grouped; +use Magento\GroupedProduct\Model\ResourceModel\Product\Link; + +/** + * Change stock status of grouped product by child product id + */ +class ChangeParentStockStatus +{ + /** + * @var Grouped + */ + private $groupedType; + + /** + * @var StockItemRepositoryInterface + */ + private $stockItemRepository; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; + + /** + * @var StockItemCriteriaInterfaceFactory + */ + private $criteriaInterfaceFactory; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * Product metadata pool + * + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param Grouped $groupedType + * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory + * @param StockItemRepositoryInterface $stockItemRepository + * @param StockConfigurationInterface $stockConfiguration + * @param ResourceConnection $resource + * @param MetadataPool $metadataPool + */ + public function __construct( + Grouped $groupedType, + StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, + StockItemRepositoryInterface $stockItemRepository, + StockConfigurationInterface $stockConfiguration, + ResourceConnection $resource, + MetadataPool $metadataPool + ) { + $this->groupedType = $groupedType; + $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; + $this->stockConfiguration = $stockConfiguration; + $this->stockItemRepository = $stockItemRepository; + $this->resource = $resource; + $this->metadataPool = $metadataPool; + } + + /** + * Change stock item for parent product depending on children stock items + * + * @param int $productId + * @return void + */ + public function execute(int $productId): void + { + $parentIds = $this->getParentEntityIdsByChild($productId); + foreach ($parentIds as $productId) { + $this->changeParentStockStatus((int)$productId); + } + } + + /** + * Change stock status of grouped product + * + * @param int $productId + * @return void + */ + private function changeParentStockStatus(int $productId): void + { + $criteria = $this->criteriaInterfaceFactory->create(); + $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); + $criteria->setProductsFilter($productId); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + if (empty($allItems)) { + return; + } + $parentStockItem = array_shift($allItems); + $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); + $criteria->setProductsFilter($groupedChildrenIds); + $stockItemCollection = $this->stockItemRepository->getList($criteria); + $allItems = $stockItemCollection->getItems(); + + $groupedChildrenIsInStock = false; + + foreach ($allItems as $childItem) { + if ($childItem->getIsInStock() === true) { + $groupedChildrenIsInStock = true; + break; + } + } + + if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { + $parentStockItem->setIsInStock($groupedChildrenIsInStock); + $parentStockItem->setStockStatusChangedAuto(1); + $this->stockItemRepository->save($parentStockItem); + } + } + + /** + * Check is parent item should be updated + * + * @param StockItemInterface $parentStockItem + * @param bool $childrenIsInStock + * @return bool + */ + private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool + { + return $parentStockItem->getIsInStock() !== $childrenIsInStock && + ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); + } + + /** + * Retrieve parent ids array by child id + * + * @param int $childId + * @return array + */ + private function getParentEntityIdsByChild(int $childId): array + { + $select = $this->resource->getConnection() + ->select() + ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) + ->join( + ['e' => $this->resource->getTableName('catalog_product_entity')], + 'e.' . + $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', + ['e.entity_id'] + ) + ->where('l.linked_product_id = ?', $childId) + ->where( + 'link_type_id = ?', + Link::LINK_TYPE_GROUPED + ); + + return $this->resource->getConnection()->fetchCol($select); + } +} diff --git a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php index 0bb102f34dd2d..2d5113edd082e 100644 --- a/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php +++ b/app/code/Magento/GroupedProduct/Model/Inventory/ParentItemProcessor.php @@ -7,17 +7,8 @@ namespace Magento\GroupedProduct\Model\Inventory; -use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\GroupedProduct\Model\Product\Type\Grouped; use Magento\Catalog\Api\Data\ProductInterface as Product; -use Magento\CatalogInventory\Api\StockItemCriteriaInterfaceFactory; -use Magento\CatalogInventory\Api\StockItemRepositoryInterface; -use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Observer\ParentItemProcessorInterface; -use Magento\CatalogInventory\Api\Data\StockItemInterface; -use Magento\GroupedProduct\Model\ResourceModel\Product\Link; -use Magento\Framework\App\ResourceConnection; /** * Process parent stock item for grouped product @@ -25,59 +16,17 @@ class ParentItemProcessor implements ParentItemProcessorInterface { /** - * @var Grouped + * @var ChangeParentStockStatus */ - private $groupedType; + private $changeParentStockStatus; /** - * @var StockItemRepositoryInterface - */ - private $stockItemRepository; - - /** - * @var StockConfigurationInterface - */ - private $stockConfiguration; - - /** - * @var StockItemCriteriaInterfaceFactory - */ - private $criteriaInterfaceFactory; - - /** - * Product metadata pool - * - * @var MetadataPool - */ - private $metadataPool; - - /** - * @var ResourceConnection - */ - private $resource; - - /** - * @param Grouped $groupedType - * @param StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory - * @param StockItemRepositoryInterface $stockItemRepository - * @param StockConfigurationInterface $stockConfiguration - * @param ResourceConnection $resource - * @param MetadataPool $metadataPool + * @param ChangeParentStockStatus $changeParentStockStatus */ public function __construct( - Grouped $groupedType, - StockItemCriteriaInterfaceFactory $criteriaInterfaceFactory, - StockItemRepositoryInterface $stockItemRepository, - StockConfigurationInterface $stockConfiguration, - ResourceConnection $resource, - MetadataPool $metadataPool + ChangeParentStockStatus $changeParentStockStatus ) { - $this->groupedType = $groupedType; - $this->criteriaInterfaceFactory = $criteriaInterfaceFactory; - $this->stockConfiguration = $stockConfiguration; - $this->stockItemRepository = $stockItemRepository; - $this->resource = $resource; - $this->metadataPool = $metadataPool; + $this->changeParentStockStatus = $changeParentStockStatus; } /** @@ -88,86 +37,6 @@ public function __construct( */ public function process(Product $product) { - $parentIds = $this->getParentEntityIdsByChild($product->getId()); - foreach ($parentIds as $productId) { - $this->processStockForParent((int)$productId); - } - } - - /** - * Change stock item for parent product depending on children stock items - * - * @param int $productId - * @return void - */ - private function processStockForParent(int $productId) - { - $criteria = $this->criteriaInterfaceFactory->create(); - $criteria->setScopeFilter($this->stockConfiguration->getDefaultScopeId()); - $criteria->setProductsFilter($productId); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - $allItems = $stockItemCollection->getItems(); - if (empty($allItems)) { - return; - } - $parentStockItem = array_shift($allItems); - $groupedChildrenIds = $this->groupedType->getChildrenIds($productId); - $criteria->setProductsFilter($groupedChildrenIds); - $stockItemCollection = $this->stockItemRepository->getList($criteria); - $allItems = $stockItemCollection->getItems(); - - $groupedChildrenIsInStock = false; - - foreach ($allItems as $childItem) { - if ($childItem->getIsInStock() === true) { - $groupedChildrenIsInStock = true; - break; - } - } - - if ($this->isNeedToUpdateParent($parentStockItem, $groupedChildrenIsInStock)) { - $parentStockItem->setIsInStock($groupedChildrenIsInStock); - $parentStockItem->setStockStatusChangedAuto(1); - $this->stockItemRepository->save($parentStockItem); - } - } - - /** - * Check is parent item should be updated - * - * @param StockItemInterface $parentStockItem - * @param bool $childrenIsInStock - * @return bool - */ - private function isNeedToUpdateParent(StockItemInterface $parentStockItem, bool $childrenIsInStock): bool - { - return $parentStockItem->getIsInStock() !== $childrenIsInStock && - ($childrenIsInStock === false || $parentStockItem->getStockStatusChangedAuto()); - } - - /** - * Retrieve parent ids array by child id - * - * @param int $childId - * @return string[] - */ - private function getParentEntityIdsByChild($childId) - { - $select = $this->resource->getConnection() - ->select() - ->from(['l' => $this->resource->getTableName('catalog_product_link')], []) - ->join( - ['e' => $this->resource->getTableName('catalog_product_entity')], - 'e.' . - $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() . ' = l.product_id', - ['e.entity_id'] - ) - ->where('l.linked_product_id = ?', $childId) - ->where( - 'link_type_id = ?', - Link::LINK_TYPE_GROUPED - ); - - return $this->resource->getConnection()->fetchCol($select); + $this->changeParentStockStatus->execute((int)$product->getId()); } } diff --git a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php index 8eac8d0b0e163..b56e8657df722 100644 --- a/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php +++ b/app/code/Magento/GroupedProduct/Model/Product/Type/Grouped.php @@ -8,6 +8,7 @@ namespace Magento\GroupedProduct\Model\Product\Type; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\File\UploaderFactory; /** * Grouped product type model @@ -102,6 +103,7 @@ class Grouped extends \Magento\Catalog\Model\Product\Type\AbstractType * @param \Magento\Framework\App\State $appState * @param \Magento\Msrp\Helper\Data $msrpData * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param UploaderFactory|null $uploaderFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -119,7 +121,8 @@ public function __construct( \Magento\Catalog\Model\Product\Attribute\Source\Status $catalogProductStatus, \Magento\Framework\App\State $appState, \Magento\Msrp\Helper\Data $msrpData, - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + UploaderFactory $uploaderFactory = null ) { $this->productLinks = $catalogProductLink; $this->_storeManager = $storeManager; @@ -136,7 +139,8 @@ public function __construct( $coreRegistry, $logger, $productRepository, - $serializer + $serializer, + $uploaderFactory ); } diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml new file mode 100644 index 0000000000000..f39e18373893d --- /dev/null +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/UpdateStockStatusGroupedProductTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="UpdateStockStatusGroupedProductTest"> + <annotations> + <features value="GroupedProduct"/> + <stories value="Create/Edit grouped product in Admin"/> + <title value="Stock status of grouped product after changing quantity of child product should be changed"/> + <description value="Change stock of grouped product after changing quantity of child product"/> + <severity value="MAJOR"/> + <testCaseId value="MC-38057"/> + <useCaseId value="MC-37718"/> + <group value="GroupedProduct"/> + </annotations> + <before> + <!--Create simple and grouped product--> + <createData entity="SimpleProduct2" stepKey="createFirstSimpleProduct"/> + <createData entity="ApiGroupedProduct" stepKey="createGroupedProduct"/> + <createData entity="OneSimpleProductLink" stepKey="addProductOne"> + <requiredEntity createDataKey="createGroupedProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <!--Delete created data--> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createGroupedProduct" stepKey="deleteGroupedProduct"/> + <!--Admin logout--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + <!--1.Open product grid page and choose "Update attributes" and set product stock status to "Out of Stock"--> + <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="setProductToOutOfStock"> + <argument name="attributes" value="UpdateAttributeQtyAndStockToOutOfStock"/> + <argument name="product" value="$$createFirstSimpleProduct$$"/> + </actionGroup> + <!--2.Run cron for updating stock status of parent product--> + <magentoCron groups="index" stepKey="runCronIndex"/> + <!--3.Check stock status of grouped product. Stock status should be "Out of Stock"--> + <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductOutOfStock"> + <argument name="productId" value="$$createGroupedProduct.id$$"/> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <!--4.Open product grid page choose "Update attributes" and set product stock status to "In Stock"--> + <actionGroup ref="AdminMassUpdateProductQtyAndStockStatusActionGroup" stepKey="returnProductToInStock"> + <argument name="attributes" value="UpdateAttributeQtyAndStockToInStock"/> + <argument name="product" value="$$createFirstSimpleProduct$$"/> + </actionGroup> + <!--5.Check stock status of grouped product. Stock status should be "In Stock"--> + <actionGroup ref="AssertAdminProductStockStatusActionGroup" stepKey="checkProductInStock"> + <argument name="productId" value="$$createGroupedProduct.id$$"/> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/GroupedProduct/etc/di.xml b/app/code/Magento/GroupedProduct/etc/di.xml index d9534c6d3fe7d..924d2d1fc9669 100644 --- a/app/code/Magento/GroupedProduct/etc/di.xml +++ b/app/code/Magento/GroupedProduct/etc/di.xml @@ -112,4 +112,11 @@ </argument> </arguments> </type> + <type name="Magento\CatalogInventory\Plugin\MassUpdateProductAttribute"> + <arguments> + <argument name="parentItemProcessorPool" xsi:type="array"> + <item name="grouped" xsi:type="object"> Magento\GroupedProduct\Model\Inventory\ParentItemProcessor</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php index 4107e19860328..26ee257c42ff2 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Download.php @@ -67,13 +67,12 @@ public function execute() return $resultRedirect; } try { - $path = 'export/' . $fileName; - $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); - if ($directory->isFile($path)) { + $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_EXPORT); + if ($directory->isFile($fileName)) { return $this->fileFactory->create( - $path, - $directory->readFile($path), - DirectoryList::VAR_DIR + $fileName, + $directory->readFile($fileName), + DirectoryList::VAR_EXPORT ); } $this->messageManager->addErrorMessage(__('%1 is not a valid file', $fileName)); diff --git a/app/code/Magento/ImportExport/Model/Export/Consumer.php b/app/code/Magento/ImportExport/Model/Export/Consumer.php index 27019780269c4..955f96fe3de2e 100644 --- a/app/code/Magento/ImportExport/Model/Export/Consumer.php +++ b/app/code/Magento/ImportExport/Model/Export/Consumer.php @@ -70,8 +70,8 @@ public function process(ExportInfoInterface $exportInfo) try { $data = $this->exportManager->export($exportInfo); $fileName = $exportInfo->getFileName(); - $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); - $directory->writeFile('export/' . $fileName, $data); + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); + $directory->writeFile($fileName, $data); $this->notifier->addMajor( __('Your export file is ready'), diff --git a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php index 2b5af6ab5ca8d..71614bafd138e 100644 --- a/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php +++ b/app/code/Magento/ImportExport/Ui/DataProvider/ExportFileDataProvider.php @@ -13,6 +13,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Filesystem\Directory\WriteInterface; /** * Data provider for export grid. @@ -29,6 +30,11 @@ class ExportFileDataProvider extends DataProvider */ private $file; + /** + * @var WriteInterface + */ + private $directory; + /** * @var Filesystem */ @@ -48,6 +54,7 @@ class ExportFileDataProvider extends DataProvider * @param array $meta * @param array $data * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( string $name, @@ -78,6 +85,7 @@ public function __construct( ); $this->fileIO = $fileIO ?: ObjectManager::getInstance()->get(File::class); + $this->directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); } /** @@ -88,13 +96,12 @@ public function __construct( */ public function getData() { - $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); $emptyResponse = ['items' => [], 'totalRecords' => 0]; - if (!$this->file->isExists($directory->getAbsolutePath() . 'export/')) { + if (!$this->directory->isExist($this->directory->getAbsolutePath())) { return $emptyResponse; } - $files = $this->getExportFiles($directory->getAbsolutePath() . 'export/'); + $files = $this->getExportFiles($this->directory->getAbsolutePath()); if (empty($files)) { return $emptyResponse; } @@ -121,12 +128,15 @@ public function getData() */ private function getPathToExportFile($file): string { - $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); + $directory = $this->fileSystem->getDirectoryRead(DirectoryList::VAR_EXPORT); $delimiter = '/'; $cutPath = explode( $delimiter, - $directory->getAbsolutePath() . 'export' + $directory->getAbsolutePath() ); + // remove . from dirname if file path is not absolute in the file system but just a file name + $file['dirname'] = $file['dirname'] !== '.' ? $file['dirname'] : ''; + $filePath = explode( $delimiter, $file['dirname'] @@ -148,14 +158,14 @@ private function getPathToExportFile($file): string private function getExportFiles(string $directoryPath): array { $sortedFiles = []; - $files = $this->file->readDirectoryRecursively($directoryPath); + $files = $this->directory->getDriver()->readDirectoryRecursively($directoryPath); if (empty($files)) { return []; } foreach ($files as $filePath) { - if ($this->file->isFile($filePath)) { - //phpcs:ignore Magento2.Functions.DiscouragedFunction - $sortedFiles[filemtime($filePath)] = $filePath; + if ($this->directory->isFile($filePath)) { + $fileModificationTime = $this->directory->stat($filePath)['mtime']; + $sortedFiles[$fileModificationTime] = $filePath; } } //sort array elements using key value diff --git a/app/code/Magento/ImportExport/etc/adminhtml/di.xml b/app/code/Magento/ImportExport/etc/adminhtml/di.xml index 04ee726349123..7b124957d5f57 100644 --- a/app/code/Magento/ImportExport/etc/adminhtml/di.xml +++ b/app/code/Magento/ImportExport/etc/adminhtml/di.xml @@ -16,11 +16,13 @@ <argument name="serializer" xsi:type="object">Magento\Framework\Serialize\Serializer\Json</argument> </arguments> </type> + <!-- deprecated as file argument is not used anymore. Can be deleted in major release to avoid BIC.--> <type name="Magento\ImportExport\Controller\Adminhtml\Export\File\Delete"> <arguments> <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> </arguments> </type> + <!-- deprecated as file argument is not used anymore. Can be deleted in major release to avoid BIC.--> <type name="Magento\ImportExport\Ui\DataProvider\ExportFileDataProvider"> <arguments> <argument name="file" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> diff --git a/app/code/Magento/Indexer/Model/Indexer.php b/app/code/Magento/Indexer/Model/Indexer.php index 2821a46f29416..ac8b9590e58f4 100644 --- a/app/code/Magento/Indexer/Model/Indexer.php +++ b/app/code/Magento/Indexer/Model/Indexer.php @@ -13,6 +13,7 @@ use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Framework\Indexer\StateInterface; use Magento\Framework\Indexer\StructureFactory; +use Magento\Framework\Indexer\IndexerInterfaceFactory; /** * Indexer model. @@ -61,6 +62,16 @@ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface */ protected $indexersFactory; + /** + * @var WorkingStateProvider + */ + private $workingStateProvider; + + /** + * @var IndexerInterfaceFactory + */ + private $indexerFactory; + /** * @param ConfigInterface $config * @param ActionFactory $actionFactory @@ -68,6 +79,8 @@ class Indexer extends \Magento\Framework\DataObject implements IndexerInterface * @param \Magento\Framework\Mview\ViewInterface $view * @param Indexer\StateFactory $stateFactory * @param Indexer\CollectionFactory $indexersFactory + * @param WorkingStateProvider $workingStateProvider + * @param IndexerInterfaceFactory $indexerFactory * @param array $data */ public function __construct( @@ -77,6 +90,8 @@ public function __construct( \Magento\Framework\Mview\ViewInterface $view, Indexer\StateFactory $stateFactory, Indexer\CollectionFactory $indexersFactory, + WorkingStateProvider $workingStateProvider, + IndexerInterfaceFactory $indexerFactory, array $data = [] ) { $this->config = $config; @@ -85,6 +100,8 @@ public function __construct( $this->view = $view; $this->stateFactory = $stateFactory; $this->indexersFactory = $indexersFactory; + $this->workingStateProvider = $workingStateProvider; + $this->indexerFactory = $indexerFactory; parent::__construct($data); } @@ -405,10 +422,20 @@ protected function getStructureInstance() */ public function reindexAll() { - if ($this->getState()->getStatus() != StateInterface::STATUS_WORKING) { + if (!$this->workingStateProvider->isWorking($this->getId())) { $state = $this->getState(); $state->setStatus(StateInterface::STATUS_WORKING); $state->save(); + + $sharedIndexers = []; + $indexerConfig = $this->config->getIndexer($this->getId()); + if ($indexerConfig['shared_index'] !== null) { + $sharedIndexers = $this->getSharedIndexers($indexerConfig['shared_index']); + } + if (!empty($sharedIndexers)) { + $this->suspendSharedViews($sharedIndexers); + } + if ($this->getView()->isEnabled()) { $this->getView()->suspend(); } @@ -416,16 +443,73 @@ public function reindexAll() $this->getActionInstance()->executeFull(); $state->setStatus(StateInterface::STATUS_VALID); $state->save(); + if (!empty($sharedIndexers)) { + $this->resumeSharedViews($sharedIndexers); + } $this->getView()->resume(); } catch (\Throwable $exception) { $state->setStatus(StateInterface::STATUS_INVALID); $state->save(); + if (!empty($sharedIndexers)) { + $this->resumeSharedViews($sharedIndexers); + } $this->getView()->resume(); throw $exception; } } } + /** + * Get indexer ids that uses same index + * + * @param string $sharedIndex + * @return array + */ + private function getSharedIndexers(string $sharedIndex) : array + { + $result = []; + foreach (array_keys($this->config->getIndexers()) as $indexerId) { + if ($indexerId === $this->getId()) { + continue; + } + $indexerConfig = $this->config->getIndexer($indexerId); + if ($indexerConfig['shared_index'] === $sharedIndex) { + $indexer = $this->indexerFactory->create(); + $indexer->load($indexerId); + $result[] = $indexer; + } + } + return $result; + } + + /** + * Suspend views of shared indexers + * + * @param array $sharedIndexers + * @return void + */ + private function suspendSharedViews(array $sharedIndexers) : void + { + foreach ($sharedIndexers as $indexer) { + if ($indexer->getView()->isEnabled()) { + $indexer->getView()->suspend(); + } + } + } + + /** + * Suspend views of shared indexers + * + * @param array $sharedIndexers + * @return void + */ + private function resumeSharedViews(array $sharedIndexers) : void + { + foreach ($sharedIndexers as $indexer) { + $indexer->getView()->resume(); + } + } + /** * Regenerate one row in index by ID * diff --git a/app/code/Magento/Indexer/Model/WorkingStateProvider.php b/app/code/Magento/Indexer/Model/WorkingStateProvider.php new file mode 100644 index 0000000000000..d77c1b67ecfd7 --- /dev/null +++ b/app/code/Magento/Indexer/Model/WorkingStateProvider.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Indexer\Model; + +use Magento\Indexer\Model\Indexer\StateFactory; +use Magento\Framework\Indexer\StateInterface; + +/** + * Provide actual working status of the indexer + */ +class WorkingStateProvider +{ + /** + * @var StateFactory + */ + private $stateFactory; + + /** + * @param StateFactory $stateFactory + */ + public function __construct( + StateFactory $stateFactory + ) { + $this->stateFactory = $stateFactory; + } + + /** + * Execute user functions + * + * @param string $indexerId + * @return bool + */ + public function isWorking(string $indexerId) : bool + { + $state = $this->stateFactory->create(); + $state->loadByIndexer($indexerId); + + return $state->getStatus() === StateInterface::STATUS_WORKING; + } +} diff --git a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php index 662856e2187d5..bcdfbea78b0b3 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/IndexerTest.php @@ -12,11 +12,13 @@ use Magento\Framework\Indexer\ConfigInterface; use Magento\Framework\Indexer\StateInterface; use Magento\Framework\Indexer\StructureFactory; +use Magento\Framework\Indexer\IndexerInterfaceFactory; use Magento\Framework\Mview\ViewInterface; use Magento\Indexer\Model\Indexer; use Magento\Indexer\Model\Indexer\CollectionFactory; use Magento\Indexer\Model\Indexer\State; use Magento\Indexer\Model\Indexer\StateFactory; +use Magento\Indexer\Model\WorkingStateProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -55,8 +57,21 @@ class IndexerTest extends TestCase */ protected $indexFactoryMock; + /** + * @var WorkingStateProvider|MockObject + */ + private $workingStateProvider; + + /** + * @var IndexerInterfaceFactory|MockObject + */ + private $indexerFactoryMock; + protected function setUp(): void { + $this->workingStateProvider = $this->getMockBuilder(WorkingStateProvider::class) + ->disableOriginalConstructor() + ->getMock(); $this->configMock = $this->getMockForAbstractClass( ConfigInterface::class, [], @@ -70,6 +85,10 @@ protected function setUp(): void ActionFactory::class, ['create'] ); + $this->indexerFactoryMock = $this->createPartialMock( + IndexerInterfaceFactory::class, + ['create'] + ); $this->viewMock = $this->getMockForAbstractClass( ViewInterface::class, [], @@ -99,7 +118,9 @@ protected function setUp(): void $structureFactory, $this->viewMock, $this->stateFactoryMock, - $this->indexFactoryMock + $this->indexFactoryMock, + $this->workingStateProvider, + $this->indexerFactoryMock ); } @@ -211,7 +232,7 @@ public function testReindexAll() $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); - $stateMock->expects($this->once())->method('getStatus')->willReturn('idle'); + $stateMock->expects($this->any())->method('getStatus')->willReturn('idle'); $stateMock->expects($this->exactly(2))->method('save')->willReturnSelf(); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); @@ -251,7 +272,7 @@ public function testReindexAllWithException() $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); - $stateMock->expects($this->once())->method('getStatus')->willReturn('idle'); + $stateMock->expects($this->any())->method('getStatus')->willReturn('idle'); $stateMock->expects($this->exactly(2))->method('save')->willReturnSelf(); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); @@ -296,7 +317,7 @@ public function testReindexAllWithError() $stateMock->expects($this->never())->method('setIndexerId'); $stateMock->expects($this->once())->method('getId')->willReturn(1); $stateMock->expects($this->exactly(2))->method('setStatus')->willReturnSelf(); - $stateMock->expects($this->once())->method('getStatus')->willReturn('idle'); + $stateMock->expects($this->any())->method('getStatus')->willReturn('idle'); $stateMock->expects($this->exactly(2))->method('save')->willReturnSelf(); $this->stateFactoryMock->expects($this->once())->method('create')->willReturn($stateMock); @@ -336,7 +357,8 @@ protected function getIndexerData() 'view_id' => 'view_test', 'action_class' => 'Some\Class\Name', 'title' => 'Indexer public name', - 'description' => 'Indexer public description' + 'description' => 'Indexer public description', + 'shared_index' => null ]; } @@ -346,7 +368,7 @@ protected function getIndexerData() protected function loadIndexer($indexId) { $this->configMock->expects( - $this->once() + $this->any() )->method( 'getIndexer' )->with( diff --git a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php index 9f9b4c2157bb7..bbb74812d99a3 100644 --- a/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php +++ b/app/code/Magento/Indexer/Test/Unit/Model/ProcessorTest.php @@ -73,11 +73,19 @@ protected function setUp(): void '', false ); + + $indexerRegistryMock = $this->getIndexRegistryMock([]); + $makeSharedValidMock = new MakeSharedIndexValid( + $this->configMock, + $indexerRegistryMock + ); + $this->model = new Processor( $this->configMock, $this->indexerFactoryMock, $this->indexersFactoryMock, - $this->viewProcessorMock + $this->viewProcessorMock, + $makeSharedValidMock ); } diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml new file mode 100644 index 0000000000000..2beb0ad12e5d0 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest"> + <annotations> + <features value="MediaGalleryCatalogUi"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <title value="User Edits Category from Category grid"/> + <description value="Edit Category from Media Gallery Category Grid"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/5034526"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetGridFilters"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"> + <argument name="categoryName" value="$category.name$"/> + </actionGroup> + <actionGroup ref="AdminAssertCategoryPageTitleActionGroup" stepKey="assertCategoryByName"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml index 2a606d8ab6a9e..739b25d1ce0ed 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCatalogUiEditCategoryGridPageTest"> + <test name="AdminMediaGalleryCatalogUiEditCategoryGridPageTest" deprecated="Use AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest instead"> <annotations> <features value="AdminMediaGalleryCategoryGrid"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> - <title value="User Edits Category from Category grid"/> + <title value="DEPRECATED. User Edits Category from Category grid"/> <stories value="Story 58: User sees entities where asset is used in" /> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/5034526"/> <description value="Edit Category from Media Gallery Category Grid"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml index f9ffda43d2547..a3f1bd7c01136 100644 --- a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest"> + <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest" deprecated="Use AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest instead"> <annotations> <features value="AdminMediaGalleryCategoryGrid"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> - <title value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <title value="DEPRECATED. User can open each entity the asset is associated with in a separate tab to manage association"/> <stories value="Story 58: User sees entities where asset is used in" /> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> <description value="User can open each entity the asset is associated with in a separate tab to manage association"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest instead</issueId> + </skip> </annotations> <before> <createData entity="SimpleSubCategory" stepKey="category"/> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml new file mode 100644 index 0000000000000..8e197b740bb11 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkedCategoryGridTest"> + <annotations> + <features value="MediaGalleryCatalogUi"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <title value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <description value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + </before> + + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openMediaGalleryCategoryGridPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="GoToAdminCategoryPageByIdActionGroup" stepKey="openCategoryPage"> + <argument name="id" value="$category.id$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear" /> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategory"/> + + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderToVerifyLink"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectCategoryImageFolder"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInCategories"> + <argument name="entityName" value="Categories"/> + </actionGroup> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterApplied"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageNumberOfRecordsActionGroup" stepKey="assertOneRecordInGrid"> + <argument name="numberOfRecords" value="1 records found"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageImageColumnActionGroup" stepKey="assertCategoryGridPageImageColumn"> + <argument name="file" value="{{UpdatedImageDetails.file}}"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageDetailsActionGroup" stepKey="assertCategoryInGrid"> + <argument name="category" value="$category$"/> + </actionGroup> + <actionGroup ref="AssertAdminCategoryGridPageProductsInMenuEnabledColumnsActionGroup" stepKey="assertCategoryGridPageProductsInMenuEnabledColumns"/> + + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetCategoriesGridFilters"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setAssetFilter"> + <argument name="filterName" value="Asset"/> + <argument name="optionName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AssertAdminMediaGalleryAssetFilterPlaceHolderActionGroup" stepKey="assertFilterAppliedAfterUrlFilterApplier"> + <argument name="filterPlaceholder" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="openCategoryImageFolder"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetailsToVerifyEmptyUsedIn"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUsedInSectionNotDisplayedActionGroup" stepKey="assertThereIsNoUsedInSection"/> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeDetails"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryRenditions/etc/config.xml b/app/code/Magento/MediaGalleryRenditions/etc/config.xml index 6b4f2351b8b10..871571a049875 100644 --- a/app/code/Magento/MediaGalleryRenditions/etc/config.xml +++ b/app/code/Magento/MediaGalleryRenditions/etc/config.xml @@ -13,6 +13,11 @@ <width>1000</width> <height>1000</height> </media_gallery_renditions> + <media_storage_configuration> + <allowed_resources> + <renditions_folder>.renditions</renditions_folder> + </allowed_resources> + </media_storage_configuration> </system> </default> </config> diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php index b4c360c3e0538..19c2569695d56 100644 --- a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -10,7 +10,6 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Driver\File; use Magento\MediaGalleryApi\Api\Data\AssetInterface; use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; @@ -74,19 +73,37 @@ public function __construct( public function execute(string $path): AssetInterface { $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); - $file = $this->getFileInfo->execute($absolutePath); - [$width, $height] = getimagesize($absolutePath); + $driver = $this->getMediaDirectory()->getDriver(); + + if ($driver instanceof Filesystem\ExtendedDriverInterface) { + $meta = $driver->getMetadata($absolutePath); + } else { + /** + * SPL file info is not compatible with remote storages and must not be used. + */ + $file = $this->getFileInfo->execute($absolutePath); + [$width, $height] = getimagesize($absolutePath); + $meta = [ + 'size' => $file->getSize(), + 'extension' => $file->getExtension(), + 'basename' => $file->getBasename(), + 'extra' => [ + 'image-width' => $width, + 'image-height' => $height + ] + ]; + } return $this->assetFactory->create( [ 'id' => null, 'path' => $path, - 'title' => $file->getBasename(), - 'width' => $width, - 'height' => $height, + 'title' => $meta['basename'], + 'width' => $meta['extra']['image-width'], + 'height' => $meta['extra']['image-height'], 'hash' => $this->getHash($path), - 'size' => $file->getSize(), - 'contentType' => 'image/' . $file->getExtension(), + 'size' => $meta['size'], + 'contentType' => 'image/' . $meta['extension'], 'source' => 'Local' ] ); @@ -105,12 +122,12 @@ private function getHash(string $path): string } /** - * Retrieve media directory instance with read access + * Retrieve media directory instance with write access * - * @return ReadInterface + * @return Filesystem\Directory\WriteInterface */ - private function getMediaDirectory(): ReadInterface + private function getMediaDirectory(): Filesystem\Directory\WriteInterface { - return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); } } diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml index 931da0ee06fef..5f7ab2d2d008f 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml @@ -13,6 +13,6 @@ <description>Edit image from the View Details panel</description> </annotations> <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.edit}}" stepKey="editImage"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryEditDetailsSection.modalTitle}}" stepKey="waitForLoadingMaskToDisappear"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml index b0bed4563003e..351367055e62b 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml @@ -16,6 +16,7 @@ <element name="addNewKeyword" type="input" selector="[data-ui-id='add-keyword']"/> <element name="removeSelectedKeyword" type="button" selector="//span[contains(text(), '{{keyword}}')]/following-sibling::button[@data-action='remove-selected-item']" parameterized="true"/> <element name="cancel" type="button" selector="#image-details-action-cancel"/> - <element name="save" type="button" selector="#image-details-action-save"/> + <element name="save" type="button" selector="#image-details-action-save" timeout="30"/> + <element name="modalTitle" type="text" selector="//aside[contains(@class, 'media-gallery-edit-image-details') and contains(@class, '_show')]//h1[contains(., 'Edit Image')]"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml index 1a8f6f553d4ce..17c3e82144d6f 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -11,8 +11,8 @@ <element name="openContextMenu" type="button" selector=".three-dots"/> <element name="contextMenuItem" type="block" selector="//div[@class='media-gallery-image']//ul[@class='action-menu _active']//li//a[@class='action-menu-item']"/> <element name="viewDetails" type="button" selector="//ul[@class='action-menu _active']//a[text()='View Details']" timeout="30"/> - <element name="delete" type="button" selector="[data-ui-id='action-delete']"/> - <element name="edit" type="button" selector="[data-ui-id='action-edit']"/> + <element name="delete" type="button" selector="//ul[@class='action-menu _active']//a[text()='Delete']"/> + <element name="edit" type="button" selector="//ul[@class='action-menu _active']//a[text()='Edit']"/> <element name="imageInGrid" type="button" selector="//li[@data-ui-id='title'and text()='{{imageTitle}}']/parent::*/parent::*/parent::div//img[@class='media-gallery-image-column']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml new file mode 100644 index 0000000000000..91a17a7c1167c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsFromGridTest.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryEditImageDetailsFromGridTest"> + <annotations> + <features value="MediaGalleryUi"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <title value="User edits image meta data in media gallery"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + </before> + + <after> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml index 960443998d010..34c3159ab769e 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminMediaGalleryEditImageDetailsTest"> + <test name="AdminMediaGalleryEditImageDetailsTest" deprecated="Use AdminMediaGalleryEditImageDetailsFromGridTest instead"> <annotations> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> - <title value="User edits image meta data in media gallery"/> + <title value="DEPRECATED. User edits image meta data in media gallery"/> <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> <description value="User edits image meta data in Standalone Media Gallery"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminMediaGalleryEditImageDetailsFromGridTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml new file mode 100644 index 0000000000000..250b42c5510a7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest"> + <annotations> + <features value="MediaGalleryUi"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in standalone media gallery"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + </before> + + <after> + <actionGroup ref="AdminEnhancedMediaGalleryDeletedAllImagesActionGroup" stepKey="deleteAllMediaGalleryImages"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="resetMediaGalleryGridFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <generateDate date="now" format="s" stepKey="secondsFromMinuteStart"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="clickViewDetails"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryUploadedImageDateTimeEqualsActionGroup" stepKey="verifyCreatedAndUpdatedAtDate" /> + + <executeJS function="return 60 - {$secondsFromMinuteStart} + 5" stepKey="calcWaitPeriod"/> + <wait time="$calcWaitPeriod" stepKey="waitTillEndOfAMinute"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageCreatedAtNotEqualsUpdatedAtTimeActionGroup" stepKey="assertUpdatedAtTimeChanged" /> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml index 58c6f32b8d72f..039e9212945e2 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -7,16 +7,19 @@ --> <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminStandaloneMediaGalleryEditImageDetailsTest"> + <test name="AdminStandaloneMediaGalleryEditImageDetailsTest" deprecated="Use AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest instead"> <annotations> <features value="MediaGallery"/> <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> - <title value="User edits image meta data in standalone media gallery"/> + <title value="DEPRECATED. User edits image meta data in standalone media gallery"/> <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> <description value="User edits image meta data in Standalone Media Gallery"/> <severity value="CRITICAL"/> <group value="media_gallery_ui"/> + <skip> + <issueId value="DEPRECATED">Use AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest instead</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php index fc8a0756a7b55..4946cd1092ff7 100644 --- a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php @@ -129,7 +129,7 @@ public function executeDataProvider(): array [ 'targetFolder' => 'media/catalog', 'type' => 'image', - 'absolutePath' => 'root/pub/media/catalog/test-image.jpeg' + 'absolutePath' => 'root/media/catalog/test-image.jpeg' ] ]; } diff --git a/app/code/Magento/MediaStorage/App/Media.php b/app/code/Magento/MediaStorage/App/Media.php index ca5ff458c52e9..34c20aab40bcb 100644 --- a/app/code/Magento/MediaStorage/App/Media.php +++ b/app/code/Magento/MediaStorage/App/Media.php @@ -11,6 +11,7 @@ use Closure; use Exception; use LogicException; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App; use Magento\Framework\App\Area; @@ -18,6 +19,7 @@ use Magento\Framework\App\ResponseInterface; use Magento\Framework\App\State; use Magento\Framework\AppInterface; +use Magento\Framework\Exception\NotFoundException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\Driver\File; @@ -103,6 +105,11 @@ class Media implements AppInterface */ private $imageResize; + /** + * @var string + */ + private $mediaUrlFormat; + /** * @param ConfigFactory $configFactory * @param SynchronizationFactory $syncFactory @@ -116,6 +123,8 @@ class Media implements AppInterface * @param State $state * @param ImageResize $imageResize * @param File $file + * @param CatalogMediaConfig $catalogMediaConfig + * @throws \Magento\Framework\Exception\FileSystemException * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -130,12 +139,19 @@ public function __construct( PlaceholderFactory $placeholderFactory, State $state, ImageResize $imageResize, - File $file + File $file, + CatalogMediaConfig $catalogMediaConfig = null ) { $this->response = $response; $this->isAllowed = $isAllowed; - $this->directoryPub = $filesystem->getDirectoryWrite(DirectoryList::PUB); - $this->directoryMedia = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->directoryPub = $filesystem->getDirectoryWrite( + DirectoryList::PUB, + Filesystem\DriverPool::FILE + ); + $this->directoryMedia = $filesystem->getDirectoryWrite( + DirectoryList::MEDIA, + Filesystem\DriverPool::FILE + ); $mediaDirectory = trim($mediaDirectory); if (!empty($mediaDirectory)) { // phpcs:ignore Magento2.Functions.DiscouragedFunction @@ -148,6 +164,9 @@ public function __construct( $this->placeholderFactory = $placeholderFactory; $this->appState = $state; $this->imageResize = $imageResize; + + $catalogMediaConfig = $catalogMediaConfig ?: App\ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaUrlFormat = $catalogMediaConfig->getMediaUrlFormat(); } /** @@ -174,10 +193,8 @@ public function launch(): ResponseInterface } try { - /** @var Synchronization $sync */ - $sync = $this->syncFactory->create(['directory' => $this->directoryPub]); - $sync->synchronize($this->relativeFileName); - $this->imageResize->resizeFromImageName($this->getOriginalImage($this->relativeFileName)); + $this->createLocalCopy(); + if ($this->directoryPub->isReadable($this->relativeFileName)) { $this->response->setFilePath($this->directoryPub->getAbsolutePath($this->relativeFileName)); } else { @@ -190,6 +207,25 @@ public function launch(): ResponseInterface return $this->response; } + /** + * Create local copy of file and perform resizing if necessary. + * + * @throws NotFoundException + */ + private function createLocalCopy(): void + { + $this->syncFactory->create(['directory' => $this->directoryPub]) + ->synchronize($this->relativeFileName); + + if ($this->directoryPub->isReadable($this->relativeFileName)) { + return; + } + + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $this->imageResize->resizeFromImageName($this->getOriginalImage($this->relativeFileName)); + } + } + /** * Check if media directory changed * @@ -219,7 +255,7 @@ private function setPlaceholderImage(): void */ private function getOriginalImage(string $resizedImagePath): string { - return preg_replace('|^.*((?:/[^/]+){3})$|', '$1', $resizedImagePath); + return preg_replace('|^.*?((?:/([^/])/([^/])/\2\3)?/?[^/]+$)|', '$1', $resizedImagePath); } /** diff --git a/app/code/Magento/MediaStorage/Model/File/Storage.php b/app/code/Magento/MediaStorage/Model/File/Storage.php index 861f2d82c7e7b..f5ebda4a8d55c 100644 --- a/app/code/Magento/MediaStorage/Model/File/Storage.php +++ b/app/code/Magento/MediaStorage/Model/File/Storage.php @@ -83,12 +83,17 @@ class Storage extends AbstractModel protected $_databaseFactory; /** - * Filesystem instance - * * @var Filesystem + * + * @deprecated */ protected $filesystem; + /** + * @var Filesystem\Directory\ReadInterface + */ + private $localMediaDirectory; + /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -125,6 +130,11 @@ public function __construct( $this->_fileFactory = $fileFactory; $this->_databaseFactory = $databaseFactory; $this->filesystem = $filesystem; + + $this->localMediaDirectory = $filesystem->getDirectoryRead( + DirectoryList::MEDIA, + Filesystem\DriverPool::FILE + ); parent::__construct($context, $registry, $resource, $resourceCollection, $data); } @@ -286,7 +296,7 @@ public function synchronize($storage) public function getScriptConfig() { $config = []; - $config['media_directory'] = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath(); + $config['media_directory'] = $this->localMediaDirectory->getAbsolutePath(); $allowedResources = $this->_coreConfig->getValue(self::XML_PATH_MEDIA_RESOURCE_WHITELIST, 'default'); foreach ($allowedResources as $allowedResource) { diff --git a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php index 7f70f5ba48e5c..068732a7225cd 100644 --- a/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php +++ b/app/code/Magento/MediaStorage/Test/Unit/App/MediaTest.php @@ -10,21 +10,23 @@ use Exception; use LogicException; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\View\Asset\Placeholder; use Magento\Catalog\Model\View\Asset\PlaceholderFactory; use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\State; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\Read; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Filesystem\DriverPool; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\MediaStorage\App\Media; use Magento\MediaStorage\Model\File\Storage\Config; use Magento\MediaStorage\Model\File\Storage\ConfigFactory; use Magento\MediaStorage\Model\File\Storage\Response; use Magento\MediaStorage\Model\File\Storage\Synchronization; use Magento\MediaStorage\Model\File\Storage\SynchronizationFactory; +use Magento\MediaStorage\Service\ImageResize; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -80,122 +82,106 @@ class MediaTest extends TestCase private $directoryMediaMock; /** - * @var \Magento\Framework\Filesystem\Directory\Read|MockObject + * @var Read|MockObject */ private $directoryPubMock; + /** + * @inheritDoc + */ protected function setUp(): void { $this->configMock = $this->createMock(Config::class); $this->sync = $this->createMock(Synchronization::class); - $this->configFactoryMock = $this->createPartialMock( - ConfigFactory::class, - ['create'] - ); - $this->configFactoryMock->expects($this->any()) - ->method('create') + $this->configFactoryMock = $this->createPartialMock(ConfigFactory::class, ['create']); + $this->responseMock = $this->createMock(Response::class); + $this->syncFactoryMock = $this->createPartialMock(SynchronizationFactory::class, ['create']); + $this->filesystemMock = $this->createMock(Filesystem::class); + $this->directoryPubMock = $this->getMockForAbstractClass(WriteInterface::class); + $this->directoryMediaMock = $this->getMockForAbstractClass(WriteInterface::class); + + $this->configFactoryMock->method('create') ->willReturn($this->configMock); - $this->syncFactoryMock = $this->createPartialMock( - SynchronizationFactory::class, - ['create'] - ); - $this->syncFactoryMock->expects($this->any()) - ->method('create') + $this->syncFactoryMock->method('create') ->willReturn($this->sync); - - $this->filesystemMock = $this->createMock(Filesystem::class); - $this->directoryPubMock = $this->getMockForAbstractClass( - WriteInterface::class, - [], - '', - false, - true, - true, - ['isReadable', 'getAbsolutePath'] - ); - $this->directoryMediaMock = $this->getMockForAbstractClass( - WriteInterface::class, - [], - '', - false, - true, - true, - ['getAbsolutePath'] - ); - $this->filesystemMock->expects($this->any()) - ->method('getDirectoryWrite') + $this->filesystemMock->method('getDirectoryWrite') ->willReturnMap([ [DirectoryList::PUB, DriverPool::FILE, $this->directoryPubMock], [DirectoryList::MEDIA, DriverPool::FILE, $this->directoryMediaMock], ]); - - $this->responseMock = $this->createMock(Response::class); } - protected function tearDown(): void + public function testProcessRequestCreatesConfigFileMediaDirectoryIsNotProvided(): void { - unset($this->mediaModel); - } - - public function testProcessRequestCreatesConfigFileMediaDirectoryIsNotProvided() - { - $this->mediaModel = $this->getMediaModel(); - $filePath = '/absolute/path/to/test/file.png'; - $this->directoryMediaMock->expects($this->once()) + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::once()) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); - $this->configMock->expects($this->once())->method('save'); - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryPubMock->expects($this->once()) + $this->configMock->expects(self::once()) + ->method('save'); + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(true); - $this->responseMock->expects($this->once())->method('setFilePath')->with($filePath); - $this->mediaModel->launch(); + $this->responseMock->expects(self::once()) + ->method('setFilePath') + ->with($filePath); + + $this->createMediaModel()->launch(); } - public function testProcessRequestReturnsFileIfItsProperlySynchronized() + public function testProcessRequestReturnsFileIfItsProperlySynchronized(): void { - $this->mediaModel = $this->getMediaModel(); + $this->mediaModel = $this->createMediaModel(); $filePath = '/absolute/path/to/test/file.png'; - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryMediaMock->expects($this->once()) + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(true); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::once()) ->method('getAbsolutePath') ->with(self::RELATIVE_FILE_PATH) ->willReturn($filePath); - $this->responseMock->expects($this->once())->method('setFilePath')->with($filePath); - $this->assertSame($this->responseMock, $this->mediaModel->launch()); + $this->responseMock->expects(self::once()) + ->method('setFilePath') + ->with($filePath); + + self::assertSame($this->responseMock, $this->mediaModel->launch()); } - public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized() + public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized(): void { - $this->mediaModel = $this->getMediaModel(); + $this->mediaModel = $this->createMediaModel(); - $this->sync->expects($this->once())->method('synchronize')->with(self::RELATIVE_FILE_PATH); - $this->directoryMediaMock->expects($this->once()) + $this->sync->expects(self::once()) + ->method('synchronize') + ->with(self::RELATIVE_FILE_PATH); + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->directoryPubMock->expects($this->once()) + $this->directoryPubMock->expects(self::exactly(2)) ->method('isReadable') ->with(self::RELATIVE_FILE_PATH) ->willReturn(false); - $this->assertSame($this->responseMock, $this->mediaModel->launch()); + + self::assertSame($this->responseMock, $this->mediaModel->launch()); } /** @@ -204,7 +190,7 @@ public function testProcessRequestReturnsNotFoundIfFileIsNotSynchronized() * * @dataProvider catchExceptionDataProvider */ - public function testCatchException($isDeveloper, $setBodyCalls) + public function testCatchException(bool $isDeveloper, int $setBodyCalls): void { /** @var Bootstrap|MockObject $bootstrap */ $bootstrap = $this->createMock(Bootstrap::class); @@ -212,41 +198,39 @@ public function testCatchException($isDeveloper, $setBodyCalls) /** @var Exception|MockObject $exception */ $exception = $this->createMock(Exception::class); - $this->responseMock->expects($this->once()) + $this->responseMock->expects(self::once()) ->method('setHttpResponseCode') ->with(404); - $bootstrap->expects($this->once()) + $bootstrap->expects(self::once()) ->method('isDeveloperMode') ->willReturn($isDeveloper); - $this->responseMock->expects($this->exactly($setBodyCalls)) + $this->responseMock->expects(self::exactly($setBodyCalls)) ->method('setBody'); - $this->responseMock->expects($this->once()) + $this->responseMock->expects(self::once()) ->method('sendResponse'); - $this->mediaModel = $this->getMediaModel(); - - $this->mediaModel->catchException($bootstrap, $exception); + $this->createMediaModel()->catchException($bootstrap, $exception); } - public function testExceptionWhenIsAllowedReturnsFalse() + public function testExceptionWhenIsAllowedReturnsFalse(): void { - $this->mediaModel = $this->getMediaModel(false); - $this->directoryMediaMock->expects($this->once()) + $this->directoryMediaMock->expects(self::once()) ->method('getAbsolutePath') ->with(null) ->willReturn(self::MEDIA_DIRECTORY); - $this->configMock->expects($this->once())->method('save'); + $this->configMock->expects(self::once()) + ->method('save'); $this->expectException(LogicException::class); $this->expectExceptionMessage('The path is not allowed: ' . self::RELATIVE_FILE_PATH); - $this->mediaModel->launch(); + $this->createMediaModel(false)->launch(); } /** * @return array */ - public function catchExceptionDataProvider() + public function catchExceptionDataProvider(): array { return [ 'default mode' => [false, 0], @@ -260,35 +244,30 @@ public function catchExceptionDataProvider() * @param bool $isAllowed * @return Media */ - protected function getMediaModel(bool $isAllowed = true): Media + protected function createMediaModel(bool $isAllowed = true): Media { - $objectManager = new ObjectManager($this); - $isAllowedCallback = function () use ($isAllowed) { return $isAllowed; }; - /** @var Media $mediaClass */ - $mediaClass = $objectManager->getObject( - Media::class, - [ - 'configFactory' => $this->configFactoryMock, - 'syncFactory' => $this->syncFactoryMock, - 'response' => $this->responseMock, - 'isAllowed' => $isAllowedCallback, - 'mediaDirectory' => false, - 'configCacheFile' => self::CACHE_FILE_PATH, - 'relativeFileName' => self::RELATIVE_FILE_PATH, - 'filesystem' => $this->filesystemMock, - 'placeholderFactory' => $this->createConfiguredMock( - PlaceholderFactory::class, - [ - 'create' => $this->createMock(Placeholder::class) - ] - ), - ] - ); + $placeholderFactory = $this->createMock(PlaceholderFactory::class); + $placeholderFactory->method('create') + ->willReturn($this->createMock(Placeholder::class)); - return $mediaClass; + return new Media( + $this->configFactoryMock, + $this->syncFactoryMock, + $this->responseMock, + $isAllowedCallback, + false, + self::CACHE_FILE_PATH, + self::RELATIVE_FILE_PATH, + $this->filesystemMock, + $placeholderFactory, + $this->createMock(State::class), + $this->createMock(ImageResize::class), + $this->createMock(Filesystem\Driver\File::class), + $this->createMock(CatalogMediaConfig::class) + ); } } diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index 1c280acd63a7b..e026bee87dcd4 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -76,10 +76,10 @@ <!-- 5. Open admin tab with page with products. Reload this page twice. --> <switchToPreviousTab stepKey="switchToPreviousTab"/> - <reloadPage stepKey="reloadAdminCatalogPageFirst"/> - <waitForPageLoad stepKey="waitForReloadFirst"/> - <reloadPage stepKey="reloadAdminCatalogPageSecond"/> - <waitForPageLoad stepKey="waitForReloadSecond"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadAdminCatalogPageFirst"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReloadFirst"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadAdminCatalogPageSecond"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReloadSecond"/> <seeInTitle userInput="Products / Inventory / Catalog / Magento Admin" stepKey="seeAdminProductsPageTitle"/> <see userInput="Products" selector="{{AdminHeaderSection.pageTitle}}" stepKey="seeAdminProductsPageHeader"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.xml new file mode 100644 index 0000000000000..c927bfc50120e --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutDisableActionGroup.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="AdminPayPalExpressCheckoutDisableActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="No" stepKey="enableSolution"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml new file mode 100644 index 0000000000000..b6b44abd7b794 --- /dev/null +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/AdminPayPalExpressCheckoutEnableActionGroup.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminPayPalExpressCheckoutEnableActionGroup"> + <annotations> + <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> + </annotations> + <arguments> + <argument name="credentials" defaultValue="SamplePaypalExpressConfig"/> + <argument name="countryCode" type="string" defaultValue="us"/> + </arguments> + <amOnPage url="{{AdminConfigPaymentMethodsPage.url}}" stepKey="navigateToPaymentConfigurationPage"/> + <waitForPageLoad stepKey="waitForPageLoad1"/> + <click selector="{{PayPalExpressCheckoutConfigSection.configureBtn(countryCode)}}" stepKey="clickPayPalConfigureBtn"/> + <waitForElementVisible selector="{{PayPalAdvancedSettingConfigSection.advancedSettingTab(countryCode)}}" stepKey="waitForAdvancedSettingTab"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.email(countryCode)}}" userInput="{{credentials.paypal_express_email}}" stepKey="inputEmailAssociatedWithPayPalMerchantAccount"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.apiMethod(countryCode)}}" userInput="API Signature" stepKey="inputAPIAuthenticationMethods"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.username(countryCode)}}" userInput="{{credentials.paypal_express_api_username}}" stepKey="inputAPIUsername"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.password(countryCode)}}" userInput="{{credentials.paypal_express_api_password}}" stepKey="inputAPIPassword"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.signature(countryCode)}}" userInput="{{credentials.paypal_express_api_signature}}" stepKey="inputAPISignature"/> + <selectOption selector ="{{PayPalExpressCheckoutConfigSection.sandboxMode(countryCode)}}" userInput="Yes" stepKey="enableSandboxMode"/> + <selectOption selector="{{PayPalExpressCheckoutConfigSection.enableSolution(countryCode)}}" userInput="Yes" stepKey="enableSolution"/> + <fillField selector ="{{PayPalExpressCheckoutConfigSection.merchantID(countryCode)}}" userInput="{{credentials.paypal_express_merchantID}}" stepKey="inputMerchantID"/> + <click selector="{{AdminConfigSection.saveButton}}" stepKey="saveConfig"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml index 23d956c8e9b8f..a7ccf0a19263b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/SampleConfigPayPalExpressCheckoutActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="SampleConfigPayPalExpressCheckoutActionGroup"> + <actionGroup name="SampleConfigPayPalExpressCheckoutActionGroup" deprecated="Use AdminPayPalExpressCheckoutEnableActionGroup instead"> <annotations> <description>Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.</description> </annotations> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml index ebdfb9e91ecf1..a616c0bb2c68b 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/AdminConfigPaymentsConflictResolutionForPayPalTest/AdminConfigPaymentsConflictResolutionForPayPalInUnitedKingdomTest.xml @@ -19,7 +19,7 @@ </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="SampleConfigPayPalExpressCheckoutActionGroup" stepKey="ConfigPayPalExpress"> + <actionGroup ref="AdminPayPalExpressCheckoutEnableActionGroup" stepKey="ConfigPayPalExpress"> <argument name="credentials" value="SamplePaypalExpressConfig"/> </actionGroup> </before> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml index 18e19c4276548..5b023e12bc55d 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/CheckShoppingCartBehaviorAfterSessionExpiredTest.xml @@ -55,8 +55,8 @@ <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> <!--Reset cookies and refresh the page--> <resetCookie userInput="PHPSESSID" stepKey="resetCookieForCart"/> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoad"/> <!--Check product exists in cart--> <see userInput="$$createProduct.name$$" stepKey="ProductExistsInCart"/> </test> diff --git a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml index 40c8674bc025a..61feeae04369d 100644 --- a/app/code/Magento/Persistent/view/frontend/templates/additional.phtml +++ b/app/code/Magento/Persistent/view/frontend/templates/additional.phtml @@ -4,13 +4,9 @@ * See COPYING.txt for license details. */ ?> -<?php if ($block->getCustomerId()) :?> - <span> - <a <?= /* @noEscape */ $block->getLinkAttributes()?>><?= $block->escapeHtml(__('Not you?'));?></a> - </span> -<?php endif;?> <script type="application/javascript"> - window.persistent = <?= /* @noEscape */ $block->getConfig(); ?>; + window.persistent = <?=/* @noEscape */ $block->getConfig()?>; + window.notYouLink = '<?=/* @noEscape */ $block->getLinkAttributes()?>'; </script> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js index 7ace6e60d1c39..8e69325860167 100644 --- a/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js +++ b/app/code/Magento/Persistent/view/frontend/web/js/view/additional-welcome.js @@ -40,6 +40,7 @@ define([ $(this).attr('data-bind', html); $(this).html(html); + $(this).after(' <span><a ' + window.notYouLink + '>' + $t('Not you?') + '</a></span>'); }); } } diff --git a/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php b/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php index 6a65fff7c5ebc..30d0573b62d87 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Block/Product/View/GalleryTest.php @@ -94,7 +94,7 @@ public function testGetMediaGalleryDataJson() $data = [ [ 'media_type' => 'external-video', - 'video_url' => 'http://magento.ce/pub/media/catalog/product/9/b/9br6ujuthnc.jpg', + 'video_url' => 'http://magento.ce/media/catalog/product/9/b/9br6ujuthnc.jpg', 'is_base' => true, ], [ diff --git a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php index f3d21e97eb9ee..770eaf1d3d342 100644 --- a/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php +++ b/app/code/Magento/ProductVideo/Test/Unit/Controller/Adminhtml/Product/Gallery/RetrieveImageTest.php @@ -99,11 +99,20 @@ class RetrieveImageTest extends TestCase */ private $fileDriverMock; - /** - * Set up - */ + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $objectManager = new ObjectManager($this); $this->contextMock = $this->createMock(Context::class); $this->validatorMock = $this diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index 0dd2b00a596ea..1533194023e3e 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -25,7 +25,7 @@ use Magento\Store\Model\StoreManagerInterface; /** - * Quote repository. + * Repository for quote entity. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -146,10 +146,16 @@ public function get($cartId, array $sharedStoreIds = []) public function getForCustomer($customerId, array $sharedStoreIds = []) { if (!isset($this->quotesByCustomerId[$customerId])) { - $quote = $this->loadQuote('loadByCustomer', 'customerId', $customerId, $sharedStoreIds); - $this->getLoadHandler()->load($quote); - $this->quotesById[$quote->getId()] = $quote; - $this->quotesByCustomerId[$customerId] = $quote; + $customerQuote = $this->loadQuote('loadByCustomer', 'customerId', $customerId, $sharedStoreIds); + $customerQuoteId = $customerQuote->getId(); + //prevent loading quote items for same quote + if (isset($this->quotesById[$customerQuoteId])) { + $customerQuote = $this->quotesById[$customerQuoteId]; + } else { + $this->getLoadHandler()->load($customerQuote); + } + $this->quotesById[$customerQuoteId] = $customerQuote; + $this->quotesByCustomerId[$customerId] = $customerQuote; } return $this->quotesByCustomerId[$customerId]; } diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 80af412439338..ee5f2fccfe203 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -124,8 +124,8 @@ <closeTab stepKey="closeTab"/> <!-- Check cart --> <wait time="60" stepKey="waitForCartToBeUpdated"/> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForCheckoutPageReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload"/> <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickMiniCart"/> <dontSeeElement selector="{{StorefrontMinicartSection.quantity}}" stepKey="dontSeeCartItem"/> <!-- Add simple product to shopping cart --> @@ -151,8 +151,9 @@ <closeTab stepKey="closeTab2"/> <!--Check cart--> <wait time="60" stepKey="waitForCartToBeUpdated2"/> - <reloadPage stepKey="reloadPage2"/> - <waitForPageLoad stepKey="waitForCheckoutPageReload2"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage2"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForCheckoutPageReload2"/> + <click selector="{{StorefrontMinicartSection.showCart}}" stepKey="clickMiniCart2"/> <dontSeeElement selector="{{StorefrontMinicartSection.quantity}}" stepKey="dontSeeCartItem2"/> </test> diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php index e19fb93255eb2..add00ba71d0fb 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteRepositoryTest.php @@ -33,6 +33,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyMethods) + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class QuoteRepositoryTest extends TestCase { @@ -223,14 +224,44 @@ public function testGet() static::assertEquals($this->quoteMock, $this->model->get($cartId)); } - public function testGetForCustomerAfterGet() + /** + * @param int $quoteId + * @param int $customerQuoteId + * @param bool $isSame + * @dataProvider getForCustomerAfterGetDataProvider + */ + public function testGetForCustomerAfterGet(int $quoteId, int $customerQuoteId, bool $isSame) { - $cartId = 15; $customerId = 23; + $customerQuote = $this->getMockBuilder(Quote::class) + ->addMethods( + [ + 'setSharedStoreIds', + 'getCustomerId' + ] + ) + ->onlyMethods( + [ + 'load', + 'loadByIdWithoutStore', + 'loadByCustomer', + 'getIsActive', + 'getId', + 'save', + 'delete', + 'getStoreId', + 'getData' + ] + ) + ->disableOriginalConstructor() + ->getMock(); $this->cartFactoryMock->expects(static::exactly(2)) ->method('create') - ->willReturn($this->quoteMock); + ->willReturnOnConsecutiveCalls( + $this->quoteMock, + $customerQuote + ); $this->storeManagerMock->expects(static::exactly(2)) ->method('getStore') ->willReturn($this->storeMock); @@ -241,24 +272,34 @@ public function testGetForCustomerAfterGet() ->method('setSharedStoreIds'); $this->quoteMock->expects(static::once()) ->method('loadByIdWithoutStore') - ->with($cartId) - ->willReturn($this->storeMock); - $this->quoteMock->expects(static::once()) + ->with($quoteId) + ->willReturnSelf(); + $customerQuote->expects(static::once()) ->method('loadByCustomer') ->with($customerId) - ->willReturn($this->storeMock); - $this->quoteMock->expects(static::exactly(3)) - ->method('getId') - ->willReturn($cartId); - $this->quoteMock->expects(static::any()) - ->method('getCustomerId') + ->willReturnSelf(); + $this->quoteMock->method('getId') + ->willReturn($quoteId); + $customerQuote->method('getId') + ->willReturn($customerQuoteId); + $this->quoteMock->method('getCustomerId') + ->willReturn($customerId); + $customerQuote->method('getCustomerId') ->willReturn($customerId); - $this->loadHandlerMock->expects(static::exactly(2)) + $this->loadHandlerMock->expects($isSame ? $this->once() : $this->exactly(2)) ->method('load') ->with($this->quoteMock); - static::assertEquals($this->quoteMock, $this->model->get($cartId)); - static::assertEquals($this->quoteMock, $this->model->getForCustomer($customerId)); + static::assertSame($this->quoteMock, $this->model->get($quoteId)); + static::assertSame($isSame ? $this->quoteMock : $customerQuote, $this->model->getForCustomer($customerId)); + } + + public function getForCustomerAfterGetDataProvider(): array + { + return [ + [15, 15, true], + [15, 16, false], + ]; } public function testGetWithSharedStoreIds() diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php index 6a57a7662af09..66cc9ed11ed9f 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CartPrices.php @@ -45,6 +45,12 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value /** @var Quote $quote */ $quote = $value['model']; + /** + * To calculate a right discount value + * before calculate totals + * need to reset Cart Fixed Rules in the quote + */ + $quote->setCartFixedRules([]); $cartTotals = $this->totalsCollector->collectQuoteTotals($quote); $currency = $quote->getQuoteCurrencyCode(); diff --git a/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php new file mode 100644 index 0000000000000..a53a203b6d550 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Console/Command/RemoteStorageSynchronizeCommand.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Console\Command; + +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\LocalizedException; +use Magento\RemoteStorage\Model\Synchronizer; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Magento\RemoteStorage\Model\Config; + +/** + * Synchronizes local storage with remote storage. + */ +class RemoteStorageSynchronizeCommand extends Command +{ + private const NAME = 'remote-storage:sync'; + + /** + * @var Synchronizer + */ + private $synchronizer; + + /** + * @var Config + */ + private $config; + + /** + * @param Synchronizer $synchronizer + * @param Config $config + */ + public function __construct( + Synchronizer $synchronizer, + Config $config + ) { + $this->synchronizer = $synchronizer; + $this->config = $config; + + parent::__construct(self::NAME); + } + + /** + * @inheritDoc + */ + protected function configure(): void + { + $this->setDescription('Synchronize media files with remote storage.'); + } + + /** + * Run synchronization. + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + * @throws LocalizedException + */ + public function execute(InputInterface $input, OutputInterface $output): int + { + if (!$this->config->isEnabled()) { + $output->writeln('<error>Remote storage is not enabled.</error>'); + + return Cli::RETURN_FAILURE; + } + + $output->writeln('<info>Uploading media files to remote storage.</info>'); + + foreach ($this->synchronizer->execute() as $file) { + $output->writeln('- ' . $file); + } + + $output->writeln('<info>End of upload.</info>'); + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverException.php b/app/code/Magento/RemoteStorage/Driver/DriverException.php new file mode 100644 index 0000000000000..b35a7e7c4d4da --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverException.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Remote storage driver. + */ +class DriverException extends LocalizedException +{ +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php new file mode 100644 index 0000000000000..b9074efc527f0 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +/** + * Factory for drivers with additional configuration. + */ +interface DriverFactoryInterface +{ + /** + * Creates pre-configured driver. + * + * @param array $config + * @param string $prefix + * @return RemoteDriverInterface + * + * @throws DriverException + */ + public function create(array $config, string $prefix): RemoteDriverInterface; +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php new file mode 100644 index 0000000000000..d13f599387d90 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverFactoryPool.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Exception\RuntimeException; + +/** + * Pool of driver factories. + */ +class DriverFactoryPool +{ + /** + * @var DriverFactoryInterface[] + */ + private $pool; + + /** + * @param DriverFactoryInterface[] $pool + */ + public function __construct(array $pool = []) + { + $this->pool = $pool; + } + + /** + * Check if factory exists. + * + * @param string $name + * @return bool + */ + public function has(string $name): bool + { + return isset($this->pool[$name]); + } + + /** + * Retrieve factory. + * + * @param string $name + * @return DriverFactoryInterface + * + * @throws RuntimeException + */ + public function get(string $name): DriverFactoryInterface + { + if (!$this->has($name)) { + throw new RuntimeException(__('Driver "%1" does not exist', $name)); + } + + return $this->pool[$name]; + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/DriverPool.php b/app/code/Magento/RemoteStorage/Driver/DriverPool.php new file mode 100644 index 0000000000000..0c085da78ddac --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/DriverPool.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\DriverPool as BaseDriverPool; +use Magento\Framework\Filesystem\DriverPoolInterface; +use Magento\RemoteStorage\Model\Config; + +/** + * The remote driver pool. + */ +class DriverPool extends BaseDriverPool implements DriverPoolInterface +{ + public const PATH_DRIVER = 'remote_storage/driver'; + public const PATH_EXPOSE_URLS = 'remote_storage/expose_urls'; + public const PATH_PREFIX = 'remote_storage/prefix'; + public const PATH_CONFIG = 'remote_storage/config'; + + /** + * Driver name. + */ + public const REMOTE = 'remote'; + + /** + * @var Config + */ + private $config; + + /** + * @var DriverFactoryPool + */ + private $driverFactoryPool; + + /** + * @var array + */ + private $pool = []; + + /** + * @param Config $config + * @param DriverFactoryPool $driverFactoryPool + * @param array $extraTypes + */ + public function __construct( + Config $config, + DriverFactoryPool $driverFactoryPool, + array $extraTypes = [] + ) { + $this->config = $config; + $this->driverFactoryPool = $driverFactoryPool; + + parent::__construct($extraTypes); + } + + /** + * Retrieves remote driver. + * + * @param string $code + * @return DriverInterface + * @throws DriverException + * @throws FileSystemException + * @throws RuntimeException + */ + public function getDriver($code = self::REMOTE): DriverInterface + { + if ($code === self::REMOTE) { + if (isset($this->pool[$code])) { + return $this->pool[$code]; + } + + $driver = $this->config->getDriver(); + + if ($driver && $this->driverFactoryPool->has($driver)) { + return $this->pool[$code] = $this->driverFactoryPool->get($driver)->create( + $this->config->getConfig(), + $this->config->getPrefix() + ); + } + + throw new RuntimeException(__('Remote driver is not available.')); + } + + return parent::getDriver($code); + } +} diff --git a/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php b/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php new file mode 100644 index 0000000000000..fc108bb388cb5 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Driver/RemoteDriverInterface.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Driver; + +use Magento\Framework\Filesystem\ExtendedDriverInterface; + +/** + * Remote storage driver. + */ +interface RemoteDriverInterface extends ExtendedDriverInterface +{ + /** + * Test storage connection. + * + * @throws DriverException + */ + public function test(): void; +} diff --git a/app/code/Magento/RemoteStorage/Filesystem.php b/app/code/Magento/RemoteStorage/Filesystem.php new file mode 100644 index 0000000000000..01af39cfc50a3 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Filesystem.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage; + +use Magento\Framework\Filesystem\Directory\ReadFactory; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Filesystem as BaseFilesystem; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; + +/** + * Filesystem implementation for remote storage. + */ +class Filesystem extends BaseFilesystem implements FilesystemInterface +{ + /** + * @var bool + */ + private $isEnabled; + + /** + * @var array + */ + private $directoryCodes; + + /** + * @var DriverPool + */ + private $driverPool; + + /** + * @param BaseFilesystem\DirectoryList $directoryList + * @param ReadFactory $readFactory + * @param WriteFactory $writeFactory + * @param Config $config + * @param DriverPool $driverPool + * @param array $directoryCodes + */ + public function __construct( + BaseFilesystem\DirectoryList $directoryList, + ReadFactory $readFactory, + WriteFactory $writeFactory, + Config $config, + DriverPool $driverPool, + array $directoryCodes = [] + ) { + $this->isEnabled = $config->isEnabled(); + $this->driverPool = $driverPool; + $this->directoryCodes = $directoryCodes; + + parent::__construct($directoryList, $readFactory, $writeFactory); + } + + /** + * @inheritDoc + */ + public function getDirectoryRead($directoryCode, $driverCode = DriverPool::REMOTE) + { + $hasCode = !$this->directoryCodes || in_array($directoryCode, $this->directoryCodes, true); + + if ($driverCode === DriverPool::REMOTE && $hasCode && $this->isEnabled) { + $code = $directoryCode . '_' . $driverCode; + + if (!array_key_exists($code, $this->readInstances)) { + $uri = $this->getUri($directoryCode) ?: '/'; + + $this->readInstances[$code] = $this->readFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $uri), + $driverCode + ); + } + + return $this->readInstances[$code]; + } + + return parent::getDirectoryRead($directoryCode); + } + + /** + * @inheritDoc + */ + public function getDirectoryWrite($directoryCode, $driverCode = DriverPool::REMOTE) + { + $hasCode = !$this->directoryCodes || in_array($directoryCode, $this->directoryCodes, true); + + if ($driverCode === DriverPool::REMOTE && $hasCode && $this->isEnabled) { + $code = $directoryCode . '_' . $driverCode; + + if (!array_key_exists($code, $this->writeInstances)) { + $uri = $this->getUri($directoryCode) ?: '/'; + + $this->writeInstances[$code] = $this->writeFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $uri), + $driverCode + ); + } + + return $this->writeInstances[$code]; + } + + return parent::getDirectoryWrite($directoryCode); + } + + /** + * @inheritDoc + */ + public function getDirectoryReadByPath($path, $driverCode = DriverPool::REMOTE) + { + if ($driverCode === DriverPool::REMOTE && $this->isEnabled) { + return $this->readFactory->create( + $this->driverPool->getDriver()->getAbsolutePath('', $path), + $driverCode + ); + } + + return parent::getDirectoryReadByPath($path); + } + + /** + * @inheritDoc + */ + public function getDirectoryCodes(): array + { + return $this->directoryCodes; + } +} diff --git a/app/code/Magento/RemoteStorage/FilesystemInterface.php b/app/code/Magento/RemoteStorage/FilesystemInterface.php new file mode 100644 index 0000000000000..42669200c0caf --- /dev/null +++ b/app/code/Magento/RemoteStorage/FilesystemInterface.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage; + +/** + * Provides extension for applicable directory codes. + */ +interface FilesystemInterface +{ + /** + * Retrieve directory codes. + */ + public function getDirectoryCodes(): array; +} diff --git a/app/code/Magento/RemoteStorage/LICENSE.txt b/app/code/Magento/RemoteStorage/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/RemoteStorage/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/RemoteStorage/LICENSE_AFL.txt b/app/code/Magento/RemoteStorage/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/RemoteStorage/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/RemoteStorage/Model/Config.php b/app/code/Magento/RemoteStorage/Model/Config.php new file mode 100644 index 0000000000000..41fbfdde15bd0 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Config.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Model; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\Framework\Filesystem\DriverPool as BaseDriverPool; + +/** + * Configuration for remote storage. + */ +class Config +{ + /** + * @var DeploymentConfig + */ + private $config; + + /** + * @param DeploymentConfig $config + */ + public function __construct(DeploymentConfig $config) + { + $this->config = $config; + } + + /** + * Retrieve driver name. + * + * @return string + * @throws FileSystemException + * @throws RuntimeException + */ + public function getDriver(): string + { + return $this->config->get(DriverPool::PATH_DRIVER, BaseDriverPool::FILE); + } + + /** + * Check if remote FS is enabled. + * + * @return bool + * @throws FileSystemException + * @throws RuntimeException + */ + public function isEnabled(): bool + { + $driver = $this->getDriver(); + + return $driver && $driver !== BaseDriverPool::FILE; + } + + /** + * Retrieves config. + * + * @return array + * @throws FileSystemException + * @throws RuntimeException + */ + public function getConfig(): array + { + return (array)$this->config->get(DriverPool::PATH_CONFIG, []); + } + + /** + * Retrieves prefix. + * + * @return string + * + * @throws FileSystemException + * @throws RuntimeException + */ + public function getPrefix(): string + { + return (string)$this->config->get(DriverPool::PATH_PREFIX, ''); + } + + /** + * Retrieves value for exposing URLs. + * + * @return bool + * @throws FileSystemException + * @throws RuntimeException + */ + public function getExposeUrls(): bool + { + return (bool)$this->config->get(DriverPool::PATH_EXPOSE_URLS, false); + } +} diff --git a/app/code/Magento/RemoteStorage/Model/Synchronizer.php b/app/code/Magento/RemoteStorage/Model/Synchronizer.php new file mode 100644 index 0000000000000..4276c7a1a2ffd --- /dev/null +++ b/app/code/Magento/RemoteStorage/Model/Synchronizer.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Model; + +use Generator; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\Framework\Filesystem\Glob; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\RemoteStorage\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; + +/** + * Synchronize files from local filesystem. + */ +class Synchronizer +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + */ + public function __construct(Filesystem $filesystem) + { + $this->filesystem = $filesystem; + } + + /** + * File upload. + * + * @return Generator + * @throws FileSystemException + * @throws ValidatorException + */ + public function execute(): Generator + { + foreach ($this->filesystem->getDirectoryCodes() as $directoryCode) { + $directory = $this->filesystem->getDirectoryWrite($directoryCode, DriverPool::FILE); + $remoteDirectory = $this->filesystem->getDirectoryWrite($directoryCode, RemoteDriverPool::REMOTE); + + yield from $this->copyRecursive($directory, $remoteDirectory, $directory->getAbsolutePath()); + } + } + + /** + * Recursive file upload. + * + * @param WriteInterface $directory + * @param WriteInterface $remoteDirectory + * @param string $path + * @param string $pattern + * @param int $flags + * @return Generator + * @throws FileSystemException + */ + private function copyRecursive( + WriteInterface $directory, + WriteInterface $remoteDirectory, + string $path, + string $pattern = '*.*', + int $flags = Glob::GLOB_NOSORT + ): Generator { + $path = rtrim($path, '/'); + $localDriver = $directory->getDriver(); + $remoteDriver = $remoteDirectory->getDriver(); + + foreach (Glob::glob($path . '/' . $pattern, $flags) as $file) { + /** + * Extracting relative path in local system to apply it for remote system. + */ + $relativeFile = $directory->getRelativePath($file); + $destination = $remoteDirectory->getAbsolutePath($relativeFile); + + if (!$remoteDirectory->isExist($destination)) { + $localDriver->copy($file, $destination, $remoteDriver); + + yield $relativeFile; + } + } + + foreach (Glob::glob($path . '/{,.}[!.,!..]*', + $flags | Glob::GLOB_ONLYDIR | Glob::GLOB_BRACE) as $childDirectory) { + $relativeDirectory = $directory->getRelativePath($childDirectory); + $destinationDirectory = $remoteDirectory->getAbsolutePath($relativeDirectory); + + if (!$remoteDirectory->isDirectory($destinationDirectory)) { + $remoteDriver->createDirectory($destinationDirectory); + + yield $relativeDirectory; + } + + yield from $this->copyRecursive($directory, $remoteDirectory, $childDirectory, $pattern, $flags); + } + } +} diff --git a/app/code/Magento/RemoteStorage/Plugin/Image.php b/app/code/Magento/RemoteStorage/Plugin/Image.php new file mode 100644 index 0000000000000..013e3fd23e168 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Image.php @@ -0,0 +1,257 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Image\Adapter\AbstractAdapter; +use Magento\RemoteStorage\Model\Config; +use Psr\Log\LoggerInterface; + +/** + * @see AbstractAdapter + */ +class Image +{ + /** + * @var Filesystem\Directory\WriteInterface + */ + private $tmpDirectoryWrite; + + /** + * @var Filesystem\Directory\WriteInterface + */ + private $remoteDirectoryWrite; + + /** + * @var array + */ + private $tmpFiles = []; + + /** + * @var bool + */ + private $isEnabled; + + /** + * @var File + */ + private $ioFile; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Filesystem $filesystem + * @param File $ioFile + * @param TargetDirectory $targetDirectory + * @param Config $config + * @param LoggerInterface $logger + * @throws FileSystemException + * @throws RuntimeException + */ + public function __construct( + Filesystem $filesystem, + File $ioFile, + TargetDirectory $targetDirectory, + Config $config, + LoggerInterface $logger + ) { + $this->tmpDirectoryWrite = $filesystem->getDirectoryWrite(DirectoryList::TMP); + $this->remoteDirectoryWrite = $targetDirectory->getDirectoryWrite(DirectoryList::ROOT); + $this->isEnabled = $config->isEnabled(); + $this->ioFile = $ioFile; + $this->logger = $logger; + } + + /** + * Copy file from remote server to tmp directory of Magento + * + * @param AbstractAdapter $subject + * @param string $filename + * @return array + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeOpen(AbstractAdapter $subject, $filename): array + { + if ($this->isEnabled) { + $filename = $this->copyFileToTmp($filename); + } + return [$filename]; + } + + /** + * Copy import file locally to validate + * + * @param AbstractAdapter $subject + * @param string $filePath + * @return string[] + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeValidateUploadFile(AbstractAdapter $subject, $filePath): array + { + if ($this->isEnabled) { + $filePath = $this->copyFileToTmp($filePath); + } + return [$filePath]; + } + + /** + * Copy watermark locally before adding it an image + * + * @param AbstractAdapter $subject + * @param string $filePath + * @return string[] + * @throws FileSystemException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeWatermark(AbstractAdapter $subject, $filePath): array + { + if ($this->isEnabled) { + $filePath = $this->copyFileToTmp($filePath); + } + return [$filePath]; + } + + /** + * Get filesystem tmp path for file and provide it to save() function + * + * @param AbstractAdapter $subject + * @param callable $proceed + * @param string|null $destination + * @param string|null $newName + * @return void + * @throws FileSystemException + */ + public function aroundSave( + AbstractAdapter $subject, + callable $proceed, + $destination = null, + $newName = null + ): void { + if ($this->isEnabled) { + $relativePath = $this->remoteDirectoryWrite->getRelativePath($destination); + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath($relativePath); + + $proceed($tmpPath, $newName); + + $this->tmpDirectoryWrite->getDriver()->rename( + $this->prepareDestination($subject, $tmpPath, $newName), + $this->prepareDestination($subject, $destination, $newName), + $this->remoteDirectoryWrite->getDriver() + ); + } else { + $proceed($destination, $newName); + } + } + + /** + * Remove created tmp files + */ + public function __destruct() + { + try { + foreach ($this->tmpFiles as $tmpFile) { + $this->tmpDirectoryWrite->delete($tmpFile); + } + } catch (\Exception $e) { + $this->logger->error($e->getMessage()); + } + } + + /** + * Move files from storage to tmp folder + * + * @param string $filePath + * @return string + * @throws FileSystemException + */ + private function copyFileToTmp(string $filePath): string + { + if ($this->fileExistsInTmp($filePath)) { + return $filePath; + } + $absolutePath = $this->remoteDirectoryWrite->getAbsolutePath($filePath); + if ($this->remoteDirectoryWrite->isFile($absolutePath)) { + $this->tmpDirectoryWrite->create(); + $tmpPath = $this->storeTmpName($filePath); + $content = $this->remoteDirectoryWrite->getDriver()->fileGetContents($filePath); + $filePath = $this->tmpDirectoryWrite->getDriver()->filePutContents($tmpPath, $content) + ? $tmpPath + : $filePath; + } + return $filePath; + } + + /** + * Store created tmp image path + * + * @param string $filePath + * @return string + */ + private function storeTmpName(string $filePath): string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $tmpPath = $this->tmpDirectoryWrite->getAbsolutePath() . basename($filePath); + + $this->tmpFiles[$filePath] = $tmpPath; + + return $tmpPath; + } + + /** + * Check is file exist in tmp folder + * + * @param string $filePath + * @return bool + */ + private function fileExistsInTmp(string $filePath): bool + { + return in_array($filePath, $this->tmpFiles, true); + } + + /** + * Prepare destination path + * + * @param AbstractAdapter $image + * @param string|null $destination + * @param string|null $newName + * @return string + */ + private function prepareDestination( + AbstractAdapter $image, + string $destination = null, + string $newName = null + ): string { + if (empty($destination)) { + $destination = $image->getFileSrcPath(); + } elseif (empty($newName)) { + $info = $this->ioFile->getPathInfo($destination); + $newName = $info['basename']; + $destination = $info['dirname']; + } + + if (empty($newName)) { + $newFileName = $image->getFileSrcName(); + } else { + $newFileName = $newName; + } + return rtrim($destination, '/') . '/' . $newFileName; + } +} diff --git a/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php new file mode 100644 index 0000000000000..12837545c533b --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/MediaStorage.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\MediaStorage\Model\File\Storage\Synchronization; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\Framework\Filesystem\DriverPool as LocalDriverPool; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Filesystem; + +/** + * Modifies the base URL. + */ +class MediaStorage +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var bool + */ + private $isEnabled; + + /** + * @var WriteInterface + */ + private $remoteDirectory; + + /** + * @var WriteInterface + */ + private $localDirectory; + + /** + * @param Config $config + * @param Filesystem $filesystem + * @throws FileSystemException + * @throws RuntimeException + */ + public function __construct(Config $config, Filesystem $filesystem) + { + $this->isEnabled = $config->isEnabled(); + $this->remoteDirectory = $filesystem->getDirectoryWrite(DirectoryList::PUB, RemoteDriverPool::REMOTE); + $this->localDirectory = $filesystem->getDirectoryWrite(DirectoryList::PUB, LocalDriverPool::FILE); + } + + /** + * Download remote file + * + * @param Synchronization $subject + * @param string $relativeFileName + * @return null + * @throws FileSystemException + * @throws ValidatorException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSynchronize(Synchronization $subject, string $relativeFileName): void + { + if ($this->isEnabled && $this->remoteDirectory->isExist($relativeFileName)) { + $file = $this->localDirectory->openFile($relativeFileName, 'w'); + try { + $file->lock(); + $file->write($this->remoteDirectory->readFile($relativeFileName)); + $file->unlock(); + $file->close(); + } catch (FileSystemException $e) { + $file->close(); + } + } + } +} diff --git a/app/code/Magento/RemoteStorage/Plugin/Scope.php b/app/code/Magento/RemoteStorage/Plugin/Scope.php new file mode 100644 index 0000000000000..6a05b63dee3a6 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Plugin/Scope.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Url\ScopeInterface; +use Magento\Framework\UrlInterface; +use Magento\RemoteStorage\Driver\DriverPool; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Filesystem; + +/** + * Modifies the base URL. + */ +class Scope +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var bool + */ + private $isEnabled; + + /** + * @param Config $config + * @param Filesystem $filesystem + */ + public function __construct(Config $config, Filesystem $filesystem) + { + $this->isEnabled = $config->isEnabled() && $config->getExposeUrls(); + $this->filesystem = $filesystem; + } + + /** + * Modifies the base URL. + * + * @param ScopeInterface $subject + * @param string $result + * @param string $type + * @return string + * @throws ValidatorException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetBaseUrl(ScopeInterface $subject, string $result, string $type = ''): string + { + if ($type === UrlInterface::URL_TYPE_MEDIA && $this->isEnabled) { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA, DriverPool::REMOTE) + ->getAbsolutePath(); + } + + return $result; + } +} diff --git a/app/code/Magento/RemoteStorage/README.md b/app/code/Magento/RemoteStorage/README.md new file mode 100644 index 0000000000000..f33b25795a995 --- /dev/null +++ b/app/code/Magento/RemoteStorage/README.md @@ -0,0 +1 @@ +# Magento_RemoteStorage module diff --git a/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php b/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php new file mode 100644 index 0000000000000..b625661479962 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Setup/ConfigOptionsList.php @@ -0,0 +1,201 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Setup; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverFactoryPool; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\TextConfigOption; +use Psr\Log\LoggerInterface; + +/** + * Remote storage options. + */ +class ConfigOptionsList implements ConfigOptionsListInterface +{ + private const OPTION_REMOTE_STORAGE_DRIVER = 'remote-storage-driver'; + private const CONFIG_PATH__REMOTE_STORAGE_DRIVER = RemoteDriverPool::PATH_DRIVER; + private const OPTION_REMOTE_STORAGE_PREFIX = 'remote-storage-prefix'; + private const CONFIG_PATH__REMOTE_STORAGE_PREFIX = RemoteDriverPool::PATH_PREFIX; + private const OPTION_REMOTE_STORAGE_BUCKET = 'remote-storage-bucket'; + private const CONFIG_PATH__REMOTE_STORAGE_BUCKET = RemoteDriverPool::PATH_CONFIG . '/bucket'; + private const OPTION_REMOTE_STORAGE_REGION = 'remote-storage-region'; + private const CONFIG_PATH__REMOTE_STORAGE_REGION = RemoteDriverPool::PATH_CONFIG . '/region'; + private const OPTION_REMOTE_STORAGE_ACCESS_KEY = 'remote-storage-key'; + private const CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY = RemoteDriverPool::PATH_CONFIG . '/credentials/key'; + private const OPTION_REMOTE_STORAGE_SECRET_KEY = 'remote-storage-secret'; + private const CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY = RemoteDriverPool::PATH_CONFIG . '/credentials/secret'; + + /** + * Map of option to config path relations. + * + * @var string[] + */ + private static $map = [ + self::OPTION_REMOTE_STORAGE_PREFIX => self::CONFIG_PATH__REMOTE_STORAGE_PREFIX, + self::OPTION_REMOTE_STORAGE_BUCKET => self::CONFIG_PATH__REMOTE_STORAGE_BUCKET, + self::OPTION_REMOTE_STORAGE_REGION => self::CONFIG_PATH__REMOTE_STORAGE_REGION, + self::OPTION_REMOTE_STORAGE_ACCESS_KEY => self::CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY, + self::OPTION_REMOTE_STORAGE_SECRET_KEY => self::CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY + ]; + + /** + * @var DriverFactoryPool + */ + private $driverFactoryPool; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param DriverFactoryPool $driverFactoryPool + * @param LoggerInterface $logger + */ + public function __construct(DriverFactoryPool $driverFactoryPool, LoggerInterface $logger) + { + $this->driverFactoryPool = $driverFactoryPool; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function getOptions(): array + { + return [ + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_DRIVER, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, + 'Remote storage driver', + DriverPool::FILE + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_PREFIX, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_PREFIX, + 'Remote storage prefix', + '' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_BUCKET, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_BUCKET, + 'Remote storage bucket' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_REGION, + TextConfigOption::FRONTEND_WIZARD_TEXT, + self::CONFIG_PATH__REMOTE_STORAGE_REGION, + 'Remote storage region' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_ACCESS_KEY, + TextConfigOption::FRONTEND_WIZARD_PASSWORD, + self::CONFIG_PATH__REMOTE_STORAGE_ACCESS_KEY, + 'Remote storage access key', + '' + ), + new TextConfigOption( + self::OPTION_REMOTE_STORAGE_SECRET_KEY, + TextConfigOption::FRONTEND_WIZARD_PASSWORD, + self::CONFIG_PATH__REMOTE_STORAGE_SECRET_KEY, + 'Remote storage secret key', + '' + ) + ]; + } + + /** + * @inheritDoc + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig): array + { + $driver = $options[self::OPTION_REMOTE_STORAGE_DRIVER] ?? DriverPool::FILE; + + if ($driver === DriverPool::FILE) { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $configData->set(self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, $driver); + } else { + $configData = $this->createConfigData($driver, $options); + } + + return [$configData]; + } + + /** + * @inheritDoc + */ + public function validate(array $options, DeploymentConfig $deploymentConfig): array + { + $driver = $options[self::OPTION_REMOTE_STORAGE_DRIVER] ?? DriverPool::FILE; + + if ($driver === DriverPool::FILE) { + return []; + } + + $errors = []; + + if (empty($options[self::OPTION_REMOTE_STORAGE_REGION])) { + $errors[] = 'Region is required'; + } + + if (empty($options[self::OPTION_REMOTE_STORAGE_BUCKET])) { + $errors[] = 'Bucket is required'; + } + + if (!$errors) { + $configData = $this->createConfigData($driver, $options); + + try { + $this->driverFactoryPool->get($driver)->create( + $configData->getData()['remote_storage']['config'], + $options[self::OPTION_REMOTE_STORAGE_PREFIX] + )->test(); + } catch (LocalizedException $exception) { + $message = $exception->getMessage(); + + $this->logger->critical($message); + + $errors[] = 'Adapter error: ' . $message; + } + } + + return $errors; + } + + /** + * Creates pre-configured config data object. + * + * @param string $driver + * @param array $options + * @return ConfigData + */ + private function createConfigData(string $driver, array $options): ConfigData + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + $configData->setOverrideWhenSave(true); + $configData->set(self::CONFIG_PATH__REMOTE_STORAGE_DRIVER, $driver); + + foreach (self::$map as $option => $configPath) { + if (!empty($options[$option])) { + $configData->set($configPath, $options[$option]); + } + } + + return $configData; + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php new file mode 100644 index 0000000000000..5c3ddb74bb0cf --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Model/SynchronizerTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\RemoteStorage\Test\Unit\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\RemoteStorage\Filesystem; +use Magento\RemoteStorage\Model\Synchronizer; +use PHPUnit\Framework\TestCase; +use Magento\Framework\Filesystem\DriverPool; +use Magento\RemoteStorage\Driver\DriverPool as RemoteDriverPool; + +/** + * @see Synchronizer + */ +class SynchronizerTest extends TestCase +{ + /** + * @var Synchronizer + */ + private $synchronizer; + + /** + * @var Filesystem + */ + private $filesystemMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->filesystemMock = $this->createMock(Filesystem::class); + + $this->synchronizer = new Synchronizer( + $this->filesystemMock + ); + } + + /** + * @throws FileSystemException + * @throws ValidatorException + */ + public function testExecute(): void + { + $this->filesystemMock->method('getDirectoryCodes') + ->willReturn(['test']); + + $localDriver = $this->createMock(DriverInterface::class); + $remoteDriver = $this->createMock(DriverInterface::class); + + $localDirectory = $this->createMock(WriteInterface::class); + $localDirectory->method('getDriver') + ->willReturn($localDriver); + $remoteDirectory = $this->createMock(WriteInterface::class); + $remoteDirectory->method('getDriver') + ->willReturn($remoteDriver); + + $this->filesystemMock->method('getDirectoryWrite') + ->willReturnMap([ + ['test', DriverPool::FILE, $localDirectory], + ['test', RemoteDriverPool::REMOTE, $remoteDirectory] + ]); + $localDirectory->method('getAbsolutePath') + ->willReturnMap([ + [null, __DIR__ . '/_files/test'] + ]); + $localDirectory->method('getRelativePath') + ->willReturnCallback(function ($arg) { + return str_replace(__DIR__, '', $arg); + }); + $remoteDirectory->expects(self::exactly(2)) + ->method('isExist') + ->willReturnMap([ + [ + 'remote:/_files/test/root_file.txt', + false + ], + [ + 'remote:/_files/test/.dot_directory/child_file.txt', + true + ] + ]); + $remoteDirectory->method('getAbsolutePath') + ->willReturnCallback(function ($arg) { + return 'remote:' . $arg; + }); + $localDriver->expects(self::once()) + ->method('copy') + ->withConsecutive( + [__DIR__ . '/_files/test/root_file.txt', 'remote:/_files/test/root_file.txt', $remoteDriver] + ); + + self::assertSame( + [ + '/_files/test/root_file.txt', + '/_files/test/.dot_directory' + ], + iterator_to_array($this->synchronizer->execute(), false) + ); + } +} diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_directory/child_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/.dot_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt b/app/code/Magento/RemoteStorage/Test/Unit/Model/_files/test/root_file.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php new file mode 100644 index 0000000000000..13d170946e343 --- /dev/null +++ b/app/code/Magento/RemoteStorage/Test/Unit/Plugin/ImageTest.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\RemoteStorage\Test\Unit\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Image\Adapter\AbstractAdapter; +use Magento\RemoteStorage\Model\Config; +use Magento\RemoteStorage\Plugin\Image; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +class ImageTest extends TestCase +{ + /** + * @var File|MockObject + */ + private $ioFile; + + /** + * @var Image + */ + private $plugin; + + /** + * @var WriteInterface|MockObject + */ + private $tmpDirectoryWrite; + + /** + * @var WriteInterface|MockObject + */ + private $targetDirectoryWrite; + + /** + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + protected function setUp(): void + { + /** @var Filesystem|MockObject $filesystem */ + $filesystem = $this->getMockBuilder(Filesystem::class)->disableOriginalConstructor()->getMock(); + $this->ioFile = $this->getMockBuilder(File::class)->disableOriginalConstructor()->getMock(); + /** @var TargetDirectory|MockObject $targetDirectory */ + $targetDirectory = $this->getMockBuilder(TargetDirectory::class)->disableOriginalConstructor()->getMock(); + /** @var Config|MockObject $config */ + $config = $this->getMockBuilder(Config::class)->disableOriginalConstructor()->getMock(); + $config->expects(self::atLeastOnce())->method('isEnabled')->willReturn(true); + $this->tmpDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->targetDirectoryWrite = $this->getMockBuilder(WriteInterface::class) + ->disableOriginalConstructor()->getMock(); + $filesystem->expects(self::atLeastOnce())->method('getDirectoryWrite')->with(DirectoryList::TMP) + ->willReturn($this->tmpDirectoryWrite); + $targetDirectory->expects(self::atLeastOnce())->method('getDirectoryWrite')->with(DirectoryList::ROOT) + ->willReturn($this->targetDirectoryWrite); + /** @var LoggerInterface|MockObject $logger */ + $logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->plugin = new Image( + $filesystem, + $this->ioFile, + $targetDirectory, + $config, + $logger + ); + } + + /** + * @dataProvider aroundSaveDataProvider + * @param string $destination + * @param string $newDestination + * @param string|null $newName + * @param string|null $oldName + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function testAroundSaveWithNewName( + string $destination, + string $newDestination, + ?string $newName, + ?string $oldName + ): void { + $tmpDestination = '/tmp/' . $destination; + /** @var AbstractAdapter $subject */ + $subject = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $proceed = function () { + }; + $targetDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getRelativePath') + ->willReturn($destination . $oldName); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($targetDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') + ->willReturn($tmpDestination); + $driver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $actualName = $newName ?? $oldName; + $driver->expects(self::atLeastOnce())->method('rename') + ->with($tmpDestination . $actualName, $newDestination, $driver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver')->willReturn($driver); + $this->ioFile->method('getPathInfo') + ->willReturnMap( + [ + [$tmpDestination, ['dirname' => $tmpDestination, 'basename' => 'old_name.file']], + [$destination . $oldName, ['dirname' => $destination, 'basename' => 'old_name.file']] + ] + ); + $this->plugin->aroundSave($subject, $proceed, $destination . $oldName, $newName); + } + + /** + * @return array + */ + public function aroundSaveDataProvider(): array + { + return [ + 'with_new_name' => [ + 'destination' => 'destination/', + 'new_destination' => 'destination/new_name.file', + 'new_name' => 'new_name.file', + 'old_name' => null + ], + 'with_old_name' => [ + 'destination' => 'destination/', + 'new_destination' => 'destination/old_name.file', + 'new_name' => null, + 'old_name' => 'old_name.file' + ] + ]; + } + + /** + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + public function testBeforeOpen(): void + { + /** @var AbstractAdapter $subject */ + $subject = $this->getMockBuilder(AbstractAdapter::class) + ->disableOriginalConstructor() + ->getMock(); + $filename = '/path/file_name.file'; + $absolutePath = 'absolute' . $filename; + $tmpAbsolutePath = '/var/www/magento2/tmp'; + $tmpFilePath = $tmpAbsolutePath . 'file_name.file'; + $content = 'Just a test'; + + $targetDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $targetDriver->expects(self::atLeastOnce())->method('fileGetContents')->with($filename) + ->willReturn($content); + $tmpDriver = $this->getMockBuilder(DriverInterface::class) + ->disableOriginalConstructor() + ->getMock(); + $tmpDriver->expects(self::atLeastOnce())->method('filePutContents')->with($tmpFilePath, $content) + ->willReturn(true); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath')->with($filename) + ->willReturn($absolutePath); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('isFile')->with($absolutePath) + ->willReturn(true); + $this->targetDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($targetDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getDriver') + ->willReturn($tmpDriver); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('create'); + $this->tmpDirectoryWrite->expects(self::atLeastOnce())->method('getAbsolutePath') + ->willReturn($tmpAbsolutePath); + + self::assertEquals([$tmpFilePath], $this->plugin->beforeOpen($subject, $filename)); + } +} diff --git a/app/code/Magento/RemoteStorage/composer.json b/app/code/Magento/RemoteStorage/composer.json new file mode 100644 index 0000000000000..7345048a159e3 --- /dev/null +++ b/app/code/Magento/RemoteStorage/composer.json @@ -0,0 +1,32 @@ +{ + "name": "magento/module-remote-storage", + "description": "N/A", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "^100.0.2" + }, + "suggest": { + "magento/module-backend": "*", + "magento/module-sitemap": "*", + "magento/module-cms": "*", + "magento/module-downloadable": "*", + "magento/module-catalog": "*", + "magento/module-media-storage": "*", + "magento/module-import-export": "*", + "magento/module-catalog-import-export": "*", + "magento/module-downloadable-import-export": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\RemoteStorage\\": "" + } + } +} diff --git a/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..5009a05d8b602 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/adminhtml/system.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_storage_configuration"> + <field id="media_storage"> + <comment><![CDATA[<strong style="color:red">Warning!</strong> Database media storage will be ignored if remote storage is enabled.]]></comment> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/RemoteStorage/etc/di.xml b/app/code/Magento/RemoteStorage/etc/di.xml new file mode 100644 index 0000000000000..9fdde517b952c --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/di.xml @@ -0,0 +1,137 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <virtualType name="remoteWriteFactory" type="Magento\Framework\Filesystem\Directory\WriteFactory"> + <arguments> + <argument name="driverPool" xsi:type="object">Magento\RemoteStorage\Driver\DriverPool</argument> + </arguments> + </virtualType> + <virtualType name="remoteReadFactory" type="Magento\Framework\Filesystem\Directory\ReadFactory"> + <arguments> + <argument name="driverPool" xsi:type="object">Magento\RemoteStorage\Driver\DriverPool</argument> + </arguments> + </virtualType> + <type name="Magento\RemoteStorage\Filesystem"> + <arguments> + <argument name="writeFactory" xsi:type="object">remoteWriteFactory</argument> + <argument name="readFactory" xsi:type="object">remoteReadFactory</argument> + </arguments> + </type> + <virtualType name="customRemoteFilesystem" type="Magento\RemoteStorage\Filesystem"> + <arguments> + <argument name="directoryCodes" xsi:type="array"> + <item name="media" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::MEDIA</item> + <item name="var_export" xsi:type="const">Magento\Framework\App\Filesystem\DirectoryList::VAR_EXPORT</item> + </argument> + </arguments> + </virtualType> + <virtualType name="fullRemoteFilesystem" type="Magento\RemoteStorage\Filesystem" /> + <preference for="Magento\Framework\Filesystem" type="customRemoteFilesystem"/> + <type name="Magento\Framework\Filesystem\Directory\TargetDirectory"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + <argument name="driverCode" xsi:type="const">Magento\RemoteStorage\Driver\DriverPool::REMOTE</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Model\Sitemap"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Save"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Block\Adminhtml\Grid\Renderer\Link"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Sitemap\Controller\Adminhtml\Sitemap\Delete"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\Console\CommandListInterface"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="remoteStorageSync" xsi:type="object">Magento\RemoteStorage\Console\Command\RemoteStorageSynchronizeCommand</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\MaintenanceMode"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\MediaStorage\Model\File\Storage\Synchronization"> + <plugin name="remoteMedia" type="Magento\RemoteStorage\Plugin\MediaStorage" /> + </type> + <type name="Magento\Framework\Data\Collection\Filesystem"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\File\Mime"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\Image\Adapter\AbstractAdapter"> + <plugin name="remoteImageFile" type="Magento\RemoteStorage\Plugin\Image" sortOrder="10"/> + </type> + <type name="Magento\Catalog\Model\Category\FileInfo"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\Framework\Url\ScopeInterface"> + <plugin name="remoteUrl" type="Magento\RemoteStorage\Plugin\Scope"/> + </type> + <type name="Magento\ImportExport\Model\Import"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Model\Import\ImageDirectoryBaseProvider"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\ImportExport\Helper\Report"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\CatalogImportExport\Model\Import\Product"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\CatalogImportExport\Model\Import\Uploader"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\DownloadableImportExport\Helper\Uploader"> + <arguments> + <argument name="filesystem" xsi:type="object">fullRemoteFilesystem</argument> + </arguments> + </type> + <type name="Magento\RemoteStorage\Model\Synchronizer"> + <arguments> + <argument name="filesystem" xsi:type="object">customRemoteFilesystem</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/RemoteStorage/etc/module.xml b/app/code/Magento/RemoteStorage/etc/module.xml new file mode 100644 index 0000000000000..c06658c11ea90 --- /dev/null +++ b/app/code/Magento/RemoteStorage/etc/module.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_RemoteStorage" > + <sequence> + <module name="Magento_Backend"/> + <module name="Magento_Sitemap"/> + <module name="Magento_Store"/> + <module name="Magento_MediaStorage"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/RemoteStorage/registration.php b/app/code/Magento/RemoteStorage/registration.php new file mode 100644 index 0000000000000..3a6d6b67a8dcf --- /dev/null +++ b/app/code/Magento/RemoteStorage/registration.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_RemoteStorage', + __DIR__ +); diff --git a/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php b/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php index afa0ce79aca6e..1d65dd5874c6e 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Grid/Shopcart.php @@ -28,6 +28,7 @@ class Shopcart extends \Magento\Backend\Block\Widget\Grid\Extended /** * StoreIds setter + * * @codeCoverageIgnore * * @param array $storeIds @@ -46,6 +47,10 @@ public function setStoreIds($storeIds) */ public function getCurrentCurrencyCode() { + if (empty($this->_storeIds)) { + $this->setStoreIds(array_keys($this->_storeManager->getStores())); + } + if ($this->_currentCurrencyCode === null) { reset($this->_storeIds); $this->_currentCurrencyCode = count( diff --git a/app/code/Magento/Reports/Model/Product/DataRetriever.php b/app/code/Magento/Reports/Model/Product/DataRetriever.php new file mode 100644 index 0000000000000..c6260a4e7bacc --- /dev/null +++ b/app/code/Magento/Reports/Model/Product/DataRetriever.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Model\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Retrieve products data for reports by entity id's + */ +class DataRetriever +{ + /** + * @var ProductCollectionFactory + */ + private $productCollectionFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * DataRetriever constructor. + * + * @param ProductCollectionFactory $productCollectionFactory + * @param StoreManagerInterface $storeManager + */ + public function __construct( + ProductCollectionFactory $productCollectionFactory, + StoreManagerInterface $storeManager + ) { + $this->productCollectionFactory = $productCollectionFactory; + $this->storeManager = $storeManager; + } + + /** + * Retrieve products data by entity id's + * + * @param array $entityIds + * @return array + */ + public function execute(array $entityIds = []): array + { + $productCollection = $this->getProductCollection($entityIds); + + return $this->prepareDataByCollection($productCollection); + } + + /** + * Get product collection filtered by entity id's + * + * @param array $entityIds + * @return ProductCollection + */ + private function getProductCollection(array $entityIds = []): ProductCollection + { + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addAttributeToSelect('name'); + $productCollection->addIdFilter($entityIds); + $productCollection->addPriceData(null, $this->getWebsiteIdForFilter()); + + return $productCollection; + } + + /** + * Retrieve website id for filter collection + * + * @return int + */ + private function getWebsiteIdForFilter(): int + { + $defaultStoreView = $this->storeManager->getDefaultStoreView(); + if ($defaultStoreView) { + $websiteId = (int)$defaultStoreView->getWebsiteId(); + } else { + $websites = $this->storeManager->getWebsites(); + $website = reset($websites); + $websiteId = (int)$website->getId(); + } + + return $websiteId; + } + + /** + * Prepare data by collection + * + * @param ProductCollection $productCollection + * @return array + */ + private function prepareDataByCollection(ProductCollection $productCollection): array + { + $productsData = []; + foreach ($productCollection as $product) { + $productsData[$product->getId()] = $product->getData(); + } + + return $productsData; + } +} diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php index 16df2d30db40d..e7dc28eb74a49 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Item/Collection.php @@ -7,7 +7,8 @@ namespace Magento\Reports\Model\ResourceModel\Quote\Item; -use Magento\Framework\App\ResourceConnection; +use Magento\Framework\App\ObjectManager; +use Magento\Reports\Model\Product\DataRetriever as ProductDataRetriever; /** * Collection of Magento\Quote\Model\Quote\Item @@ -49,6 +50,11 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab */ protected $orderResource; + /** + * @var ProductDataRetriever + */ + private $productDataRetriever; + /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory * @param \Psr\Log\LoggerInterface $logger @@ -59,6 +65,9 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @param \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource + * @param ProductDataRetriever|null $productDataRetriever + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Data\Collection\EntityFactory $entityFactory, @@ -69,7 +78,8 @@ public function __construct( \Magento\Customer\Model\ResourceModel\Customer $customerResource, \Magento\Sales\Model\ResourceModel\Order\Collection $orderResource, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, - \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null + \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null, + ?ProductDataRetriever $productDataRetriever = null ) { parent::__construct( $entityFactory, @@ -82,6 +92,8 @@ public function __construct( $this->productResource = $productResource; $this->customerResource = $customerResource; $this->orderResource = $orderResource; + $this->productDataRetriever = $productDataRetriever + ?? ObjectManager::getInstance()->get(ProductDataRetriever::class); } /** @@ -225,7 +237,7 @@ protected function _afterLoad() foreach ($items as $item) { $productIds[] = $item->getProductId(); } - $productData = $this->getProductData($productIds); + $productData = $this->productDataRetriever->execute($productIds); $orderData = $this->getOrdersData($productIds); foreach ($items as $item) { $item->setId($item->getProductId()); diff --git a/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php new file mode 100644 index 0000000000000..25dcccdb1ef7a --- /dev/null +++ b/app/code/Magento/Reports/Test/Unit/Block/Adminhtml/Grid/ShopcartTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Test\Unit\Block\Adminhtml\Grid; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Reports\Block\Adminhtml\Grid\Shopcart; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\StoreManagerInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for class \Magento\Reports\Block\Adminhtml\Grid\Shopcart. + */ +class ShopcartTest extends TestCase +{ + /** + * @var Shopcart|MockObject + */ + private $model; + + /** + * @var StoreManagerInterface|MockObject + */ + private $storeManagerMock; + + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->storeManagerMock = $this->getMockForAbstractClass( + StoreManagerInterface::class, + [], + '', + true, + true, + true, + ['getStore'] + ); + + $this->model = $objectManager->getObject( + Shopcart::class, + ['_storeManager' => $this->storeManagerMock] + ); + } + + /** + * @param $storeIds + * + * @dataProvider getCurrentCurrencyCodeDataProvider + */ + public function testGetCurrentCurrencyCode($storeIds) + { + $storeMock = $this->getMockForAbstractClass( + StoreInterface::class, + [], + '', + true, + true, + true, + ['getBaseCurrencyCode'] + ); + + $this->model->setStoreIds($storeIds); + + if ($storeIds) { + $expectedCurrencyCode = 'EUR'; + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with($storeIds[0]) + ->willReturn($storeMock); + $storeMock->expects($this->once()) + ->method('getBaseCurrencyCode') + ->willReturn($expectedCurrencyCode); + } else { + $expectedCurrencyCode = 'USD'; + $this->storeManagerMock->expects($this->once()) + ->method('getStore') + ->with(1) + ->willReturn($storeMock); + $this->storeManagerMock->expects($this->once()) + ->method('getStores') + ->willReturn([1 => $storeMock]); + $storeMock->expects($this->once()) + ->method('getBaseCurrencyCode') + ->willReturn($expectedCurrencyCode); + } + + $currencyCode = $this->model->getCurrentCurrencyCode(); + $this->assertEquals($expectedCurrencyCode, $currencyCode); + } + + /** + * DataProvider for testGetCurrentCurrencyCode. + * + * @return array + */ + public function getCurrentCurrencyCodeDataProvider() + { + return [ + [[]], + [[2]], + ]; + } +} diff --git a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php index 6e7d5bdce16f5..90d224ee417db 100644 --- a/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php +++ b/app/code/Magento/Reports/Test/Unit/Model/ResourceModel/Report/Quote/CollectionTest.php @@ -7,15 +7,17 @@ namespace Magento\Reports\Test\Unit\Model\ResourceModel\Report\Quote; -use Magento\Eav\Model\Entity\AbstractEntity; +use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; -use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\DB\Select; use Magento\Framework\Event\ManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Model\ResourceModel\Quote; -use Magento\Reports\Model\ResourceModel\Quote\Collection; +use Magento\Reports\Model\Product\DataRetriever as ProductDataRetriever; +use Magento\Reports\Model\ResourceModel\Quote\Collection as QuoteCollection; +use Magento\Reports\Model\ResourceModel\Quote\Item\Collection as QuoteItemCollection; +use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -34,16 +36,22 @@ class CollectionTest extends TestCase */ protected $selectMock; + /** + * @var ProductDataRetriever|MockObject + */ + private $productDataRetriever; + protected function setUp(): void { $this->objectManager = new ObjectManager($this); $this->selectMock = $this->createMock(Select::class); + $this->productDataRetriever = $this->createMock(ProductDataRetriever::class); } public function testGetSelectCountSql() { /** @var MockObject $collection */ - $collection = $this->getMockBuilder(Collection::class) + $collection = $this->getMockBuilder(QuoteCollection::class) ->setMethods(['getSelect']) ->disableOriginalConstructor() ->getMock(); @@ -61,8 +69,8 @@ public function testPrepareActiveCartItems() { /** @var MockObject $collection */ $constructArgs = $this->objectManager - ->getConstructArguments(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class); - $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class) + ->getConstructArguments(QuoteItemCollection::class); + $collection = $this->getMockBuilder(QuoteItemCollection::class) ->setMethods(['getSelect', 'getTable', 'getFlag', 'setFlag']) ->disableOriginalConstructor() ->setConstructorArgs($constructArgs) @@ -88,18 +96,18 @@ public function testLoadWithFilter() { /** @var MockObject $collection */ $constructArgs = $this->objectManager - ->getConstructArguments(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class); + ->getConstructArguments(QuoteItemCollection::class); $constructArgs['eventManager'] = $this->getMockForAbstractClass(ManagerInterface::class); - $connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); $resourceMock = $this->createMock(Quote::class); $resourceMock->expects($this->any())->method('getConnection') ->willReturn($this->createMock(Mysql::class)); $constructArgs['resource'] = $resourceMock; - $productResourceMock = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); + $productResourceMock = $this->createMock(ProductCollection::class); $constructArgs['productResource'] = $productResourceMock; - $orderResourceMock = $this->createMock(\Magento\Sales\Model\ResourceModel\Order\Collection::class); + $orderResourceMock = $this->createMock(OrderCollection::class); $constructArgs['orderResource'] = $orderResourceMock; - $collection = $this->getMockBuilder(\Magento\Reports\Model\ResourceModel\Quote\Item\Collection::class) + $constructArgs['productDataRetriever'] = $this->productDataRetriever; + $collection = $this->getMockBuilder(QuoteItemCollection::class) ->setMethods( [ '_beforeLoad', @@ -129,24 +137,12 @@ public function testLoadWithFilter() //productLoad() $productAttributeMock = $this->createMock(AbstractAttribute::class); $priceAttributeMock = $this->createMock(AbstractAttribute::class); - $productResourceMock->expects($this->once())->method('getConnection')->willReturn($connectionMock); $productResourceMock->expects($this->any())->method('getAttribute') ->willReturnMap([['name', $productAttributeMock], ['price', $priceAttributeMock]]); - $productResourceMock->expects($this->once())->method('getSelect')->willReturn($this->selectMock); - $eavEntity = $this->createMock(AbstractEntity::class); - $eavEntity->expects($this->once())->method('getLinkField')->willReturn('entity_id'); - $productResourceMock->expects($this->once())->method('getEntity')->willReturn($eavEntity); - $this->selectMock->expects($this->once())->method('reset')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('from')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('useStraightJoin')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('joinInner')->willReturnSelf(); - $this->selectMock->expects($this->once())->method('joinLeft')->willReturnSelf(); $collection->expects($this->once())->method('getOrdersData')->willReturn([]); - $productAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf(); - $priceAttributeMock->expects($this->once())->method('getBackend')->willReturnSelf(); - $connectionMock->expects($this->once())->method('fetchAssoc')->willReturn([1, 2, 3]); //_afterLoad() $collection->expects($this->once())->method('getItems')->willReturn([]); + $this->productDataRetriever->expects($this->once())->method('execute')->willReturn([]); $collection->loadWithFilter(); } } diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 80e0ce168d7f5..1f23e4480ec1c 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -670,12 +670,14 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q $productOptions = $orderItem->getProductOptions(); if ($productOptions !== null && !empty($productOptions['options'])) { $formattedOptions = []; - $useFrontendCalendar = $this->useFrontendCalendar(); foreach ($productOptions['options'] as $option) { - if (in_array($option['option_type'], ['date', 'date_time']) && $useFrontendCalendar) { + if (in_array($option['option_type'], ['date', 'date_time', 'time', 'file'])) { $product->setSkipCheckRequiredOption(false); - break; + $formattedOptions[$option['option_id']] = + $buyRequest->getDataByKey('options')[$option['option_id']]; + continue; } + $formattedOptions[$option['option_id']] = $option['option_value']; } if (!empty($formattedOptions)) { @@ -2129,17 +2131,4 @@ private function isAddressesAreEqual(Order $order) return $shippingData == $billingData; } - - /** - * Use Calendar on frontend or not - * - * @return bool - */ - private function useFrontendCalendar(): bool - { - return (bool)$this->_scopeConfig->getValue( - 'catalog/custom_options/use_calendar', - \Magento\Store\Model\ScopeInterface::SCOPE_STORE - ); - } } diff --git a/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php b/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php new file mode 100644 index 0000000000000..256d097b9eef0 --- /dev/null +++ b/app/code/Magento/Sales/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCapture.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Plugin\Model\Service\Invoice; + +use Magento\Framework\DB\TransactionFactory; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Model\Service\InvoiceService; + +/** + * Plugin to add transaction comment after capture invoice + */ +class AddTransactionCommentAfterCapture +{ + /** + * @var InvoiceRepositoryInterface + */ + private $invoiceRepository; + + /** + * @var TransactionFactory + */ + private $transactionFactory; + + /** + * @param InvoiceRepositoryInterface $invoiceRepository + * @param TransactionFactory $transactionFactory + */ + public function __construct( + InvoiceRepositoryInterface $invoiceRepository, + TransactionFactory $transactionFactory + ) { + $this->transactionFactory = $transactionFactory; + $this->invoiceRepository = $invoiceRepository; + } + + /** + * Add transaction comment to the order after capture invoice + * + * @param InvoiceService $subject + * @param bool $result + * @param int $invoiceId + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSetCapture(InvoiceService $subject, bool $result, $invoiceId): bool + { + if ($result) { + $invoice = $this->invoiceRepository->get($invoiceId); + $invoice->getOrder()->setIsInProcess(true); + $this->transactionFactory->create() + ->addObject($invoice) + ->addObject($invoice->getOrder()) + ->save(); + } + + return $result; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php b/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php new file mode 100644 index 0000000000000..8d1a2f5256370 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Plugin/Model/Service/Invoice/AddTransactionCommentAfterCaptureTest.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Plugin\Model\Service\Invoice; + +use Magento\Framework\DB\Transaction; +use Magento\Framework\DB\TransactionFactory; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Service\InvoiceService; +use Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test to add transaction comment to the order after capture invoice + */ +class AddTransactionCommentAfterCaptureTest extends TestCase +{ + /** + * @var InvoiceRepositoryInterface|MockObject + */ + private $invoiceRepository; + + /** + * @var TransactionFactory|MockObject + */ + private $transactionFactory; + + /** + * @var AddTransactionCommentAfterCapture + */ + private $plugin; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->invoiceRepository = $this->createMock(InvoiceRepositoryInterface::class); + $this->transactionFactory = $this->createMock(TransactionFactory::class); + + $this->plugin = new AddTransactionCommentAfterCapture( + $this->invoiceRepository, + $this->transactionFactory + ); + } + + /** + * Test to add transaction comment after capture invoice + */ + public function testPlugin(): void + { + $result = true; + $invoiceId = 3; + + $orderMock = $this->createMock(Order::class); + $invoiceMock = $this->createMock(Invoice::class); + $invoiceMock->method('getOrder')->willReturn($orderMock); + $this->invoiceRepository->method('get')->with($invoiceId)->willReturn($invoiceMock); + + $transactionMock = $this->createMock(Transaction::class); + $transactionMock->expects($this->at(0))->method('addObject')->with($invoiceMock)->willReturnSelf(); + $transactionMock->expects($this->at(1))->method('addObject')->with($orderMock)->willReturnSelf(); + $transactionMock->expects($this->once())->method('save'); + $this->transactionFactory->method('create')->willReturn($transactionMock); + + /** @var InvoiceService $invoiceService */ + $invoiceService = $this->createMock(InvoiceService::class); + + $this->assertEquals( + $result, + $this->plugin->afterSetCapture($invoiceService, $result, $invoiceId) + ); + } +} diff --git a/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php new file mode 100644 index 0000000000000..c539e965b9df9 --- /dev/null +++ b/app/code/Magento/Sales/ViewModel/Customer/Address/Billing/AddressDataProvider.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\ViewModel\Customer\Address\Billing; + +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Sales\Model\AdminOrder\Create; + +/** + * Customer billing address data provider + */ +class AddressDataProvider implements ArgumentInterface +{ + /** + * @var Create + */ + private $orderCreate; + + /** + * Customer billing address + * + * @param Create $orderCreate + */ + public function __construct( + Create $orderCreate + ) { + $this->orderCreate = $orderCreate; + } + + /** + * Get save billing address in the address book + * + * @return int + */ + public function getSaveInAddressBook(): int + { + return (int)$this->orderCreate->getBillingAddress()->getSaveInAddressBook(); + } +} diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index ab524a0f552f6..491772e7e65a0 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -825,6 +825,9 @@ <index referenceId="SALES_SHIPMENT_GRID_BILLING_NAME" indexType="btree"> <column name="billing_name"/> </index> + <index referenceId="SALES_SHIPMENT_GRID_ORDER_ID" indexType="btree"> + <column name="order_id"/> + </index> <index referenceId="FTI_086B40C8955F167B8EA76653437879B4" indexType="fulltext"> <column name="increment_id"/> <column name="order_increment_id"/> diff --git a/app/code/Magento/Sales/etc/db_schema_whitelist.json b/app/code/Magento/Sales/etc/db_schema_whitelist.json index 087fe6c9eb5ac..02efd7d5a0050 100644 --- a/app/code/Magento/Sales/etc/db_schema_whitelist.json +++ b/app/code/Magento/Sales/etc/db_schema_whitelist.json @@ -479,6 +479,7 @@ "SALES_SHIPMENT_GRID_ORDER_CREATED_AT": true, "SALES_SHIPMENT_GRID_SHIPPING_NAME": true, "SALES_SHIPMENT_GRID_BILLING_NAME": true, + "SALES_SHIPMENT_GRID_ORDER_ID": true, "FTI_086B40C8955F167B8EA76653437879B4": true }, "constraint": { diff --git a/app/code/Magento/Sales/etc/webapi_rest/di.xml b/app/code/Magento/Sales/etc/webapi_rest/di.xml index 5d7838297a7c7..12ad410279a08 100644 --- a/app/code/Magento/Sales/etc/webapi_rest/di.xml +++ b/app/code/Magento/Sales/etc/webapi_rest/di.xml @@ -19,4 +19,7 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> + </type> </config> diff --git a/app/code/Magento/Sales/etc/webapi_soap/di.xml b/app/code/Magento/Sales/etc/webapi_soap/di.xml index 5d7838297a7c7..12ad410279a08 100644 --- a/app/code/Magento/Sales/etc/webapi_soap/di.xml +++ b/app/code/Magento/Sales/etc/webapi_soap/di.xml @@ -19,4 +19,7 @@ </argument> </arguments> </type> + <type name="Magento\Sales\Model\Service\InvoiceService"> + <plugin name="addTransactionCommentAfterCapture" type="Magento\Sales\Plugin\Model\Service\Invoice\AddTransactionCommentAfterCapture"/> + </type> </config> diff --git a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml index c52f81d5cb56d..91148d86055fc 100644 --- a/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml +++ b/app/code/Magento/Sales/view/adminhtml/layout/sales_order_create_load_block_billing_address.xml @@ -12,6 +12,7 @@ <arguments> <argument name="customerAddressFormatter" xsi:type="object">Magento\Sales\ViewModel\Customer\AddressFormatter</argument> <argument name="customerAddressCollection" xsi:type="object">Magento\Customer\Model\ResourceModel\Address\Collection</argument> + <argument name="billingAddressDataProvider" xsi:type="object">Magento\Sales\ViewModel\Customer\Address\Billing\AddressDataProvider</argument> </arguments> </block> </referenceContainer> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 12927dcf526a3..69b26d70e684a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -24,6 +24,11 @@ endif; */ $customerAddressFormatter = $block->getData('customerAddressFormatter'); +/** + * @var \Magento\Sales\ViewModel\Customer\Address\Billing\AddressDataProvider $billingAddressDataProvider + */ +$billingAddressDataProvider = $block->getData('billingAddressDataProvider'); + /** * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address| * \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block @@ -114,7 +119,8 @@ endif; ?> type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1" - <?php if (!$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> + <?php if ($billingAddressDataProvider && $billingAddressDataProvider->getSaveInAddressBook() || + $block->getIsShipping() && !$block->getDontSaveInAddressBook() && !$block->getAddressId()): ?> checked="checked" <?php endif; ?> class="admin__control-checkbox"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 6577ff9440456..b3d81cea7f97f 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -59,8 +59,8 @@ <argument name="consumerName" value="{{AdminCodeGeneratorMessageConsumerData.consumerName}}"/> <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> </actionGroup> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitFormToReload1"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1"/> <click selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" stepKey="expandCouponSection2"/> <!-- Assert coupon codes grid header is correct --> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml index 7a12bca389672..74542be376c45 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontAutoGeneratedCouponCodeTest.xml @@ -63,8 +63,8 @@ <argument name="consumerName" value="{{AdminCodeGeneratorMessageConsumerData.consumerName}}"/> <argument name="maxMessages" value="{{AdminCodeGeneratorMessageConsumerData.messageLimit}}"/> </actionGroup> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitFormToReload1"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitFormToReload1"/> <conditionalClick selector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" dependentSelector="{{AdminCartPriceRulesFormSection.manageCouponCodesHeader}}" visible="true" stepKey="clickManageCouponCodes2"/> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml index c3418049a38a0..71299b33ff159 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml @@ -227,8 +227,8 @@ <div class="grid_prepare admin__page-subsection"></div> </div> </section> - <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <div id="packages_content"></div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <?php $scriptString = <<<script require(['jquery'], function($){ $("div#packages_content").on('click', "button[data-action='package-save-items']", diff --git a/app/code/Magento/Sitemap/Block/Robots.php b/app/code/Magento/Sitemap/Block/Robots.php index a074e95ce2f80..2fe7f8807d6a0 100644 --- a/app/code/Magento/Sitemap/Block/Robots.php +++ b/app/code/Magento/Sitemap/Block/Robots.php @@ -11,6 +11,7 @@ use Magento\Robots\Model\Config\Value; use Magento\Sitemap\Helper\Data as SitemapHelper; use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; +use Magento\Sitemap\Model\Sitemap; use Magento\Sitemap\Model\SitemapConfigReader; use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; @@ -115,6 +116,9 @@ protected function getSitemapLinks(array $storeIds) $collection->addStoreFilter($storeIds); $sitemapLinks = []; + /** + * @var Sitemap $sitemap + */ foreach ($collection as $sitemap) { $sitemapUrl = $sitemap->getSitemapUrl($sitemap->getSitemapPath(), $sitemap->getSitemapFilename()); $sitemapLinks[$sitemapUrl] = 'Sitemap: ' . $sitemapUrl; diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 9a8d2c57a280c..ddb04f28d58d1 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -475,12 +475,9 @@ public function generateXml() if ($this->_sitemapIncrement == 1) { // In case when only one increment file was created use it as default sitemap - $path = rtrim( - $this->getSitemapPath(), - '/' - ) . '/' . $this->_getCurrentSitemapFilename( - $this->_sitemapIncrement - ); + $path = rtrim($this->getSitemapPath(), '/') + . '/' + . $this->_getCurrentSitemapFilename($this->_sitemapIncrement); $destination = rtrim($this->getSitemapPath(), '/') . '/' . $this->getSitemapFilename(); $this->_directory->renameFile($path, $destination); diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php index 116a574b7c670..26f1f9cd6f56f 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/ItemProvider/ProductTest.php @@ -61,7 +61,7 @@ public function testGetItems(array $products) */ public function productProvider() { - $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; + $storeBaseMediaUrl = 'http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; return [ [ [ diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php index bfd2c47164cf6..866b3afd322a0 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Model/SitemapTest.php @@ -533,7 +533,7 @@ protected function getModelMock($mockBeforeSave = false) $methods[] = 'beforeSave'; } - $storeBaseMediaUrl = 'http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; + $storeBaseMediaUrl = 'http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/'; $this->itemProviderMock->expects($this->any()) ->method('getItems') diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml index ff8087a52e42f..03cfdaaead18a 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-1-4.xml @@ -13,18 +13,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml index 93b9e159d4b04..f9913d5070fbd 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml +++ b/app/code/Magento/Sitemap/Test/Unit/Model/_files/sitemap-single.xml @@ -31,18 +31,18 @@ <changefreq>monthly</changefreq> <priority>0.5</priority> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image1.png</image:loc> <image:title>Product & > title < "</image:title> <image:caption>Copyright © caption &trade; & > title < "</image:caption> </image:image> <image:image> - <image:loc>http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> + <image:loc>http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/i/m/image_no_caption.png</image:loc> <image:title>Product & > title < "</image:title> </image:image> <PageMap xmlns="http://www.google.com/schemas/sitemap-pagemap/1.0"> <DataObject type="thumbnail"> <Attribute name="name" value="Product & > title < ""/> - <Attribute name="src" value="http://store.com/pub/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> + <Attribute name="src" value="http://store.com/media/catalog/product/cache/c9e0b0ef589f3508e5ba515cde53c5ff/t/h/thumbnail.jpg"/> </DataObject> </PageMap> </url> diff --git a/app/code/Magento/Sitemap/etc/config.xml b/app/code/Magento/Sitemap/etc/config.xml index 36b2cc2207422..614421b9dd752 100644 --- a/app/code/Magento/Sitemap/etc/config.xml +++ b/app/code/Magento/Sitemap/etc/config.xml @@ -57,5 +57,12 @@ </jobs> </default> </crontab> + <system> + <media_storage_configuration> + <allowed_resources> + <sitemap_folder>sitemap</sitemap_folder> + </allowed_resources> + </media_storage_configuration> + </system> </default> </config> diff --git a/app/code/Magento/Sitemap/etc/di.xml b/app/code/Magento/Sitemap/etc/di.xml index 4c4a5f98f737a..4771da2f11144 100644 --- a/app/code/Magento/Sitemap/etc/di.xml +++ b/app/code/Magento/Sitemap/etc/di.xml @@ -52,4 +52,16 @@ <argument name="configReader" xsi:type="object">Magento\Sitemap\Model\ItemProvider\CmsPageConfigReader</argument> </arguments> </type> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="dirs" xsi:type="array"> + <item name="exclude" xsi:type="array"> + <item name="sitemap" xsi:type="array"> + <item name="regexp" xsi:type="boolean">true</item> + <item name="name" xsi:type="string">media[/\\]+sitemap[/\\]*$</item> + </item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php index c17e2846e22df..0622869c0b963 100644 --- a/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php +++ b/app/code/Magento/Store/Test/Unit/Model/Service/StoreConfigManagerTest.php @@ -136,10 +136,10 @@ public function testGetStoreConfigs() $secureBaseUrl = 'https://magento/base_url'; $baseLinkUrl = 'http://magento/base_url/links'; $secureBaseLinkUrl = 'https://magento/base_url/links'; - $baseStaticUrl = 'http://magento/base_url/pub/static'; + $baseStaticUrl = 'http://magento/base_url/static'; $secureBaseStaticUrl = 'https://magento/base_url/static'; - $baseMediaUrl = 'http://magento/base_url/pub/media'; - $secureBaseMediaUrl = 'https://magento/base_url/pub/media'; + $baseMediaUrl = 'http://magento/base_url/media'; + $secureBaseMediaUrl = 'https://magento/base_url/media'; $locale = 'en_US'; $timeZone = 'America/Los_Angeles'; $baseCurrencyCode = 'USD'; diff --git a/app/code/Magento/Swatches/Helper/Data.php b/app/code/Magento/Swatches/Helper/Data.php index d2cd1baca894b..dd257de331b91 100644 --- a/app/code/Magento/Swatches/Helper/Data.php +++ b/app/code/Magento/Swatches/Helper/Data.php @@ -310,7 +310,7 @@ private function addFilterByParent(ProductCollection $productCollection, $parent * Method getting full media gallery for current Product * * Array structure: [ - * ['image'] => 'http://url/pub/media/catalog/product/2/0/blabla.jpg', + * ['image'] => 'http://url/media/catalog/product/2/0/blabla.jpg', * ['mediaGallery'] => [ * galleryImageId1 => simpleProductImage1.jpg, * galleryImageId2 => simpleProductImage2.jpg, diff --git a/app/code/Magento/Swatches/Helper/Media.php b/app/code/Magento/Swatches/Helper/Media.php index f3694515ecb26..6787fba534893 100644 --- a/app/code/Magento/Swatches/Helper/Media.php +++ b/app/code/Magento/Swatches/Helper/Media.php @@ -6,8 +6,9 @@ namespace Magento\Swatches\Helper; use Magento\Catalog\Helper\Image; -use Magento\Framework\App\Area; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; /** * Helper to move images from tmp to catalog directory @@ -72,6 +73,11 @@ class Media extends \Magento\Framework\App\Helper\AbstractHelper */ private $imageConfig; + /** + * @var string + */ + private $mediaUrlFormat; + /** * @param \Magento\Catalog\Model\Product\Media\Config $mediaConfig * @param \Magento\Framework\Filesystem $filesystem @@ -80,6 +86,8 @@ class Media extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\Image\Factory $imageFactory * @param \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection * @param \Magento\Framework\View\ConfigInterface $configInterface + * @param CatalogMediaConfig $catalogMediaConfig + * @throws \Magento\Framework\Exception\FileSystemException */ public function __construct( \Magento\Catalog\Model\Product\Media\Config $mediaConfig, @@ -88,7 +96,8 @@ public function __construct( \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Image\Factory $imageFactory, \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection, - \Magento\Framework\View\ConfigInterface $configInterface + \Magento\Framework\View\ConfigInterface $configInterface, + CatalogMediaConfig $catalogMediaConfig = null ) { $this->mediaConfig = $mediaConfig; $this->fileStorageDb = $fileStorageDb; @@ -97,6 +106,9 @@ public function __construct( $this->imageFactory = $imageFactory; $this->themeCollection = $themeCollection; $this->viewConfig = $configInterface; + + $catalogMediaConfig = $catalogMediaConfig ?: ObjectManager::getInstance()->get(CatalogMediaConfig::class); + $this->mediaUrlFormat = $catalogMediaConfig->getMediaUrlFormat(); } /** @@ -106,17 +118,35 @@ public function __construct( */ public function getSwatchAttributeImage($swatchType, $file) { - $generationPath = $swatchType . '/' . $this->getFolderNameSize($swatchType) . $file; - $absoluteImagePath = $this->mediaDirectory - ->getAbsolutePath($this->getSwatchMediaPath() . '/' . $generationPath); - if (!file_exists($absoluteImagePath)) { - try { - $this->generateSwatchVariations($file); - } catch (\Exception $e) { - return ''; + $basePath = $this->getSwatchMediaUrl(); + + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $generationPath = $swatchType . '/' . $this->getFolderNameSize($swatchType) . $file; + $absoluteImagePath = $this->mediaDirectory + ->getAbsolutePath($this->getSwatchMediaPath() . '/' . $generationPath); + if (!$this->mediaDirectory->isExist(($absoluteImagePath))) { + try { + $this->generateSwatchVariations($file); + } catch (\Exception $e) { + return ''; + } } + + return $basePath . '/' . $generationPath; } - return $this->getSwatchMediaUrl() . '/' . $generationPath; + + return $basePath . '/' . $this->getRelativeTransformationParametersPath($swatchType, $file); + } + + private function getRelativeTransformationParametersPath($swatchType, $file) + { + $imageConfig = $this->getImageConfig(); + return $this->prepareFile($file) . '?' . http_build_query([ + 'width' => $imageConfig[$swatchType]['width'], + 'height' => $imageConfig[$swatchType]['height'], + 'store' => $this->storeManager->getStore()->getCode(), + 'image-type' => $swatchType + ]); } /** @@ -156,7 +186,7 @@ public function moveImageFromTmp($file) /** * Check whether file to move exists. Getting unique name * - * @param <type> $file + * @param string $file * @return string */ protected function getUniqueFileName($file) @@ -167,14 +197,19 @@ protected function getUniqueFileName($file) $file ); } else { - $destFile = dirname($file) . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( - $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($file)) + $destFile = rtrim(dirname($file), '/.') . '/' . \Magento\MediaStorage\Model\File\Uploader::getNewFileName( + $this->getOriginalFilePath($file) ); } return $destFile; } + private function getOriginalFilePath($file) + { + return $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($file)); + } + /** * Generate swatch thumb and small swatch image * @@ -183,16 +218,19 @@ protected function getUniqueFileName($file) */ public function generateSwatchVariations($imageUrl) { - $absoluteImagePath = $this->mediaDirectory->getAbsolutePath($this->getAttributeSwatchPath($imageUrl)); - foreach ($this->swatchImageTypes as $swatchType) { - $imageConfig = $this->getImageConfig(); - $swatchNamePath = $this->generateNamePath($imageConfig, $imageUrl, $swatchType); - $image = $this->imageFactory->create($absoluteImagePath); - $this->setupImageProperties($image); - $image->resize($imageConfig[$swatchType]['width'], $imageConfig[$swatchType]['height']); - $this->setupImageProperties($image, true); - $image->save($swatchNamePath['path_for_save'], $swatchNamePath['name']); + if ($this->mediaUrlFormat === CatalogMediaConfig::HASH) { + $absoluteImagePath = $this->getOriginalFilePath($imageUrl); + foreach ($this->swatchImageTypes as $swatchType) { + $imageConfig = $this->getImageConfig(); + $swatchNamePath = $this->generateNamePath($imageConfig, $imageUrl, $swatchType); + $image = $this->imageFactory->create($absoluteImagePath); + $this->setupImageProperties($image); + $image->resize($imageConfig[$swatchType]['width'], $imageConfig[$swatchType]['height']); + $this->setupImageProperties($image, true); + $image->save($swatchNamePath['path_for_save'], $swatchNamePath['name']); + } } + return $this; } @@ -281,7 +319,7 @@ protected function prepareFileName($imageUrl) } /** - * Url type http://url/pub/media/attribute/swatch/ + * Url type http://url/media/attribute/swatch/ * * @return string */ diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index 2dba9b293453c..7647d3ec87a02 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -154,8 +154,8 @@ </actionGroup> <!-- Verify swatch tooltips are not visible --> - <reloadPage stepKey="refreshPage"/> - <waitForPageLoad stepKey="waitForPageReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="refreshPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageReload"/> <moveMouseOver selector="{{StorefrontProductInfoMainSection.nthSwatchOption('1')}}" stepKey="hoverDisabledSwatch"/> <wait time="1" stepKey="waitForTooltip2"/> <dontSeeElement selector="{{StorefrontProductInfoMainSection.swatchOptionTooltip}}" stepKey="swatchTooltipNotVisible"/> diff --git a/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php b/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php index e4988bdf9308c..9e9978b499150 100644 --- a/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Helper/MediaTest.php @@ -7,13 +7,16 @@ namespace Magento\Swatches\Test\Unit\Helper; +use Magento\Catalog\Model\Config\CatalogMediaConfig; use Magento\Catalog\Model\Product\Media\Config; use Magento\Framework\Config\View; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Framework\Image; use Magento\Framework\Image\Factory; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Store\Model\Store; @@ -59,8 +62,23 @@ class MediaTest extends TestCase /** @var Media|ObjectManager */ protected $mediaHelperObject; + /** @var CatalogMediaConfig|MockObject */ + private $catalogMediaConfigMock; + + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $objectManager = new ObjectManager($this); $this->mediaConfigMock = $this->createMock(Config::class); @@ -78,6 +96,9 @@ protected function setUp(): void $this->storeMock = $this->createPartialMock(Store::class, ['getBaseUrl']); + $this->catalogMediaConfigMock = $this->createPartialMock(CatalogMediaConfig::class, ['getMediaUrlFormat']); + $this->catalogMediaConfigMock->method('getMediaUrlFormat')->willReturn(CatalogMediaConfig::HASH); + $this->mediaDirectoryMock = $this->createMock(Write::class); $this->fileSystemMock = $this->createPartialMock(Filesystem::class, ['getDirectoryWrite']); $this->fileSystemMock @@ -94,6 +115,7 @@ protected function setUp(): void 'storeManager' => $this->storeManagerMock, 'imageFactory' => $this->imageFactoryMock, 'configInterface' => $this->viewConfigMock, + 'catalogMediaConfig' => $this->catalogMediaConfigMock, ] ); } @@ -112,7 +134,7 @@ public function testGetSwatchAttributeImage($swatchType, $expectedResult) ->expects($this->once()) ->method('getBaseUrl') ->with('media') - ->willReturn('http://url/pub/media/'); + ->willReturn('http://url/media/'); $this->generateImageConfig(); @@ -120,7 +142,7 @@ public function testGetSwatchAttributeImage($swatchType, $expectedResult) $result = $this->mediaHelperObject->getSwatchAttributeImage($swatchType, '/f/i/file.png'); - $this->assertEquals($result, $expectedResult); + $this->assertEquals($expectedResult, $result); } /** @@ -131,11 +153,11 @@ public function dataForFullPath() return [ [ 'swatch_image', - 'http://url/pub/media/attribute/swatch/swatch_image/30x20/f/i/file.png', + 'http://url/media/attribute/swatch/swatch_image/30x20/f/i/file.png', ], [ 'swatch_thumb', - 'http://url/pub/media/attribute/swatch/swatch_thumb/110x90/f/i/file.png', + 'http://url/media/attribute/swatch/swatch_thumb/110x90/f/i/file.png', ], ]; } @@ -153,6 +175,10 @@ public function testMoveImageFromTmpNoDb() { $this->fileStorageDbMock->method('checkDbUsage')->willReturn(false); $this->fileStorageDbMock->method('renameFile')->willReturnSelf(); + $this->mediaDirectoryMock + ->expects($this->atLeastOnce()) + ->method('getAbsolutePath') + ->willReturn('attribute/swatch/f/i/file.tmp'); $result = $this->mediaHelperObject->moveImageFromTmp('file.tmp'); $this->assertNotNull($result); } @@ -177,7 +203,7 @@ public function testGenerateSwatchVariations() $this->imageFactoryMock->expects($this->any())->method('create')->willReturn($image); $this->generateImageConfig(); - $image->expects($this->any())->method('resize')->willReturnSelf(); + $image->method('resize')->willReturnSelf(); $image->expects($this->atLeastOnce())->method('backgroundColor')->with([255, 255, 255])->willReturnSelf(); $this->mediaHelperObject->generateSwatchVariations('/e/a/earth.png'); } @@ -195,11 +221,11 @@ public function testGetSwatchMediaUrl() ->expects($this->once()) ->method('getBaseUrl') ->with('media') - ->willReturn('http://url/pub/media/'); + ->willReturn('http://url/media/'); $result = $this->mediaHelperObject->getSwatchMediaUrl(); - $this->assertEquals($result, 'http://url/pub/media/attribute/swatch'); + $this->assertEquals($result, 'http://url/media/attribute/swatch'); } /** @@ -282,7 +308,7 @@ protected function generateImageConfig() ], ]; - $configMock->expects($this->any())->method('getMediaEntities')->willReturn($imageConfig); + $configMock->method('getMediaEntities')->willReturn($imageConfig); } public function testGetAttributeSwatchPath() diff --git a/app/code/Magento/Swatches/etc/config.xml b/app/code/Magento/Swatches/etc/config.xml index 9d36d9692b295..236e9237fb29b 100644 --- a/app/code/Magento/Swatches/etc/config.xml +++ b/app/code/Magento/Swatches/etc/config.xml @@ -14,6 +14,13 @@ <show_swatch_tooltip>1</show_swatch_tooltip> </frontend> </catalog> + <system> + <media_storage_configuration> + <allowed_resources> + <swatches_folder>attribute</swatches_folder> + </allowed_resources> + </media_storage_configuration> + </system> <general> <validator_data> <input_types> diff --git a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php index 844cfc535cfb2..f23fe8ffae7ae 100644 --- a/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php +++ b/app/code/Magento/TaxImportExport/Controller/Adminhtml/Rate/ExportPost.php @@ -81,7 +81,10 @@ public function execute() $content .= $rate->toString($template) . "\n"; } - return $this->fileFactory->create('tax_rates.csv', $content, DirectoryList::VAR_DIR); + // pass 'rm' parameter to delete a file after download + $fileContent = ['type' => 'string', 'value' => $content, 'rm' => true]; + + return $this->fileFactory->create('tax_rates.csv', $fileContent, DirectoryList::VAR_DIR); } /** diff --git a/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php index 0c8d0cf80544b..f4d31f3e421eb 100644 --- a/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php +++ b/app/code/Magento/TaxImportExport/Test/Unit/Controller/Adminhtml/Rate/ExportPostTest.php @@ -101,10 +101,11 @@ public function testExecute() ]); $rateCollectionMock->expects($this->once())->method('joinCountryTable')->willReturnSelf(); $rateCollectionMock->expects($this->once())->method('joinRegionTable')->willReturnSelf(); + $fileContent = ['type' => 'string', 'value' => $content, 'rm' => true]; $this->fileFactoryMock ->expects($this->once()) ->method('create') - ->with('tax_rates.csv', $content, DirectoryList::VAR_DIR); + ->with('tax_rates.csv', $fileContent, DirectoryList::VAR_DIR); $this->controller->execute(); } } diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php index 0bbf35e244241..1978362810763 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/Header/LogoTest.php @@ -35,7 +35,7 @@ public function testGetLogoSrc() )->method( 'getBaseUrl' )->willReturn( - 'http://localhost/pub/media/' + 'http://localhost/media/' ); $mediaDirectory->expects($this->any())->method('isFile')->willReturn(true); @@ -53,7 +53,7 @@ public function testGetLogoSrc() ]; $block = $objectManager->getObject(Logo::class, $arguments); - $this->assertEquals('http://localhost/pub/media/logo/default/image.gif', $block->getLogoSrc()); + $this->assertEquals('http://localhost/media/logo/default/image.gif', $block->getLogoSrc()); } /** diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php index 78a56013ae042..691a94e37e932 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php @@ -195,7 +195,7 @@ public function testAfterLoad() $this->urlBuilder->expects($this->once()) ->method('getBaseUrl') ->with(['_type' => UrlInterface::URL_TYPE_MEDIA]) - ->willReturn('http://magento2.com/pub/media/'); + ->willReturn('http://magento2.com/media/'); $this->mediaDirectory->expects($this->once()) ->method('getRelativePath') ->with('value') @@ -212,7 +212,7 @@ public function testAfterLoad() $this->assertEquals( [ [ - 'url' => 'http://magento2.com/pub/media/design/file/' . $value, + 'url' => 'http://magento2.com/media/design/file/' . $value, 'file' => $value, 'size' => 234234, 'exists' => true, @@ -241,7 +241,7 @@ public function testBeforeSave(string $fileName) 'scope_id' => 1, 'value' => [ [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $fileName, + 'url' => 'http://magento2.com/media/tmp/image/' . $fileName, 'file' => $fileName, 'size' => 234234, ] @@ -314,7 +314,7 @@ public function testBeforeSaveWithExistingFile() [ 'value' => [ [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $value, + 'url' => 'http://magento2.com/media/tmp/image/' . $value, 'file' => $value, 'size' => 234234, 'exists' => true @@ -358,7 +358,7 @@ public function getRelativeMediaPathDataProvider(): array { return [ 'Normal path' => ['pub/media/', 'filename.jpg'], - 'Complex path' => ['some_path/pub/media/', 'filename.jpg'], + 'Complex path' => ['some_path/media/', 'filename.jpg'], ]; } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php index ab7d622801f63..c16d7a49a7e6f 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Config/FileUploader/FileProcessorTest.php @@ -111,7 +111,7 @@ public function testSaveToTmp() $this->store->expects($this->once()) ->method('getBaseUrl') ->with(UrlInterface::URL_TYPE_MEDIA) - ->willReturn('http://magento2.com/pub/media/'); + ->willReturn('http://magento2.com/media/'); $this->directoryWrite->expects($this->once()) ->method('getAbsolutePath') ->with('tmp/' . FileProcessor::FILE_DIR) @@ -160,7 +160,7 @@ public function testSaveToTmp() 'name' => 'file.jpg', 'size' => '234234', 'type' => 'image/jpg', - 'url' => 'http://magento2.com/pub/media/tmp/' . FileProcessor::FILE_DIR . '/file.jpg' + 'url' => 'http://magento2.com/media/tmp/' . FileProcessor::FILE_DIR . '/file.jpg' ], $this->fileProcessor->saveToTmp($fieldCode) ); diff --git a/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php b/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php index 77cf71f75ac28..0ccaf9e65b675 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Favicon/FaviconTest.php @@ -105,7 +105,7 @@ public function testGetFaviconFileNegative() public function testGetFaviconFile() { $scopeConfigValue = 'path'; - $urlToMediaDir = 'http://magento.url/pub/media/'; + $urlToMediaDir = 'http://magento.url/media/'; $expectedFile = ImageFavicon::UPLOAD_DIR . '/' . $scopeConfigValue; $expectedUrl = $urlToMediaDir . $expectedFile; diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index e30ab98982b78..4eff032ce160e 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -120,8 +120,8 @@ </actionGroup> <!-- 3. Go to storefront and click on cart button on the top --> - <reloadPage stepKey="reloadPage"/> - <waitForPageLoad stepKey="waitForReload"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPage"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForReload"/> <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> <!-- Check button "Proceed to Checkout". There must be red borders and "book" icons on labels that can be translated. --> @@ -490,8 +490,9 @@ <resetCookie userInput="mage-translation-file-version" stepKey="resetTranslationFileVersion"/> <!-- Reload page after full clear --> - <reloadPage stepKey="reloadPageAfterFullClean"/> - <waitForPageLoad stepKey="waitForPageLoadAfterFullClean"/> + <actionGroup ref="ReloadPageActionGroup" stepKey="reloadPageAfterFullClean"/> + <comment userInput="Replacing reload action and preserve Backward Compatibility" stepKey="waitForPageLoadAfterFullClean"/> + <!-- Add product to cart and go through Checkout process like you did in steps ##3-6 and check translation you maid. --> <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProductPage1"> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml new file mode 100644 index 0000000000000..5928833bf4794 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAssertNumberOfRecordsInUiGridActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertNumberOfRecordsInUiGridActionGroup"> + <annotations> + <description>Validates that the Number of Records listed on the Ui grid page is present and correct.</description> + </annotations> + <arguments> + <argument name="number" type="string" defaultValue="1"/> + </arguments> + <see userInput="{{number}} records found" selector="{{AdminGridHeaders.totalRecords}}" stepKey="seeRecords"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml new file mode 100644 index 0000000000000..3976a2ac0f872 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/ReloadPageActionGroup.xml @@ -0,0 +1,19 @@ +<?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="ReloadPageActionGroup"> + <annotations> + <description>Reload page and wait for page load.</description> + </annotations> + + <reloadPage stepKey="reloadPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml index 89831359657bf..c7aa7604d7ade 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminGridControlsSection/AdminGridHeadersSection.xml @@ -11,5 +11,6 @@ <element name="title" type="text" selector=".page-title-wrapper h1"/> <element name="headerByName" type="text" selector="//div[@data-role='grid-wrapper']//span[@class='data-grid-cell-content' and contains(text(), '{{var1}}')]/parent::*" parameterized="true"/> <element name="columnsNames" type="text" selector="[data-role='grid-wrapper'] .data-grid-th > span"/> + <element name="totalRecords" type="text" selector="div.admin__data-grid-header-row.row.row-gutter div.row div.admin__control-support-text"/> </section> </sections> diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/region.js b/app/code/Magento/Ui/view/base/web/js/form/element/region.js index cd9c2aee85dc6..68b480d25a38c 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/region.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/region.js @@ -23,6 +23,22 @@ define([ } }, + /** + * {@inheritdoc} + */ + initialize: function () { + var option; + + this._super(); + + option = _.find(this.countryOptions, function (row) { + return row['is_default'] === true; + }); + this.hideRegion(option); + + return this; + }, + /** * Method called every time country selector's value gets changed. * Updates all validations and requirements for certain country. @@ -42,16 +58,9 @@ define([ return; } - defaultPostCodeResolver.setUseDefaultPostCode(!option['is_zipcode_optional']); - - if (option['is_region_visible'] === false) { - // Hide select and corresponding text input field if region must not be shown for selected country. - this.setVisible(false); + this.hideRegion(option); - if (this.customEntry) { // eslint-disable-line max-depth - this.toggleInput(false); - } - } + defaultPostCodeResolver.setUseDefaultPostCode(!option['is_zipcode_optional']); isRegionRequired = !this.skipValidation && !!option['is_region_required']; @@ -67,7 +76,24 @@ define([ input.validation['required-entry'] = isRegionRequired; input.validation['validate-not-number-first'] = !this.options().length; }.bind(this)); + }, + + /** + * Hide select and corresponding text input field if region must not be shown for selected country. + * + * @private + * @param {Object}option + */ + hideRegion: function (option) { + if (!option || option['is_region_visible'] !== false) { + return; + } + + this.setVisible(false); + + if (this.customEntry) { + this.toggleInput(false); + } } }); }); - diff --git a/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js b/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js index 1f5a4210793ba..1f25e0d2c089f 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/key-codes.js @@ -21,6 +21,7 @@ define([], function () { 17: 'ctrlKey', 18: 'altKey', 16: 'shiftKey', + 191: 'forwardSlashKey', 66: 'bKey', 73: 'iKey', 85: 'uKey' diff --git a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html index 1bff60064b983..cbb00f379a655 100644 --- a/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html +++ b/app/code/Magento/Ui/view/base/web/templates/dynamic-rows/cells/thumbnail.html @@ -4,4 +4,4 @@ * See COPYING.txt for license details. */ --> -<img class = 'admin__control-thumbnail' data-bind="attr: {src: $data.value}"> +<img class = 'admin__control-thumbnail' style="max-height: 75px; max-width: 75px;" data-bind="attr: {src: $data.value}"> diff --git a/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php index 7afc9dc93f46e..6a23b5c66e5ba 100644 --- a/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Widget/Test/Unit/Model/Template/FilterTest.php @@ -267,7 +267,7 @@ public function testMediaDirective() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; $this->storeMock->expects($this->once()) ->method('getBaseUrl') @@ -285,7 +285,7 @@ public function testMediaDirectiveWithEncodedQuotes() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; $this->storeMock->expects($this->once()) ->method('getBaseUrl') diff --git a/app/code/Magento/Wishlist/CustomerData/Wishlist.php b/app/code/Magento/Wishlist/CustomerData/Wishlist.php index ae54289d4b1c9..2f6b57a8650c4 100644 --- a/app/code/Magento/Wishlist/CustomerData/Wishlist.php +++ b/app/code/Magento/Wishlist/CustomerData/Wishlist.php @@ -68,7 +68,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getSectionData() { @@ -80,6 +80,8 @@ public function getSectionData() } /** + * Get counter + * * @return string */ protected function getCounter() @@ -156,7 +158,6 @@ protected function getItemData(\Magento\Wishlist\Model\Item $wishlistItem) * * @param \Magento\Catalog\Model\Product $product * @return array - * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function getImageData($product) { @@ -164,27 +165,11 @@ protected function getImageData($product) $helper = $this->imageHelperFactory->create() ->init($product, 'wishlist_sidebar_block'); - $template = 'Magento_Catalog/product/image_with_borders'; - - try { - $imagesize = $helper->getResizedImageInfo(); - } catch (NotLoadInfoImageException $exception) { - $imagesize = [$helper->getWidth(), $helper->getHeight()]; - } - - $width = $helper->getFrame() - ? $helper->getWidth() - : $imagesize[0]; - - $height = $helper->getFrame() - ? $helper->getHeight() - : $imagesize[1]; - return [ - 'template' => $template, + 'template' => 'Magento_Catalog/product/image_with_borders', 'src' => $helper->getUrl(), - 'width' => $width, - 'height' => $height, + 'width' => $helper->getWidth(), + 'height' => $helper->getHeight(), 'alt' => $helper->getLabel(), ]; } diff --git a/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php index 79ab3c9ba2082..0a1e40253b71c 100644 --- a/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/CustomerData/WishlistTest.php @@ -199,9 +199,6 @@ public function testGetSectionData() $this->catalogImageHelperMock->expects($this->any()) ->method('getFrame') ->willReturn(true); - $this->catalogImageHelperMock->expects($this->once()) - ->method('getResizedImageInfo') - ->willReturn([]); $this->wishlistHelperMock->expects($this->once()) ->method('getProductUrl') @@ -400,9 +397,6 @@ public function testGetSectionDataWithTwoItems() $this->catalogImageHelperMock->expects($this->any()) ->method('getFrame') ->willReturn(true); - $this->catalogImageHelperMock->expects($this->exactly(2)) - ->method('getResizedImageInfo') - ->willReturn([]); $this->wishlistHelperMock->expects($this->exactly(2)) ->method('getProductUrl') diff --git a/app/design/adminhtml/Magento/backend/web/js/theme.js b/app/design/adminhtml/Magento/backend/web/js/theme.js index 05d73ac20fcbd..069970deae681 100644 --- a/app/design/adminhtml/Magento/backend/web/js/theme.js +++ b/app/design/adminhtml/Magento/backend/web/js/theme.js @@ -312,8 +312,9 @@ define('globalNavigation', [ define('globalSearch', [ 'jquery', - 'jquery/ui' -], function ($) { + 'Magento_Ui/js/lib/key-codes', + 'jquery-ui-modules/widget' +], function ($, keyCodes) { 'use strict'; $.widget('mage.globalSearch', { @@ -345,6 +346,25 @@ define('globalSearch', [ this.input.on('focus.activateGlobalSearchForm', function () { self.field.addClass(self.options.fieldActiveClass); }); + + $(document).on('keydown.activateGlobalSearchForm', function (event) { + var inputs = [ + 'input', + 'select', + 'textarea' + ]; + + if (keyCodes[event.which] !== 'forwardSlashKey' || + inputs.indexOf(event.target.tagName.toLowerCase()) !== -1 || + event.target.isContentEditable + ) { + return; + } + + event.preventDefault(); + + self.input.focus(); + }); } }); diff --git a/composer.json b/composer.json index 57fbfaaa35c2b..b5a484d3828b8 100644 --- a/composer.json +++ b/composer.json @@ -80,7 +80,10 @@ "tedivm/jshrink": "~1.3.0", "tubalmartin/cssmin": "4.1.1", "webonyx/graphql-php": "^0.13.8", - "wikimedia/less.php": "~1.8.0" + "wikimedia/less.php": "~1.8.0", + "league/flysystem": "^1.0", + "league/flysystem-aws-s3-v3": "^1.0", + "league/flysystem-cached-adapter": "^1.0" }, "require-dev": { "allure-framework/allure-phpunit": "~1.2.0", @@ -323,7 +326,9 @@ "twbs/bootstrap": "3.1.0", "tinymce/tinymce": "3.4.7", "magento/module-tinymce-3": "*", - "magento/module-csp": "*" + "magento/module-csp": "*", + "magento/module-aws-s-3": "*", + "magento/module-remote-storage": "*" }, "conflict": { "gene/bluefoot": "*" diff --git a/composer.lock b/composer.lock index b06e0e9fa9e5c..f4ece70a22e62 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,93 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a03edc1c8ee05f82886eebd6ed288df8", + "content-hash": "50fd3418a729ef9b577d214fe6c9b0b1", "packages": [ + { + "name": "aws/aws-sdk-php", + "version": "3.158.19", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "b1c3c763e227e518768f0416cbd2b29c11f79561" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b1c3c763e227e518768f0416cbd2b29c11f79561", + "reference": "b1c3c763e227e518768f0416cbd2b29c11f79561", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "^2.5", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^4.8.35|^5.4.3", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2020-11-02T19:49:21+00:00" + }, { "name": "colinmollenhour/cache-backend-file", "version": "v1.4.5", @@ -206,20 +291,34 @@ "ssl", "tls" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], "time": "2020-08-23T12:54:47+00:00" }, { "name": "composer/composer", - "version": "1.10.15", + "version": "1.10.17", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12" + "reference": "09d42e18394d8594be24e37923031c4b7442a1cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/547c9ee73fe26c77af09a0ea16419176b1cdbd12", - "reference": "547c9ee73fe26c77af09a0ea16419176b1cdbd12", + "url": "https://api.github.com/repos/composer/composer/zipball/09d42e18394d8594be24e37923031c4b7442a1cb", + "reference": "09d42e18394d8594be24e37923031c4b7442a1cb", "shasum": "" }, "require": { @@ -286,7 +385,21 @@ "dependency", "package" ], - "time": "2020-10-13T13:59:09+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-10-30T21:31:58+00:00" }, { "name": "composer/semver", @@ -347,6 +460,20 @@ "validation", "versioning" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], "time": "2020-09-27T13:13:07+00:00" }, { @@ -407,20 +534,34 @@ "spdx", "validator" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], "time": "2020-07-15T15:35:07+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.3", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5" + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ebd27a9866ae8254e873866f795491f02418c5a5", - "reference": "ebd27a9866ae8254e873866f795491f02418c5a5", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6e076a124f7ee146f2487554a94b6a19a74887ba", + "reference": "6e076a124f7ee146f2487554a94b6a19a74887ba", "shasum": "" }, "require": { @@ -451,7 +592,21 @@ "Xdebug", "performance" ], - "time": "2020-08-19T10:27:58+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:39:10+00:00" }, { "name": "container-interop/container-interop", @@ -1304,6 +1459,12 @@ "BSD-3-Clause" ], "description": "Replace zendframework and zfcampus packages with their Laminas Project equivalents.", + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-05-20T13:45:39+00:00" }, { @@ -1537,6 +1698,12 @@ "events", "laminas" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-08-25T11:10:44+00:00" }, { @@ -1604,6 +1771,12 @@ "feed", "laminas" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-08-18T13:45:04+00:00" }, { @@ -1755,6 +1928,12 @@ "form", "laminas" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-07-14T13:53:27+00:00" }, { @@ -1807,6 +1986,12 @@ "http client", "laminas" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-08-18T17:11:58+00:00" }, { @@ -1875,23 +2060,23 @@ }, { "name": "laminas/laminas-i18n", - "version": "2.10.3", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-i18n.git", - "reference": "94ff957a1366f5be94f3d3a9b89b50386649e3ae" + "reference": "85678f444b6dcb48e8a04591779e11c24e5bb901" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/94ff957a1366f5be94f3d3a9b89b50386649e3ae", - "reference": "94ff957a1366f5be94f3d3a9b89b50386649e3ae", + "url": "https://api.github.com/repos/laminas/laminas-i18n/zipball/85678f444b6dcb48e8a04591779e11c24e5bb901", + "reference": "85678f444b6dcb48e8a04591779e11c24e5bb901", "shasum": "" }, "require": { "ext-intl": "*", "laminas/laminas-stdlib": "^2.7 || ^3.0", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ~8.0.0" }, "conflict": { "phpspec/prophecy": "<1.9.0" @@ -1905,10 +2090,10 @@ "laminas/laminas-config": "^2.6", "laminas/laminas-eventmanager": "^2.6.2 || ^3.0", "laminas/laminas-filter": "^2.6.1", - "laminas/laminas-servicemanager": "^2.7.5 || ^3.0.3", + "laminas/laminas-servicemanager": "^3.2.1", "laminas/laminas-validator": "^2.6", "laminas/laminas-view": "^2.6.3", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.16" + "phpunit/phpunit": "^9.3" }, "suggest": { "laminas/laminas-cache": "Laminas\\Cache component", @@ -1922,10 +2107,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" - }, "laminas": { "component": "Laminas\\I18n", "config-provider": "Laminas\\I18n\\ConfigProvider" @@ -1946,7 +2127,13 @@ "i18n", "laminas" ], - "time": "2020-03-29T12:51:08+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-10-24T13:14:32+00:00" }, { "name": "laminas/laminas-inputfilter", @@ -2251,6 +2438,12 @@ "laminas", "mail" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-08-12T14:51:33+00:00" }, { @@ -2423,6 +2616,12 @@ "laminas", "modulemanager" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-08-25T09:29:22+00:00" }, { @@ -2745,23 +2944,23 @@ }, { "name": "laminas/laminas-session", - "version": "2.9.3", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-session.git", - "reference": "519e8966146536cd97c1cc3d59a21b095fb814d7" + "reference": "921e6a9f807ee243a9a4f8a8a297929d0c2b50cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-session/zipball/519e8966146536cd97c1cc3d59a21b095fb814d7", - "reference": "519e8966146536cd97c1cc3d59a21b095fb814d7", + "url": "https://api.github.com/repos/laminas/laminas-session/zipball/921e6a9f807ee243a9a4f8a8a297929d0c2b50cd", + "reference": "921e6a9f807ee243a9a4f8a8a297929d0c2b50cd", "shasum": "" }, "require": { - "laminas/laminas-eventmanager": "^2.6.2 || ^3.0", + "laminas/laminas-eventmanager": "^3.0", "laminas/laminas-stdlib": "^3.2.1", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ~8.0.0" }, "replace": { "zendframework/zend-session": "^2.9.1" @@ -2772,11 +2971,12 @@ "laminas/laminas-coding-standard": "~1.0.0", "laminas/laminas-db": "^2.7", "laminas/laminas-http": "^2.5.4", - "laminas/laminas-servicemanager": "^2.7.5 || ^3.0.3", + "laminas/laminas-servicemanager": "^3.0.3", "laminas/laminas-validator": "^2.6", "mongodb/mongodb": "^1.0.1", "php-mock/php-mock-phpunit": "^1.1.2 || ^2.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20" + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.3" }, "suggest": { "laminas/laminas-cache": "Laminas\\Cache component", @@ -2788,10 +2988,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.9.x-dev", - "dev-develop": "2.10.x-dev" - }, "laminas": { "component": "Laminas\\Session", "config-provider": "Laminas\\Session\\ConfigProvider" @@ -2812,7 +3008,13 @@ "laminas", "session" ], - "time": "2020-03-29T13:26:04+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-10-31T15:33:31+00:00" }, { "name": "laminas/laminas-soap", @@ -2919,6 +3121,12 @@ "laminas", "stdlib" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-08-25T09:08:16+00:00" }, { @@ -2975,38 +3183,32 @@ }, { "name": "laminas/laminas-uri", - "version": "2.7.1", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-uri.git", - "reference": "6be8ce19622f359b048ce4faebf1aa1bca73a7ff" + "reference": "8651611b6285529f25a4cb9a466c686d9b31468e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-uri/zipball/6be8ce19622f359b048ce4faebf1aa1bca73a7ff", - "reference": "6be8ce19622f359b048ce4faebf1aa1bca73a7ff", + "url": "https://api.github.com/repos/laminas/laminas-uri/zipball/8651611b6285529f25a4cb9a466c686d9b31468e", + "reference": "8651611b6285529f25a4cb9a466c686d9b31468e", "shasum": "" }, "require": { "laminas/laminas-escaper": "^2.5", "laminas/laminas-validator": "^2.10", "laminas/laminas-zendframework-bridge": "^1.0", - "php": "^5.6 || ^7.0" + "php": "^7.3 || ~8.0.0" }, "replace": { - "zendframework/zend-uri": "self.version" + "zendframework/zend-uri": "^2.7.1" }, "require-dev": { - "laminas/laminas-coding-standard": "~1.0.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.4" + "laminas/laminas-coding-standard": "^2.1", + "phpunit/phpunit": "^9.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7.x-dev", - "dev-develop": "2.8.x-dev" - } - }, "autoload": { "psr-4": { "Laminas\\Uri\\": "src/" @@ -3022,7 +3224,13 @@ "laminas", "uri" ], - "time": "2019-12-31T17:56:00+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-10-31T20:20:07+00:00" }, { "name": "laminas/laminas-validator", @@ -3240,65 +3448,307 @@ "laminas", "zf" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-09-14T14:23:00+00:00" }, { - "name": "magento/composer", - "version": "1.6.0", + "name": "league/flysystem", + "version": "1.1.3", "source": { "type": "git", - "url": "https://github.com/magento/composer.git", - "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9" + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/composer/zipball/fcc66f535d631788f2ba160ff547357086d9b2c9", - "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a", + "reference": "9be3b16c877d477357c015cec057548cf9b2a14a", "shasum": "" }, "require": { - "composer/composer": "^1.9", - "php": "~7.3.0||~7.4.0", - "symfony/console": "~4.4.0" + "ext-fileinfo": "*", + "league/mime-type-detection": "^1.3", + "php": "^7.2.5 || ^8.0" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" }, "require-dev": { - "phpunit/phpunit": "^9" + "phpspec/prophecy": "^1.11.1", + "phpunit/phpunit": "^8.5.8" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, "autoload": { "psr-4": { - "Magento\\Composer\\": "src" + "League\\Flysystem\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "OSL-3.0", - "AFL-3.0" + "MIT" ], - "description": "Magento composer library helps to instantiate Composer application and run composer commands.", - "time": "2020-06-15T17:52:31+00:00" - }, - { - "name": "magento/magento-composer-installer", - "version": "0.1.13", - "source": { - "type": "git", - "url": "https://github.com/magento/magento-composer-installer.git", - "reference": "8b6c32f53b4944a5d6656e86344cd0f9784709a1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/magento/magento-composer-installer/zipball/8b6c32f53b4944a5d6656e86344cd0f9784709a1", - "reference": "8b6c32f53b4944a5d6656e86344cd0f9784709a1", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0" - }, - "replace": { - "magento-hackathon/magento-composer-installer": "*" - }, - "require-dev": { + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], + "time": "2020-08-23T07:39:11+00:00" + }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "1.0.29", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "4e25cc0582a36a786c31115e419c6e40498f6972" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/4e25cc0582a36a786c31115e419c6e40498f6972", + "reference": "4e25cc0582a36a786c31115e419c6e40498f6972", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.20.0", + "league/flysystem": "^1.0.40", + "php": ">=5.5.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3v3\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Flysystem adapter for the AWS S3 SDK v3.x", + "time": "2020-10-08T18:58:37+00:00" + }, + { + "name": "league/flysystem-cached-adapter", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-cached-adapter.git", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-cached-adapter/zipball/d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "reference": "d1925efb2207ac4be3ad0c40b8277175f99ffaff", + "shasum": "" + }, + "require": { + "league/flysystem": "~1.0", + "psr/cache": "^1.0.0" + }, + "require-dev": { + "mockery/mockery": "~0.9", + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7", + "predis/predis": "~1.0", + "tedivm/stash": "~0.12" + }, + "suggest": { + "ext-phpredis": "Pure C implemented extension for PHP" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Cached\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "frankdejonge", + "email": "info@frenky.net" + } + ], + "description": "An adapter decorator to enable meta-data caching.", + "time": "2020-07-25T15:56:04+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa", + "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.36", + "phpunit/phpunit": "^8.5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2020-10-18T11:50:25+00:00" + }, + { + "name": "magento/composer", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/magento/composer.git", + "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/magento/composer/zipball/fcc66f535d631788f2ba160ff547357086d9b2c9", + "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9", + "shasum": "" + }, + "require": { + "composer/composer": "^1.9", + "php": "~7.3.0||~7.4.0", + "symfony/console": "~4.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Magento\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "description": "Magento composer library helps to instantiate Composer application and run composer commands.", + "time": "2020-06-15T17:52:31+00:00" + }, + { + "name": "magento/magento-composer-installer", + "version": "0.1.13", + "source": { + "type": "git", + "url": "https://github.com/magento/magento-composer-installer.git", + "reference": "8b6c32f53b4944a5d6656e86344cd0f9784709a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/magento/magento-composer-installer/zipball/8b6c32f53b4944a5d6656e86344cd0f9784709a1", + "reference": "8b6c32f53b4944a5d6656e86344cd0f9784709a1", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "replace": { + "magento-hackathon/magento-composer-installer": "*" + }, + "require-dev": { "composer/composer": "*@dev", "firegento/phpcs": "dev-patch-1", "mikey179/vfsstream": "*", @@ -3479,8 +3929,75 @@ "logging", "psr-3" ], + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], "time": "2020-07-23T08:35:51+00:00" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb", + "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb", + "shasum": "" + }, + "require": { + "php": "^5.4 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^1.4", + "phpunit/phpunit": "^4.8.36 || ^7.5.15" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2020-07-31T21:01:56+00:00" + }, { "name": "paragonie/random_compat", "version": "v9.99.99", @@ -3893,8 +4410,68 @@ "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-09-08T04:24:43+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.0.0", @@ -4256,6 +4833,16 @@ "parser", "validator" ], + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], "time": "2020-08-25T06:56:57+00:00" }, { @@ -4304,16 +4891,16 @@ }, { "name": "symfony/console", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124" + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/90933b39c7b312fc3ceaa1ddeac7eb48cb953124", - "reference": "90933b39c7b312fc3ceaa1ddeac7eb48cb953124", + "url": "https://api.github.com/repos/symfony/console/zipball/20f73dd143a5815d475e0838ff867bce1eebd9d5", + "reference": "20f73dd143a5815d475e0838ff867bce1eebd9d5", "shasum": "" }, "require": { @@ -4348,11 +4935,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" @@ -4377,31 +4959,40 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2020-09-15T07:58:55+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9" + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/e544e24472d4c97b2d11ade7caacd446727c6bf9", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\CssSelector\\": "" @@ -4430,20 +5021,34 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2020-05-20T17:43:50+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd" + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e17bb5e0663dc725f7cdcafc932132735b4725cd", - "reference": "e17bb5e0663dc725f7cdcafc932132735b4725cd", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4204f13d2d0b7ad09454f221bb2195fccdf1fe98", + "reference": "4204f13d2d0b7ad09454f221bb2195fccdf1fe98", "shasum": "" }, "require": { @@ -4472,11 +5077,6 @@ "symfony/http-kernel": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -4501,7 +5101,21 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2020-09-18T14:07:46+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4563,20 +5177,34 @@ "interoperability", "standards" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-07-06T13:19:58+00:00" }, { "name": "symfony/filesystem", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae" + "reference": "df08650ea7aee2d925380069c131a66124d79177" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/1a8697545a8d87b9f2f6b1d32414199cc5e20aae", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/df08650ea7aee2d925380069c131a66124d79177", + "reference": "df08650ea7aee2d925380069c131a66124d79177", "shasum": "" }, "require": { @@ -4584,11 +5212,6 @@ "symfony/polyfill-ctype": "~1.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" @@ -4613,31 +5236,40 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2020-09-27T14:02:37+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/finder", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8" + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", - "reference": "2c3ba7ad6884e6c4451ce2340e2dc23f6fa3e0d8", + "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Finder\\": "" @@ -4662,24 +5294,38 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2020-09-02T16:23:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b" + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/aed596913b70fae57be53d86faa2e9ef85a2297b", - "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-ctype": "For best performance" @@ -4687,7 +5333,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.19-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4724,26 +5370,39 @@ "polyfill", "portable" ], - "time": "2020-10-23T09:01:57+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "4ad5115c0f5d5172a9fe8147675ec6de266d8826" + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/4ad5115c0f5d5172a9fe8147675ec6de266d8826", - "reference": "4ad5115c0f5d5172a9fe8147675ec6de266d8826", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117", + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117", "shasum": "" }, "require": { - "php": ">=5.3.3", + "php": ">=7.1", "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php70": "^1.10", "symfony/polyfill-php72": "^1.10" }, "suggest": { @@ -4752,7 +5411,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.19-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4795,24 +5454,38 @@ "portable", "shim" ], - "time": "2020-10-21T09:57:48+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8db0ae7936b42feb370840cf24de1a144fb0ef27" + "reference": "727d1096295d807c309fb01a851577302394c897" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8db0ae7936b42feb370840cf24de1a144fb0ef27", - "reference": "8db0ae7936b42feb370840cf24de1a144fb0ef27", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", + "reference": "727d1096295d807c309fb01a851577302394c897", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -4820,7 +5493,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.19-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4862,24 +5535,38 @@ "portable", "shim" ], - "time": "2020-10-23T09:01:57+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "b5f7b932ee6fa802fc792eabd77c4c88084517ce" + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/b5f7b932ee6fa802fc792eabd77c4c88084517ce", - "reference": "b5f7b932ee6fa802fc792eabd77c4c88084517ce", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-mbstring": "For best performance" @@ -4887,7 +5574,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.19-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4925,92 +5612,43 @@ "portable", "shim" ], - "time": "2020-10-23T09:01:57+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.19.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "3fe414077251a81a1b15b1c709faf5c2fbae3d4e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/3fe414077251a81a1b15b1c709faf5c2fbae3d4e", - "reference": "3fe414077251a81a1b15b1c709faf5c2fbae3d4e", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "~1.0|~2.0|~9.99", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.19-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "url": "https://github.com/fabpot", + "type": "github" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2020-10-23T09:01:57+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "beecef6b463b06954638f02378f52496cb84bacc" + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/beecef6b463b06954638f02378f52496cb84bacc", - "reference": "beecef6b463b06954638f02378f52496cb84bacc", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.19-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5047,29 +5685,43 @@ "portable", "shim" ], - "time": "2020-10-23T09:01:57+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9d920e3218205554171b2503bb3e4a1366824a16" + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9d920e3218205554171b2503bb3e4a1366824a16", - "reference": "9d920e3218205554171b2503bb3e4a1366824a16", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.19-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5109,29 +5761,43 @@ "portable", "shim" ], - "time": "2020-10-23T09:01:57+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.19.0", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "f54ef00f4678f348f133097fa8c3701d197ff44d" + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/f54ef00f4678f348f133097fa8c3701d197ff44d", - "reference": "f54ef00f4678f348f133097fa8c3701d197ff44d", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", "shasum": "" }, "require": { - "php": ">=7.0.8" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.19-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -5175,31 +5841,40 @@ "portable", "shim" ], - "time": "2020-10-23T09:01:57+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/process", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "9b887acc522935f77555ae8813495958c7771ba7" + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/9b887acc522935f77555ae8813495958c7771ba7", - "reference": "9b887acc522935f77555ae8813495958c7771ba7", + "url": "https://api.github.com/repos/symfony/process/zipball/2f4b049fb80ca5e9874615a2a85dc2a502090f05", + "reference": "2f4b049fb80ca5e9874615a2a85dc2a502090f05", "shasum": "" }, "require": { "php": ">=7.1.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -5224,7 +5899,21 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2020-09-02T16:08:58+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/service-contracts", @@ -5286,6 +5975,20 @@ "interoperability", "standards" ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-09-07T11:33:47+00:00" }, { @@ -5480,6 +6183,12 @@ "safe writer", "webimpress" ], + "funding": [ + { + "url": "https://github.com/michalbundyra", + "type": "github" + } + ], "time": "2020-08-25T07:21:11+00:00" }, { @@ -5532,6 +6241,12 @@ "api", "graphql" ], + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" + } + ], "time": "2020-07-02T05:49:25+00:00" }, { @@ -5746,95 +6461,10 @@ "cases", "phpunit", "report", - "steps", - "testing" - ], - "time": "2018-10-25T12:03:54+00:00" - }, - { - "name": "aws/aws-sdk-php", - "version": "3.158.12", - "source": { - "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "ba2956c3cb5ff0d7b808683b1c57ebc3f5cc9633" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ba2956c3cb5ff0d7b808683b1c57ebc3f5cc9633", - "reference": "ba2956c3cb5ff0d7b808683b1c57ebc3f5cc9633", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0", - "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.4.1", - "mtdowling/jmespath.php": "^2.5", - "php": ">=5.5" - }, - "require-dev": { - "andrewsville/php-token-reflection": "^1.4", - "aws/aws-php-sns-message-validator": "~1.0", - "behat/behat": "~3.0", - "doctrine/cache": "~1.4", - "ext-dom": "*", - "ext-openssl": "*", - "ext-pcntl": "*", - "ext-sockets": "*", - "nette/neon": "^2.3", - "paragonie/random_compat": ">= 2", - "phpunit/phpunit": "^4.8.35|^5.4.3", - "psr/cache": "^1.0", - "psr/simple-cache": "^1.0", - "sebastian/comparator": "^1.2.3" - }, - "suggest": { - "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", - "doctrine/cache": "To use the DoctrineCacheAdapter", - "ext-curl": "To send requests using cURL", - "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", - "ext-sockets": "To use client-side monitoring" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "Aws\\": "src/" - }, - "files": [ - "src/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" - } - ], - "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", - "homepage": "http://aws.amazon.com/sdkforphp", - "keywords": [ - "amazon", - "aws", - "cloud", - "dynamodb", - "ec2", - "glacier", - "s3", - "sdk" + "steps", + "testing" ], - "time": "2020-10-22T18:12:00+00:00" + "time": "2018-10-25T12:03:54+00:00" }, { "name": "beberlei/assert", @@ -6052,16 +6682,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.8", + "version": "4.1.11", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "41036e8af66e727c4587012f0366b7f0576a99da" + "reference": "bf2b548a358750a5ecb3d1aa2b32ebfb82a46061" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/41036e8af66e727c4587012f0366b7f0576a99da", - "reference": "41036e8af66e727c4587012f0366b7f0576a99da", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/bf2b548a358750a5ecb3d1aa2b32ebfb82a46061", + "reference": "bf2b548a358750a5ecb3d1aa2b32ebfb82a46061", "shasum": "" }, "require": { @@ -6073,7 +6703,7 @@ "ext-json": "*", "ext-mbstring": "*", "guzzlehttp/psr7": "~1.4", - "php": ">=5.6.0 <8.0", + "php": ">=5.6.0 <9.0", "symfony/console": ">=2.7 <6.0", "symfony/css-selector": ">=2.7 <6.0", "symfony/event-dispatcher": ">=2.7 <6.0", @@ -6091,7 +6721,7 @@ "monolog/monolog": "~1.8", "squizlabs/php_codesniffer": "~2.0", "symfony/process": ">=2.7 <6.0", - "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0" + "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0 | ^5.0" }, "suggest": { "codeception/specify": "BDD-style code blocks", @@ -6133,7 +6763,13 @@ "functional testing", "unit testing" ], - "time": "2020-10-11T17:54:58+00:00" + "funding": [ + { + "url": "https://opencollective.com/codeception", + "type": "open_collective" + } + ], + "time": "2020-11-03T17:34:51+00:00" }, { "name": "codeception/lib-asserts", @@ -6240,24 +6876,21 @@ }, { "name": "codeception/module-sequence", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/Codeception/module-sequence.git", - "reference": "70563527b768194d6ab22e1ff943a5e69741c5dd" + "reference": "b75be26681ae90824cde8f8df785981f293667e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-sequence/zipball/70563527b768194d6ab22e1ff943a5e69741c5dd", - "reference": "70563527b768194d6ab22e1ff943a5e69741c5dd", + "url": "https://api.github.com/repos/Codeception/module-sequence/zipball/b75be26681ae90824cde8f8df785981f293667e1", + "reference": "b75be26681ae90824cde8f8df785981f293667e1", "shasum": "" }, "require": { - "codeception/codeception": "4.0.x-dev | ^4.0", - "php": ">=5.6.0 <8.0" - }, - "require-dev": { - "codeception/util-robohelpers": "dev-master" + "codeception/codeception": "^4.0", + "php": ">=5.6.0 <9.0" }, "type": "library", "autoload": { @@ -6279,30 +6912,27 @@ "keywords": [ "codeception" ], - "time": "2019-10-10T12:08:50+00:00" + "time": "2020-10-31T18:36:26+00:00" }, { "name": "codeception/module-webdriver", - "version": "1.1.2", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/Codeception/module-webdriver.git", - "reference": "d055c645f600e991e33d1f289a9645eee46c384e" + "reference": "b7dc227f91730e7abb520439decc9ad0677b8a55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/d055c645f600e991e33d1f289a9645eee46c384e", - "reference": "d055c645f600e991e33d1f289a9645eee46c384e", + "url": "https://api.github.com/repos/Codeception/module-webdriver/zipball/b7dc227f91730e7abb520439decc9ad0677b8a55", + "reference": "b7dc227f91730e7abb520439decc9ad0677b8a55", "shasum": "" }, "require": { "codeception/codeception": "^4.0", - "php": ">=5.6.0 <8.0", + "php": ">=5.6.0 <9.0", "php-webdriver/webdriver": "^1.6.0" }, - "require-dev": { - "codeception/util-robohelpers": "dev-master" - }, "suggest": { "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" }, @@ -6334,7 +6964,7 @@ "browser-testing", "codeception" ], - "time": "2020-10-11T18:54:47+00:00" + "time": "2020-10-24T15:41:19+00:00" }, { "name": "codeception/phpunit-wrapper", @@ -6564,16 +7194,16 @@ }, { "name": "doctrine/annotations", - "version": "1.10.4", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f" + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/bfe91e31984e2ba76df1c1339681770401ec262f", - "reference": "bfe91e31984e2ba76df1c1339681770401ec262f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", "shasum": "" }, "require": { @@ -6583,13 +7213,14 @@ }, "require-dev": { "doctrine/cache": "1.*", + "doctrine/coding-standard": "^6.0 || ^8.1", "phpstan/phpstan": "^0.12.20", "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.9.x-dev" + "dev-master": "1.11.x-dev" } }, "autoload": { @@ -6624,13 +7255,13 @@ } ], "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", "keywords": [ "annotations", "docblock", "parser" ], - "time": "2020-08-10T19:35:50+00:00" + "time": "2020-10-26T10:28:16+00:00" }, { "name": "doctrine/cache", @@ -6712,6 +7343,20 @@ "redis", "xcache" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], "time": "2020-07-07T18:54:01+00:00" }, { @@ -6835,6 +7480,20 @@ "constructor", "instantiate" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -6897,31 +7556,45 @@ "parser", "php" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], "time": "2020-05-25T17:44:05+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.4", + "version": "v2.16.7", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", - "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4e35806a6d7d8510d6842ae932e8832363d22c87", + "reference": "4e35806a6d7d8510d6842ae932e8832363d22c87", "shasum": "" }, "require": { - "composer/semver": "^1.4", + "composer/semver": "^1.4 || ^2.0 || ^3.0", "composer/xdebug-handler": "^1.2", "doctrine/annotations": "^1.2", "ext-json": "*", "ext-tokenizer": "*", - "php": "^5.6 || ^7.0", + "php": "^7.1", "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0", + "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", "symfony/finder": "^3.0 || ^4.0 || ^5.0", @@ -6934,14 +7607,14 @@ "require-dev": { "johnkary/phpunit-speedtrap": "^1.1 || ^2.0 || ^3.0", "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.2", + "keradus/cli-executor": "^1.4", "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.1", + "php-coveralls/php-coveralls": "^2.4.1", "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.1", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", - "phpunitgoodpractices/traits": "^1.8", + "phpunitgoodpractices/traits": "^1.9.1", "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, @@ -6988,7 +7661,13 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2020-06-27T23:57:46+00:00" + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2020-10-27T22:44:27+00:00" }, { "name": "hoa/consistency", @@ -7713,132 +8392,6 @@ ], "time": "2020-02-22T20:59:37+00:00" }, - { - "name": "league/flysystem", - "version": "1.1.3", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/flysystem.git", - "reference": "9be3b16c877d477357c015cec057548cf9b2a14a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/9be3b16c877d477357c015cec057548cf9b2a14a", - "reference": "9be3b16c877d477357c015cec057548cf9b2a14a", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "league/mime-type-detection": "^1.3", - "php": "^7.2.5 || ^8.0" - }, - "conflict": { - "league/flysystem-sftp": "<1.0.6" - }, - "require-dev": { - "phpspec/prophecy": "^1.11.1", - "phpunit/phpunit": "^8.5.8" - }, - "suggest": { - "ext-fileinfo": "Required for MimeType", - "ext-ftp": "Allows you to use FTP server storage", - "ext-openssl": "Allows you to use FTPS server storage", - "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", - "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", - "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", - "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", - "league/flysystem-webdav": "Allows you to use WebDAV storage", - "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", - "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Flysystem\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frenky.net" - } - ], - "description": "Filesystem abstraction: Many filesystems, one API.", - "keywords": [ - "Cloud Files", - "WebDAV", - "abstraction", - "aws", - "cloud", - "copy.com", - "dropbox", - "file systems", - "files", - "filesystem", - "filesystems", - "ftp", - "rackspace", - "remote", - "s3", - "sftp", - "storage" - ], - "time": "2020-08-23T07:39:11+00:00" - }, - { - "name": "league/mime-type-detection", - "version": "1.5.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/353f66d7555d8a90781f6f5e7091932f9a4250aa", - "reference": "353f66d7555d8a90781f6f5e7091932f9a4250aa", - "shasum": "" - }, - "require": { - "ext-fileinfo": "*", - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^0.12.36", - "phpunit/phpunit": "^8.5.8" - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\MimeTypeDetection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Frank de Jonge", - "email": "info@frankdejonge.nl" - } - ], - "description": "Mime-type detection for Flysystem", - "time": "2020-10-18T11:50:25+00:00" - }, { "name": "lusitanian/oauth", "version": "v0.8.11", @@ -7947,16 +8500,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.1.1", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "c6760313811f2c04545a261c706d2a73dd727b9a" + "reference": "0ec0c87335af996cbf3c0aace375d4e659e7a6dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/c6760313811f2c04545a261c706d2a73dd727b9a", - "reference": "c6760313811f2c04545a261c706d2a73dd727b9a", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/0ec0c87335af996cbf3c0aace375d4e659e7a6dc", + "reference": "0ec0c87335af996cbf3c0aace375d4e659e7a6dc", "shasum": "" }, "require": { @@ -8034,7 +8587,7 @@ "magento", "testing" ], - "time": "2020-09-28T18:26:59+00:00" + "time": "2020-11-05T15:57:52+00:00" }, { "name": "mikey179/vfsstream", @@ -8082,63 +8635,6 @@ "homepage": "http://vfs.bovigo.org/", "time": "2019-10-30T15:31:00+00:00" }, - { - "name": "mtdowling/jmespath.php", - "version": "2.6.0", - "source": { - "type": "git", - "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb", - "reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb", - "shasum": "" - }, - "require": { - "php": "^5.4 || ^7.0 || ^8.0", - "symfony/polyfill-mbstring": "^1.17" - }, - "require-dev": { - "composer/xdebug-handler": "^1.4", - "phpunit/phpunit": "^4.8.36 || ^7.5.15" - }, - "bin": [ - "bin/jp.php" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev" - } - }, - "autoload": { - "psr-4": { - "JmesPath\\": "src/" - }, - "files": [ - "src/JmesPath.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Declaratively specify how to extract elements from a JSON document", - "keywords": [ - "json", - "jsonpath" - ], - "time": "2020-07-31T21:01:56+00:00" - }, { "name": "mustache/mustache", "version": "v2.13.0", @@ -8231,6 +8727,12 @@ "object", "object graph" ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], "time": "2020-06-29T13:22:24+00:00" }, { @@ -8882,6 +9384,12 @@ "phpmd", "pmd" ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/phpmd/phpmd", + "type": "tidelift" + } + ], "time": "2020-09-23T22:06:32+00:00" }, { @@ -8937,6 +9445,16 @@ "php", "type" ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], "time": "2020-07-20T17:29:33+00:00" }, { @@ -9042,6 +9560,20 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], "time": "2020-05-05T12:55:44+00:00" }, { @@ -9106,6 +9638,12 @@ "testing", "xunit" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-05-23T08:02:54+00:00" }, { @@ -9156,6 +9694,12 @@ "filesystem", "iterator" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-09-28T05:57:25+00:00" }, { @@ -9209,20 +9753,26 @@ "keywords": [ "process" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-09-28T05:58:55+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84" + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/18c887016e60e52477e54534956d7b47bc52cd84", - "reference": "18c887016e60e52477e54534956d7b47bc52cd84", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", "shasum": "" }, "require": { @@ -9258,7 +9808,13 @@ "keywords": [ "template" ], - "time": "2020-09-28T06:03:05+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" }, { "name": "phpunit/php-timer", @@ -9307,6 +9863,12 @@ "keywords": [ "timer" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-04-20T06:00:37+00:00" }, { @@ -9356,6 +9918,12 @@ "keywords": [ "tokenizer" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "abandoned": true, "time": "2020-08-04T08:28:15+00:00" }, @@ -9445,53 +10013,17 @@ "testing", "xunit" ], - "time": "2020-05-22T13:54:05+00:00" - }, - { - "name": "psr/cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" } ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "time": "2016-08-06T20:24:11+00:00" + "time": "2020-05-22T13:54:05+00:00" }, { "name": "psr/simple-cache", @@ -9543,16 +10075,16 @@ }, { "name": "sebastian/code-unit", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab" + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/59236be62b1bb9919e6d7f60b0b832dc05cef9ab", - "reference": "59236be62b1bb9919e6d7f60b0b832dc05cef9ab", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", "shasum": "" }, "require": { @@ -9585,7 +10117,13 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", - "time": "2020-10-02T14:47:54+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -9630,20 +10168,26 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-09-28T05:30:19+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120" + "reference": "55f4261989e546dc112258c7a75935a81a7ce382" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7a8ff306445707539c1a6397372a982a1ec55120", - "reference": "7a8ff306445707539c1a6397372a982a1ec55120", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", + "reference": "55f4261989e546dc112258c7a75935a81a7ce382", "shasum": "" }, "require": { @@ -9694,20 +10238,26 @@ "compare", "equality" ], - "time": "2020-09-30T06:47:25+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T15:49:45+00:00" }, { "name": "sebastian/diff", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092" + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ffc949a1a2aae270ea064453d7535b82e4c32092", - "reference": "ffc949a1a2aae270ea064453d7535b82e4c32092", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", "shasum": "" }, "require": { @@ -9750,7 +10300,13 @@ "unidiff", "unified diff" ], - "time": "2020-09-28T05:32:55+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:10:38+00:00" }, { "name": "sebastian/environment", @@ -9803,6 +10359,12 @@ "environment", "hhvm" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-09-28T05:52:38+00:00" }, { @@ -9870,6 +10432,12 @@ "export", "exporter" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-09-28T05:24:23+00:00" }, { @@ -9975,16 +10543,16 @@ }, { "name": "sebastian/object-enumerator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "f6f5957013d84725427d361507e13513702888a4" + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f6f5957013d84725427d361507e13513702888a4", - "reference": "f6f5957013d84725427d361507e13513702888a4", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", "shasum": "" }, "require": { @@ -10018,20 +10586,26 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2020-09-28T05:55:06+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5" + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", - "reference": "d9d0ab3b12acb1768bc1e0a89b23c90d2043cbe5", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", "shasum": "" }, "require": { @@ -10063,7 +10637,13 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2020-09-28T05:56:16+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" }, { "name": "sebastian/phpcpd", @@ -10118,16 +10698,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7" + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", - "reference": "ed8c9cd355089134bc9cba421b5cfdd58f0eaef7", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", + "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", "shasum": "" }, "require": { @@ -10167,7 +10747,13 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2020-09-28T05:17:32+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:17:30+00:00" }, { "name": "sebastian/resource-operations", @@ -10212,20 +10798,26 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-09-28T06:45:17+00:00" }, { "name": "sebastian/type", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909" + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fa592377f3923946cb90bf1f6a71ba2e5f229909", - "reference": "fa592377f3923946cb90bf1f6a71ba2e5f229909", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", "shasum": "" }, "require": { @@ -10258,7 +10850,13 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "time": "2020-10-06T08:41:03+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:18:59+00:00" }, { "name": "sebastian/version", @@ -10301,6 +10899,12 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-09-28T06:39:44+00:00" }, { @@ -10427,16 +11031,16 @@ }, { "name": "symfony/config", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191" + "reference": "11baeefa4c179d6908655a7b6be728f62367c193" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/6ad8be6e1280f6734150d8a04a9160dd34ceb191", - "reference": "6ad8be6e1280f6734150d8a04a9160dd34ceb191", + "url": "https://api.github.com/repos/symfony/config/zipball/11baeefa4c179d6908655a7b6be728f62367c193", + "reference": "11baeefa4c179d6908655a7b6be728f62367c193", "shasum": "" }, "require": { @@ -10460,11 +11064,6 @@ "symfony/yaml": "To use the yaml reference dumper" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Config\\": "" @@ -10489,20 +11088,34 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2020-09-02T16:23:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b" + "reference": "829ca6bceaf68036a123a13a979f3c89289eae78" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/2dea4a3ef2eb79138354c1d49e9372cc921af20b", - "reference": "2dea4a3ef2eb79138354c1d49e9372cc921af20b", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/829ca6bceaf68036a123a13a979f3c89289eae78", + "reference": "829ca6bceaf68036a123a13a979f3c89289eae78", "shasum": "" }, "require": { @@ -10535,11 +11148,6 @@ "symfony/yaml": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" @@ -10564,7 +11172,21 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2020-10-01T12:14:45+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-27T10:11:13+00:00" }, { "name": "symfony/deprecation-contracts", @@ -10614,20 +11236,34 @@ ], "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-09-07T11:33:47+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b" + "reference": "a2860ec970404b0233ab1e59e0568d3277d32b6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/353b42e7b4fd1c898aab09a059466c9cea74039b", - "reference": "353b42e7b4fd1c898aab09a059466c9cea74039b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a2860ec970404b0233ab1e59e0568d3277d32b6f", + "reference": "a2860ec970404b0233ab1e59e0568d3277d32b6f", "shasum": "" }, "require": { @@ -10646,11 +11282,6 @@ "symfony/mime": "To use the file extension guesser" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\HttpFoundation\\": "" @@ -10675,20 +11306,34 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2020-09-27T14:14:57+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/mime", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "4404d6545125863561721514ad9388db2661eec5" + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/4404d6545125863561721514ad9388db2661eec5", - "reference": "4404d6545125863561721514ad9388db2661eec5", + "url": "https://api.github.com/repos/symfony/mime/zipball/f5485a92c24d4bcfc2f3fc648744fb398482ff1b", + "reference": "f5485a92c24d4bcfc2f3fc648744fb398482ff1b", "shasum": "" }, "require": { @@ -10705,11 +11350,6 @@ "symfony/dependency-injection": "^4.4|^5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Mime\\": "" @@ -10738,20 +11378,34 @@ "mime", "mime-type" ], - "time": "2020-09-02T16:23:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd" + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/4c7e155bf7d93ea4ba3824d5a14476694a5278dd", - "reference": "4c7e155bf7d93ea4ba3824d5a14476694a5278dd", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", + "reference": "c6a02905e4ffc7a1498e8ee019db2b477cd1cc02", "shasum": "" }, "require": { @@ -10760,11 +11414,6 @@ "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\OptionsResolver\\": "" @@ -10794,20 +11443,99 @@ "configuration", "options" ], - "time": "2020-09-27T03:44:28+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", + "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", - "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/3d9f57c89011f0266e6b1d469e5c0110513859d5", + "reference": "3d9f57c89011f0266e6b1d469e5c0110513859d5", "shasum": "" }, "require": { @@ -10815,11 +11543,6 @@ "symfony/service-contracts": "^1.0|^2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Stopwatch\\": "" @@ -10844,20 +11567,34 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2020-05-20T17:43:50+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/yaml", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a" + "reference": "f284e032c3cefefb9943792132251b79a6127ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f284e032c3cefefb9943792132251b79a6127ca6", + "reference": "f284e032c3cefefb9943792132251b79a6127ca6", "shasum": "" }, "require": { @@ -10878,11 +11615,6 @@ "Resources/bin/yaml-lint" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" @@ -10907,20 +11639,34 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2020-09-27T03:44:28+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:03:25+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.3.2", + "version": "v1.3.3", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "72d9fee55e14e07a6283c9b3e28c09e85923a148" + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/72d9fee55e14e07a6283c9b3e28c09e85923a148", - "reference": "72d9fee55e14e07a6283c9b3e28c09e85923a148", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", "shasum": "" }, "require": { @@ -11042,7 +11788,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-10-22T09:17:04+00:00" + "time": "2020-10-28T17:51:34+00:00" }, { "name": "theseer/fdomdocument", @@ -11122,6 +11868,12 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], "time": "2020-07-12T23:59:07+00:00" }, { @@ -11184,6 +11936,16 @@ "env", "environment" ], + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], "time": "2020-07-14T17:54:18+00:00" }, { @@ -11297,6 +12059,5 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [], - "plugin-api-version": "1.1.0" + "platform-dev": [] } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php index 629cc077a63ea..85509dabdf415 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryLinkManagementTest.php @@ -4,10 +4,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Api; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Represents CategoryLinkManagementTest Class + */ class CategoryLinkManagementTest extends WebapiAbstract { const SERVICE_WRITE_NAME = 'catalogCategoryLinkManagementV1'; @@ -43,11 +48,21 @@ public function testInfoNoSuchEntityException() } } + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testDuplicatedProductsInChildCategories() + { + $result = $this->getAssignedProducts(3, 'all'); + $this->assertCount(3, $result); + } + /** * @param int $id category id - * @return string + * @param string|null $storeCode + * @return array|string */ - protected function getAssignedProducts($id) + private function getAssignedProducts(int $id, ?string $storeCode = null) { $serviceInfo = [ 'rest' => [ @@ -60,6 +75,6 @@ protected function getAssignedProducts($id) 'operation' => self::SERVICE_WRITE_NAME . 'GetAssignedProducts', ], ]; - return $this->_webApiCall($serviceInfo, ['categoryId' => $id]); + return $this->_webApiCall($serviceInfo, ['categoryId' => $id], null, $storeCode); } } 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 98bda8d60dac1..8751f2a39921d 100644 --- a/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Cms/Api/PageRepositoryTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Cms\Api; use Magento\Authorization\Model\Role; @@ -11,15 +13,20 @@ use Magento\Authorization\Model\RulesFactory; use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\Data\PageInterfaceFactory; +use Magento\Cms\Model\ResourceModel\Page as PageResource; +use Magento\Cms\Ui\Component\DataProvider as CmsDataProvider; use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Api\SortOrder; use Magento\Framework\Api\SortOrderBuilder; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Reflection\DataObjectProcessor; use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Api\AdminTokenServiceInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -79,22 +86,59 @@ class PageRepositoryTest extends WebapiAbstract private $adminTokens; /** - * @var array + * @var PageInterface[] */ private $createdPages = []; + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var CmsDataProvider + */ + private $cmsUiDataProvider; + + /** + * @var PageResource + */ + private $pageResource; + /** * @inheritdoc */ protected function setUp(): void { - $this->pageFactory = Bootstrap::getObjectManager()->create(PageInterfaceFactory::class); - $this->pageRepository = Bootstrap::getObjectManager()->create(PageRepositoryInterface::class); - $this->dataObjectHelper = Bootstrap::getObjectManager()->create(DataObjectHelper::class); - $this->dataObjectProcessor = Bootstrap::getObjectManager()->create(DataObjectProcessor::class); - $this->roleFactory = Bootstrap::getObjectManager()->get(RoleFactory::class); - $this->rulesFactory = Bootstrap::getObjectManager()->get(RulesFactory::class); - $this->adminTokens = Bootstrap::getObjectManager()->get(AdminTokenServiceInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->pageFactory = $this->objectManager->create(PageInterfaceFactory::class); + $this->pageRepository = $this->objectManager->create(PageRepositoryInterface::class); + $this->dataObjectHelper = $this->objectManager->create(DataObjectHelper::class); + $this->dataObjectProcessor = $this->objectManager->create(DataObjectProcessor::class); + $this->roleFactory = $this->objectManager->get(RoleFactory::class); + $this->rulesFactory = $this->objectManager->get(RulesFactory::class); + $this->adminTokens = $this->objectManager->get(AdminTokenServiceInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->filterBuilder = $this->objectManager->get(FilterBuilder::class); + $this->cmsUiDataProvider = $this->objectManager->create( + CmsDataProvider::class, + [ + 'name' => 'cms_page_listing_data_source', + 'primaryFieldName' => 'page_id', + 'requestFieldName' => 'id', + ] + ); + $this->pageResource = $this->objectManager->get(PageResource::class); } /** @@ -108,7 +152,9 @@ protected function tearDown(): void } foreach ($this->createdPages as $page) { - $this->pageRepository->delete($page); + if ($page->getId()) { + $this->pageRepository->delete($page); + } } } @@ -127,17 +173,11 @@ public function testGet(): void ->setIdentifier($pageIdentifier); $this->currentPage = $this->pageRepository->save($pageDataObject); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $this->currentPage->getId(), - 'httpMethod' => Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'GetById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'GetById', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . '/' . $this->currentPage->getId() + ); $page = $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $this->currentPage->getId()]); $this->assertNotNull($page['id']); @@ -147,6 +187,36 @@ public function testGet(): void $this->assertEquals($pageData->getIdentifier(), $pageIdentifier); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testGetByStores(string $requestStore): void + { + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $this->updatePage('page100', 0, ['store_id' => $newStoreId]); + $page = $this->loadPageByIdentifier('page100', $newStoreId); + $expectedData = array_intersect_key( + $this->dataObjectProcessor->buildOutputDataArray($page, PageInterface::class), + $this->getPageRequestData()['page'] + ); + $serviceInfo = $this->getServiceInfo( + 'GetById', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = []; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData[PageInterface::PAGE_ID] = $page->getId(); + } + + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertResponseData($page, $expectedData); + } + /** * Test create page * @@ -161,17 +231,7 @@ public function testCreate(): void $pageDataObject->setTitle($pageTitle) ->setIdentifier($pageIdentifier); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ 'page' => [ @@ -187,10 +247,37 @@ public function testCreate(): void $this->assertEquals($this->currentPage->getIdentifier(), $pageIdentifier); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testCreateByStores(string $requestStore): void + { + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); + $requestData = $this->getPageRequestData(); + + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->createdPages[] = $this->loadPageByIdentifier( + $requestData['page'][PageInterface::IDENTIFIER], + $newStoreId + ); + $this->assertResponseData($page, $requestData['page']); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); + $this->assertTrue( + $this->isPageInArray($pageGridData['items'], $page['id']), + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $requestStore) + ); + } + /** * Test update \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testUpdate() + public function testUpdate(): void { $pageTitle = self::PAGE_TITLE; $newPageTitle = self::PAGE_TITLE_NEW; @@ -210,17 +297,10 @@ public function testUpdate() PageInterface::class ); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_POST + ); $page = $this->_webApiCall($serviceInfo, ['page' => $pageData]); $this->assertNotNull($page['id']); @@ -249,17 +329,11 @@ public function testUpdateOneField(): void $this->currentPage = $this->pageRepository->save($pageDataObject); $pageId = $this->currentPage->getId(); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $pageId, - 'httpMethod' => Request::HTTP_METHOD_PUT, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_PUT, + self::RESOURCE_PATH . '/' . $pageId + ); $data = [ 'page' => [ @@ -283,12 +357,45 @@ public function testUpdateOneField(): void $this->assertEquals($page['content'], $content); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testUpdateByStores(string $requestStore): void + { + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $page = $this->updatePage('page100', 0, ['store_id' => $newStoreId]); + $serviceInfo = $this->getServiceInfo( + 'Save', + Request::HTTP_METHOD_PUT, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = $this->getPageRequestData(); + + $page = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->createdPages[] = $this->loadPageByIdentifier( + $requestData['page'][PageInterface::IDENTIFIER], + $newStoreId + ); + $this->assertResponseData($page, $requestData['page']); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); + $this->assertTrue( + $this->isPageInArray($pageGridData['items'], $page['id']), + sprintf('The "%s" page is missing from the "%s" store', $page['title'], $requestStore) + ); + } + /** * Test delete \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testDelete() + public function testDelete(): void { - $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); + $this->expectException(NoSuchEntityException::class); $pageTitle = self::PAGE_TITLE; $pageIdentifier = self::PAGE_IDENTIFIER_PREFIX . uniqid(); @@ -298,34 +405,60 @@ public function testDelete() ->setIdentifier($pageIdentifier); $this->currentPage = $this->pageRepository->save($pageDataObject); - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $this->currentPage->getId(), - 'httpMethod' => Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'DeleteById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $this->currentPage->getId() + ); $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $this->currentPage->getId()]); $this->pageRepository->getById($this->currentPage['id']); } + /** + * @dataProvider byStoresProvider + * @magentoApiDataFixture Magento/Cms/_files/pages.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_store_group_and_store.php + * @param string $requestStore + * @return void + */ + public function testDeleteByStores(string $requestStore): void + { + $newStoreId = $this->getStoreIdByRequestStore($requestStore); + $page = $this->updatePage('page100', 0, ['store_id' => $newStoreId]); + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $page->getId() + ); + $requestData = []; + if (TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP) { + $requestData[PageInterface::PAGE_ID] = $page->getId(); + } + + $pageResponse = $this->_webApiCall($serviceInfo, $requestData, null, $requestStore); + $this->assertTrue($pageResponse); + $pageGridData = $this->getPageGridDataByStoreCode($requestStore); + $this->assertFalse( + $this->isPageInArray($pageGridData['items'], (int)$page->getId()), + sprintf('The "%s" page should not be present on the "%s" store', $page->getTitle(), $requestStore) + ); + } + /** * Test search \Magento\Cms\Api\Data\PageInterface + * + * @return void */ - public function testSearch() + public function testSearch(): void { $cmsPages = $this->prepareCmsPages(); /** @var FilterBuilder $filterBuilder */ - $filterBuilder = Bootstrap::getObjectManager()->create(FilterBuilder::class); + $filterBuilder = $this->objectManager->create(FilterBuilder::class); /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = Bootstrap::getObjectManager() + $searchCriteriaBuilder = $this->objectManager ->create(SearchCriteriaBuilder::class); $filter1 = $filterBuilder @@ -351,7 +484,7 @@ public function testSearch() $searchCriteriaBuilder->addFilters([$filter3, $filter4]); /** @var SortOrderBuilder $sortOrderBuilder */ - $sortOrderBuilder = Bootstrap::getObjectManager()->create(SortOrderBuilder::class); + $sortOrderBuilder = $this->objectManager->create(SortOrderBuilder::class); /** @var SortOrder $sortOrder */ $sortOrder = $sortOrderBuilder->setField(PageInterface::IDENTIFIER) @@ -365,17 +498,11 @@ public function testSearch() $searchData = $searchCriteriaBuilder->create()->__toArray(); $requestData = ['searchCriteria' => $searchData]; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . "/search" . '?' . http_build_query($requestData), - 'httpMethod' => Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'GetList', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'GetList', + Request::HTTP_METHOD_GET, + self::RESOURCE_PATH . "/search" . '?' . http_build_query($requestData) + ); $searchResult = $this->_webApiCall($serviceInfo, $requestData); $this->assertEquals(2, $searchResult['total_count']); @@ -388,8 +515,10 @@ public function testSearch() /** * Create page with the same identifier after one was removed. + * + * @return void */ - public function testCreateSamePage() + public function testCreateSamePage(): void { $pageIdentifier = self::PAGE_IDENTIFIER_PREFIX . uniqid(); @@ -399,10 +528,30 @@ public function testCreateSamePage() $this->currentPage = $this->pageRepository->getById($id); } + /** + * Get stores for CRUD operations + * + * @return array + */ + public function byStoresProvider(): array + { + return [ + 'default_store' => [ + 'request_store' => 'default', + ], + 'second_store' => [ + 'request_store' => 'fixture_second_store', + ], + 'all' => [ + 'request_store' => 'all', + ], + ]; + } + /** * @return PageInterface[] */ - private function prepareCmsPages() + private function prepareCmsPages(): array { $result = []; @@ -435,21 +584,11 @@ private function prepareCmsPages() /** * Create page with hard-coded identifier to test with create-delete-create flow. * @param string $identifier - * @return string + * @return int */ - private function createPageWithIdentifier($identifier) + private function createPageWithIdentifier($identifier): int { - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'Save', - ], - ]; + $serviceInfo = $this->getServiceInfo('Save', Request::HTTP_METHOD_POST); $requestData = [ 'page' => [ PageInterface::IDENTIFIER => $identifier, @@ -466,19 +605,13 @@ private function createPageWithIdentifier($identifier) * @param string $pageId * @return void */ - private function deletePageByIdentifier($pageId) + private function deletePageByIdentifier($pageId): void { - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $pageId, - 'httpMethod' => Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'DeleteById', - ], - ]; + $serviceInfo = $this->getServiceInfo( + 'DeleteById', + Request::HTTP_METHOD_DELETE, + self::RESOURCE_PATH . '/' . $pageId + ); $this->_webApiCall($serviceInfo, [PageInterface::PAGE_ID => $pageId]); } @@ -547,7 +680,7 @@ public function testSaveDesign(): void //Updating the user role to allow access to design properties. /** @var Rules $rules */ - $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules = $this->objectManager->create(Rules::class); $rules->setRoleId($role->getId()); $rules->setResources(['Magento_Cms::save', 'Magento_Cms::save_design']); $rules->saveRel(); @@ -562,7 +695,7 @@ public function testSaveDesign(): void //Updating our role to remove design properties access. /** @var Rules $rules */ - $rules = Bootstrap::getObjectManager()->create(Rules::class); + $rules = $this->objectManager->create(Rules::class); $rules->setRoleId($role->getId()); $rules->setResources(['Magento_Cms::save']); $rules->saveRel(); @@ -587,4 +720,146 @@ public function testSaveDesign(): void //We don't have permissions to do that. $this->assertEquals('You are not allowed to change CMS pages design settings', $exceptionMessage); } + + /** + * Get service info array + * + * @param string $soapOperation + * @param string $httpMethod + * @param string $resourcePath + * @return array + */ + private function getServiceInfo( + string $soapOperation, + string $httpMethod, + string $resourcePath = self::RESOURCE_PATH + ): array { + return [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => $httpMethod, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . $soapOperation, + ], + ]; + } + + /** + * Check that the page is in the page grid data + * + * @param array $pageGridData + * @param int $pageId + * @return bool + */ + private function isPageInArray(array $pageGridData, int $pageId): bool + { + $isPagePresent = false; + foreach ($pageGridData as $pageData) { + if ($pageData['page_id'] == $pageId) { + $isPagePresent = true; + break; + } + } + + return $isPagePresent; + } + + /** + * Update page with data + * + * @param string $pageIdentifier + * @param int $storeId + * @param array $pageData + * @return PageInterface + */ + private function updatePage(string $pageIdentifier, int $storeId, array $pageData): PageInterface + { + $page = $this->loadPageByIdentifier($pageIdentifier, $storeId); + $page->addData($pageData); + + return $this->pageRepository->save($page); + } + + /** + * Get request data for create or update page + * + * @return array + */ + private function getPageRequestData(): array + { + return [ + 'page' => [ + PageInterface::IDENTIFIER => self::PAGE_IDENTIFIER_PREFIX . uniqid(), + PageInterface::TITLE => self::PAGE_TITLE . uniqid(), + 'active' => true, + PageInterface::PAGE_LAYOUT => '1column', + PageInterface::CONTENT => self::PAGE_CONTENT, + ] + ]; + } + + /** + * Get store id by request store code + * + * @param string $requestStoreCode + * @return int + */ + private function getStoreIdByRequestStore(string $requestStoreCode): int + { + $storeCode = $requestStoreCode === 'all' ? 'admin' : $requestStoreCode; + $store = $this->storeManager->getStore($storeCode); + + return (int)$store->getId(); + } + + /** + * Check that the response data is as expected + * + * @param array $page + * @param array $expectedData + * @return void + */ + private function assertResponseData(array $page, array $expectedData): void + { + $this->assertNotNull($page['id']); + $actualData = array_intersect_key($page, $expectedData); + $this->assertEquals($expectedData, $actualData, 'Response data does not match expected.'); + } + + /** + * Get page grid data of cms ui dataprovider filtering by store code + * + * @param string $requestStore + * @return array + */ + private function getPageGridDataByStoreCode(string $requestStore): array + { + if ($requestStore !== 'all') { + $store = $this->storeManager->getStore($requestStore); + $this->cmsUiDataProvider->addFilter( + $this->filterBuilder->setField('store_id')->setValue($store->getId())->create() + ); + } + + return $this->cmsUiDataProvider->getData(); + } + + /** + * Load page by identifier and store id + * + * @param string $identifier + * @param int $storeId + * @return PageInterface + */ + private function loadPageByIdentifier(string $identifier, int $storeId): PageInterface + { + $page = $this->pageFactory->create(); + $page->setStoreId($storeId); + $this->pageResource->load($page, $identifier, PageInterface::IDENTIFIER); + + return $page; + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php index 641253cc34c2c..dbbeaebc15936 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoryTreeTest.php @@ -649,7 +649,7 @@ public function categoryImageDataProvider(): array 'image_prefix' => '' ], 'with_pub_media_strategy' => [ - 'image_prefix' => '/pub/media/catalog/category/' + 'image_prefix' => '/media/catalog/category/' ], 'catalog_category_strategy' => [ 'image_prefix' => 'catalog/category/' diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php index d31a0aa88efc1..72b014fd39f0e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryTest.php @@ -712,7 +712,7 @@ public function categoryImageDataProvider(): array 'image_prefix' => '' ], 'with_pub_media_strategy' => [ - 'image_prefix' => '/pub/media/catalog/category/' + 'image_prefix' => '/media/catalog/category/' ], 'catalog_category_strategy' => [ 'image_prefix' => 'catalog/category/' diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php index bd4530d0724a1..41a5d41f2641d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchAggregationsTest.php @@ -16,10 +16,7 @@ class ProductSearchAggregationsTest extends GraphQlAbstract */ public function testAggregationBooleanAttribute() { - $this->markTestSkipped( - 'MC-22184: Elasticsearch returns incorrect aggregation options for booleans' - . 'MC-36768: Custom attribute not appears in elasticsearch' - ); + $this->markTestSkipped('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); $query = $this->getGraphQlQuery( '"search_product_1", "search_product_2", "search_product_3", "search_product_4" ,"search_product_5"' @@ -44,7 +41,6 @@ function ($a) { $this->assertEquals('boolean_attribute', $booleanAggregation['attribute_code']); $this->assertContainsEquals(['label' => '1', 'value'=> '1', 'count' => '3'], $booleanAggregation['options']); - $this->markTestSkipped('MC-22184: Elasticsearch returns incorrect aggregation options for booleans'); $this->assertEquals(2, $booleanAggregation['count']); $this->assertCount(2, $booleanAggregation['options']); $this->assertContainsEquals(['label' => '0', 'value'=> '0', 'count' => '2'], $booleanAggregation['options']); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 1be43a322c13e..7b14fe9159c57 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -20,6 +20,7 @@ use Magento\Eav\Model\Config; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\ObjectManager; @@ -68,14 +69,11 @@ public function testFilterForNonExistingCategory() * Verify that layered navigation filters and aggregations are correct for product query * * Filter products by an array of skus - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterLn() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); $query = <<<QUERY { products ( @@ -150,14 +148,12 @@ private function compareFilterNames(array $a, array $b) * Layered navigation for Configurable products with out of stock options * Two configurable products each having two variations and one of the child products of one Configurable set to OOS * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testLayeredNavigationForConfigurableProducts() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'test_configurable'; @@ -257,12 +253,11 @@ private function getQueryProductsWithArrayOfCustomAttributes($attributeCode, $fi * Filter products by custom attribute of dropdown type and filterTypeInput eq * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterProductsByDropDownCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); CacheCleaner::cleanAll(); $attributeCode = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attributeCode); @@ -456,12 +451,11 @@ private function getDefaultAttributeOptionValue(string $attributeCode): string * Full text search for Products and then filter the results by custom attribute (default sort is relevance) * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testSearchAndFilterByCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); $attribute_code = 'second_test_configurable'; $optionValue = $this->getDefaultAttributeOptionValue($attribute_code); @@ -604,18 +598,19 @@ public function testSearchAndFilterByCustomAttribute() * Filter by category and custom attribute * * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php + * @magentoApiDataFixture Magento/Indexer/_files/reindex_all_invalid.php * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testFilterByCategoryIdAndCustomAttribute() { - $this->markTestSkipped('MC-36768: Custom attribute value created in integration tests' - . 'fixtures product not appears in elasticsearch'); - $categoryId = 13; + /** @var GetCategoryByName $getCategoryByName */ + $getCategoryByName = Bootstrap::getObjectManager()->get(GetCategoryByName::class); + $category = $getCategoryByName->execute('Category 1.2'); $optionValue = $this->getDefaultAttributeOptionValue('second_test_configurable'); $query = <<<QUERY { products(filter:{ - category_id : {eq:"{$categoryId}"} + category_id : {eq:"{$category->getId()}"} second_test_configurable: {eq: "{$optionValue}"} }, pageSize: 3 @@ -2390,7 +2385,6 @@ public function testFilterProductsThatAreOutOfStockWithConfigSettings() /** * Verify that invalid current page return an error * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testInvalidCurrentPage() @@ -2421,7 +2415,6 @@ public function testInvalidCurrentPage() /** * Verify that invalid page size returns an error. * - * @magentoApiDataFixture Magento/Catalog/_files/category.php * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute.php */ public function testInvalidPageSize() diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php index d33d0ee0569cd..a9dadccaa5373 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/ApplyCouponsToCartTest.php @@ -21,6 +21,9 @@ class ApplyCouponsToCartTest extends GraphQlAbstract */ private $getMaskedQuoteIdByReservedOrderId; + /** + * @inheritdoc + */ protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); @@ -36,12 +39,17 @@ protected function setUp(): void public function testApplyCouponsToCart() { $couponCode = '2?ds5!2d'; + $expectedGrandTotal = 15.00; $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $query = $this->getQuery($maskedQuoteId, $couponCode); $response = $this->graphQlMutation($query); self::assertArrayHasKey('applyCouponToCart', $response); self::assertEquals($couponCode, $response['applyCouponToCart']['cart']['applied_coupons'][0]['code']); + self::assertEquals( + $expectedGrandTotal, + $response['applyCouponToCart']['cart']['prices']['grand_total']['value'] + ); } /** @@ -146,6 +154,11 @@ private function getQuery(string $maskedQuoteId, string $couponCode): string applied_coupons { code } + prices { + grand_total { + value + } + } } } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php index d29187dc7986d..5bd01e8eaff20 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderConfigurableWithVariationsTest.php @@ -7,10 +7,12 @@ namespace Magento\GraphQl\Sales; +use Magento\CatalogInventory\Model\StockRegistryStorage; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; /** @@ -73,7 +75,6 @@ public function testVariations() $productSku = 'simple_20'; /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get($productSku); - $this->assertValidVariations(); $this->assertWithOutOfStockVariation($productRepository, $product); } @@ -141,6 +142,10 @@ private function assertWithOutOfStockVariation( \Magento\Catalog\Api\ProductRepositoryInterface $productRepository, \Magento\Catalog\Api\Data\ProductInterface $product ): void { + /** @var $stockRegistryStorage StockRegistryStorage */ + $stockRegistryStorage = Bootstrap::getObjectManager()->get(StockRegistryStorage::class); + // clean stock registry + $stockRegistryStorage->clean(); // make product available in stock but disable and make reorder $product->setStockData( [ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php index 1514613987b40..ae34ea31f0d51 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Swatches/ProductSwatchDataTest.php @@ -32,8 +32,7 @@ protected function setUp(): void } /** - * @magentoApiDataFixture Magento/Swatches/_files/text_swatch_attribute.php - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @magentoApiDataFixture Magento/Swatches/_files/configurable_product_text_swatch_attribute.php */ public function testTextSwatchDataValues() { @@ -68,14 +67,15 @@ public function testTextSwatchDataValues() $option = $product['configurable_options'][0]; $this->assertArrayHasKey('values', $option); $length = count($option['values']); + $swatchData = ['Swatch 1', 'Swatch 2', 'Swatch 3']; for ($i = 0; $i < $length; $i++) { - $this->assertEquals('option ' . ($i + 1), $option['values'][$i]['swatch_data']['value']); + $swatchValue = $option['values'][$i]['swatch_data']['value']; + $this->assertContains($swatchValue, $swatchData); } } /** - * @magentoApiDataFixture Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php - * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_products.php + * @magentoApiDataFixture Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute.php */ public function testVisualSwatchDataValues() { diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/Plugin/PreventCachingPreloadedStockDataInToStockRegistry.php b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/Plugin/PreventCachingPreloadedStockDataInToStockRegistry.php new file mode 100644 index 0000000000000..95705afb0c5a8 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/Plugin/PreventCachingPreloadedStockDataInToStockRegistry.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleCatalogInventoryCache\Plugin; + +class PreventCachingPreloadedStockDataInToStockRegistry +{ + public function aroundSetStockItems(): void + { + //do not cache + } + + public function aroundSetStockStatuses(): void + { + //do not cache + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/di.xml b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/di.xml new file mode 100644 index 0000000000000..d539c3aad158d --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\CatalogInventory\Model\StockRegistryPreloader"> + <plugin name="prevent_caching_preloaded_stock_data" type="Magento\TestModuleCatalogInventoryCache\Plugin\PreventCachingPreloadedStockDataInToStockRegistry"/> + </type> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/module.xml new file mode 100644 index 0000000000000..4446f4186d30c --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleCatalogInventoryCache" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/registration.php b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/registration.php new file mode 100644 index 0000000000000..15279c9839dd2 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCatalogInventoryCache/registration.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogInventoryCache') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCatalogInventoryCache', __DIR__); +} diff --git a/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php new file mode 100644 index 0000000000000..a1d2b24d54b0e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Analytics/Cron/CollectDataTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Analytics\Cron; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks data collection process behaviour + * + * @see \Magento\Analytics\Cron\CollectData + * + * @magentoAppArea adminhtml + */ +class CollectDataTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CollectData */ + private $collectDataService; + + /** @var WriteInterface */ + private $mediaDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->collectDataService = $this->objectManager->get(CollectData::class); + $this->mediaDirectory = $this->objectManager->get(Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); + $this->removeAnalyticsDirectory(); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->removeAnalyticsDirectory(); + + parent::tearDown(); + } + + /** + * @magentoConfigFixture current_store analytics/subscription/enabled 1 + * @magentoConfigFixture default/analytics/general/token 123 + * + * @return void + */ + public function testExecute(): void + { + $this->collectDataService->execute(); + $this->assertTrue( + $this->mediaDirectory->isDirectory('analytics'), + 'Analytics was not created' + ); + $files = $this->mediaDirectory->getDriver() + ->readDirectoryRecursively($this->mediaDirectory->getAbsolutePath('analytics')); + $file = array_filter($files, function ($element) { + return substr($element, -8) === 'data.tgz'; + }); + $this->assertNotEmpty($file, 'File was not created'); + } + + /** + * Remove Analytics directory + * + * @return void + */ + private function removeAnalyticsDirectory(): void + { + $directoryToRemove = $this->mediaDirectory->getAbsolutePath('analytics'); + if ($this->mediaDirectory->isDirectory($directoryToRemove)) { + $this->mediaDirectory->delete($directoryToRemove); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php new file mode 100644 index 0000000000000..c9ad4827c2838 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Dashboard/Chart/PeriodTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Backend\Block\Dashboard\Chart; + +use Magento\Backend\ViewModel\ChartsPeriod; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Checks chart periods on Magento dashboard + * + * @magentoAppArea adminhtml + */ +class PeriodTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Template */ + private $block; + + /** @var LayoutInterface */ + private $layout; + + /** + * @inheritdoc + */ + public function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(Template::class); + $this->block->setTemplate("Magento_Backend::dashboard/chart/period.phtml"); + $this->block->setData('view_model', $this->objectManager->get(ChartsPeriod::class)); + } + + /** + * @return void + */ + public function testChartPeriodOptions(): void + { + $html = $this->block->toHtml(); + $dropDownList = [ + __('Last 24 Hours'), + __('Last 7 Days'), + __('Current Month'), + __('YTD'), + __('2YTD') + ]; + foreach ($dropDownList as $item) { + $xPath = "//select[@id='dashboard_chart_period']/option[normalize-space(text())='{$item}']"; + $this->assertEquals(1, Xpath::getElementsCountForXpath($xPath, $html)); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php new file mode 100644 index 0000000000000..afb0e66558aaa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/ResourceModel/Indexer/PriceTest.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Bundle\Model\ResourceModel\Indexer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Price; +use Magento\Customer\Model\Group; +use Magento\Framework\Indexer\ActionInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Catalog\Model\Product\Price\GetPriceIndexDataByProductId; +use Magento\CatalogInventory\Model\Indexer\Stock; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class PriceTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ActionInterface + */ + private $indexer; + + /** + * @var GetPriceIndexDataByProductId + */ + private $getPriceIndexDataByProductId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var WebsiteRepositoryInterface + */ + private $websiteRepository; + + /** + * @var Stock + */ + private $stockIndexer; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->indexer = $this->objectManager->get(Price::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->getPriceIndexDataByProductId = $this->objectManager->get(GetPriceIndexDataByProductId::class); + $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->stockIndexer = $this->objectManager->get(Stock::class); + } + + /** + * Test get bundle index price if enabled show out off stock + * + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Bundle/_files/bundle_product_with_dynamic_price.php + * @magentoConfigFixture default_store cataloginventory/options/show_out_of_stock 1 + * + * @return void + */ + public function testExecuteRowWithShowOutOfStock(): void + { + + $expectedPrices = [ + 'price' => 0, + 'final_price' => 0, + 'min_price' => 15.99, + 'max_price' => 15.99, + 'tier_price' => null + ]; + $product = $this->productRepository->get('simple1'); + $product->setStockData(['qty' => 0]); + $this->productRepository->save($product); + $this->stockIndexer->executeRow($product->getId()); + $bundleProduct = $this->productRepository->get('bundle_product_with_dynamic_price'); + $this->indexer->executeRow($bundleProduct->getId()); + $this->assertIndexTableData($bundleProduct->getId(), $expectedPrices); + } + + /** + * Asserts price data in index table. + * + * @param int $productId + * @param array $expectedPrices + * @return void + */ + private function assertIndexTableData(int $productId, array $expectedPrices): void + { + $data = $this->getPriceIndexDataByProductId->execute( + $productId, + Group::NOT_LOGGED_IN_ID, + (int)$this->websiteRepository->get('base')->getId() + ); + $data = reset($data); + foreach ($expectedPrices as $column => $price) { + $this->assertEquals($price, $data[$column]); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php index 7a999f1d205f2..7e94484961f9e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/ContentTest.php @@ -73,11 +73,11 @@ public function testGetImagesJson(bool $isProductNew) $imagesJson = $this->block->getImagesJson(); $images = json_decode($imagesJson); $image = array_shift($images); - $this->assertMatchesRegularExpression('/\/m\/a\/magento_image/', $image->file); + $this->assertMatchesRegularExpression('~/m/a/magento_image~', $image->file); $this->assertSame('image', $image->media_type); $this->assertSame('Image Alt Text', $image->label); $this->assertSame('Image Alt Text', $image->label_default); - $this->assertMatchesRegularExpression('/\/pub\/media\/catalog\/product\/m\/a\/magento_image/', $image->url); + $this->assertMatchesRegularExpression('~/media/catalog/product/m/a/magento_image~', $image->url); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php new file mode 100644 index 0000000000000..7bc359935bf60 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Category/TitleTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Block\Category; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use PHPUnit\Framework\TestCase; + +/** + * Category title check + * + * @magentoAppArea frontend + * @see \Magento\Theme\Block\Html\Title + */ +class TitleTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var Registry */ + private $registry; + + /** @var PageFactory */ + private $pageFactory; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_category'); + + parent::tearDown(); + } + + /** + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Store/_files/store.php + * @return void + */ + public function testCategoryNameOnStoreView(): void + { + $id = 333; + $categoryNameForSecondStore = 'Category Name For Second Store'; + $this->executeInStoreContext->execute( + 'test', + [$this, 'updateCategoryName'], + $this->categoryRepository->get($id), + $categoryNameForSecondStore + ); + $this->registerCategory($this->categoryRepository->get($id)); + $this->assertStringContainsString('Category 1', $this->getBlockTitle(), 'Wrong category name'); + $this->registerCategory($this->categoryRepository->get($id, $this->storeManager->getStore('test')->getId())); + $this->assertStringContainsString($categoryNameForSecondStore, $this->getBlockTitle(), 'Wrong category name'); + } + + /** + * Update category name + * + * @param CategoryInterface $category + * @param string $categoryName + * @return void + */ + public function updateCategoryName(CategoryInterface $category, string $categoryName): void + { + $category->setName($categoryName); + $this->categoryRepository->save($category); + } + + /** + * Get title block + * + * @return string + */ + private function getBlockTitle(): string + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'catalog_category_view', + ]); + $page->getLayout()->generateXml(); + $block = $page->getLayout()->getBlock('page.main.title'); + $this->assertNotFalse($block); + + return $block->stripTags($block->toHtml()); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('current_category'); + $this->registry->register('current_category', $category); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php index 52e2047917e8e..b88edc656176c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ListProduct/SortingTest.php @@ -439,7 +439,7 @@ public function productListWithOutOfStockSortOrderDataProvider(): array 'default_order_price_desc' => [ 'sort' => 'price', 'direction' => Collection::SORT_ORDER_DESC, - 'expectation' => ['simple3', 'simple2', 'simple1', 'configurable'], + 'expectation' => ['configurable', 'simple3', 'simple2', 'simple1'], ], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php index e5c6b1f8c1dd6..b57969280cdf3 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/GalleryTest.php @@ -120,9 +120,23 @@ public function testGetGalleryImagesJsonWithoutImages(): void $this->assertImages(reset($result), $this->placeholderExpectation); } + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @magentoDbIsolation enabled + * @return void + */ + public function testGetGalleryImagesJsonWithoutImagesWithImageOptimizationParametersInUrl(): void + { + $this->block->setData('product', $this->getProduct()); + $result = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + $this->assertImages(reset($result), $this->placeholderExpectation); + } + /** * @dataProvider galleryDisabledImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation enabled * @param array $images * @param array $expectation @@ -141,6 +155,7 @@ public function testGetGalleryImagesJsonWithDisabledImage(array $images, array $ * @dataProvider galleryDisabledImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation disabled * @param array $images * @param array $expectation @@ -173,6 +188,8 @@ public function galleryDisabledImagesDataProvider(): array } /** + * Test default image generation format. + * * @dataProvider galleryImagesDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDbIsolation enabled @@ -230,10 +247,95 @@ public function galleryImagesDataProvider(): array ]; } + /** + * @dataProvider galleryImagesWithImageOptimizationParametersInUrlDataProvider + * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php + * @magentoConfigFixture default/web/url/catalog_media_url_format image_optimization_parameters + * @magentoDbIsolation enabled + * @param array $images + * @param array $expectation + * @return void + */ + public function testGetGalleryImagesJsonWithImageOptimizationParametersInUrl( + array $images, + array $expectation + ): void { + $product = $this->getProduct(); + $this->setGalleryImages($product, $images); + $this->block->setData('product', $this->getProduct()); + [$firstImage, $secondImage] = $this->serializer->unserialize($this->block->getGalleryImagesJson()); + [$firstExpectedImage, $secondExpectedImage] = $expectation; + $this->assertImages($firstImage, $firstExpectedImage); + $this->assertImages($secondImage, $secondExpectedImage); + } + + /** + * @return array + */ + public function galleryImagesWithImageOptimizationParametersInUrlDataProvider(): array + { + + $imageExpectation = [ + 'thumb' => '/m/a/magento_image.jpg?width=88&height=110&store=default&image-type=thumbnail', + 'img' => '/m/a/magento_image.jpg?width=700&height=700&store=default&image-type=image', + 'full' => '/m/a/magento_image.jpg?store=default&image-type=image', + 'caption' => 'Image Alt Text', + 'position' => '1', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + $thumbnailExpectation = [ + 'thumb' => '/m/a/magento_thumbnail.jpg?width=88&height=110&store=default&image-type=thumbnail', + 'img' => '/m/a/magento_thumbnail.jpg?width=700&height=700&store=default&image-type=image', + 'full' => '/m/a/magento_thumbnail.jpg?store=default&image-type=image', + 'caption' => 'Thumbnail Image', + 'position' => '2', + 'isMain' => false, + 'type' => 'image', + 'videoUrl' => null, + ]; + + return [ + 'with_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => ['main' => true], + ], + 'expectation' => [ + $imageExpectation, + array_merge($thumbnailExpectation, ['isMain' => true]), + ], + ], + 'without_main_image' => [ + 'images' => [ + '/m/a/magento_image.jpg' => [], + '/m/a/magento_thumbnail.jpg' => [], + ], + 'expectation' => [ + array_merge($imageExpectation, ['isMain' => true]), + $thumbnailExpectation, + ], + ], + 'with_changed_position' => [ + 'images' => [ + '/m/a/magento_image.jpg' => ['position' => '2'], + '/m/a/magento_thumbnail.jpg' => ['position' => '1'], + ], + 'expectation' => [ + array_merge($thumbnailExpectation, ['position' => '1']), + array_merge($imageExpectation, ['position' => '2', 'isMain' => true]), + ], + ], + ]; + } + /** * @dataProvider galleryImagesOnStoreViewDataProvider * @magentoDataFixture Magento/Catalog/_files/product_with_multiple_images.php * @magentoDataFixture Magento/Store/_files/second_store.php + * @magentoConfigFixture default/web/url/catalog_media_url_format hash * @magentoDbIsolation disabled * @param array $images * @param array $expectation diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php index b88980181fb63..283a3834eab59 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Gallery/UploadTest.php @@ -108,7 +108,7 @@ public function uploadActionDataProvider(): array 'name' => 'magento_image.jpg', 'type' => 'image/jpeg', 'file' => '/m/a/magento_image.jpg.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.jpg', + 'url' => 'http://localhost/media/tmp/catalog/product/m/a/magento_image.jpg', 'tmp_media_path' => '/m/a/magento_image.jpg', ], ], @@ -122,7 +122,7 @@ public function uploadActionDataProvider(): array 'name' => 'product_image.png', 'type' => 'image/png', 'file' => '/p/r/product_image.png.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/p/r/product_image.png', + 'url' => 'http://localhost/media/tmp/catalog/product/p/r/product_image.png', 'tmp_media_path' => '/p/r/product_image.png', ], ], @@ -136,7 +136,7 @@ public function uploadActionDataProvider(): array 'name' => 'magento_image.gif', 'type' => 'image/gif', 'file' => '/m/a/magento_image.gif.tmp', - 'url' => 'http://localhost/pub/media/tmp/catalog/product/m/a/magento_image.gif', + 'url' => 'http://localhost/media/tmp/catalog/product/m/a/magento_image.gif', 'tmp_media_path' => '/m/a/magento_image.gif', ], ], diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php index ad62a4ec2df29..931bbf835521e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Category/CategoryUrlRewriteTest.php @@ -7,16 +7,25 @@ namespace Magento\Catalog\Controller\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Layer\Category; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Catalog\Model\Session as CatalogSession; use Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Response\Http; use Magento\Framework\Registry; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Request; +use Magento\TestFramework\Response; +use Magento\TestFramework\Store\ExecuteInStoreContext; use Magento\TestFramework\TestCase\AbstractController; /** * Checks category availability on storefront by url rewrite * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 * @magentoDbIsolation enabled */ @@ -31,6 +40,18 @@ class CategoryUrlRewriteTest extends AbstractController /** @var string */ private $categoryUrlSuffix; + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var CatalogSession */ + private $catalogSession; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -44,6 +65,10 @@ protected function setUp(): void CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, ScopeInterface::SCOPE_STORE ); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->catalogSession = $this->_objectManager->get(CatalogSession::class); + $this->executeInStoreContext = $this->_objectManager->get(ExecuteInStoreContext::class); } /** @@ -87,4 +112,87 @@ public function categoryRewriteProvider(): array ], ]; } + + /** + * Test category url on different store view + * + * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoDataFixture Magento/Store/_files/store.php + * @return void + */ + public function testCategoryUrlOnStoreView(): void + { + $id = 333; + $secondStoreUrlKey = 'category-1-second'; + $currentStore = $this->storeManager->getStore(); + $secondStore = $this->storeManager->getStore('test'); + $this->executeInStoreContext->execute( + $secondStore, + [$this, 'updateCategoryUrlKey'], + $id, + (int)$secondStore->getId(), + $secondStoreUrlKey + ); + $url = sprintf('/' . $secondStoreUrlKey . '%s', $this->categoryUrlSuffix); + $this->executeInStoreContext->execute($secondStore, [$this, 'dispatch'], $url); + $this->assertCategoryIsVisible(); + $this->assertEquals( + $secondStoreUrlKey, + $this->categoryRepository->get($id, (int)$secondStore->getId())->getUrlKey(), + 'Wrong category is registered' + ); + $this->cleanUpCachedObjects(); + $defaultStoreUrlKey = $this->categoryRepository->get($id, $currentStore->getId())->getUrlKey(); + $this->dispatch(sprintf($defaultStoreUrlKey . '%s', $this->categoryUrlSuffix)); + $this->assertCategoryIsVisible(); + } + + /** + * Assert that category is available in storefront + * + * @return void + */ + private function assertCategoryIsVisible(): void + { + $this->assertEquals( + Response::STATUS_CODE_200, + $this->getResponse()->getHttpResponseCode(), + 'Wrong response code is returned' + ); + $this->assertNotNull((int)$this->catalogSession->getData('last_viewed_category_id')); + } + + /** + * Clean up cached objects + * + * @return void + */ + private function cleanUpCachedObjects(): void + { + $this->catalogSession->clearStorage(); + $this->registry->unregister('current_category'); + $this->registry->unregister('category'); + $this->_objectManager->removeSharedInstance(Request::class); + $this->_objectManager->removeSharedInstance(Response::class); + $this->_objectManager->removeSharedInstance(Resolver::class); + $this->_objectManager->removeSharedInstance(Category::class); + $this->_objectManager->removeSharedInstance('categoryFilterList'); + $this->_response = null; + $this->_request = null; + } + + /** + * Update category url key + * + * @param int $id + * @param int $storeId + * @param string $categoryUrlKey + * @return void + */ + public function updateCategoryUrlKey(int $id, int $storeId, string $categoryUrlKey): void + { + $category = $this->categoryRepository->get($id, $storeId); + $category->setUrlKey($categoryUrlKey); + $this->categoryRepository->save($category); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php index 3f9f788dc28c7..a02a2b7aeef92 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php @@ -170,7 +170,7 @@ public function testGalleryAction(): void $this->dispatch(sprintf('catalog/product/gallery/id/%s', $product->getEntityId())); $this->assertStringContainsString( - 'http://localhost/pub/media/catalog/product/', + 'http://localhost/media/catalog/product/', $this->getResponse()->getBody() ); $this->assertStringContainsString($this->getProductImageFile(), $this->getResponse()->getBody()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php index 98f623e5f193b..4c0f74f009330 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Helper/ProductTest.php @@ -5,34 +5,69 @@ */ namespace Magento\Catalog\Helper; -class ProductTest extends \PHPUnit\Framework\TestCase +use Exception; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\Session; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppIsolation enabled + * @magentoAppArea frontend + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ProductTest extends TestCase { /** - * @var \Magento\Catalog\Helper\Product + * @var ProductHelper */ protected $helper; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ProductInterfaceFactory + */ + private $productFactory; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheridoc + */ protected function setUp(): void { - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\App\State::class) - ->setAreaCode('frontend'); - $this->helper = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Helper\Product::class - ); - - /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ - $this->productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->helper = $this->objectManager->get(ProductHelper::class); + /** @var ProductInterfaceFactory $productInterfaceFactory */ + $this->productFactory = $this->objectManager->get(ProductInterfaceFactory::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->registry = $this->objectManager->get(Registry::class); } /** * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php - * @magentoAppIsolation enabled */ public function testGetProductUrl() { @@ -46,20 +81,16 @@ public function testGetProductUrl() public function testGetPrice() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $product->setPrice(49.95); $this->assertEquals(49.95, $this->helper->getPrice($product)); } public function testGetFinalPrice() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $product->setPrice(49.95); $product->setFinalPrice(49.95); $this->assertEquals(49.95, $this->helper->getFinalPrice($product)); @@ -67,10 +98,8 @@ public function testGetFinalPrice() public function testGetImageUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/image.jpg', $this->helper->getImageUrl($product)); $product->setImage('test_image.png'); @@ -79,10 +108,8 @@ public function testGetImageUrl() public function testGetSmallImageUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/small_image.jpg', $this->helper->getSmallImageUrl($product)); $product->setSmallImage('test_image.png'); @@ -91,10 +118,8 @@ public function testGetSmallImageUrl() public function testGetThumbnailUrl() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertStringEndsWith('placeholder/thumbnail.jpg', $this->helper->getThumbnailUrl($product)); $product->setThumbnail('test_image.png'); $this->assertStringEndsWith('/test_image.png', $this->helper->getThumbnailUrl($product)); @@ -102,26 +127,20 @@ public function testGetThumbnailUrl() public function testGetEmailToFriendUrl() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + $product = $this->productFactory->create(); $product->setId(100); - $category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Category::class - ); + $category = $this->objectManager->create(CategoryInterfaceFactory::class)->create(); $category->setId(10); - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $objectManager->get(\Magento\Framework\Registry::class)->register('current_category', $category); + $this->registry->register('current_category', $category); try { $this->assertStringEndsWith( 'sendfriend/product/send/id/100/cat_id/10/', $this->helper->getEmailToFriendUrl($product) ); - $objectManager->get(\Magento\Framework\Registry::class)->unregister('current_category'); - } catch (\Exception $e) { - $objectManager->get(\Magento\Framework\Registry::class)->unregister('current_category'); + $this->registry->unregister('current_category'); + } catch (Exception $e) { + $this->registry->unregister('current_category'); throw $e; } } @@ -137,17 +156,15 @@ public function testGetStatuses() public function testCanShow() { // non-visible or disabled - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); + /** @var $product Product */ + $product = $this->productFactory->create(); $this->assertFalse($this->helper->canShow($product)); $existingProduct = $this->productRepository->get('simple'); // enabled and visible $product->setId($existingProduct->getId()); - $product->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED); - $product->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH); + $product->setStatus(Status::STATUS_ENABLED); + $product->setVisibility(Visibility::VISIBILITY_BOTH); $this->assertTrue($this->helper->canShow($product)); $this->assertTrue($this->helper->canShow((int)$product->getId())); @@ -193,39 +210,27 @@ public function testGetAttributeSourceModelByInputType() } /** - * @magentoDataFixture Magento/Catalog/_files/categories.php * @magentoDbIsolation enabled - * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Catalog/_files/categories.php */ public function testInitProduct() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $objectManager->get(\Magento\Catalog\Model\Session::class)->setLastVisitedCategoryId(2); + $this->objectManager->get(Session::class)->setLastVisitedCategoryId(2); $product = $this->productRepository->get('simple'); $this->helper->initProduct($product->getId(), 'view'); - $this->assertInstanceOf( - \Magento\Catalog\Model\Product::class, - $objectManager->get(\Magento\Framework\Registry::class)->registry('current_product') - ); - $this->assertInstanceOf( - \Magento\Catalog\Model\Category::class, - $objectManager->get(\Magento\Framework\Registry::class)->registry('current_category') - ); + $this->assertInstanceOf(Product::class, $this->registry->registry('current_product')); + $this->assertInstanceOf(Category::class, $this->registry->registry('current_category')); } public function testPrepareProductOptions() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class - ); - $buyRequest = new \Magento\Framework\DataObject(['qty' => 100, 'options' => ['option' => 'value']]); + /** @var $product Product */ + $product = $this->productFactory->create(); + $buyRequest = new DataObject(['qty' => 100, 'options' => ['option' => 'value']]); $this->helper->prepareProductOptions($product, $buyRequest); $result = $product->getPreconfiguredValues(); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $result); + $this->assertInstanceOf(DataObject::class, $result); $this->assertEquals(100, $result->getQty()); $this->assertEquals(['option' => 'value'], $result->getOptions()); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php index 1d846fc154fc0..6ae6669956f62 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php @@ -3,14 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Category; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; +use Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager; use Magento\Catalog\Model\CategoryFactory; use Magento\Framework\App\Config\ScopeConfigInterface; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Registry; use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface; @@ -19,42 +22,36 @@ use PHPUnit\Framework\TestCase; /** + * Testing category form data provider. + * * @magentoDbIsolation enabled * @magentoAppIsolation enabled * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DataProviderTest extends TestCase { - /** - * @var DataProvider - */ + /** @var DataProvider */ private $dataProvider; - /** - * @var Registry - */ + /** @var Registry */ private $registry; - /** - * @var CategoryFactory - */ + /** @var CategoryFactory */ private $categoryFactory; - /** - * @var CategoryLayoutUpdateManager - */ + /** @var CategoryLayoutUpdateManager */ private $fakeFiles; - /** - * @var ScopeConfigInterface - */ + /** @var ScopeConfigInterface */ private $scopeConfig; - /** - * @var StoreManagerInterface - */ + /** @var StoreManagerInterface */ private $storeManager; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + /** * Create subject instance. * @@ -80,8 +77,7 @@ protected function setUp(): void $objectManager = Bootstrap::getObjectManager(); $objectManager->configure([ 'preferences' => [ - \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class - => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + LayoutUpdateManager::class => CategoryLayoutUpdateManager::class ] ]); parent::setUp(); @@ -91,6 +87,15 @@ protected function setUp(): void $this->fakeFiles = $objectManager->get(CategoryLayoutUpdateManager::class); $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); $this->storeManager = $objectManager->get(StoreManagerInterface::class); + $this->categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('category'); } /** @@ -267,7 +272,7 @@ public function testExistingCategoryLayoutUnaffectedByDefaults(): void /** * Check if category page layout default value setting will apply to the new category during it's creation * - * @throws NoSuchEntityException + * @return void */ public function testNewCategoryLayoutMatchesDefault(): void { @@ -288,4 +293,32 @@ public function testNewCategoryLayoutMatchesDefault(): void $this->assertEquals($categoryDefaultPageLayout, $categoryPageLayout); } + + /** + * @magentoDataFixture Magento/Catalog/_files/category_on_second_store.php + * @return void + */ + public function testCategoryStoreView(): void + { + $id = 333; + $secondStore = $this->storeManager->getStore('test'); + $category = $this->categoryRepository->get($id, $secondStore->getId()); + $this->registerCategory($category); + $data = $this->dataProvider->getData(); + $this->assertNotEmpty($data); + $this->assertEquals('Category 1 Second', $data[$id]['name']); + $this->assertEquals('category-1-second-url-key', $data[$id]['url_key']); + } + + /** + * Register category in registry + * + * @param CategoryInterface $category + * @return void + */ + private function registerCategory(CategoryInterface $category): void + { + $this->registry->unregister('category'); + $this->registry->register('category', $category); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php new file mode 100644 index 0000000000000..e2b80a975502f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/AuthorizationTest.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product; + +use Laminas\Stdlib\Parameters; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Controller\Adminhtml\Product\Initialization\Helper; +use Magento\Catalog\Model\Product; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Exception\AuthorizationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Verify additional authorization for product operations + * + * @magentoAppArea adminhtml + */ +class AuthorizationTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Helper + */ + private $initializationHelper; + + /** + * @var HttpRequest + */ + private $request; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->initializationHelper = $this->objectManager->get(Helper::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->request = $this->objectManager->get(HttpRequest::class); + } + + /** + * Verify AuthorizedSavingOf + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @param array $data + * + * @dataProvider postRequestData + */ + public function testAuthorizedSavingOf(array $data): void + { + $this->request->setPost(new Parameters($data)); + + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + + $product = $this->initializationHelper->initialize($product); + $this->assertEquals('simple_new', $product->getName()); + $this->assertEquals( + 'container2', + $product->getCustomAttribute('options_container')->getValue() + ); + } + + /** + * @return array + */ + public function postRequestData(): array + { + return [ + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'custom_design' => '', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_layout_update' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout_update_file' => '', + ], + 'use_default' => [ + 'custom_design' => '1', + 'page_layout' => '1', + 'options_container' => '1', + 'custom_layout' => '1', + 'custom_design_from' => '1', + 'custom_design_to' => '1', + 'custom_layout_update_file' => '1', + ], + ], + ], + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'page_layout' => '', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout' => '', + 'custom_layout_update_file' => '__no_update__', + ], + 'use_default' => null, + ], + ], + ]; + } + + /** + * Verify AuthorizedSavingOf when change design attributes + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @param array $data + * + * @dataProvider postRequestDataException + */ + public function testAuthorizedSavingOfWithException(array $data): void + { + $this->expectException(AuthorizationException::class); + $this->expectErrorMessage('Not allowed to edit the product\'s design attributes'); + $this->request->setPost(new Parameters($data)); + + /** @var Product $product */ + $product = $this->productRepository->get('simple'); + + $this->initializationHelper->initialize($product); + } + + /** + * @return array + */ + public function postRequestDataException(): array + { + return [ + [ + [ + 'product' => [ + 'name' => 'simple_new', + 'page_layout' => '1column', + 'options_container' => 'container2', + 'custom_design' => '', + 'custom_design_from' => '', + 'custom_design_to' => '', + 'custom_layout' => '', + 'custom_layout_update_file' => '__no_update__', + ], + 'use_default' => null, + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php index f836fe9cbb96a..fb384253e27a7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/ProcessorTest.php @@ -46,9 +46,10 @@ public static function setUpBeforeClass(): void $mediaDirectory->create($config->getBaseTmpMediaPath()); $mediaDirectory->create($config->getBaseMediaPath()); - copy($fixtureDir . "/magento_image.jpg", self::$_mediaTmpDir . "/magento_image.jpg"); - copy($fixtureDir . "/magento_image.jpg", self::$_mediaDir . "/magento_image.jpg"); - copy($fixtureDir . "/magento_small_image.jpg", self::$_mediaTmpDir . "/magento_small_image.jpg"); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaTmpDir . "/magento_image.jpg", file_get_contents($fixtureDir . "/magento_image.jpg")); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaDir . "/magento_image.jpg", file_get_contents($fixtureDir . "/magento_image.jpg")); + $mediaDirectory->getDriver()->filePutContents(self::$_mediaTmpDir . "/magento_small_image.jpg", file_get_contents($fixtureDir . "/magento_small_image.jpg")); + } public static function tearDownAfterClass(): void diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php index 568859c1c83f0..481ec6aeac0f2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Gallery/UpdateHandlerTest.php @@ -507,6 +507,92 @@ public function testDeleteWithMultiWebsites(): void $this->assertArrayNotHasKey($secondStoreId, $imageRolesPerStore); } + /** + * Check that product images should be updated successfully regardless if the existing images exist or not + * + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + * @dataProvider updateImageDataProvider + * @param string $newFile + * @param string $expectedFile + * @param bool $exist + * @return void + */ + public function testUpdateImage(string $newFile, string $expectedFile, bool $exist): void + { + $product = $this->getProduct(Store::DEFAULT_STORE_ID); + $images = $product->getData('media_gallery')['images']; + $this->assertCount(1, $images); + $oldImage = reset($images) ?: []; + $this->assertEquals($oldImage['file'], $product->getImage()); + $this->assertEquals($oldImage['file'], $product->getSmallImage()); + $this->assertEquals($oldImage['file'], $product->getThumbnail()); + $path = $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $oldImage['file']); + $tmpPath = $this->mediaDirectory->getAbsolutePath($this->config->getBaseTmpMediaPath() . $oldImage['file']); + $this->assertFileExists($path); + $this->mediaDirectory->getDriver()->copy($path, $tmpPath); + if (!$exist) { + $this->mediaDirectory->getDriver()->deleteFile($path); + $this->assertFileDoesNotExist($path); + } + // delete old image + $oldImage['removed'] = 1; + $newImage = [ + 'file' => $newFile, + 'position' => 1, + 'label' => 'New Image Alt Text', + 'disabled' => 0, + 'media_type' => 'image' + ]; + $newImageRoles = [ + 'image' => $newFile, + 'small_image' => 'no_selection', + 'thumbnail' => 'no_selection', + ]; + $product->setData('media_gallery', ['images' => [$oldImage, $newImage]]); + $product->addData($newImageRoles); + $this->updateHandler->execute($product); + $product = $this->getProduct(Store::DEFAULT_STORE_ID); + $images = $product->getData('media_gallery')['images']; + $this->assertCount(1, $images); + $image = reset($images) ?: []; + $this->assertEquals($newImage['label'], $image['label']); + $this->assertEquals($expectedFile, $product->getImage()); + $this->assertEquals($newImageRoles['small_image'], $product->getSmallImage()); + $this->assertEquals($newImageRoles['thumbnail'], $product->getThumbnail()); + $path = $this->mediaDirectory->getAbsolutePath($this->config->getBaseMediaPath() . $product->getImage()); + // Assert that the image exists on disk. + $this->assertFileExists($path); + } + + /** + * @return array[] + */ + public function updateImageDataProvider(): array + { + return [ + [ + '/m/a/magento_image.jpg', + '/m/a/magento_image_1.jpg', + true + ], + [ + '/m/a/magento_image.jpg', + '/m/a/magento_image.jpg', + false + ], + [ + '/m/a/magento_small_image.jpg', + '/m/a/magento_small_image.jpg', + true + ], + [ + '/m/a/magento_small_image.jpg', + '/m/a/magento_small_image.jpg', + false + ] + ]; + } + /** * Check product image link and product image exist * diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php index 1c9b8f2ce1918..b741285ebb6f1 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/ImageTest.php @@ -52,7 +52,7 @@ public function testSaveFilePlaceholder($model) public function testGetUrlPlaceholder($model) { $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', + 'http://localhost/static/%s/frontend/%s/Magento_Catalog/images/product/placeholder/image.jpg', $model->getUrl() ); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php index 0be889f546a2b..64b009b5b8d13 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Option/Type/File/ValidatorFileTest.php @@ -362,8 +362,8 @@ protected function expectedValidate() return [ 'type' => 'image/jpeg', 'title' => 'test.jpg', - 'quote_path' => 'custom_options/quote/t/e/RandomString', - 'order_path' => 'custom_options/order/t/e/RandomString', + 'quote_path' => 'custom_options/quote/R/a/RandomString', + 'order_path' => 'custom_options/order/R/a/RandomString', 'size' => '3046', 'width' => 136, 'height' => 131, diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php index c72e7e0e1d078..4cd9d74e58418 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Type/AbstractTypeTest.php @@ -3,41 +3,60 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductRepository; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Config; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Registry; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AbstractTypeTest extends \PHPUnit\Framework\TestCase +class AbstractTypeTest extends TestCase { /** - * @var \Magento\Catalog\Model\Product\Type\AbstractType + * @var AbstractType */ protected $_model; protected function setUp(): void { - $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Api\ProductRepositoryInterface::class + $productRepository = Bootstrap::getObjectManager()->get( + ProductRepositoryInterface::class ); - $catalogProductOption = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\Product\Option::class + $catalogProductOption = Bootstrap::getObjectManager()->get( + Option::class ); - $catalogProductType = $this->createMock(\Magento\Catalog\Model\Product\Type::class); - $eventManager = $this->createPartialMock(\Magento\Framework\Event\ManagerInterface::class, ['dispatch']); - $fileStorageDb = $this->createMock(\Magento\MediaStorage\Helper\File\Storage\Database::class); - $filesystem = $this->createMock(\Magento\Framework\Filesystem::class); - $registry = $this->createMock(\Magento\Framework\Registry::class); - $logger = $this->createMock(\Psr\Log\LoggerInterface::class); - $serializer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Serialize\Serializer\Json::class + $catalogProductType = $this->createMock(Type::class); + $eventManager = $this->createPartialMock(ManagerInterface::class, ['dispatch']); + $fileStorageDb = $this->createMock(Database::class); + $filesystem = $this->createMock(Filesystem::class); + $registry = $this->createMock(Registry::class); + $logger = $this->createMock(LoggerInterface::class); + $serializer = Bootstrap::getObjectManager()->get( + Json::class ); $this->_model = $this->getMockForAbstractClass( - \Magento\Catalog\Model\Product\Type\AbstractType::class, + AbstractType::class, [ $catalogProductOption, - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class), + Bootstrap::getObjectManager()->get(Config::class), $catalogProductType, $eventManager, $fileStorageDb, @@ -53,7 +72,7 @@ protected function setUp(): void public function testGetRelationInfo() { $info = $this->_model->getRelationInfo(); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $info); + $this->assertInstanceOf(DataObject::class, $info); $this->assertNotSame($info, $this->_model->getRelationInfo()); } @@ -72,8 +91,8 @@ public function testGetParentIdsByChild() */ public function testGetSetAttributes() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -85,7 +104,7 @@ public function testGetSetAttributes() $this->assertArrayHasKey('name', $attributes); $isTypeExists = false; foreach ($attributes as $attribute) { - $this->assertInstanceOf(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class, $attribute); + $this->assertInstanceOf(Attribute::class, $attribute); $applyTo = $attribute->getApplyTo(); if (count($applyTo) > 0 && !in_array('simple', $applyTo)) { $isTypeExists = true; @@ -97,9 +116,9 @@ public function testGetSetAttributes() public function testAttributesCompare() { - $attribute[1] = new \Magento\Framework\DataObject(['group_sort_path' => 1, 'sort_path' => 10]); - $attribute[2] = new \Magento\Framework\DataObject(['group_sort_path' => 1, 'sort_path' => 5]); - $attribute[3] = new \Magento\Framework\DataObject(['group_sort_path' => 2, 'sort_path' => 10]); + $attribute[1] = new DataObject(['group_sort_path' => 1, 'sort_path' => 10]); + $attribute[2] = new DataObject(['group_sort_path' => 1, 'sort_path' => 5]); + $attribute[3] = new DataObject(['group_sort_path' => 2, 'sort_path' => 10]); $this->assertEquals(1, $this->_model->attributesCompare($attribute[1], $attribute[2])); $this->assertEquals(-1, $this->_model->attributesCompare($attribute[2], $attribute[1])); $this->assertEquals(-1, $this->_model->attributesCompare($attribute[1], $attribute[3])); @@ -110,9 +129,9 @@ public function testAttributesCompare() public function testGetAttributeById() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class )->load( 1 ); @@ -120,8 +139,8 @@ public function testGetAttributeById() $this->assertNull($this->_model->getAttributeById(-1, $product)); $this->assertNull($this->_model->getAttributeById(null, $product)); - $sku = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Eav\Model\Config::class + $sku = Bootstrap::getObjectManager()->get( + Config::class )->getAttribute( 'catalog_product', 'sku' @@ -140,8 +159,8 @@ public function testGetAttributeById() */ public function testIsVirtual() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertFalse($this->_model->isVirtual($product)); } @@ -151,8 +170,8 @@ public function testIsVirtual() */ public function testIsSalable() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertTrue($this->_model->isSalable($product)); @@ -169,20 +188,20 @@ public function testIsSalable() */ public function testPrepareForCart() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->load(10); // fixture $this->assertEmpty($product->getCustomOption('info_buyRequest')); $requestData = ['qty' => 5]; - $result = $this->_model->prepareForCart(new \Magento\Framework\DataObject($requestData), $product); + $result = $this->_model->prepareForCart(new DataObject($requestData), $product); $this->assertArrayHasKey(0, $result); $this->assertSame($product, $result[0]); $buyRequest = $product->getCustomOption('info_buyRequest'); - $this->assertInstanceOf(\Magento\Framework\DataObject::class, $buyRequest); + $this->assertInstanceOf(DataObject::class, $buyRequest); $this->assertEquals($product->getId(), $buyRequest->getProductId()); $this->assertSame($product, $buyRequest->getProduct()); $this->assertEquals(json_encode($requestData), $buyRequest->getValue()); @@ -193,15 +212,15 @@ public function testPrepareForCart() */ public function testPrepareForCartOptionsException() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture $this->assertStringContainsString( "The product's required option(s) weren't entered. Make sure the options are entered and try again.", - $this->_model->prepareForCart(new \Magento\Framework\DataObject(), $product) + $this->_model->prepareForCart(new DataObject(), $product) ); } @@ -215,9 +234,9 @@ public function testGetSpecifyOptionMessage() public function testCheckProductBuyState() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->setSkipCheckRequiredOption('_'); $this->_model->checkProductBuyState($product); @@ -228,10 +247,10 @@ public function testCheckProductBuyState() */ public function testCheckProductBuyStateException() { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -243,9 +262,9 @@ public function testCheckProductBuyStateException() */ public function testGetOrderOptions() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertEquals([], $this->_model->getOrderOptions($product)); @@ -283,8 +302,8 @@ public function testGetOrderOptions() */ public function testBeforeSave() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -299,8 +318,8 @@ public function testBeforeSave() */ public function testGetSku() { - $repository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ProductRepository::class + $repository = Bootstrap::getObjectManager()->create( + ProductRepository::class ); $product = $repository->get('simple'); // fixture @@ -312,9 +331,9 @@ public function testGetSku() */ public function testGetOptionSku() { - /** @var $product \Magento\Catalog\Model\Product */ - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create( + Product::class ); $this->assertEmpty($this->_model->getOptionSku($product)); @@ -336,7 +355,7 @@ public function testGetOptionSku() public function testGetWeight() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertEmpty($this->_model->getWeight($product)); $product->setWeight('value'); $this->assertEquals('value', $this->_model->getWeight($product)); @@ -346,16 +365,16 @@ public function testHasOptions() { $this->markTestIncomplete('Bug MAGE-2814'); - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertFalse($this->_model->hasOptions($product)); - $product = new \Magento\Framework\DataObject(['has_options' => true]); + $product = new DataObject(['has_options' => true]); $this->assertTrue($this->_model->hasOptions($product)); } public function testHasRequiredOptions() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertFalse($this->_model->hasRequiredOptions($product)); $product->setRequiredOptions(1); $this->assertTrue($this->_model->hasRequiredOptions($product)); @@ -363,7 +382,7 @@ public function testHasRequiredOptions() public function testGetSetStoreFilter() { - $product = new \Magento\Framework\DataObject(); + $product = new DataObject(); $this->assertNull($this->_model->getStoreFilter($product)); $store = new \StdClass(); $this->_model->setStoreFilter($store, $product); @@ -374,8 +393,8 @@ public function testGetForceChildItemQtyChanges() { $this->assertFalse( $this->_model->getForceChildItemQtyChanges( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -387,8 +406,8 @@ public function testPrepareQuoteItemQty() 3.0, $this->_model->prepareQuoteItemQty( 3, - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -396,12 +415,12 @@ public function testPrepareQuoteItemQty() public function testAssignProductToOption() { - $product = new \Magento\Framework\DataObject(); - $option = new \Magento\Framework\DataObject(); + $product = new DataObject(); + $option = new DataObject(); $this->_model->assignProductToOption($product, $option, $product); $this->assertSame($product, $option->getProduct()); - $option = new \Magento\Framework\DataObject(); + $option = new DataObject(); $this->_model->assignProductToOption(null, $option, $product); $this->assertSame($product, $option->getProduct()); } @@ -415,8 +434,8 @@ public function testSetConfig() { $this->assertFalse( $this->_model->isComposite( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -425,8 +444,8 @@ public function testSetConfig() $this->_model->setConfig($config); $this->assertTrue( $this->_model->isComposite( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + Bootstrap::getObjectManager()->create( + Product::class ) ) ); @@ -438,8 +457,8 @@ public function testSetConfig() */ public function testGetSearchableData() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); $product->load(1); // fixture @@ -467,10 +486,41 @@ public function testProcessBuyRequest() public function testCheckProductConfiguration() { - $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\Product::class + $product = Bootstrap::getObjectManager()->create( + Product::class ); - $buyRequest = new \Magento\Framework\DataObject(['qty' => 5]); + $buyRequest = new DataObject(['qty' => 5]); $this->_model->checkProductConfiguration($product, $buyRequest); } + + /** + * Test that only one exception appears instead of multiple identical exceptions + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * + * @return void + */ + public function testPrepareOptions(): void + { + $exceptionMessage = + "The product's required option(s) weren't entered. Make sure the options are entered and try again."; + $product = Bootstrap::getObjectManager()->create( + Product::class + ); + $product->load(1); + $buyRequest = new DataObject(['product' => 1]); + $method = new \ReflectionMethod( + AbstractType::class, + '_prepareOptions' + ); + $method->setAccessible(true); + $exceptionIsThrown = false; + try { + $method->invoke($this->_model, $buyRequest, $product, 'full'); + } catch (LocalizedException $exception) { + $this->assertEquals($exceptionMessage, $exception->getMessage()); + $exceptionIsThrown = true; + } + $this->assertTrue($exceptionIsThrown); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php index c63a3c8249e77..e973a25d07354 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/UpdateProductWebsiteTest.php @@ -10,7 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\ResourceModel\Product\Website\Link; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -82,10 +82,10 @@ public function testUnassignProductFromWebsite(): void */ public function testAssignNonExistingWebsite(): void { - $messageFormat = 'The website with id %s that was requested wasn\'t found. Verify the website and try again.'; + $messageFormat = 'The product was unable to be saved. Please try again.'; $nonExistingWebsiteId = 921564; - $this->expectException(NoSuchEntityException::class); - $this->expectExceptionMessage((string)__(sprintf($messageFormat, $nonExistingWebsiteId))); + $this->expectException(CouldNotSaveException::class); + $this->expectExceptionMessage((string)__($messageFormat)); $this->updateProductWebsites('simple2', [$nonExistingWebsiteId]); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php index b0f36f250991b..8acb243a706c2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductTest.php @@ -246,7 +246,7 @@ protected function _copyFileToBaseTmpMediaPath($sourceFile) $mediaDirectory->create($config->getBaseTmpMediaPath()); $targetFile = $config->getTmpMediaPath(basename($sourceFile)); - copy($sourceFile, $mediaDirectory->getAbsolutePath($targetFile)); + $mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($targetFile), file_get_contents($sourceFile)); return $targetFile; } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php index 3491065323c9f..7a2ad0fefac8a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_category_image.php @@ -10,13 +10,14 @@ $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ -$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class) - ->getDirectoryWrite(DirectoryList::MEDIA); +$mediaDirectory = $objectManager->get(\Magento\Framework\Filesystem::class)->getDirectoryWrite(DirectoryList::MEDIA); $fileName = 'magento_small_image.jpg'; $fileNameLong = 'magento_long_image_name_magento_long_image_name_magento_long_image_name.jpg'; $filePath = 'catalog/category/' . $fileName; $filePathLong = 'catalog/category/' . $fileNameLong; $mediaDirectory->create('catalog/category'); - -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($filePath)); -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileNameLong, $mediaDirectory->getAbsolutePath($filePathLong)); +$shortImageContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileName); +$longImageContent = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileNameLong); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($filePath), $shortImageContent); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($filePathLong), $longImageContent); +unset($shortImageContent, $longImageContent); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php index 2562acdda2dc3..ce688f38ed1ec 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/catalog_tmp_category_image.php @@ -15,5 +15,4 @@ $fileName = 'magento_small_image.jpg'; $tmpFilePath = 'catalog/tmp/category/' . $fileName; $mediaDirectory->create('catalog/tmp/category'); - -copy(__DIR__ . DIRECTORY_SEPARATOR . $fileName, $mediaDirectory->getAbsolutePath($tmpFilePath)); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($tmpFilePath), file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . $fileName)); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php index 4255d7d3c98e5..9b743542b8573 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/categories.php @@ -3,29 +3,48 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$defaultAttributeSet = $objectManager->get(Magento\Eav\Model\Config::class) - ->getEntityType('catalog_product') - ->getDefaultAttributeSetId(); +use Magento\Catalog\Api\CategoryLinkManagementInterface; +use Magento\Catalog\Api\CategoryLinkRepositoryInterface; +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Eav\Model\Config; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; -$productRepository = $objectManager->create( - \Magento\Catalog\Api\ProductRepositoryInterface::class -); +$objectManager = Bootstrap::getObjectManager(); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); +$rootCategoryId = $baseWebsite->getDefaultStore()->getRootCategoryId(); + +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); + +$defaultAttributeSet = $objectManager->get(Config::class)->getEntityType(Product::ENTITY)->getDefaultAttributeSetId(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$categoryFactory = $objectManager->get(CategoryInterfaceFactory::class); $categoryLinkRepository = $objectManager->create( - \Magento\Catalog\Api\CategoryLinkRepositoryInterface::class, + CategoryLinkRepositoryInterface::class, [ - 'productRepository' => $productRepository + 'productRepository' => $productRepository, ] ); /** @var Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ -$categoryLinkManagement = $objectManager->create(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); +$categoryLinkManagement = $objectManager->get(CategoryLinkManagementInterface::class); $reflectionClass = new \ReflectionClass(get_class($categoryLinkManagement)); $properties = [ 'productRepository' => $productRepository, - 'categoryLinkRepository' => $categoryLinkRepository + 'categoryLinkRepository' => $categoryLinkRepository, ]; foreach ($properties as $key => $value) { if ($reflectionClass->hasProperty($key)) { @@ -39,7 +58,7 @@ * After installation system has two categories: root one with ID:1 and Default category with ID:2 */ /** @var $category \Magento\Catalog\Model\Category */ -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(3) ->setName('Category 1') @@ -52,7 +71,7 @@ ->setPosition(1) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(4) ->setName('Category 1.1') @@ -67,7 +86,7 @@ ->setDescription('Category 1.1 description.') ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(5) ->setName('Category 1.1.1') @@ -83,7 +102,7 @@ ->setDescription('This is the description for Category 1.1.1') ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(6) ->setName('Category 2') @@ -96,7 +115,7 @@ ->setPosition(2) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(7) ->setName('Movable') @@ -109,7 +128,7 @@ ->setPosition(3) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(8) ->setName('Inactive') @@ -122,7 +141,7 @@ ->setPosition(4) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(9) ->setName('Movable Position 1') @@ -135,7 +154,7 @@ ->setPosition(5) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(10) ->setName('Movable Position 2') @@ -148,7 +167,7 @@ ->setPosition(6) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(11) ->setName('Movable Position 3') @@ -161,7 +180,7 @@ ->setPosition(7) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(12) ->setName('Category 12') @@ -174,7 +193,7 @@ ->setPosition(8) ->save(); -$category = $objectManager->create(\Magento\Catalog\Model\Category::class); +$category = $categoryFactory->create(); $category->isObjectNew(true); $category->setId(13) ->setName('Category 1.2') @@ -189,84 +208,86 @@ ->setPosition(2) ->save(); -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product') ->setSku('simple') ->setPrice(10) ->setWeight(18) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple1 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple1->getSku(), [2, 3, 4, 13] ); -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Two') ->setSku('12345') // SKU intentionally contains digits only ->setPrice(45.67) ->setWeight(56) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple2 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple2->getSku(), [5, 4] ); -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Not Visible On Storefront') ->setSku('simple-3') ->setPrice(15) ->setWeight(2) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_NOT_VISIBLE) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED); + +$simple3 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple3->getSku(), [10, 11, 12] ); -/** @var $product \Magento\Catalog\Model\Product */ -$product = $objectManager->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($defaultAttributeSet) - ->setStoreId(1) - ->setWebsiteIds([1]) + ->setStoreId($storeManager->getDefaultStoreView()->getId()) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Simple Product Three') ->setSku('simple-4') ->setPrice(10) ->setWeight(18) ->setStockData(['use_config_manage_stock' => 0]) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED); + +$simple4 = $productRepository->save($product); $categoryLinkManagement->assignProductToCategories( - $product->getSku(), + $simple4->getSku(), [10, 11, 12, 13] ); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php new file mode 100644 index 0000000000000..0b094ba29290e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/store.php'); + +$objectManager = Bootstrap::getObjectManager(); +$storeManager = $objectManager->get(StoreManagerInterface::class); +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +$executeInStoreContext = $objectManager->get(ExecuteInStoreContext::class); + +$currentStore = $storeManager->getStore(); +$secondStore = $storeManager->getStore('test'); +$category = $categoryRepository->get(333); +$category->setName('Category 1 Second'); +$category->setUrlKey('category-1-second-url-key'); +$executeInStoreContext->execute($secondStore, function ($categoryRepository, $category) { + $categoryRepository->save($category); +}, $categoryRepository, $category); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php new file mode 100644 index 0000000000000..b7b8491612fec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_on_second_store_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php index c2c3782c8cd23..6737aef1eb487 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php @@ -5,29 +5,26 @@ */ declare(strict_types=1); -use Magento\TestFramework\Workaround\Override\Fixture\Resolver; - -Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); - +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\TestFramework\Helper\CacheCleaner; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); -/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$objectManager = Bootstrap::getObjectManager(); -$eavConfig->clear(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var $attribute Attribute */ +$attribute = $attributeRepository->get('test_configurable'); $attribute->setIsSearchable(1) - ->setIsVisibleInAdvancedSearch(1) - ->setIsFilterable(true) - ->setIsFilterableInSearch(true) + ->setIsVisibleInAdvancedSearch(1) + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) ->setIsVisibleOnFront(1); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); - CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php index 57b918fb5e663..6f81d6b659996 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_boolean_attribute.php @@ -5,51 +5,56 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\Catalog\Setup\CategorySetup; -use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Eav\Model\Entity\Attribute\Source\Boolean; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); -/** @var Attribute $attribute */ -$attribute = $objectManager->create(Attribute::class); -/** @var $installer CategorySetup */ -$installer = $objectManager->create(CategorySetup::class); -try { - $attributeRepository->get(CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, 'boolean_attribute'); -} catch (NoSuchEntityException $e) { - $attribute->setData( - [ - 'attribute_code' => 'boolean_attribute', - 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, - 'is_global' => 0, - 'is_user_defined' => 1, - 'frontend_input' => 'boolean', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 0, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 0, - 'frontend_label' => ['Boolean Attribute'], - 'backend_type' => 'int', - 'source_model' => Boolean::class - ] - ); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var $installer CategorySetup */ +$installer = $objectManager->get(CategorySetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); - $attributeRepository->save($attribute); +/** @var ProductAttributeInterface $attributeModel */ +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'boolean_attribute', + 'entity_type_id' => CategorySetup::CATALOG_PRODUCT_ENTITY_TYPE_ID, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'boolean', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 0, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Boolean Attribute'], + 'backend_type' => 'int', + 'source_model' => Boolean::class + ] +); +$attribute = $attributeRepository->save($attributeModel); - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'Attributes', $attribute->getId()); -} +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php index 962a66f11f532..1794530832d04 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_image.php @@ -24,12 +24,11 @@ $images = ['magento_image.jpg', 'magento_small_image.jpg', 'magento_thumbnail.jpg']; foreach ($images as $image) { - $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . DIRECTORY_SEPARATOR . $targetTmpDirPath - . DIRECTORY_SEPARATOR . $image; + $targetTmpFilePath = $mediaDirectory->getAbsolutePath() . $targetTmpDirPath . $image; $sourceFilePath = __DIR__ . DIRECTORY_SEPARATOR . $image; + $mediaDirectory->getDriver()->filePutContents($targetTmpFilePath, file_get_contents($sourceFilePath)); - copy($sourceFilePath, $targetTmpFilePath); // Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir $database->saveFile($targetTmpFilePath); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php index 252f99c97b787..688a3bd199570 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_image.php @@ -38,7 +38,7 @@ $mediaDirectory->create($targetTmpDirPath); $dist = $mediaDirectory->getAbsolutePath($mediaConfig->getBaseMediaPath() . DIRECTORY_SEPARATOR . 'magento_image.jpg'); -copy(__DIR__ . '/magento_image.jpg', $dist); +$mediaDirectory->getDriver()->filePutContents($dist, file_get_contents(__DIR__ . '/magento_image.jpg')); /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php index 29812aa942ab5..3bc3fef56e32e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php @@ -5,125 +5,135 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\CategoryInterfaceFactory; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Indexer\Model\Indexer; +use Magento\Indexer\Model\Indexer\Collection; +use Magento\Msrp\Model\Product\Attribute\Source\Type as SourceType; +use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\TestFramework\Helper\CacheCleaner; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); - +$objectManager = Bootstrap::getObjectManager(); + +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); + +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); + +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] +); +$attribute = $attributeRepository->save($attributeModel); + +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); +CacheCleaner::cleanAll(); $eavConfig->clear(); -/** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); - -if (!$attribute->getId()) { - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute->setData( - [ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default' => ['option_0'] - ] - ); - - $attributeRepository->save($attribute); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); - CacheCleaner::cleanAll(); -} - -$eavConfig->clear(); - -/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setId(10) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product1') ->setSku('simple1') ->setTaxClassId('none') ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') - ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) + ->setMsrpDisplayActualPriceType(SourceType::TYPE_IN_CART) ->setPrice(10) ->setWeight(1) ->setMetaTitle('meta title') ->setMetaKeyword('meta keyword') ->setMetaDescription('meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setWebsiteIds([1]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('5.99') - ->save(); + ->setSpecialPrice('5.99'); +$simple1 = $productRepository->save($product); -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setId(11) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product2') ->setSku('simple2') ->setTaxClassId('none') ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') - ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_ON_GESTURE) + ->setMsrpDisplayActualPriceType(SourceType::TYPE_ON_GESTURE) ->setPrice(20) ->setWeight(1) ->setMetaTitle('meta title') ->setMetaKeyword('meta keyword') ->setMetaDescription('meta description') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setWebsiteIds([1]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('15.99') - ->save(); + ->setSpecialPrice('15.99'); +$simple2 = $productRepository->save($product); -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->isObjectNew(true); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setId(12) - ->setAttributeSetId(4) +/** @var Product $product */ +$product = $productInterfaceFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product3') ->setSku('simple3') ->setTaxClassId('none') @@ -131,44 +141,42 @@ ->setShortDescription('short description') ->setPrice(30) ->setWeight(1) - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) - ->setWebsiteIds([1]) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_DISABLED) + ->setWebsiteIds([$baseWebsite->getId()]) ->setCategoryIds([]) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 140, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->setSpecialPrice('25.99') - ->save(); + ->setSpecialPrice('25.99'); +$simple3 = $productRepository->save($product); -$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +/** @var CategoryInterfaceFactory $categoryInterfaceFactory */ +$categoryInterfaceFactory = $objectManager->get(CategoryInterfaceFactory::class); + +$category = $categoryInterfaceFactory->create(); $category->isObjectNew(true); -$category->setId( - 333 -)->setCreatedAt( - '2014-06-23 09:50:07' -)->setName( - 'Category 1' -)->setParentId( - 2 -)->setPath( - '1/2/333' -)->setLevel( - 2 -)->setAvailableSortBy( - ['position', 'name'] -)->setDefaultSortBy( - 'name' -)->setIsActive( - true -)->setPosition( - 1 -)->setPostedProducts( - [10 => 10, 11 => 11, 12 => 12] -)->save(); +$category->setId(333) + ->setCreatedAt('2014-06-23 09:50:07') + ->setName('Category 1') + ->setParentId(2) + ->setPath('1/2/333') + ->setLevel(2) + ->setAvailableSortBy(['position', 'name']) + ->setDefaultSortBy('name') + ->setIsActive(true) + ->setPosition(1) + ->setPostedProducts( + [ + $simple1->getId() => 10, + $simple2->getId() => 11, + $simple3->getId() => 12 + ] + ); +$category->save(); -/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ -$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +/** @var Collection $indexerCollection */ +$indexerCollection = $objectManager->get(Collection::class); $indexerCollection->load(); -/** @var \Magento\Indexer\Model\Indexer $indexer */ +/** @var Indexer $indexer */ foreach ($indexerCollection->getItems() as $indexer) { $indexer->reindexAll(); } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php index dd89f8974a647..47e6a4e71cb69 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_rollback.php @@ -5,47 +5,57 @@ */ declare(strict_types=1); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); - +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Catalog\Model\GetCategoryByName; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); foreach (['simple1', 'simple2', 'simple3'] as $sku) { try { $product = $productRepository->get($sku, false, null, true); $productRepository->delete($product); - } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + } catch (NoSuchEntityException $exception) { //Product already removed } } -$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); -foreach ($productCollection as $product) { - $product->delete(); +/** @var CategoryRepositoryInterface $categoryRepository */ +$categoryRepository = $objectManager->get(CategoryRepositoryInterface::class); +/** @var GetCategoryByName $getCategoryByName */ +$getCategoryByName = $objectManager->get(GetCategoryByName::class); +$category = $getCategoryByName->execute('Category 1'); +try { + if ($category->getId()) { + $categoryRepository->delete($category); + } +} catch (NoSuchEntityException $exception) { + //Category already removed } -/** @var $category \Magento\Catalog\Model\Category */ -$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); -$category->load(333); -if ($category->getId()) { - $category->delete(); -} +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); -$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); -if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute - && $attribute->getId() -) { - $attribute->delete(); +try { + $attribute = $attributeRepository->get('test_configurable'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $exception) { + //Attribute already removed } $eavConfig->clear(); - $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 4dd088e148d75..76056f2fa9e0d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -5,8 +5,15 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use Magento\TestFramework\Eav\Model\GetAttributeSetByName; @@ -15,136 +22,117 @@ Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/categories.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var GetAttributeSetByName $getAttributeSetByName */ -$getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); -$attributeSet = $getAttributeSetByName->execute('second_attribute_set'); -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); - -$eavConfig->clear(); - -$attribute1 = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); -$eavConfig->clear(); - -/** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); - -if (!$attribute->getId()) { - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute->setData( - [ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default_value' => 'option_0' - ] - ); - $attributeRepository->save($attribute); - - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); -} -// create a second attribute -if (!$attribute1->getId()) { - - /** @var $attribute1 \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute1 = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute1->setData( - [ - 'attribute_code' => 'second_test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 1, - 'is_visible_in_advanced_search' => 1, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Second Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default' => ['option_0'] - ] - ); - - $attributeRepository->save($attribute1); - - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup( - 'catalog_product', - $attributeSet->getId(), - $attributeSet->getDefaultGroupId(), - $attribute1->getId() - ); -} +/** @var Config $eavConfig */ +$eavConfig = $objectManager->get(Config::class); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); +/** @var GetAttributeSetByName $getAttributeSetByName */ +$getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); +$secondAttributeSet = $getAttributeSetByName->execute('second_attribute_set'); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$defaultAttributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$defaultGroupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $defaultAttributeSetId); + +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] +); +$attribute = $attributeRepository->save($attributeModel); +$installer->addAttributeToGroup( + Product::ENTITY, + $defaultAttributeSetId, + $defaultGroupId, + $attribute->getId() +); $eavConfig->clear(); -/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var ProductAttributeInterface $attribute */ +$attributeModel2 = $attributeFactory->create(); +$attributeModel2->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Second Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'], + ] +); +$attribute2 = $attributeRepository->save($attributeModel2); +$installer->addAttributeToGroup( + Product::ENTITY, + $secondAttributeSet->getId(), + $secondAttributeSet->getDefaultGroupId(), + $attribute2->getId() +); /** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ -$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); $productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; foreach ($productsWithNewAttributeSet as $sku) { try { $product = $productRepository->get($sku, false, null, true); - $product->setAttributeSetId($attributeSet->getId()); + $product->setAttributeSetId($secondAttributeSet->getId()); $product->setStockData( - ['use_config_manage_stock' => 1, + [ + 'use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, - 'is_in_stock' => 1] + 'is_in_stock' => 1, + ] ); $productRepository->save($product); - } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + } catch (NoSuchEntityException $e) { } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php index 96ddb797a6dea..945f582b8cbdd 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/validate_image_info.php @@ -12,10 +12,10 @@ /** @var Magento\Catalog\Model\Product\Media\Config $config */ $config = $objectManager->get(\Magento\Catalog\Model\Product\Media\Config::class); -/** @var $tmpDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ -$tmpDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); -$tmpDirectory->create($config->getBaseTmpMediaPath()); +/** @var $mediaDirectory \Magento\Framework\Filesystem\Directory\WriteInterface */ +$mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA); +$mediaDirectory->create($config->getBaseTmpMediaPath()); -$targetTmpFilePath = $tmpDirectory->getAbsolutePath($config->getBaseTmpMediaPath() . '/magento_small_image.jpg'); -copy(__DIR__ . '/magento_small_image.jpg', $targetTmpFilePath); +$targetTmpFilePath = $mediaDirectory->getAbsolutePath($config->getBaseTmpMediaPath() . '/magento_small_image.jpg'); +$mediaDirectory->getDriver()->filePutContents($targetTmpFilePath, file_get_contents(__DIR__ . '/magento_small_image.jpg')); // Copying the image to target dir is not necessary because during product save, it will be moved there from tmp dir diff --git a/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php b/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php index 3878cd2e5176e..4a3c8f2e6b96c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/controllers/_files/products.php @@ -24,7 +24,7 @@ $baseTmpMediaPath = $config->getBaseTmpMediaPath(); $mediaDirectory->create($baseTmpMediaPath); -copy(__DIR__ . '/product_image.png', $mediaDirectory->getAbsolutePath($baseTmpMediaPath . '/product_image.png')); +$mediaDirectory->getDriver()->filePutContents($mediaDirectory->getAbsolutePath($baseTmpMediaPath . '/product_image.png'), file_get_contents(__DIR__ . '/product_image.png')); /** @var $productOne \Magento\Catalog\Model\Product */ $productOne = $objectManager->create(\Magento\Catalog\Model\Product::class); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php b/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php index 46eb1e98ddc6a..7d3bf7ec1a1ea 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Helper/Wysiwyg/ImagesTest.php @@ -82,7 +82,7 @@ public function testGetImageHtmlDeclaration( public function providerGetImageHtmlDeclaration() { return [ - [true, 'wysiwyg/hello.png', true, '<img src="http://example.com/pub/media/wysiwyg/hello.png" alt="" />'], + [true, 'wysiwyg/hello.png', true, '<img src="http://example.com/media/wysiwyg/hello.png" alt="" />'], [ false, 'wysiwyg/hello.png', @@ -96,7 +96,7 @@ function ($actualResult) { $this->assertStringContainsString($expectedResult, parse_url($actualResult, PHP_URL_PATH)); } ], - [true, 'wysiwyg/hello.png', false, 'http://example.com/pub/media/wysiwyg/hello.png'], + [true, 'wysiwyg/hello.png', false, 'http://example.com/media/wysiwyg/hello.png'], [false, 'wysiwyg/hello.png', true, '<img src="{{media url="wysiwyg/hello.png"}}" alt="" />'], ]; } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php index 3d6cbe98cf160..53b9dfee46aac 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/ConfigTest.php @@ -46,7 +46,7 @@ public function testGetConfig() public function testGetConfigCssUrls() { $config = $this->model->getConfig(); - $publicPathPattern = 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/%s'; + $publicPathPattern = 'http://localhost/static/%s/adminhtml/Magento/backend/en_US/%s'; $tinyMce4Config = $config->getData('tinymce4'); $contentCss = $tinyMce4Config['content_css']; if (is_array($contentCss)) { diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php index 076a669f3f8ad..7ce695cb476fe 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/GetInsertImageContentTest.php @@ -105,7 +105,7 @@ public function imageDataProvider(): array true, false, 1, - '/pub/media/catalog/category/test-image.jpg' + '/media/catalog/category/test-image.jpg' ], [ 'test-image.jpg', diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php index cb96ca2a14cac..96084981fe0b8 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Wysiwyg/Images/StorageTest.php @@ -109,11 +109,12 @@ public function testGetFilesCollection(): void $collection = $this->storage->getFilesCollection(self::$_baseDir, 'image'); $this->assertInstanceOf(Collection::class, $collection); foreach ($collection as $item) { + $thumbUrl = parse_url($item->getThumbUrl(), PHP_URL_PATH); $this->assertInstanceOf(DataObject::class, $item); $this->assertStringEndsWith('/' . $fileName, $item->getUrl()); $this->assertEquals( - '/pub/media/.thumbsMagentoCmsModelWysiwygImagesStorageTest/magento_image.jpg', - parse_url($item->getThumbUrl(), PHP_URL_PATH), + '/media/.thumbsMagentoCmsModelWysiwygImagesStorageTest/magento_image.jpg', + $thumbUrl, "Check if Thumbnail URL is equal to the generated URL" ); $this->assertEquals( @@ -387,17 +388,17 @@ public function getThumbnailUrlDataProvider(): array [ '/', 'image1.png', - '/pub/media/.thumbs/image1.png' + '/media/.thumbs/image1.png' ], [ '/cms', 'image2.png', - '/pub/media/.thumbscms/image2.png' + '/media/.thumbscms/image2.png' ], [ '/cms/pages', 'image3.png', - '/pub/media/.thumbscms/pages/image3.png' + '/media/.thumbscms/pages/image3.png' ] ]; } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php new file mode 100644 index 0000000000000..710c49241d82c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Cms/Ui/Component/DataProviderTest.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Cms\Ui\Component; + +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Cms\Api\Data\PageInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Framework\View\Element\UiComponentInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks Cms UI component data provider behaviour + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class DataProviderTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var UiComponentFactory */ + private $componentFactory; + + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->componentFactory = $this->objectManager->get(UiComponentFactory::class); + } + + /** + * @magentoDataFixture Magento/Cms/_files/pages.php + * + * @return void + */ + public function testPageFilteringByTitlePart(): void + { + $this->request->setParams(['search' => 'Cms Page 1']); + $data = $this->getComponentProvidedData('cms_page_listing'); + $items = $data['items']; + $this->assertCount(1, $items); + $this->assertEquals('page100', reset($items)[PageInterface::IDENTIFIER]); + } + + /** + * @magentoDataFixture Magento/Cms/_files/blocks.php + * + * @return void + */ + public function testBlockFilteringByTitlePart(): void + { + $this->request->setParams(['search' => 'Enabled CMS Block']); + $data = $this->getComponentProvidedData('cms_block_listing'); + $items = $data['items']; + $this->assertCount(1, $items); + $this->assertEquals('enabled_block', reset($items)[BlockInterface::IDENTIFIER]); + } + + /** + * Call prepare method in the child components + * + * @param UiComponentInterface $component + * @return void + */ + private function prepareChildComponents(UiComponentInterface $component): void + { + foreach ($component->getChildComponents() as $child) { + $this->prepareChildComponents($child); + } + + $component->prepare(); + } + + /** + * Get component provided data + * + * @param string $namespace + * @return array + */ + private function getComponentProvidedData(string $namespace): array + { + $component = $this->componentFactory->create($namespace); + $this->prepareChildComponents($component); + + return $component->getContext()->getDataProvider()->getData(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php index 8458a26e44659..1fd45ba1c87ba 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/Config/Backend/Admin/RobotsTest.php @@ -6,6 +6,8 @@ namespace Magento\Config\Model\Config\Backend\Admin; use Magento\Config\Model\Config\Reader\Source\Deployed\DocumentRoot; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; /** * @magentoAppArea adminhtml @@ -34,10 +36,7 @@ protected function setUp(): void $this->model->setPath('design/search_engine_robots/custom_instructions'); $this->model->afterLoad(); - $documentRootPath = $objectManager->get(DocumentRoot::class)->getPath(); - $this->rootDirectory = $objectManager->get( - \Magento\Framework\Filesystem::class - )->getDirectoryRead($documentRootPath); + $this->rootDirectory = $objectManager->get(Filesystem::class)->getDirectoryRead(DirectoryList::PUB); } /** @@ -57,7 +56,8 @@ public function testAfterLoadRobotsTxtNotExists() */ public function testAfterLoadRobotsTxtExists() { - $this->assertEquals('Sitemap: http://store.com/sitemap.xml', $this->model->getValue()); + $value = $this->model->getValue(); + $this->assertEquals('Sitemap: http://store.com/sitemap.xml', $value); } /** @@ -92,7 +92,8 @@ protected function _modifyConfig() { $robotsTxt = "User-Agent: *\nDisallow: /checkout"; $this->model->setValue($robotsTxt)->save(); - $this->assertStringEqualsFile($this->rootDirectory->getAbsolutePath('robots.txt'), $robotsTxt); + $file = $this->rootDirectory->getAbsolutePath('robots.txt'); + $this->assertStringEqualsFile($file, $robotsTxt); } /** diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php b/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php index bbb229221bac3..d840261669992 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/_files/no_robots_txt.php @@ -9,7 +9,7 @@ $rootDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( \Magento\Framework\Filesystem::class )->getDirectoryWrite( - DirectoryList::ROOT + DirectoryList::PUB ); if ($rootDirectory->isExist('robots.txt')) { $rootDirectory->delete('robots.txt'); diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php b/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php index c4fb2c92c45a5..3097132b74c2c 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/_files/robots_txt.php @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\TestFramework\Helper\Bootstrap; -/** @var \Magento\Framework\Filesystem\Directory\Write $rootDirectory */ -$rootDirectory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\Filesystem::class -)->getDirectoryWrite( - DirectoryList::ROOT -); -$rootDirectory->copyFile($rootDirectory->getRelativePath(__DIR__ . '/robots.txt'), 'robots.txt'); +/** @var $fileSystem Filesystem */ +$fileSystem = Bootstrap::getObjectManager()->get(Filesystem::class); +$pubDirectory = $fileSystem->getDirectoryWrite(DirectoryList::PUB); +$rootDirectory = $fileSystem->getDirectoryRead(DirectoryList::ROOT); +$source = $rootDirectory->getAbsolutePath(__DIR__ . '/robots.txt'); +$content = $rootDirectory->readFile(__DIR__ . '/robots.txt'); +$pubDirectory->writeFile('robots.txt', $content); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index ffa84ca740e62..28cbf80703d51 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\ProductRepositoryInterface; @@ -153,10 +155,44 @@ public function testReindexWithCorrectPriority() true ); - $configurableProduct = $this->getConfigurableProductFromCollection($configurableProduct->getId()); + $configurableProduct = $this->getConfigurableProductFromCollection((int)$configurableProduct->getId()); $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); } + /** + * Test get product minimal price if all children is out of stock + * + * @magentoConfigFixture current_store cataloginventory/options/show_out_of_stock 1 + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoDbIsolation disabled + * + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function testReindexIfAllChildrenIsOutOfStock(): void + { + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + + $childProduct1 = $this->productRepository->getById(10, false, null, true); + $stockItem = $childProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $childProduct2 = $this->productRepository->getById(20, false, null, true); + $stockItem = $childProduct2->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct1 = $this->productRepository->getById(1, false, null, true); + $stockItem = $configurableProduct1->getExtensionAttributes()->getStockItem(); + $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); + $this->stockRepository->save($stockItem); + + $configurableProduct = $this->getConfigurableProductFromCollection(1); + $this->assertEquals(10, $configurableProduct->getMinimalPrice()); + } + /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php index 12f63993cb2d3..939c1d261b3c6 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute.php @@ -4,59 +4,67 @@ * See COPYING.txt for license details. */ +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$objectManager = Bootstrap::getObjectManager(); -$eavConfig->clear(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var ProductAttributeInterfaceFactory $attributeFactory */ +$attributeFactory = $objectManager->get(ProductAttributeInterfaceFactory::class); -/** @var $installer \Magento\Catalog\Setup\CategorySetup */ -$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); - -if (!$attribute->getId()) { - - /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ - $attribute = Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); - - /** @var AttributeRepositoryInterface $attributeRepository */ - $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); - - $attribute->setData( - [ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 0, - 'is_visible_in_advanced_search' => 0, - 'is_comparable' => 0, - 'is_filterable' => 0, - 'is_filterable_in_search' => 0, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 0, - 'used_in_product_listing' => 0, - 'used_for_sort_by' => 0, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - ] - ); - - $attributeRepository->save($attribute); - - /* Assign attribute to attribute set */ - $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +try { + $attributeRepository->get('test_configurable'); + Resolver::getInstance() + ->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php'); +} catch (NoSuchEntityException $e) { } +$eavConfig = $objectManager->get(Config::class); + +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); +$groupId = $installer->getDefaultAttributeGroupId(Product::ENTITY, $attributeSetId); +/** @var ProductAttributeInterface $attributeModel */ +$attributeModel = $attributeFactory->create(); +$attributeModel->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId(Product::ENTITY), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + ] +); + +$attribute = $attributeRepository->save($attributeModel); + +$installer->addAttributeToGroup(Product::ENTITY, $attributeSetId, $groupId, $attribute->getId()); $eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php index f6e6261c75662..618b554aaa2cc 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_products.php @@ -3,49 +3,63 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Setup\CategorySetup; use Magento\ConfigurableProduct\Helper\Product\Options\Factory; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\Eav\Api\Data\AttributeOptionInterface; -use Magento\Eav\Model\Config; +use Magento\Eav\Setup\EavSetup; +use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); $objectManager = Bootstrap::getObjectManager(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager - ->get(ProductRepositoryInterface::class); -/** @var Config $eavConfig */ -$eavConfig = $objectManager->get(Config::class); -$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); -/** @var $installer CategorySetup */ -$installer = $objectManager->create(CategorySetup::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsite = $websiteRepository->get('base'); -/* Create simple products per each option value*/ +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productInterfaceFactory */ +$productInterfaceFactory = $objectManager->get(ProductInterfaceFactory::class); + +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var $attribute Attribute */ +$attribute = $attributeRepository->get('test_configurable'); /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); +/** @var $installer EavSetup */ +$installer = $objectManager->get(EavSetup::class); +$attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); + +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +/* Create simple products per each option value*/ + $attributeValues = []; -$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); $associatedProductIds = []; $productIds = [10, 20]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { /** @var $product Product */ - $product = $objectManager->create(Product::class); + $product = $productInterfaceFactory->create(); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Option' . $option->getLabel()) ->setSku('simple_' . $productId) ->setPrice($productId) @@ -53,20 +67,18 @@ ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) ->setStatus(Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $product = $productRepository->save($product); + $simple1 = $productRepository->save($product); $attributeValues[] = [ 'label' => 'test', 'attribute_id' => $attribute->getId(), 'value_index' => $option->getValue(), ]; - $associatedProductIds[] = $product->getId(); + $associatedProductIds[] = $simple1->getId(); } /** @var $product Product */ -$product = $objectManager->create(Product::class); -/** @var Factory $optionsFactory */ -$optionsFactory = $objectManager->create(Factory::class); +$product = $productInterfaceFactory->create(); $configurableAttributesData = [ [ 'attribute_id' => $attribute->getId(), @@ -84,7 +96,7 @@ $product->setTypeId(Configurable::TYPE_CODE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Product') ->setSku('configurable') ->setVisibility(Visibility::VISIBILITY_BOTH) @@ -98,18 +110,17 @@ $options = $attribute->getOptions(); $attributeValues = []; -$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); $associatedProductIds = []; $productIds = [30, 40]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { /** @var $product Product */ - $product = $objectManager->create(Product::class); + $product = $productInterfaceFactory->create(); $productId = array_shift($productIds); $product->setTypeId(Type::TYPE_SIMPLE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Option' . $option->getLabel()) ->setSku('simple_' . $productId) ->setPrice($productId) @@ -117,21 +128,18 @@ ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) ->setStatus(Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $product = $productRepository->save($product); + $simple2 = $productRepository->save($product); $attributeValues[] = [ 'label' => 'test', 'attribute_id' => $attribute->getId(), 'value_index' => $option->getValue(), ]; - $associatedProductIds[] = $product->getId(); + $associatedProductIds[] = $simple2->getId(); } /** @var $product Product */ -$product = $objectManager->create(Product::class); - -/** @var Factory $optionsFactory */ -$optionsFactory = $objectManager->create(Factory::class); +$product = $productInterfaceFactory->create(); $configurableAttributesData = [ [ @@ -153,7 +161,7 @@ $product->setTypeId(Configurable::TYPE_CODE) ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) + ->setWebsiteIds([$baseWebsite->getId()]) ->setName('Configurable Product 12345') ->setSku('configurable_12345') ->setVisibility(Visibility::VISIBILITY_BOTH) diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php new file mode 100644 index 0000000000000..5399f6903ee9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ForgotPasswordToken/ConfirmCustomerByTokenTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\ForgotPasswordToken; + +use Magento\Customer\Model\Customer; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Customer\Model\ForgotPasswordToken\ConfirmCustomerByToken. + */ +class ConfirmCustomerByTokenTest extends TestCase +{ + private const STUB_CUSTOMER_RESET_TOKEN = 'token12345'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var ConfirmCustomerByToken + */ + private $confirmCustomerByToken; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + + $resource = $this->objectManager->get(ResourceConnection::class); + $this->connection = $resource->getConnection(); + + $this->confirmCustomerByToken = $this->objectManager->get(ConfirmCustomerByToken::class); + } + + /** + * Customer address shouldn't validate during confirm customer by token + * + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Customer/_files/customer_address.php + * + * @return void + */ + public function testExecuteWithInvalidAddress(): void + { + $id = 1; + + $customerModel = $this->objectManager->create(Customer::class); + $customerModel->load($id); + $customerModel->setRpToken(self::STUB_CUSTOMER_RESET_TOKEN); + $customerModel->setRpTokenCreatedAt(date('Y-m-d H:i:s')); + $customerModel->setConfirmation($customerModel->getRandomConfirmationKey()); + $customerModel->save(); + + //make city address invalid + $this->makeCityInvalid($id); + + $this->confirmCustomerByToken->execute(self::STUB_CUSTOMER_RESET_TOKEN); + $this->assertNull($customerModel->load($id)->getConfirmation()); + } + + /** + * Set city invalid for customer address + * + * @param int $id + * @return void + */ + private function makeCityInvalid(int $id): void + { + $this->connection->update( + $this->connection->getTableName('customer_address_entity'), + ['city' => ''], + $this->connection->quoteInto('entity_id = ?', $id) + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php index 740afcda11386..87dfd2a4a3981 100644 --- a/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Directory/Model/Country/Postcode/Config/ReaderTest.php @@ -6,18 +6,20 @@ namespace Magento\Directory\Model\Country\Postcode\Config; -class ReaderTest extends \PHPUnit\Framework\TestCase +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +class ReaderTest extends TestCase { /** - * @var \Magento\Directory\Model\Country\Postcode\Config\Reader + * @var Reader */ private $reader; protected function setUp(): void { - $this->reader = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Directory\Model\Country\Postcode\Config\Reader::class - ); + $this->reader = Bootstrap::getObjectManager() + ->create(Reader::class); } public function testRead() @@ -39,5 +41,21 @@ public function testRead() $this->assertEquals('test1', $result['NL_NEW']['pattern_1']['example']); $this->assertEquals('^[0-2]{4}[A-Z]{2}$', $result['NL_NEW']['pattern_1']['pattern']); + + $this->assertArrayHasKey('AR', $result); + $this->assertArrayHasKey('pattern_1', $result['AR']); + $this->assertArrayHasKey('pattern_2', $result['AR']); + $this->assertEquals('1234', $result['AR']['pattern_1']['example']); + $this->assertEquals('^[0-9]{4}$', $result['AR']['pattern_1']['pattern']); + $this->assertEquals('A1234BCD', $result['AR']['pattern_2']['example']); + $this->assertEquals('^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$', $result['AR']['pattern_2']['pattern']); + + $this->assertArrayHasKey('KR', $result); + $this->assertArrayHasKey('pattern_1', $result['KR']); + $this->assertArrayHasKey('pattern_2', $result['KR']); + $this->assertEquals('123-456', $result['KR']['pattern_1']['example']); + $this->assertEquals('^[0-9]{3}-[0-9]{3}$', $result['KR']['pattern_1']['pattern']); + $this->assertEquals('12345', $result['KR']['pattern_2']['example']); + $this->assertEquals('^[0-9]{5}$', $result['KR']['pattern_2']['pattern']); } } diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php index 9679b4f232ee2..6df4d8fbb2d92 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/ReindexAllTest.php @@ -130,6 +130,45 @@ public function testSort() $this->assertEquals($productSimpleId, $firstInSearchResults); } + /** + * Test sorting of products with lower and upper case names after full reindex + * + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store catalog/search/elasticsearch_index_prefix indexerhandlertest + * @magentoDataFixture Magento/Elasticsearch/_files/case_sensitive.php + */ + public function testSortCaseSensitive(): void + { + $productFirst = $this->productRepository->get('fulltext-1'); + $productSecond = $this->productRepository->get('fulltext-2'); + $productThird = $this->productRepository->get('fulltext-3'); + $productFourth = $this->productRepository->get('fulltext-4'); + $productFifth = $this->productRepository->get('fulltext-5'); + $correctSortedIds = [ + $productFirst->getId(), + $productFourth->getId(), + $productSecond->getId(), + $productFifth->getId(), + $productThird->getId(), + ]; + $this->reindexAll(); + $result = $this->sortByName(); + $firstInSearchResults = (int) $result[0]['_id']; + $secondInSearchResults = (int) $result[1]['_id']; + $thirdInSearchResults = (int) $result[2]['_id']; + $fourthInSearchResults = (int) $result[3]['_id']; + $fifthInSearchResults = (int) $result[4]['_id']; + $actualSortedIds = [ + $firstInSearchResults, + $secondInSearchResults, + $thirdInSearchResults, + $fourthInSearchResults, + $fifthInSearchResults + ]; + $this->assertCount(5, $result); + $this->assertEquals($correctSortedIds, $actualSortedIds); + } + /** * Test search of specific product after full reindex * diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php new file mode 100644 index 0000000000000..1b664f958dd46 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute.php'); + +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = Bootstrap::getObjectManager(); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->get('fulltext-1'); +} catch (NoSuchEntityException $e) { + /** @var $productFirst Product */ + $productFirst = $objectManager->create(Product::class); + $productFirst->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('A') + ->setSku('fulltext-1') + ->setPrice(10) + ->setMetaTitle('first meta title') + ->setMetaKeyword('first meta keyword') + ->setMetaDescription('first meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-2'); +} catch (NoSuchEntityException $e) { + /** @var $productSecond Product */ + $productSecond = $objectManager->create(Product::class); + $productSecond->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('B') + ->setSku('fulltext-2') + ->setPrice(20) + ->setMetaTitle('second meta title') + ->setMetaKeyword('second meta keyword') + ->setMetaDescription('second meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-3'); +} catch (NoSuchEntityException $e) { + /** @var $productThird Product */ + $productThird = $objectManager->create(Product::class); + $productThird->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('C') + ->setSku('fulltext-3') + ->setPrice(20) + ->setMetaTitle('third meta title') + ->setMetaKeyword('third meta keyword') + ->setMetaDescription('third meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(1) + ->save(); +} + +try { + $productRepository->get('fulltext-4'); +} catch (NoSuchEntityException $e) { + /** @var $productFourth Product */ + $productFourth = $objectManager->create(Product::class); + $productFourth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('a') + ->setSku('fulltext-4') + ->setPrice(20) + ->setMetaTitle('fourth meta title') + ->setMetaKeyword('fourth meta keyword') + ->setMetaDescription('fourth meta description') + ->setUrlKey('aa') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} + +try { + $productRepository->get('fulltext-5'); +} catch (NoSuchEntityException $e) { + /** @var $productFifth Product */ + $productFifth = $objectManager->create(Product::class); + $productFifth->setTypeId('simple') + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('b') + ->setSku('fulltext-5') + ->setPrice(20) + ->setMetaTitle('fifth meta title') + ->setMetaKeyword('fifth meta keyword') + ->setMetaDescription('fifth meta description') + ->setUrlKey('bb') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 0]) + ->setBooleanAttribute(0) + ->save(); +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php new file mode 100644 index 0000000000000..a97faa29a1588 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/_files/case_sensitive_rollback.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_boolean_attribute_rollback.php'); + +/** @var $objectManager \Magento\Framework\ObjectManagerInterface */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ +$collection = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +$collection->addAttributeToSelect('id')->load(); +if ($collection->count() > 0) { + $collection->delete(); +} + +/** @var \Magento\Store\Model\Store $store */ +$store = $objectManager->create(\Magento\Store\Model\Store::class); +$storeCode = 'secondary'; +$store->load($storeCode); +if ($store->getId()) { + $store->delete(); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php index d7b492bf5153c..7bd4b3a99d1bf 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/App/Filesystem/CreatePdfFileTest.php @@ -8,7 +8,6 @@ namespace Magento\Framework\App\Filesystem; -use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Response\Http\FileFactory; use Magento\Framework\Filesystem; use Magento\TestFramework\Helper\Bootstrap; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php b/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php deleted file mode 100644 index c6aeaf9e0f927..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Framework\Console; - -use Magento\Framework\App\DeploymentConfig; -use Magento\Framework\App\DeploymentConfig\FileReader; -use Magento\Framework\App\DeploymentConfig\Writer; -use Magento\Framework\App\Filesystem\DirectoryList; -use Magento\Framework\Config\File\ConfigFilePool; -use Magento\Framework\Filesystem; -use Magento\Framework\ObjectManagerInterface; -use Magento\TestFramework\Helper\Bootstrap; - -class CliTest extends \PHPUnit\Framework\TestCase -{ - /** - * @var ObjectManagerInterface - */ - private $objectManager; - - /** - * @var Filesystem - */ - private $filesystem; - - /** - * @var ConfigFilePool - */ - private $configFilePool; - - /** - * @var FileReader - */ - private $reader; - - /** - * @var Writer - */ - private $writer; - - /** - * @var array - */ - private $envConfig; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $this->objectManager = Bootstrap::getObjectManager(); - $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); - $this->filesystem = $this->objectManager->get(Filesystem::class); - $this->reader = $this->objectManager->get(FileReader::class); - $this->writer = $this->objectManager->get(Writer::class); - - $this->envConfig = $this->reader->load(ConfigFilePool::APP_ENV); - } - - /** - * @inheritdoc - */ - protected function tearDown(): void - { - $this->filesystem->getDirectoryWrite(DirectoryList::CONFIG)->writeFile( - $this->configFilePool->getPath(ConfigFilePool::APP_ENV), - "<?php\n return array();\n" - ); - - $this->writer->saveConfig([ConfigFilePool::APP_ENV => $this->envConfig], true); - } - - /** - * Checks that settings from env.php config file are applied - * to created application instance. - * - * @magentoAppIsolation enabled - * @param bool $isPub - * @param array $params - * @dataProvider documentRootIsPubProvider - */ - public function testDocumentRootIsPublic($isPub, $params) - { - $config = include __DIR__ . '/_files/env.php'; - $config['directories']['document_root_is_pub'] = $isPub; - $this->writer->saveConfig([ConfigFilePool::APP_ENV => $config], true); - - $cli = new Cli(); - $cliReflection = new \ReflectionClass($cli); - - $serviceManagerProperty = $cliReflection->getProperty('serviceManager'); - $serviceManagerProperty->setAccessible(true); - $serviceManager = $serviceManagerProperty->getValue($cli); - $deploymentConfig = $this->objectManager->get(DeploymentConfig::class); - $serviceManager->setAllowOverride(true); - $serviceManager->setService(DeploymentConfig::class, $deploymentConfig); - $serviceManagerProperty->setAccessible(false); - - $documentRootResolver = $cliReflection->getMethod('documentRootResolver'); - $documentRootResolver->setAccessible(true); - - self::assertEquals($params, $documentRootResolver->invoke($cli)); - } - - /** - * Provides document root setting and expecting - * properties for object manager creation. - * - * @return array - */ - public function documentRootIsPubProvider(): array - { - return [ - [true, [ - 'MAGE_DIRS' => [ - 'pub' => ['uri' => ''], - 'media' => ['uri' => 'media'], - 'static' => ['uri' => 'static'], - 'upload' => ['uri' => 'media/upload'] - ] - ]], - [false, []] - ]; - } -} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php b/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php deleted file mode 100644 index e314e7638c22c..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/_files/env.php +++ /dev/null @@ -1,46 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -return [ - 'backend' => [ - 'frontName' => 'admin', - ], - 'crypt' => [ - 'key' => 'some_key', - ], - 'session' => [ - 'save' => 'files', - ], - 'db' => [ - 'table_prefix' => '', - 'connection' => [], - ], - 'resource' => [], - 'x-frame-options' => 'SAMEORIGIN', - 'MAGE_MODE' => 'default', - 'cache_types' => [ - 'config' => 1, - 'layout' => 1, - 'block_html' => 1, - 'collections' => 1, - 'reflection' => 1, - 'db_ddl' => 1, - 'eav' => 1, - 'customer_notification' => 1, - 'config_integration' => 1, - 'config_integration_api' => 1, - 'full_page' => 1, - 'translate' => 1, - 'config_webservice' => 1, - ], - 'install' => [ - 'date' => 'Thu, 09 Feb 2017 14:28:00 +0000', - ], - 'directories' => [ - 'document_root_is_pub' => true - ] -]; diff --git a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html index d5b6f35421ac6..518926ed52d69 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Css/_files/css/test-input.html @@ -1,3 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> @@ -30,7 +36,7 @@ height="52" - src="http://magento2.vagrant236/pub/static/version1502812784/frontend/Magento/blank/en_US/Magento_Email/logo_email.png" + src="http://magento2.vagrant236/static/version1502812784/frontend/Magento/blank/en_US/Magento_Email/logo_email.png" alt="Main Website Store" border="0" /> diff --git a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php index 3a2a02a0a5776..917b79588312c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Error/ProcessorTest.php @@ -150,6 +150,6 @@ public function testGetViewFileUrl(): void $this->processor->_errorDir = __DIR__ . '/version2/magento2'; $this->assertStringNotContainsString('version2/magento2', $this->processor->getViewFileUrl()); - $this->assertStringContainsString('pub/errors/', $this->processor->getViewFileUrl()); + $this->assertStringContainsString('errors/', $this->processor->getViewFileUrl()); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html b/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html index 573f3b166db35..0afba67d3b031 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html +++ b/dev/tests/integration/testsuite/Magento/Framework/Translate/_files/_inline_page_expected.html @@ -14,12 +14,12 @@ <div data-translate="[{"shown":"shown_1","translated":"translated_1","original":"original_1","location":"Tag attribute (ALT, TITLE, etc.)","scope":"scope_1"}]"title="some_title_shown_1_in_div"> some_text_<span data-translate="[{"shown":"shown_2","translated":"translated_2","original":"original_2","location":"Text","scope":"scope_2"}]">shown_2</span>_in_div </div> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/prototype/window.js"></script> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/static/frontend/Magento/luma/en_US/prototype/windows/themes/default.css"/> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/prototype/magento.css"/> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/edit-trigger.js"></script> -<script type="text/javascript" src="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/translate-inline.js"></script> -<link rel="stylesheet" type="text/css" href="http://localhost/pub/static/frontend/Magento/luma/en_US/mage/translate-inline.css"/> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/prototype/window.js"></script> +<link rel="stylesheet" type="text/css" href="http://localhost/static/frontend/Magento/luma/en_US/prototype/windows/themes/default.css"/> +<link rel="stylesheet" type="text/css" href="http://localhost/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/prototype/magento.css"/> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/mage/edit-trigger.js"></script> +<script type="text/javascript" src="http://localhost/static/frontend/Magento/luma/en_US/mage/translate-inline.js"></script> +<link rel="stylesheet" type="text/css" href="http://localhost/static/frontend/Magento/luma/en_US/mage/translate-inline.css"/> <script type="text/javascript"> (function($){ @@ -27,7 +27,7 @@ $(this).translateInline({ ajaxUrl: 'http://localhost/index.php/translation/ajax/index/', area: 'frontend', - editTrigger: {img: 'http://localhost/pub/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/fam_book_open.png'} + editTrigger: {img: 'http://localhost/media/theme/static/frontend/{{design_package}}/default/en_US/Magento_Theme/fam_book_open.png'} }); }); })(jQuery); diff --git a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php index 785637a9470cb..ad4491b166cfe 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php @@ -181,7 +181,7 @@ public function testGetBaseUrlWithTypeRestoring() * Get url with type specified in params */ $mediaUrl = $this->model->getBaseUrl(['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA]); - $this->assertEquals('http://localhost/pub/media/', $mediaUrl, 'Incorrect media url'); + $this->assertEquals('http://localhost/media/', $mediaUrl, 'Incorrect media url'); $this->assertEquals('http://localhost/index.php/', $this->model->getBaseUrl(), 'Incorrect link url'); } diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php index fa664756d65f1..f584b8f7cfcd3 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Element/AbstractBlockTest.php @@ -490,7 +490,7 @@ public function testGetViewFileUrl() { $actualResult = $this->_block->getViewFileUrl('css/styles.css'); $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/frontend/%s/en_US/css/styles.css', + 'http://localhost/static/%s/frontend/%s/en_US/css/styles.css', $actualResult ); } diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php index 277e6af871650..2128516189474 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Controller/Adminhtml/Export/File/DownloadTest.php @@ -105,7 +105,7 @@ public function testExecute($file): void 'Incorrect response header "content-type"' ); $this->assertEquals( - 'attachment; filename="export/' . $this->fileName . '"', + 'attachment; filename="' . $this->fileName . '"', $contentDisposition->getFieldValue(), 'Incorrect response header "content-disposition"' ); diff --git a/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php b/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php new file mode 100644 index 0000000000000..f243d39c24d26 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Indexer/_files/reindex_all_invalid.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Indexer\Model\Processor; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Processor $processor */ +$processor = Bootstrap::getObjectManager()->get(Processor::class); +$processor->reindexAllInvalid(); diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php index c923a809441ba..42390f5303a94 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Block/Header/AdditionalTest.php @@ -6,41 +6,47 @@ namespace Magento\Persistent\Block\Header; +use Magento\Customer\Model\Session; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Helper\Session as SessionHelper; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * @magentoDataFixture Magento/Persistent/_files/persistent.php */ -class AdditionalTest extends \PHPUnit\Framework\TestCase +class AdditionalTest extends TestCase { /** - * @var \Magento\Persistent\Block\Header\Additional + * @var Additional */ protected $_block; /** - * @var \Magento\Persistent\Helper\Session + * @var SessionHelper */ protected $_persistentSessionHelper; /** - * @var \Magento\Customer\Model\Session + * @var Session */ protected $_customerSession; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $_objectManager; protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->_objectManager = Bootstrap::getObjectManager(); - /** @var \Magento\Persistent\Helper\Session $persistentSessionHelper */ - $this->_persistentSessionHelper = $this->_objectManager->create(\Magento\Persistent\Helper\Session::class); + /** @var Session $persistentSessionHelper */ + $this->_persistentSessionHelper = $this->_objectManager->create(Session::class); - $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); + $this->_customerSession = $this->_objectManager->get(Session::class); - $this->_block = $this->_objectManager->create(\Magento\Persistent\Block\Header\Additional::class); + $this->_block = $this->_objectManager->create(Additional::class); } /** @@ -54,12 +60,7 @@ protected function setUp(): void public function testToHtml() { $this->_customerSession->loginById(1); - $translation = __('Not you?'); - - $this->assertStringContainsString( - '<a href="' . $this->_block->getHref() . '">' . $translation . '</a>', - $this->_block->toHtml() - ); + $this->assertStringContainsString($this->_block->getHref(), $this->_block->toHtml()); $this->_customerSession->logout(); } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php b/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php new file mode 100644 index 0000000000000..086078685ae94 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/Model/Product/DataRetrieverTest.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Reports\Model\Product; + +use Magento\Catalog\Model\Indexer\Product\Price\Processor; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea adminhtml + */ +class DataRetrieverTest extends TestCase +{ + /** + * @var DataRetriever + */ + private $dataRetriever; + + /** + * @var Processor + */ + private $priceIndexerProcessor; + + protected function setUp(): void + { + $this->dataRetriever = Bootstrap::getObjectManager()->create(DataRetriever::class); + $this->priceIndexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); + } + + /** + * Test retrieve products data for reports by entity id's + * Do not use magentoDbIsolation because index statement changing "tears" transaction (triggers creating) + * + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoConfigFixture default/reports/options/enabled 1 + * @magentoDbIsolation disabled + * + * @return void + */ + public function testExecute(): void + { + $productId = 1; + $this->priceIndexerProcessor->reindexAll(); + $actualResult = $this->dataRetriever->execute([$productId]); + $this->assertNotEmpty($actualResult); + $this->assertCount(1, $actualResult); + $this->assertEquals(10, $actualResult[$productId]['price']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php new file mode 100644 index 0000000000000..27423c67ffe19 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/ReorderTest.php @@ -0,0 +1,211 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Create; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Request; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Customer\Api\AccountManagementInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Framework\App\Request\Http; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\Helper\Xpath; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Test for reorder controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Reorder + * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ReorderTest extends AbstractBackendController +{ + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var CartInterface */ + private $quote; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var array + */ + private $customerIds = []; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var CustomerInterfaceFactory + */ + private $customerFactory; + + /** + * @var AccountManagementInterface + */ + private $accountManagement; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $this->orderRepository = $this->_objectManager->get(OrderRepositoryInterface::class); + $this->customerFactory = $this->_objectManager->get(CustomerInterfaceFactory::class); + $this->accountManagement = $this->_objectManager->get(AccountManagementInterface::class); + $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + foreach ($this->customerIds as $customerId) { + try { + $this->customerRepository->deleteById($customerId); + } catch (NoSuchEntityException $e) { + //customer already deleted + } + } + parent::tearDown(); + } + + /** + * Reorder with JS calendar options + * + * @magentoConfigFixture current_store catalog/custom_options/use_calendar 1 + * @magentoDataFixture Magento/Sales/_files/order_with_date_time_option_product.php + * + * @return void + */ + public function testReorderAfterJSCalendarEnabled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $this->dispatchReorderRequest((int)$order->getId()); + $this->assertRedirect($this->stringContains('backend/sales/order_create')); + $this->quote = $this->getQuote('customer@example.com'); + $this->assertTrue(!empty($this->quote)); + } + + /** + * Test load billing address by reorder for delegating customer + * + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_address.php + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testLoadBillingAddressAfterReorderWithDelegatingCustomer(): void + { + $orderId = $this->getOrderWithDelegatingCustomer()->getId(); + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/loadBlock/block/billing_address'); + $html = $this->getResponse()->getBody(); + $this->assertEquals( + 0, + Xpath::getElementsCountForXpath( + '//*[@id="order-billing_address_save_in_address_book" and contains(@checked, "checked")]', + $html + ), + 'Billing address checked "Save in address book"' + ); + } + + /** + * Get Order with delegating customer + * + * @return OrderInterface + */ + private function getOrderWithDelegatingCustomer(): OrderInterface + { + $orderAutoincrementId = '100000001'; + /** @var Order $orderModel */ + $orderModel = $this->orderFactory->create(); + $orderModel->loadByIncrementId($orderAutoincrementId); + //Saving new customer with prepared data from order. + /** @var CustomerInterface $customer */ + $customer = $this->customerFactory->create(); + $customer->setWebsiteId(1) + ->setEmail('customer_order_delegate@example.com') + ->setGroupId(1) + ->setStoreId(1) + ->setPrefix('Mr.') + ->setFirstname('John') + ->setMiddlename('A') + ->setLastname('Smith') + ->setSuffix('Esq.') + ->setTaxvat('12') + ->setGender(0); + $createdCustomer = $this->accountManagement->createAccount( + $customer, + '12345abcD' + ); + $this->customerIds[] = $createdCustomer->getId(); + $orderModel->setCustomerId($createdCustomer->getId()); + + return $this->orderRepository->save($orderModel); + } + + /** + * Dispatch reorder request. + * + * @param null|int $orderId + * @return void + */ + private function dispatchReorderRequest(?int $orderId = null): void + { + $this->getRequest()->setMethod(Request::METHOD_GET); + $this->getRequest()->setParam('order_id', $orderId); + $this->dispatch('backend/sales/order_create/reorder'); + } + + /** + * Gets quote by reserved order id. + * + * @return \Magento\Quote\Api\Data\CartInterface + */ + private function getQuote(string $customerEmail): \Magento\Quote\Api\Data\CartInterface + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->_objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('customer_email', $customerEmail) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->_objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria)->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php index 4e64aa0349b80..1b56a7e8c4448 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_configurable_product_rollback.php @@ -5,4 +5,5 @@ */ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/product_configurable_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php new file mode 100644 index 0000000000000..23fbeb94d2004 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'month' => '3', + 'day' => '5', + 'year' => '2020', + 'hour' => '2', + 'minute' => '15', + 'day_part' => 'am', + 'date_internal' => '2020-09-30 02:15:00' + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; +$optionsDate = [ + [ + 'label' => 'date', + 'value' => 'Mar 5, 2020', + 'print_value' => 'Mar 5, 2020', + 'option_id' => '1', + 'option_type' => 'date', + 'option_value' => '2020-03-05 00:00:00', + 'custom_view' => '', + ] +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setSku($product->getSku()); +$orderItem->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions([ + 'info_buyRequest' => $requestInfo, + 'options' => $optionsDate, +]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000001'); +$order->setState(\Magento\Sales\Model\Order::STATE_NEW); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_NEW)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@example.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(100); +$order->setBaseSubtotal(100); +$order->setBaseGrandTotal(100); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php new file mode 100644 index 0000000000000..0966f21645e3b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_date_time_option_product_rollback.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php index 4dfe01eed2d01..3120cf399d96c 100644 --- a/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Sitemap/Model/ResourceModel/Catalog/ProductTest.php @@ -17,8 +17,8 @@ class ProductTest extends \PHPUnit\Framework\TestCase /** * Base product image path */ - const BASE_IMAGE_PATH = '#http\:\/\/localhost\/pub\/media\/catalog\/product\/cache\/[a-z0-9]{32}:path:#'; - + const BASE_IMAGE_PATH = '#http://localhost/media/catalog/product/cache/[a-z0-9]{32}:path:#'; + /** * Test getCollection None images * 1) Check that image attributes were not loaded @@ -52,6 +52,7 @@ public function testGetCollectionNone() * 3) Check thumbnails when no thumbnail selected * * @magentoConfigFixture default_store sitemap/product/image_include all + * @magentoConfigFixture default/web/url/catalog_media_url_format hash */ public function testGetCollectionAll() { @@ -120,6 +121,7 @@ public function testGetCollectionAll() * 3) Check thumbnails when no thumbnail selected * * @magentoConfigFixture default_store sitemap/product/image_include base + * @magentoConfigFixture default/web/url/catalog_media_url_format hash */ public function testGetCollectionBase() { diff --git a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php index e4d78de54d308..c506b77e45442 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Controller/Store/SwitchActionTest.php @@ -3,9 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Store\Controller\Store; +use Magento\Catalog\Api\CategoryRepositoryInterface; use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Http\Context; +use Magento\Framework\App\Response\RedirectInterface; use Magento\Framework\Encryption\UrlCoder; use Magento\Framework\Interception\InterceptorInterface; use Magento\Store\Api\StoreResolverInterface; @@ -16,8 +21,11 @@ use Magento\Store\Model\StoreSwitcher\RedirectDataGenerator; use Magento\Store\Model\StoreSwitcher\RedirectDataPostprocessorInterface; use Magento\Store\Model\StoreSwitcher\RedirectDataPreprocessorInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\AbstractController; use PHPUnit\Framework\MockObject\MockObject; +use Magento\Store\Api\Data\StoreInterfaceFactory; +use Magento\Store\Model\ResourceModel\Store as StoreResource; /** * Test for store switch controller. @@ -27,23 +35,42 @@ */ class SwitchActionTest extends AbstractController { - /** - * @var RedirectDataPreprocessorInterface - */ + /** @var RedirectDataPreprocessorInterface */ private $preprocessor; - /** - * @var MockObject - */ + + /** @var MockObject */ private $preprocessorMock; - /** - * @var RedirectDataPostprocessorInterface - */ + + /** @var RedirectDataPostprocessorInterface */ private $postprocessor; - /** - * @var MockObject - */ + + /** @var MockObject */ private $postprocessorMock; + /** @var RedirectDataGenerator */ + private $redirectDataGenerator; + + /** @var ContextInterfaceFactory */ + private $contextFactory; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var UrlCoder */ + private $urlEncoder; + + /** @var RedirectInterface */ + private $redirect; + + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var StoreResource */ + private $storeResource; + + /** @var StoreInterfaceFactory */ + private $storeFactory; + /** * @inheritDoc */ @@ -53,10 +80,17 @@ protected function setUp(): void $this->preprocessor = $this->_objectManager->get(RedirectDataPreprocessorInterface::class); $this->preprocessorMock = $this->createMock(RedirectDataPreprocessorInterface::class); $this->_objectManager->addSharedInstance($this->preprocessorMock, $this->getClassName($this->preprocessor)); - $this->postprocessor = $this->_objectManager->get(RedirectDataPostprocessorInterface::class); $this->postprocessorMock = $this->createMock(RedirectDataPostprocessorInterface::class); $this->_objectManager->addSharedInstance($this->postprocessorMock, $this->getClassName($this->postprocessor)); + $this->redirectDataGenerator = $this->_objectManager->get(RedirectDataGenerator::class); + $this->contextFactory = $this->_objectManager->get(ContextInterfaceFactory::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->urlEncoder = $this->_objectManager->get(UrlCoder::class); + $this->redirect = $this->_objectManager->get(RedirectInterface::class); + $this->categoryRepository = $this->_objectManager->get(CategoryRepositoryInterface::class); + $this->storeResource = $this->_objectManager->get(StoreResource::class); + $this->storeFactory = $this->_objectManager->get(StoreInterfaceFactory::class); } /** @@ -80,8 +114,9 @@ protected function tearDown(): void * @magentoConfigFixture fixture_second_store_store web/unsecure/base_link_url http://second_store.test/ * @magentoConfigFixture fixture_second_store_store web/secure/base_url http://second_store.test/ * @magentoConfigFixture fixture_second_store_store web/secure/base_link_url http://second_store.test/ + * @return void */ - public function testSwitch() + public function testSwitch(): void { $data = ['key1' => 'value1', 'key2' => 1]; $this->preprocessorMock->method('process') @@ -131,6 +166,7 @@ function (ContextInterface $context) { * Return class name of the given object * * @param mixed $instance + * @return string */ private function getClassName($instance): string { @@ -150,19 +186,20 @@ private function getClassName($instance): string * incorrect work of page cache. * * @magentoDbIsolation enabled + * @return void */ - public function testExecuteWithCustomDefaultStore() + public function testExecuteWithCustomDefaultStore(): void { - \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + Bootstrap::getInstance()->reinitialize(); $defaultStoreCode = 'default'; $modifiedDefaultCode = 'modified_default_code'; $this->changeStoreCode($defaultStoreCode, $modifiedDefaultCode); $this->dispatch('stores/store/switch'); - /** @var \Magento\Framework\App\Http\Context $httpContext */ - $httpContext = $this->_objectManager->get(\Magento\Framework\App\Http\Context::class); - $httpContext->unsValue(\Magento\Store\Model\Store::ENTITY); - $this->assertEquals($modifiedDefaultCode, $httpContext->getValue(\Magento\Store\Model\Store::ENTITY)); + /** @var Context $httpContext */ + $httpContext = $this->_objectManager->get(Context::class); + $httpContext->unsValue(Store::ENTITY); + $this->assertEquals($modifiedDefaultCode, $httpContext->getValue(Store::ENTITY)); $this->changeStoreCode($modifiedDefaultCode, $defaultStoreCode); } @@ -172,13 +209,54 @@ public function testExecuteWithCustomDefaultStore() * * @param string $from * @param string $to + * @return void */ - private function changeStoreCode($from, $to) + private function changeStoreCode(string $from, string $to): void { /** @var Store $store */ - $store = $this->_objectManager->create(Store::class); - $store->load($from, 'code'); + $store = $this->storeFactory->create(); + $this->storeResource->load($store, $from, 'code'); $store->setCode($to); - $store->save(); + $this->storeResource->save($store); + } + + /** + * Switch to category on second store + * + * @magentoDataFixture Magento/Catalog/_files/category_on_second_store.php + * @magentoDbIsolation disabled + * @return void + */ + public function testSwitchToCategoryOnSecondStore(): void + { + $id = 333; + $fromStore = $this->storeManager->getStore(); + $targetStore = $this->storeManager->getStore('test'); + $category = $this->categoryRepository->get($id, $fromStore->getId()); + + $redirectData = $this->redirectDataGenerator->generate( + $this->contextFactory->create( + [ + 'fromStore' => $fromStore, + 'targetStore' => $targetStore, + 'redirectUrl' => $this->redirect->getRedirectUrl(), + ] + ) + ); + + $this->getRequest()->setParams( + [ + '___from_store' => $fromStore->getCode(), + StoreManagerInterface::PARAM_NAME => $targetStore->getCode(), + ActionInterface::PARAM_NAME_URL_ENCODED => $this->urlEncoder->encode($category->getUrl()), + 'data' => $redirectData->getData(), + 'time_stamp' => $redirectData->getTimestamp(), + 'signature' => $redirectData->getSignature(), + ] + ); + + $this->dispatch('stores/store/switch'); + $categorySecond = $this->categoryRepository->get($id, $targetStore->getId()); + $this->assertRedirect($this->stringContains($categorySecond->getUrlKey())); } } diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php index 3f7c3c5a9a452..d81a6fa52ea48 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreTest.php @@ -169,14 +169,14 @@ public function getBaseUrlDataProvider() [UrlInterface::URL_TYPE_DIRECT_LINK, false, true, 'http://localhost/index.php/'], [UrlInterface::URL_TYPE_DIRECT_LINK, true, false, 'http://localhost/'], [UrlInterface::URL_TYPE_DIRECT_LINK, true, true, 'http://localhost/'], - [UrlInterface::URL_TYPE_STATIC, false, false, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, false, true, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, true, false, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_STATIC, true, true, 'http://localhost/pub/static/'], - [UrlInterface::URL_TYPE_MEDIA, false, false, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, false, true, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, true, false, 'http://localhost/pub/media/'], - [UrlInterface::URL_TYPE_MEDIA, true, true, 'http://localhost/pub/media/'] + [UrlInterface::URL_TYPE_STATIC, false, false, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, false, true, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, true, false, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_STATIC, true, true, 'http://localhost/static/'], + [UrlInterface::URL_TYPE_MEDIA, false, false, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, false, true, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, true, false, 'http://localhost/media/'], + [UrlInterface::URL_TYPE_MEDIA, true, true, 'http://localhost/media/'] ]; } @@ -196,8 +196,8 @@ public function testGetBaseUrlInPub() $this->model = $this->_getStoreModel(); $this->model->load('default'); - $this->assertEquals('http://localhost/pub/static/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_STATIC)); - $this->assertEquals('http://localhost/pub/media/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_MEDIA)); + $this->assertEquals('http://localhost/static/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_STATIC)); + $this->assertEquals('http://localhost/media/', $this->model->getBaseUrl(UrlInterface::URL_TYPE_MEDIA)); } /** diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php new file mode 100644 index 0000000000000..3dc610c5fb943 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +/** @var \Magento\Config\Model\ResourceModel\Config $configResource */ +$configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); +$configResource->saveConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + 'EUR', + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $websiteId +); +$configResource->saveConfig( + \Magento\Catalog\Helper\Data::XML_PATH_PRICE_SCOPE, + \Magento\Store\Model\Store::PRICE_SCOPE_WEBSITE, + 'default', + 0 +); + +/** + * Configuration cache clean is required to reload currency setting + */ +/** @var Magento\Config\App\Config\Type\System $config */ +$config = $objectManager->get(\Magento\Config\App\Config\Type\System::class); +$config->clean(); + +$observer = $objectManager->get(\Magento\Framework\Event\Observer::class); +$objectManager->get(\Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange::class) + ->execute($observer); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php new file mode 100644 index 0000000000000..4fac07ae4f51f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_base_second_currency_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Config\Model\ResourceModel\Config $configResource */ +$configResource = $objectManager->get(\Magento\Config\Model\ResourceModel\Config::class); +$configResource->deleteConfig( + \Magento\Catalog\Helper\Data::XML_PATH_PRICE_SCOPE, + 'default', + 0 +); +$website = $objectManager->create(\Magento\Store\Model\Website::class); +/** @var $website \Magento\Store\Model\Website */ +$websiteId = $website->load('test', 'code')->getId(); +if ($websiteId) { + $configResource->deleteConfig( + \Magento\Directory\Model\Currency::XML_PATH_CURRENCY_BASE, + \Magento\Store\Model\ScopeInterface::SCOPE_WEBSITES, + $websiteId + ); +} + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php index 38fade9013cd1..0bc5e2e6e595e 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/configurable_product_with_visual_swatch_attribute_rollback.php @@ -12,9 +12,6 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -Resolver::getInstance()->requireDataFixture( - 'Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php' -); $objectManager = Bootstrap::getObjectManager(); /** @var Registry $registry */ $registry = $objectManager->get(Registry::class); @@ -42,5 +39,9 @@ //Product already removed } +Resolver::getInstance()->requireDataFixture( + 'Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php' +); + $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php index 8d2b427d7f7f3..77b3e198bd5ab 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type.php @@ -30,7 +30,7 @@ 'image-height' => 90, 'image-name' => $imageName, ]); -$imagePath = substr($swatchesMedia->moveImageFromTmp($imageName), 1); +$imagePath = $swatchesMedia->moveImageFromTmp($imageName); $swatchesMedia->generateSwatchVariations($imagePath); // Add attribute data diff --git a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php index c480906619a4a..c5e1e1fc287ba 100644 --- a/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Swatches/_files/visual_swatch_attribute_with_different_options_type_rollback.php @@ -5,20 +5,33 @@ */ declare(strict_types=1); +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; use Magento\Swatches\Helper\Media as SwatchesMedia; use Magento\TestFramework\Helper\Bootstrap; +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $attributeRepository */ +$attributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); + +try { + $attribute = $attributeRepository->get('test_configurable'); + $attributeRepository->delete($attribute); +} catch (NoSuchEntityException $exception) { + //Product already removed +} + /** @var WriteInterface $mediaDirectory */ -$mediaDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) +$mediaDirectory = $objectManager->get(Filesystem::class) ->getDirectoryWrite( DirectoryList::MEDIA ); /** @var SwatchesMedia $swatchesMedia */ -$swatchesMedia = Bootstrap::getObjectManager()->get(SwatchesMedia::class); +$swatchesMedia = $objectManager->get(SwatchesMedia::class); $testImageName = 'visual_swatch_attribute_option_type_image.jpg'; $testImageSwatchPath = $swatchesMedia->getAttributeSwatchPath($testImageName); diff --git a/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php new file mode 100644 index 0000000000000..57d4322ded9a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/Block/Adminhtml/Widget/InstanceTest.php @@ -0,0 +1,307 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Widget\Block\Adminhtml\Widget; + +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme as ThemeResource; +use Magento\Theme\Model\ThemeFactory; +use PHPUnit\Framework\TestCase; + +/** + * Checks widget grid filtering and sorting + * + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class InstanceTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var PageFactory */ + private $pageFactory; + + /** @var RequestInterface */ + private $request; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->request = $this->objectManager->get(RequestInterface::class); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + } + + /** + * @dataProvider gridFiltersDataProvider + * + * @magentoDataFixture Magento/Widget/_files/widgets.php + * + * @param array $filter + * @param array $expectedWidgets + * @return void + */ + public function testGridFiltering(array $filter, array $expectedWidgets): void + { + $this->request->setParams($filter); + $collection = $this->getGridCollection(); + + $this->assertWidgets($expectedWidgets, $collection); + } + + /** + * @return array + */ + public function gridFiltersDataProvider(): array + { + return [ + 'first_page' => [ + 'filter' => [ + 'limit' => 2, + 'page' => 1, + ], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + ], + ], + 'second_page' => [ + 'filter' => [ + 'limit' => 2, + 'page' => 2, + ], + 'expected_widgets' => [ + 'recently compared products', + ], + ], + 'filter_by_title' => [ + 'filter' => [ + 'filter' => base64_encode('title=product link widget title'), + ], + 'expected_widgets' => [ + 'product link widget title', + ], + ], + 'filter_by_type' => [ + 'filter' => [ + 'filter' => base64_encode('type=Magento%5CCms%5CBlock%5CWidget%5CPage%5CLink'), + ], + 'expected_widgets' => [ + 'cms page widget title', + ], + ], + 'filter_by_theme' => [ + 'filter' => [ + 'filter' => base64_encode('theme_id=' . $this->loadThemeIdByCode('Magento/blank')), + ], + 'expected_widgets' => [ + 'recently compared products', + ], + ], + 'filter_by_sort_order' => [ + 'filter' => [ + 'filter' => base64_encode('sort_order=1'), + ], + 'expected_widgets' => [ + 'recently compared products' + ], + ], + 'filter_by_multiple_filters' => [ + 'filter' => [ + 'filter' => base64_encode( + 'type=Magento%5CCatalog%5CBlock%5CWidget%5CRecentlyCompared&sort_order=1' + ), + ], + 'expected_widgets' => [ + 'recently compared products', + ], + ], + ]; + } + + /** + * @dataProvider gridSortDataProvider + * + * @magentoDataFixture Magento/Widget/_files/widgets.php + * + * @param array $filter + * @param array $expectedWidgets + * @return void + */ + public function testGridSorting(array $filter, array $expectedWidgets): void + { + $this->request->setParams($filter); + $collection = $this->getGridCollection(); + $this->assertEquals($expectedWidgets, $collection->getColumnValues('title')); + } + + /** + * @return array + */ + public function gridSortDataProvider(): array + { + return [ + 'sort_by_id_asc' => [ + 'filter' => ['sort' => 'instance_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_id_desc' => [ + 'filter' => ['sort' => 'instance_id', 'dir' => 'desc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_title_asc' => [ + 'filter' => ['sort' => 'title', 'dir' => 'asc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_title_desc' => [ + 'filter' => ['sort' => 'title', 'dir' => 'desc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_type_asc' => [ + 'filter' => ['sort' => 'type', 'dir' => 'asc'], + 'expected_widgets' => [ + 'product link widget title', + 'recently compared products', + 'cms page widget title', + ], + ], + 'sort_by_type_desc' => [ + 'filter' => ['sort' => 'type', 'dir' => 'desc'], + 'expected_widgets' => [ + 'cms page widget title', + 'recently compared products', + 'product link widget title', + ], + ], + 'sort_by_sort_order_asc' => [ + 'filter' => ['sort' => 'sort_order', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'product link widget title', + 'cms page widget title', + ], + ], + 'sort_by_sort_order_desc' => [ + 'filter' => ['sort' => 'sort_order', 'dir' => 'desc'], + 'expected_widgets' => [ + 'cms page widget title', + 'product link widget title', + 'recently compared products', + ], + ], + 'sort_by_theme_asc' => [ + 'filter' => ['sort' => 'theme_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'cms page widget title', + 'product link widget title', + ], + ], + 'sort_by_theme_desc' => [ + 'filter' => ['sort' => 'theme_id', 'dir' => 'asc'], + 'expected_widgets' => [ + 'recently compared products', + 'cms page widget title', + 'product link widget title', + ], + ], + ]; + } + + /** + * Load theme by theme id + * + * @param string $code + * @return int + */ + private function loadThemeIdByCode(string $code): int + { + $objectManager = Bootstrap::getObjectManager(); + /** @var ThemeFactory $themeFactory */ + $themeFactory = $objectManager->get(ThemeFactory::class); + /** @var ThemeResource $themeResource */ + $themeResource = $objectManager->get(ThemeResource::class); + $theme = $themeFactory->create(); + $themeResource->load($theme, $code, 'code'); + + return (int)$theme->getId(); + } + + /** + * Assert widget instances + * + * @param $expectedWidgets + * @param AbstractCollection $collection + * @return void + */ + private function assertWidgets($expectedWidgets, AbstractCollection $collection): void + { + $this->assertCount(count($expectedWidgets), $collection); + foreach ($expectedWidgets as $widgetTitle) { + $item = $collection->getItemByColumnValue('title', $widgetTitle); + $this->assertNotNull($item); + } + } + + /** + * Prepare page layout + * + * @return LayoutInterface + */ + private function preparePageLayout(): LayoutInterface + { + $page = $this->pageFactory->create(); + $page->addHandle([ + 'default', + 'adminhtml_widget_instance_index', + ]); + + return $page->getLayout()->generateXml(); + } + + /** + * Get prepared grid collection + * + * @return AbstractCollection + */ + private function getGridCollection(): AbstractCollection + { + $layout = $this->preparePageLayout(); + $containerBlock = $layout->getBlock('adminhtml.widget.instance.grid.container'); + $grid = $containerBlock->getChildBlock('grid'); + $this->assertNotFalse($grid); + + return $grid->getPreparedCollection(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php b/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php index fc3b0399d0497..d4a14420c4ae6 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Model/Template/FilterTest.php @@ -11,7 +11,7 @@ public function testMediaDirective() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; /** @var \Magento\Widget\Model\Template\Filter $filter */ $filter = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( @@ -25,7 +25,7 @@ public function testMediaDirectiveWithEncodedQuotes() { $image = 'wysiwyg/VB.png'; $construction = ['{{media url="' . $image . '"}}', 'media', ' url="' . $image . '"']; - $baseUrl = 'http://localhost/pub/media/'; + $baseUrl = 'http://localhost/media/'; /** @var \Magento\Widget\Model\Template\Filter $filter */ $filter = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( diff --git a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php index a9d21ec84e32b..fb13ea57475ad 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Model/Widget/ConfigTest.php @@ -46,7 +46,7 @@ public function testGetPluginSettings() $jsFilename = $plugins['src']; $this->assertStringMatchesFormat( - 'http://localhost/pub/static/%s/adminhtml/Magento/backend/en_US/%s/editor_plugin.js', + 'http://localhost/static/%s/adminhtml/Magento/backend/en_US/%s/editor_plugin.js', $jsFilename ); diff --git a/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php new file mode 100644 index 0000000000000..fa0f7d9fe9918 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets.php @@ -0,0 +1,103 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Block\Product\Widget\Link as ProductLink; +use Magento\Catalog\Block\Widget\RecentlyCompared; +use Magento\Cms\Api\GetPageByIdentifierInterface; +use Magento\Cms\Block\Widget\Page\Link as PageLink; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme as ThemeResource; +use Magento\Theme\Model\ThemeFactory; +use Magento\Widget\Model\ResourceModel\Widget\Instance as InstanceResource; +use Magento\Widget\Model\Widget\InstanceFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ThemeFactory $themeFactory */ +$themeFactory = $objectManager->get(ThemeFactory::class); +/** @var ThemeResource $themeResource */ +$themeResource = $objectManager->get(ThemeResource::class); +$lumaTheme = $themeFactory->create(); +$themeResource->load($lumaTheme, 'Magento/luma', 'code'); +$blankTheme = $themeFactory->create(); +$themeResource->load($blankTheme, 'Magento/blank', 'code'); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$defaultStoreId = (int)$storeManager->getStore('default')->getId(); +/** @var GetPageByIdentifierInterface $getPageByIdentifier */ +$getPageByIdentifier = $objectManager->get(GetPageByIdentifierInterface::class); +$homePage = $getPageByIdentifier->execute('home', $defaultStoreId); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$productId = (int)$productRepository->get('simple2')->getId(); +/** @var InstanceFactory $widgetFactory */ +$widgetFactory = $objectManager->get(InstanceFactory::class); +/** @var InstanceResource $widgetResource */ +$widgetResource = $objectManager->get(InstanceResource::class); +$cmsPageWidget = $widgetFactory->create(); +$cmsPageWidgetData = [ + 'instance_type' => PageLink::class, + 'instance_code' => 'cms_page_link', + 'theme_id' => $lumaTheme->getId(), + 'title' => 'cms page widget title', + 'sort_order' => 3, + 'store_ids' => [$defaultStoreId], + 'widget_parameters' => [ + 'page_id' => $homePage->getId(), + ], +]; +$cmsPageWidget->setData($cmsPageWidgetData); +$widgetResource->save($cmsPageWidget); + +$productLinkWidget = $widgetFactory->create(); +$productLinkWidgetData = [ + 'instance_type' => ProductLink::class, + 'instance_code' => 'catalog_product_link', + 'theme_id' => $lumaTheme->getId(), + 'title' => 'product link widget title', + 'sort_order' => 2, + 'store_ids' => [$defaultStoreId], + 'pages_groups' => [ + 'page_group' => 'all_pages', + 'all_pages' => [ + 'page_id' => 0, + 'layout_handle' => 'default', + 'for' => 'all', + 'block' => 'content', + 'template' => 'product/widget/link/link_block.phtml', + ], + ], + 'widget_parameters' => [ + 'product/' . $productId, + ], +]; + +$productLinkWidget->setData($productLinkWidgetData); +$widgetResource->save($productLinkWidget); + +$recentlyComparedProductWidget = $widgetFactory->create(); +$recentlyComparedProductWidgetData = [ + 'instance_type' => RecentlyCompared::class, + 'instance_code' => 'catalog_recently_compared', + 'theme_id' => $blankTheme->getId(), + 'title' => 'recently compared products', + 'store_ids' => [$defaultStoreId], + 'sort_order' => 1, + 'widget_parameters' => [ + 'uiComponent' => 'widget_recently_compared', + 'page_size' => 5, + 'show_attributes' => ['name'], + 'show_buttons' => ['add_to_cart'], + ], +]; +$recentlyComparedProductWidget->setData($recentlyComparedProductWidgetData); +$widgetResource->save($recentlyComparedProductWidget); diff --git a/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php new file mode 100644 index 0000000000000..63c8183fe3431 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Widget/_files/widgets_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Widget\Model\ResourceModel\Widget\Instance; +use Magento\Widget\Model\ResourceModel\Widget\Instance\CollectionFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CollectionFactory $collectionFactory */ +$collectionFactory = $objectManager->get(CollectionFactory::class); +/** @var Instance $widgetResourceModel */ +$widgetResourceModel = $objectManager->get(Instance::class); + +$titles = ['cms page widget title', 'product link widget title', 'recently compared products']; +$widgets = $collectionFactory->create()->addFieldToFilter('title', $titles); +foreach ($widgets as $widget) { + $widgetResourceModel->delete($widget); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js index a957db5d1c119..517e13281d402 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/form/element/region.test.js @@ -47,6 +47,40 @@ define([ }); }); + describe('initialize method', function () { + it('Hides region field when it should be hidden for default country', function () { + model.countryOptions = { + 'DefaultCountryCode': { + 'is_default': true, + 'is_region_visible': false + }, + 'NonDefaultCountryCode': { + 'is_region_visible': true + } + }; + + model.initialize(); + + expect(model.visible()).toEqual(false); + }); + + it('Shows region field when it should be visible for default country', function () { + model.countryOptions = { + 'CountryCode': { + 'is_default': true, + 'is_region_visible': true + }, + 'NonDefaultCountryCode': { + 'is_region_visible': false + } + }; + + model.initialize(); + + expect(model.visible()).toEqual(true); + }); + }); + describe('update method', function () { it('makes field optional when there is no corresponding country', function () { var value = 'Value'; diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php index 9507e50d71638..43aacecb6982e 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php @@ -125,7 +125,7 @@ public function splitQuote() ); $command = $this->getCliScriptCommand() . ' setup:db-schema:split-quote ' . implode(" ", array_keys($installParams)) . - ' -vvv --magento-init-params="' . + ' -vvv --no-interaction --magento-init-params="' . $initParams['magento-init-params'] . '"'; $this->shell->execute($command, array_values($installParams)); diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 6fdeeb816f2cf..6ece76690157c 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -10,11 +10,14 @@ namespace Magento\TestFramework\Dependency; use Magento\Framework\App\Utility\Files; +use Magento\Framework\Config\Reader\Filesystem as ConfigReader; +use Magento\Framework\Exception\ConfigurationMismatchException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\UrlInterface; use Magento\TestFramework\Dependency\Reader\ClassScanner; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Exception\NoSuchActionException; +use Magento\TestFramework\Inspection\Exception; /** * Rule to check the dependencies between modules based on references, getUrl and layout blocks @@ -58,6 +61,12 @@ class PhpRule implements RuleInterface */ protected $_mapLayoutBlocks = []; + /** + * Used to retrieve information from WebApi urls + * @var ConfigReader + */ + protected $configReader; + /** * Default modules list. * @@ -85,28 +94,36 @@ class PhpRule implements RuleInterface */ private $classScanner; + /** + * @var array + */ + private $serviceMethods; + /** * @param array $mapRouters * @param array $mapLayoutBlocks + * @param ConfigReader $configReader * @param array $pluginMap * @param array $whitelists * @param ClassScanner|null $classScanner - * - * @throws LocalizedException + * @param RouteMapper|null $routeMapper */ public function __construct( array $mapRouters, array $mapLayoutBlocks, + ConfigReader $configReader, array $pluginMap = [], array $whitelists = [], - ClassScanner $classScanner = null + ClassScanner $classScanner = null, + RouteMapper $routeMapper = null ) { $this->_mapRouters = $mapRouters; $this->_mapLayoutBlocks = $mapLayoutBlocks; + $this->configReader = $configReader; $this->pluginMap = $pluginMap ?: null; - $this->routeMapper = new RouteMapper(); $this->whitelists = $whitelists; $this->classScanner = $classScanner ?? new ClassScanner(); + $this->routeMapper = $routeMapper ?? new RouteMapper(); } /** @@ -132,7 +149,7 @@ public function getDependencyInfo($currentModule, $fileType, $file, &$contents) ); $dependenciesInfo = $this->considerCaseDependencies( $dependenciesInfo, - $this->_caseGetUrl($currentModule, $contents) + $this->_caseGetUrl($currentModule, $contents, $file) ); $dependenciesInfo = $this->considerCaseDependencies( $dependenciesInfo, @@ -290,41 +307,29 @@ private function isPluginDependency($dependent, $dependency) * * @param string $currentModule * @param string $contents + * @param string $file * @return array * @throws LocalizedException - * @throws \Exception - * @SuppressWarnings(PMD.CyclomaticComplexity) */ - protected function _caseGetUrl(string $currentModule, string &$contents): array + protected function _caseGetUrl(string $currentModule, string &$contents, string $file): array { - $pattern = '#(\->|:)(?<source>getUrl\(([\'"])(?<route_id>[a-z0-9\-_]{3,}|\*)' - .'(/(?<controller_name>[a-z0-9\-_]+|\*))?(/(?<action_name>[a-z0-9\-_]+|\*))?\3)#i'; - $dependencies = []; + $pattern = '#(\->|:)(?<source>getUrl\(([\'"])(?<path>[a-zA-Z0-9\-_*/]+)\3)\s*[,)]#'; if (!preg_match_all($pattern, $contents, $matches, PREG_SET_ORDER)) { return $dependencies; } - try { foreach ($matches as $item) { - $routeId = $item['route_id']; - $controllerName = $item['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; - $actionName = $item['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; - - // skip rest - if ($routeId === "rest") { //MC-19890 - continue; + $path = $item['path']; + $modules = []; + if (strpos($path, '*') !== false) { + $modules = $this->processWildcardUrl($path, $file); + } elseif (preg_match('#rest(?<service>/V1/.+)#i', $path, $apiMatch)) { + $modules = $this->processApiUrl($apiMatch['service']); + } else { + $modules = $this->processStandardUrl($path); } - // skip wildcards - if ($routeId === "*" || $controllerName === "*" || $actionName === "*") { //MC-19890 - continue; - } - $modules = $this->routeMapper->getDependencyByRoutePath( - $routeId, - $controllerName, - $actionName - ); - if (!in_array($currentModule, $modules)) { + if ($modules && !in_array($currentModule, $modules)) { $dependencies[] = [ 'modules' => $modules, 'type' => RuleInterface::TYPE_HARD, @@ -337,10 +342,136 @@ protected function _caseGetUrl(string $currentModule, string &$contents): array throw new LocalizedException(__('Invalid URL path: %1', $e->getMessage()), $e); } } - return $dependencies; } + /** + * Helper method to get module dependencies used by a wildcard Url + * + * @param string $urlPath + * @param string $filePath + * @return string[] + * @throws NoSuchActionException + */ + private function processWildcardUrl(string $urlPath, string $filePath) + { + $filePath = strtolower($filePath); + $urlRoutePieces = explode('/', $urlPath); + $routeId = array_shift($urlRoutePieces); + //Skip route wildcard processing as this requires using the routeMapper + if ('*' === $routeId) { + return []; + } + + /** + * Only handle Controllers. ie: Ignore Blocks, Templates, and Models due to complexity in static resolution + * of route + */ + if (!preg_match( + '#controller/(adminhtml/)?(?<controller_name>.+)/(?<action_name>\w+).php$#', + $filePath, + $fileParts + )) { + return []; + } + + $controllerName = array_shift($urlRoutePieces); + if ('*' === $controllerName) { + $controllerName = str_replace('/', '_', $fileParts['controller_name']); + } + + if (empty($urlRoutePieces) || !$urlRoutePieces[0]) { + $actionName = UrlInterface::DEFAULT_ACTION_NAME; + } else { + $actionName = array_shift($urlRoutePieces); + if ('*' === $actionName) { + $actionName = $fileParts['action_name']; + } + } + + return $this->routeMapper->getDependencyByRoutePath( + strtolower($routeId), + strtolower($controllerName), + strtolower($actionName) + ); + } + + /** + * Helper method to get module dependencies used by a standard URL + * + * @param string $path + * @return string[] + * @throws NoSuchActionException + */ + private function processStandardUrl(string $path) + { + $pattern = '#(?<route_id>[a-z0-9\-_]{3,})' + . '(/(?<controller_name>[a-z0-9\-_]+))?(/(?<action_name>[a-z0-9\-_]+))?#i'; + if (!preg_match($pattern, $path, $match)) { + throw new NoSuchActionException('Failed to parse standard url path: ' . $path); + } + $routeId = $match['route_id']; + $controllerName = $match['controller_name'] ?? UrlInterface::DEFAULT_CONTROLLER_NAME; + $actionName = $match['action_name'] ?? UrlInterface::DEFAULT_ACTION_NAME; + + return $this->routeMapper->getDependencyByRoutePath( + $routeId, + $controllerName, + $actionName + ); + } + + /** + * Create regex patterns from service url paths + * + * @return array + */ + private function getServiceMethodRegexps(): array + { + if (!$this->serviceMethods) { + $this->serviceMethods = []; + $serviceRoutes = $this->configReader->read()['routes']; + foreach ($serviceRoutes as $serviceRouteUrl => $methods) { + $pattern = '#:\w+#'; + $replace = '\w+'; + $serviceRouteUrlRegex = preg_replace($pattern, $replace, $serviceRouteUrl); + $serviceRouteUrlRegex = '#^' . $serviceRouteUrlRegex . '$#'; + $this->serviceMethods[$serviceRouteUrlRegex] = $methods; + } + } + return $this->serviceMethods; + } + + /** + * Helper method to get module dependencies used by an API URL + * + * @param string $path + * @return string[] + * + * @throws NoSuchActionException + * @throws Exception + */ + private function processApiUrl(string $path): array + { + foreach ($this->getServiceMethodRegexps() as $serviceRouteUrlRegex => $methods) { + /** + * Since we expect that every service method should be within the same module, we can use the class from + * any method + */ + if (preg_match($serviceRouteUrlRegex, $path)) { + $method = reset($methods); + + $className = $method['service']['class']; + //get module from className + if (preg_match('#^(?<module>\w+[\\\]\w+)#', $className, $match)) { + return [$match['module']]; + } + throw new Exception('Failed to parse class from className: ' . $className); + } + } + throw new NoSuchActionException('Failed to match service with url path: ' . $path); + } + /** * Check layout blocks * diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php index 3ddfbefaa346f..0d12a62884a39 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/TestFramework/Dependency/PhpRuleTest.php @@ -3,12 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\TestFramework\Dependency; +use Magento\Framework\Config\Reader\Filesystem; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\TestFramework\Dependency\Reader\ClassScanner; -use Magento\TestFramework\Exception\NoSuchActionException; +use Magento\TestFramework\Dependency\Route\RouteMapper; /** * Test for PhpRule dependency check @@ -30,32 +32,41 @@ class PhpRuleTest extends \PHPUnit\Framework\TestCase */ private $classScanner; + /** + * @var \PHPUnit\Framework\MockObject\MockObject | Filesystem + */ + private $webApiConfigReader; + + private $pluginMap; + private $mapRoutes; + private $mapLayoutBlocks; + private $whitelist; + /** * @inheritDoc * @throws \Exception */ protected function setUp(): void { - $mapRoutes = ['someModule' => ['Magento\SomeModule'], 'anotherModule' => ['Magento\OneModule']]; - $mapLayoutBlocks = ['area' => ['block.name' => ['Magento\SomeModule' => 'Magento\SomeModule']]]; - $pluginMap = [ + $this->mapRoutes = ['someModule' => ['Magento\SomeModule'], 'anotherModule' => ['Magento\OneModule']]; + $this->mapLayoutBlocks = ['area' => ['block.name' => ['Magento\SomeModule' => 'Magento\SomeModule']]]; + $this->pluginMap = [ 'Magento\Module1\Plugin1' => 'Magento\Module1\Subject', 'Magento\Module1\Plugin2' => 'Magento\Module2\Subject', ]; - $whitelist = []; + $this->whitelist = []; $this->objectManagerHelper = new ObjectManagerHelper($this); $this->classScanner = $this->createMock(ClassScanner::class); + $this->webApiConfigReader = $this->makeWebApiConfigReaderMock(); - $this->model = $this->objectManagerHelper->getObject( - PhpRule::class, - [ - 'mapRouters' => $mapRoutes, - 'mapLayoutBlocks' => $mapLayoutBlocks, - 'pluginMap' => $pluginMap, - 'whitelists' => $whitelist, - 'classScanner' => $this->classScanner - ] + $this->model = new PhpRule( + $this->mapRoutes, + $this->mapLayoutBlocks, + $this->webApiConfigReader, + $this->pluginMap, + $this->whitelist, + $this->classScanner ); } @@ -107,7 +118,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento\SomeModule\Any\ClassName', ] ] @@ -125,7 +136,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento_SomeModule', ] ] @@ -143,7 +154,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento\SomeModule\Any\ClassName', ] ] @@ -161,7 +172,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'getBlock(\'block.name\')', ] ] @@ -185,7 +196,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\Module2'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_SOFT, + 'type' => RuleInterface::TYPE_SOFT, 'source' => 'Magento\Module2\Subject', ] ], @@ -197,7 +208,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\Module2'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_SOFT, + 'type' => RuleInterface::TYPE_SOFT, 'source' => 'Magento\Module2\NotSubject', ] ] @@ -209,7 +220,7 @@ public function getDependencyInfoDataProvider() [ [ 'modules' => ['Magento\OtherModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'Magento\OtherModule\NotSubject', ] ] @@ -252,11 +263,150 @@ public function getDependencyInfoDataCaseGetUrlDataProvider() [ [ 'modules' => ['Magento\Cms'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'getUrl("cms/index/index"', ] ] ], + 'getUrl from API of same module with parameter' => [ + 'Magento\Catalog\SomeClass', + '$this->getUrl("rest/V1/products/3")', + [] + ], + 'getUrl from API of same module without parameter' => [ + 'Magento\Catalog\SomeClass', + '$this->getUrl("rest/V1/products")', + [] + ], + 'getUrl from API of different module with parameter' => [ + 'Magento\Backend\SomeClass', + '$this->getUrl("rest/V1/products/43/options")', + [ + [ + 'modules' => ['Magento\Catalog'], + 'type' => RuleInterface::TYPE_HARD, + 'source' => 'getUrl("rest/V1/products/43/options"' + ] + ], + ], + 'getUrl from routeid wildcard' => [ + 'Magento\Catalog\Controller\ControllerName\SomeClass', + '$this->getUrl("*/Invalid/*")', + [] + ], + 'getUrl from wildcard url within ignored Block class' => [ + 'Magento\Cms\Block\SomeClass', + '$this->getUrl("Catalog/*/View")', + [] + ], + 'getUrl from wildcard url within ignored Model file' => [ + 'Magento\Cms\Model\SomeClass', + '$this->getUrl("Catalog/*/View")', + [] + ], + 'getUrl with in admin controller for controllerName wildcard' => [ + 'Magento\Backend\Controller\Adminhtml\System\Store\DeleteStore', + '$this->getUrl("adminhtml/*/deleteStorePost")', + [] + ], + ]; + } + + /** + * @param string $template + * @param string $content + * @param array $expected + * @throws \Exception + * @dataProvider getDependencyInfoDataCaseGetTemplateUrlDataProvider + */ + public function testGetDependencyInfoCaseTemplateGetUrl( + string $template, + string $content, + array $expected + ) { + $module = $this->getModuleFromClass($template); + + $this->assertEquals($expected, $this->model->getDependencyInfo($module, 'php', $template, $content)); + } + + /** + * @return array[] + */ + public function getDependencyInfoDataCaseGetTemplateUrlDataProvider() + { + return [ 'getUrl from ignore template' => [ + 'app/code/Magento/Backend/view/adminhtml/templates/dashboard/totalbar/script.phtml', + '$getUrl("adminhtml/*/ajaxBlock")', + []]]; + } + + /** + * @param string $class + * @param string $content + * @param array $expected + * @dataProvider processWildcardUrlDataProvider + */ + public function testProcessWildcardUrl( + string $class, + string $content, + array $expected + ) { + $routeMapper = $this->createMock(RouteMapper::class); + $routeMapper->expects($this->once()) + ->method('getDependencyByRoutePath') + ->with( + $this->equalTo($expected['route_id']), + $this->equalTo($expected['controller_name']), + $this->equalTo($expected['action_name']) + ); + $phpRule = new PhpRule( + $this->mapRoutes, + $this->mapLayoutBlocks, + $this->webApiConfigReader, + $this->pluginMap, + $this->whitelist, + $this->classScanner, + $routeMapper + ); + $file = $this->makeMockFilepath($class); + $module = $this->getModuleFromClass($class); + + $phpRule->getDependencyInfo($module, 'php', $file, $content); + } + + /** + * @return array[] + */ + public function processWildcardUrlDataProvider() + { + return [ + 'wildcard controller route' => [ + 'Magento\SomeModule\Controller\ControllerName\SomeClass', + '$this->getUrl("cms/*/index")', + [ + 'route_id' => 'cms', + 'controller_name' => 'controllername', + 'action_name' => 'index' + ] + ], + 'adminhtml wildcard controller route' => [ + 'Magento\Backend\Controller\Adminhtml\System\Store\DeleteStore', + '$this->getUrl("adminhtml/*/deleteStorePost")', + [ + 'route_id' => 'adminhtml', + 'controller_name' => 'system_store', + 'action_name' => 'deletestorepost' + ] + ], + 'index wildcard' => [ + 'Magento\Backend\Controller\System\Store\DeleteStore', + '$this->getUrl("routeid/controllername/*")', + [ + 'route_id' => 'routeid', + 'controller_name' => 'controllername', + 'action_name' => 'deletestore' + ] + ] ]; } @@ -290,6 +440,11 @@ public function getDependencyInfoDataCaseGetUrlExceptionDataProvider() '$this->getUrl("someModule")', new LocalizedException(__('Invalid URL path: %1', 'somemodule/index/index')), ], + 'getUrl from unknown wildcard path' => [ + 'Magento\Catalog\Controller\Product\View', + '$this->getUrl("Catalog/*/INVALID")', + new LocalizedException(__('Invalid URL path: %1', 'catalog/product/invalid')), + ], ]; } @@ -309,7 +464,7 @@ public function testGetDefaultModelDependency($module, $content, array $expected ], ], ]; - $this->model = new PhpRule([], $mapLayoutBlocks); + $this->model = new PhpRule([], $mapLayoutBlocks, $this->webApiConfigReader); $this->assertEquals($expected, $this->model->getDependencyInfo($module, 'template', 'any', $content)); } @@ -325,7 +480,7 @@ public function getDefaultModelDependencyDataProvider() [ [ 'modules' => ['Magento\SomeModule'], - 'type' => \Magento\TestFramework\Dependency\RuleInterface::TYPE_HARD, + 'type' => RuleInterface::TYPE_HARD, 'source' => 'getBlock(\'block.name\')', ] ], @@ -356,4 +511,35 @@ private function getModuleFromClass(string $class): string $moduleNameLength = strpos($class, '\\', strpos($class, '\\') + 1); return substr($class, 0, $moduleNameLength); } + + /** + * Returns an example list of services that would be parsed via the configReader + * + * @return \PHPUnit\Framework\MockObject\MockObject | Filesystem + */ + private function makeWebApiConfigReaderMock() + { + $services = [ 'routes' => [ + '/V1/products/:sku' => [ + 'GET' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductRepositoryInterface', + 'method' => 'get' + ] ], + 'PUT' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductRepositoryInterface', + 'method' => 'save' + ] ], + ], + '/V1/products/:sku/options' => ['GET' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface', + 'method' => 'getList' + ] ] ], + '/V1/products' => ['GET' => ['service' => [ + 'class' => 'Magento\Catalog\Api\ProductCustomOptionRepositoryInterface', + 'method' => 'getList' + ] ] ] + ] ]; + + return $this->createConfiguredMock(Filesystem::class, [ 'read' => $services ]); + } } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php new file mode 100644 index 0000000000000..226512a7a40ed --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/Converter.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Test\Integrity\Dependency; + +/** + * Converter of webapi.xml content into array format. + */ +class Converter implements \Magento\Framework\Config\ConverterInterface +{ + /**#@+ + * Array keys for config internal representation. + */ + private const KEY_URL = 'url'; + private const KEY_CLASS = 'class'; + private const KEY_METHOD = 'method'; + private const KEY_ROUTE = 'route'; + private const KEY_ROUTES = 'routes'; + private const KEY_SERVICE = 'service'; + /**#@-*/ + + /** + * @inheritdoc + */ + public function convert($source) + { + $result = []; + /** @var \DOMNodeList $routes */ + $routes = $source->getElementsByTagName(self::KEY_ROUTE); + /** @var \DOMElement $route */ + foreach ($routes as $route) { + if ($route->nodeType != XML_ELEMENT_NODE) { + continue; + } + /** @var \DOMElement $service */ + $service = $route->getElementsByTagName(self::KEY_SERVICE)->item(0); + $serviceClass = $service->attributes->getNamedItem(self::KEY_CLASS)->nodeValue; + $serviceMethod = $service->attributes->getNamedItem(self::KEY_METHOD)->nodeValue; + $url = trim($route->attributes->getNamedItem(self::KEY_URL)->nodeValue); + + $method = $route->attributes->getNamedItem(self::KEY_METHOD)->nodeValue; + + // We could handle merging here by checking if the route already exists + $result[self::KEY_ROUTES][$url][$method] = [ + self::KEY_SERVICE => [ + self::KEY_CLASS => $serviceClass, + self::KEY_SERVICE => $serviceMethod, + ], + ]; + } + return $result; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php new file mode 100644 index 0000000000000..2295d147c7366 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/SchemaLocator.php @@ -0,0 +1,50 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Test\Integrity\Dependency; + +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Config\SchemaLocatorInterface; + +class SchemaLocator implements SchemaLocatorInterface +{ + /** + * @var string + */ + private $schema; + + /** + * @var string + */ + private $perFileSchema; + + public function __construct(ComponentRegistrar $componentRegistrar) + { + $module_path = $componentRegistrar->getPath(ComponentRegistrar::MODULE, 'Magento_Webapi'); + $this->schema = $module_path . '/etc/webapi_merged.xsd'; + $this->perFileSchema = $module_path . '/etc/webapi.xsd'; + } + + /** + * Return webapi_merged.xsd path + * + * @return string + */ + public function getSchema() + { + return $this->schema; + } + + /** + * Return webapi.xsd path + * + * @return string + */ + public function getPerFileSchema() + { + return $this->perFileSchema; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php new file mode 100644 index 0000000000000..63b011f2c8a90 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/WebapiFileResolver.php @@ -0,0 +1,49 @@ +<?php + +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Test\Integrity\Dependency; + +use Magento\Framework\Component\ComponentRegistrar; + +/** + * Collects all webapi.xml files + */ +class WebapiFileResolver implements \Magento\Framework\Config\FileResolverInterface +{ + /** + * @var ComponentRegistrar + */ + private $componentRegistrar; + + /** + * @var string[] + */ + private $webapiXmlPaths; + + public function __construct(ComponentRegistrar $componentRegistrar) + { + $this->componentRegistrar = $componentRegistrar; + } + + /** + * @inheritDoc + */ + public function get($filename, $scope) + { + if (!$this->webapiXmlPaths) { + $paths = $this->componentRegistrar->getPaths(ComponentRegistrar::MODULE); + $webapiXmlPaths = []; + foreach ($paths as $path) { + $path = $path . '/etc/webapi.xml'; + if (file_exists($path)) { + $webapiXmlPaths[$path] = file_get_contents($path); + } + } + $this->webapiXmlPaths = $webapiXmlPaths; + } + return $this->webapiXmlPaths; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 85f6a0aabfee0..2620c48659d98 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -11,15 +11,20 @@ use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Config\Reader\Filesystem as Reader; +use Magento\Framework\Config\ValidationState\Configurable; use Magento\Framework\Exception\LocalizedException; +use Magento\Test\Integrity\Dependency\Converter; use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; use Magento\Test\Integrity\Dependency\GraphQlSchemaDependencyProvider; +use Magento\Test\Integrity\Dependency\SchemaLocator; +use Magento\Test\Integrity\Dependency\WebapiFileResolver; +use Magento\TestFramework\Dependency\AnalyticsConfigRule; use Magento\TestFramework\Dependency\DbRule; use Magento\TestFramework\Dependency\DiRule; use Magento\TestFramework\Dependency\LayoutRule; use Magento\TestFramework\Dependency\PhpRule; use Magento\TestFramework\Dependency\ReportsConfigRule; -use Magento\TestFramework\Dependency\AnalyticsConfigRule; use Magento\TestFramework\Dependency\Route\RouteMapper; use Magento\TestFramework\Dependency\VirtualType\VirtualTypeMapper; @@ -279,10 +284,24 @@ protected static function _initRules() // In case primary module declaring the table cannot be identified, use any module referencing this table $tableToModuleMap = array_merge($tableToAnyModuleMap, $tableToPrimaryModuleMap); + $webApiConfigReader = new Reader( + new WebapiFileResolver(self::getComponentRegistrar()), + new Converter(), + new SchemaLocator(self::getComponentRegistrar()), + new Configurable(false), + 'webapi.xml', + [ + '/routes/route' => ['url', 'method'], + '/routes/route/resources/resource' => 'ref', + '/routes/route/data/parameter' => 'name', + ], + ); + self::$_rulesInstances = [ new PhpRule( self::$routeMapper->getRoutes(), self::$_mapLayoutBlocks, + $webApiConfigReader, [], ['routes' => self::getRoutesWhitelist()] ), @@ -316,6 +335,17 @@ private static function getRoutesWhitelist(): array return self::$routesWhitelist; } + /** + * @return ComponentRegistrar + */ + private static function getComponentRegistrar() + { + if (!isset(self::$componentRegistrar)) { + self::$componentRegistrar = new ComponentRegistrar(); + } + return self::$componentRegistrar; + } + /** * Get full path to app/code directory, assuming these tests are run from the dev/tests directory. * @@ -554,18 +584,16 @@ function ($fileType, $file) use ($blackList) { */ private function getModuleNameForRelevantFile($file) { - if (!isset(self::$componentRegistrar)) { - self::$componentRegistrar = new ComponentRegistrar(); - } + $componentRegistrar = self::getComponentRegistrar(); // Validates file when it belongs to default themes - foreach (self::$componentRegistrar->getPaths(ComponentRegistrar::THEME) as $themeDir) { + foreach ($componentRegistrar->getPaths(ComponentRegistrar::THEME) as $themeDir) { if (strpos($file, $themeDir . '/') !== false) { return ''; } } $foundModuleName = ''; - foreach (self::$componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { if (strpos($file, $moduleDir . '/') !== false) { $foundModuleName = str_replace('_', '\\', $moduleName); break; diff --git a/index.php b/index.php deleted file mode 100644 index 9ac7f6ffa71b2..0000000000000 --- a/index.php +++ /dev/null @@ -1,39 +0,0 @@ -<?php -/** - * Application entry point - * - * Example - run a particular store or website: - * -------------------------------------------- - * require __DIR__ . '/app/bootstrap.php'; - * $params = $_SERVER; - * $params[\Magento\Store\Model\StoreManager::PARAM_RUN_CODE] = 'website2'; - * $params[\Magento\Store\Model\StoreManager::PARAM_RUN_TYPE] = 'website'; - * $bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $params); - * \/** @var \Magento\Framework\App\Http $app *\/ - * $app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); - * $bootstrap->run($app); - * -------------------------------------------- - * - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -try { - require __DIR__ . '/app/bootstrap.php'; -} catch (\Exception $e) { - echo <<<HTML -<div style="font:12px/1.35em arial, helvetica, sans-serif;"> - <div style="margin:0 0 25px 0; border-bottom:1px solid #ccc;"> - <h3 style="margin:0;font-size:1.7em;font-weight:normal;text-transform:none;text-align:left;color:#2f2f2f;"> - Autoload error</h3> - </div> - <p>{$e->getMessage()}</p> -</div> -HTML; - exit(1); -} - -$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER); -/** @var \Magento\Framework\App\Http $app */ -$app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); -$bootstrap->run($app); diff --git a/lib/internal/Magento/Framework/Api/Uploader.php b/lib/internal/Magento/Framework/Api/Uploader.php index 5cea3a34569a9..3f98b38bc2fdf 100644 --- a/lib/internal/Magento/Framework/Api/Uploader.php +++ b/lib/internal/Magento/Framework/Api/Uploader.php @@ -13,6 +13,8 @@ class Uploader extends \Magento\Framework\File\Uploader { /** * Avoid running the default constructor specific to FILE upload + * + * phpcs:disable Magento2.CodeAnalysis.EmptyBlock */ public function __construct() { @@ -30,6 +32,8 @@ public function processFileAttributes($fileAttributes) $this->_file = $fileAttributes; if (!file_exists($this->_file['tmp_name'])) { $code = empty($this->_file['tmp_name']) ? self::TMP_NAME_EMPTY : 0; + + // phpcs:ignore Magento2.Exceptions.DirectThrow.FoundDirectThrow throw new \Exception('File was not processed correctly.', $code); } else { $this->_fileExists = true; diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php index 6caf2c0f88dfa..fdf524348293b 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php @@ -62,6 +62,11 @@ class DirectoryList extends \Magento\Framework\Filesystem\DirectoryList */ const VAR_EXPORT = 'var_export'; + /** + * Storage of files which were imported. + */ + const VAR_IMPORT = 'var_import'; + /** * Temporary files */ @@ -151,18 +156,18 @@ public static function getDefaultConfig() self::CONFIG => [parent::PATH => 'app/etc'], self::LIB_INTERNAL => [parent::PATH => 'lib/internal'], self::VAR_DIR => [parent::PATH => 'var'], - self::VAR_EXPORT => [parent::PATH => 'var/export'], + self::VAR_EXPORT => [parent::PATH => 'var/export', parent::URL_PATH => 'export'], self::CACHE => [parent::PATH => 'var/cache'], self::LOG => [parent::PATH => 'var/log'], self::DI => [parent::PATH => 'generated/metadata'], self::GENERATION => [parent::PATH => Io::DEFAULT_DIRECTORY], self::SESSION => [parent::PATH => 'var/session'], - self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'pub/media'], - self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'pub/static'], - self::PUB => [parent::PATH => 'pub', parent::URL_PATH => 'pub'], + self::MEDIA => [parent::PATH => 'pub/media', parent::URL_PATH => 'media'], + self::STATIC_VIEW => [parent::PATH => 'pub/static', parent::URL_PATH => 'static'], + self::PUB => [parent::PATH => 'pub', parent::URL_PATH => ''], self::LIB_WEB => [parent::PATH => 'lib/web'], self::TMP => [parent::PATH => 'var/tmp'], - self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'pub/media/upload'], + self::UPLOAD => [parent::PATH => 'pub/media/upload', parent::URL_PATH => 'media/upload'], self::TMP_MATERIALIZATION_DIR => [parent::PATH => 'var/view_preprocessed/pub/static'], self::TEMPLATE_MINIFICATION_DIR => [parent::PATH => 'var/view_preprocessed'], self::SETUP => [parent::PATH => 'setup/src'], @@ -170,6 +175,7 @@ public static function getDefaultConfig() self::GENERATED => [parent::PATH => 'generated'], self::GENERATED_CODE => [parent::PATH => Io::DEFAULT_DIRECTORY], self::GENERATED_METADATA => [parent::PATH => 'generated/metadata'], + self::VAR_IMPORT => [parent::PATH => 'var/import', parent::URL_PATH => 'var/import'], ]; return parent::getDefaultConfig() + $result; } diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php index 5ad3d888ffb57..c756fb43cf584 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryResolver.php @@ -16,6 +16,7 @@ class DirectoryResolver { /** * @var DirectoryList + * @deprecated $this->filesystem->getDirectoryWrite() can be used for getting directory */ private $directoryList; @@ -51,7 +52,7 @@ public function validatePath($path, $directoryConfig = DirectoryList::MEDIA) { $directory = $this->filesystem->getDirectoryWrite($directoryConfig); $realPath = $directory->getDriver()->getRealPathSafety($path); - $root = $this->directoryList->getPath($directoryConfig); + $root = $directory->getAbsolutePath(); return strpos($realPath, $root) === 0; } diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php b/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php index 5549c34fa7701..2763dea8ef1e1 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/Filesystem/DirectoryResolverTest.php @@ -75,8 +75,7 @@ public function testValidatePath(string $path, bool $expectedResult): void ->willReturnArgument(0); $this->filesystem->expects($this->atLeastOnce())->method('getDirectoryWrite')->with($directoryConfig) ->willReturn($directory); - $this->directoryList->expects($this->atLeastOnce())->method('getPath')->with($directoryConfig) - ->willReturn($rootPath); + $directory->expects($this->atLeastOnce())->method('getAbsolutePath')->willReturn($rootPath); $this->assertEquals($expectedResult, $this->directoryResolver->validatePath($path, $directoryConfig)); } diff --git a/lib/internal/Magento/Framework/Console/Cli.php b/lib/internal/Magento/Framework/Console/Cli.php index f22c452549a78..c7192e7dfbb33 100644 --- a/lib/internal/Magento/Framework/Console/Cli.php +++ b/lib/internal/Magento/Framework/Console/Cli.php @@ -174,7 +174,6 @@ private function initObjectManager() { $params = (new ComplexParameter(self::INPUT_KEY_BOOTSTRAP))->mergeFromArgv($_SERVER, $_SERVER); $params[Bootstrap::PARAM_REQUIRE_MAINTENANCE] = null; - $params = $this->documentRootResolver($params); $requestParams = $this->serviceManager->get('magento-init-params'); $appBootstrapKey = Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS; @@ -230,26 +229,4 @@ protected function getVendorCommands($objectManager) return array_merge([], ...$commands); } - - /** - * Provides updated configuration in accordance to document root settings. - * - * @param array $config - * @return array - */ - private function documentRootResolver(array $config = []): array - { - $params = []; - $deploymentConfig = $this->serviceManager->get(DeploymentConfig::class); - if ((bool)$deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB)) { - $params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = [ - DirectoryList::PUB => [DirectoryList::URL_PATH => ''], - DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], - DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], - DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], - ]; - } - - return array_merge_recursive($config, $params); - } } diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index c5e17a97c9f01..5765a3a7fe1b2 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -56,7 +56,7 @@ class Mysql extends \Zend_Db_Adapter_Pdo_Mysql implements AdapterInterface public const DDL_CREATE = 2; public const DDL_INDEX = 3; public const DDL_FOREIGN_KEY = 4; - public const DDL_EXISTS = 5; + private const DDL_EXISTS = 5; public const DDL_CACHE_PREFIX = 'DB_PDO_MYSQL_DDL'; public const DDL_CACHE_TAG = 'DB_PDO_MYSQL_DDL'; @@ -665,11 +665,9 @@ protected function _prepareQuery(&$sql, &$bind = []) } // Mixed bind is not supported - so remember whether it is named bind, to normalize later if required - $isNamedBind = false; if ($bind) { foreach ($bind as $k => $v) { if (!is_int($k)) { - $isNamedBind = true; if ($k[0] != ':') { $bind[":{$k}"] = $v; unset($bind[$k]); diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 6103a7df5bf0d..9b16687620e8f 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -7,7 +7,12 @@ namespace Magento\Framework\Data\Collection; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Collection; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Phrase; /** * Filesystem items collection @@ -126,6 +131,26 @@ class Filesystem extends \Magento\Framework\Data\Collection */ protected $_collectedFiles = []; + /** + * @var WriteInterface + */ + private $rootDirectory; + + /** + * @param EntityFactoryInterface|null $_entityFactory + * @param \Magento\Framework\Filesystem $filesystem + */ + public function __construct( + EntityFactoryInterface $_entityFactory = null, + \Magento\Framework\Filesystem $filesystem = null + ) { + $this->_entityFactory = $_entityFactory ?? ObjectManager::getInstance()->get(EntityFactoryInterface::class); + + $filesystem = $filesystem ?? ObjectManager::getInstance()->get(\Magento\Framework\Filesystem::class); + $this->rootDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + parent::__construct($this->_entityFactory); + } + /** * Allowed dirs mask setter. Set empty to not filter. * @@ -208,9 +233,8 @@ public function setCollectRecursively($value) public function addTargetDir($value) { $value = (string)$value; - if (!is_dir($value)) { - // phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception('Unable to set target directory.'); + if (!$this->rootDirectory->isDirectory($value)) { + throw new FileSystemException(__('Unable to set target directory.')); } $this->_targetDirs[$value] = $value; return $this; @@ -235,6 +259,7 @@ public function setDirsFirst($value) * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @throws FileSystemException */ protected function _collectRecursive($dir) { @@ -243,9 +268,9 @@ protected function _collectRecursive($dir) $dir = [$dir]; } foreach ($dir as $folder) { - if ($nodes = glob($folder . '/*', GLOB_NOSORT)) { + if ($nodes = $this->rootDirectory->search('/*', $folder)) { foreach ($nodes as $node) { - $collectedResult[] = $node; + $collectedResult[] = $this->rootDirectory->getAbsolutePath($node); } } } @@ -254,7 +279,9 @@ protected function _collectRecursive($dir) } foreach ($collectedResult as $item) { - if (is_dir($item) && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item)))) { + if ($this->rootDirectory->isDirectory($item) + && (!$this->_allowedDirsMask || preg_match($this->_allowedDirsMask, basename($item))) + ) { if ($this->_collectDirs) { if ($this->_dirsFirst) { $this->_collectedDirs[] = $item; @@ -265,7 +292,7 @@ protected function _collectRecursive($dir) if ($this->_collectRecursively) { $this->_collectRecursive($item); } - } elseif ($this->_collectFiles && is_file( + } elseif ($this->_collectFiles && $this->rootDirectory->isFile( $item ) && (!$this->_allowedFilesMask || preg_match( $this->_allowedFilesMask, @@ -369,7 +396,7 @@ private function _generateAndFilterAndSort($attributeName) * * @param array $a * @param array $b - * @return int + * @return int|void */ protected function _usort($a, $b) { diff --git a/lib/internal/Magento/Framework/File/Mime.php b/lib/internal/Magento/Framework/File/Mime.php index e0b22e4c944d9..d61f5054990e8 100644 --- a/lib/internal/Magento/Framework/File/Mime.php +++ b/lib/internal/Magento/Framework/File/Mime.php @@ -6,8 +6,16 @@ namespace Magento\Framework\File; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; + /** * Utility for mime type retrieval + * + * @deprecated + * @see Filesystem\ExtendedDriverInterface::getMetadata() */ class Mime { @@ -15,146 +23,102 @@ class Mime * Mime types * * @var array + * + * @deprecated */ protected $mimeTypes = [ - 'txt' => 'text/plain', - 'htm' => 'text/html', + 'txt' => 'text/plain', + 'htm' => 'text/html', 'html' => 'text/html', - 'php' => 'text/html', - 'css' => 'text/css', - 'js' => 'application/javascript', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', 'json' => 'application/json', - 'xml' => 'application/xml', - 'swf' => 'application/x-shockwave-flash', - 'flv' => 'video/x-flv', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', // images - 'png' => 'image/png', - 'jpe' => 'image/jpeg', + 'png' => 'image/png', + 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'gif' => 'image/gif', - 'bmp' => 'image/bmp', - 'ico' => 'image/vnd.microsoft.icon', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', 'tiff' => 'image/tiff', - 'tif' => 'image/tiff', - 'svg' => 'image/svg+xml', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml', // archives - 'zip' => 'application/zip', - 'rar' => 'application/x-rar-compressed', - 'exe' => 'application/x-msdownload', - 'msi' => 'application/x-msdownload', - 'cab' => 'application/vnd.ms-cab-compressed', + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', // audio/video - 'mp3' => 'audio/mpeg', - 'qt' => 'video/quicktime', - 'mov' => 'video/quicktime', + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', // adobe - 'pdf' => 'application/pdf', - 'psd' => 'image/vnd.adobe.photoshop', - 'ai' => 'application/postscript', - 'eps' => 'application/postscript', - 'ps' => 'application/postscript', + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', ]; /** - * List of mime types that can be defined by file extension. - * - * @var array + * @var Filesystem */ - private $defineByExtensionList = [ - 'txt' => 'text/plain', - 'htm' => 'text/html', - 'html' => 'text/html', - 'php' => 'text/html', - 'css' => 'text/css', - 'js' => 'application/javascript', - 'json' => 'application/json', - 'xml' => 'application/xml', - 'svg' => 'image/svg+xml', - ]; + private $filesystem; /** - * List of generic MIME types - * - * The file mime type should be detected by the file's extension if the native mime type is one of the listed below. - * - * @var array + * @param Filesystem|null $filesystem */ - private $genericMimeTypes = [ - 'application/x-empty', - 'inode/x-empty', - ]; + public function __construct(Filesystem $filesystem = null) + { + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); + } /** - * Get mime type of a file + * Get mime type of a file. * * @param string $file * @return string - * @throws \InvalidArgumentException + * @throws FileSystemException + * + * @deprecated */ public function getMimeType($file) { - if (!file_exists($file)) { - throw new \InvalidArgumentException("File '$file' doesn't exist"); + $driver = $this->filesystem->getDirectoryWrite( + DirectoryList::ROOT, + Filesystem\DriverPool::FILE + )->getDriver(); + + /** + * Try with non-local driver. + */ + if (!$driver->isExists($file)) { + $driver = $this->filesystem->getDirectoryWrite( + DirectoryList::ROOT + )->getDriver(); } - $result = null; - $extension = $this->getFileExtension($file); - - if (function_exists('mime_content_type')) { - $result = $this->getNativeMimeType($file); - } else { - $imageInfo = getimagesize($file); - $result = $imageInfo['mime']; + if (!$driver->isExists($file)) { + throw new FileSystemException(__("File '$file' doesn't exist")); } - if (null === $result && isset($this->mimeTypes[$extension])) { - $result = $this->mimeTypes[$extension]; - } elseif (null === $result) { - $result = 'application/octet-stream'; + if ($driver instanceof Filesystem\ExtendedDriverInterface) { + return $driver->getMetadata($file)['mimetype']; } - return $result; - } + $mime = new Filesystem\Driver\File\Mime(); - /** - * Get mime type by the native mime_content_type function. - * - * Search for extended mime type if mime_content_type() returned 'application/octet-stream' or 'text/plain' - * - * @param string $file - * @return string - */ - private function getNativeMimeType(string $file): string - { - $extension = $this->getFileExtension($file); - $result = mime_content_type($file); - if (isset($this->mimeTypes[$extension], $this->defineByExtensionList[$extension]) - && ( - strpos($result, 'text/') === 0 - || strpos($result, 'image/svg') === 0 - || in_array($result, $this->genericMimeTypes, true) - ) - ) { - $result = $this->mimeTypes[$extension]; - } - - return $result; - } - - /** - * Get file extension by file name. - * - * @param string $file - * @return string - */ - private function getFileExtension(string $file): string - { - return strtolower(pathinfo($file, PATHINFO_EXTENSION)); + return $mime->getMimeType($file); } } diff --git a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php index 7a54a7966b500..db42e03363236 100644 --- a/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php +++ b/lib/internal/Magento/Framework/File/Test/Unit/MimeTest.php @@ -7,11 +7,16 @@ namespace Magento\Framework\File\Test\Unit; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test mime type utility for correct + * Test mime type utility for correct. + * + * @deprecated */ class MimeTest extends TestCase { @@ -20,18 +25,65 @@ class MimeTest extends TestCase */ private $object; + /** + * @var Filesystem\DriverInterface|MockObject + */ + private $localDriverMock; + + /** + * @var Filesystem\DriverInterface|MockObject + */ + private $remoteDriverMock; + + /** + * @var Filesystem|MockObject + */ + private $filesystemMock; + + /** + * @var Filesystem\Directory\WriteInterface|MockObject + */ + private $localDirectoryMock; + + /** + * @var Filesystem\Directory\WriteInterface|MockObject + */ + private $remoteDirectoryMock; + /** * @inheritDoc */ protected function setUp(): void { - $this->object = new Mime(); + $this->localDriverMock = $this->getMockForAbstractClass(Filesystem\DriverInterface::class); + $this->remoteDriverMock = $this->getMockForAbstractClass(Filesystem\ExtendedDriverInterface::class); + + $this->localDirectoryMock = $this->getMockForAbstractClass(Filesystem\Directory\WriteInterface::class); + $this->localDirectoryMock->method('getDriver') + ->willReturn($this->localDriverMock); + $this->remoteDirectoryMock = $this->getMockForAbstractClass(Filesystem\Directory\WriteInterface::class); + $this->remoteDirectoryMock->method('getDriver') + ->willReturn($this->remoteDriverMock); + + /** @var Filesystem|MockObject $filesystem */ + $this->filesystemMock = $this->createMock(Filesystem::class); + + $this->object = new Mime($this->filesystemMock); } - public function testGetMimeTypeNonexistentFileException() + public function testGetMimeTypeNonexistentFileException(): void { - $this->expectException('InvalidArgumentException'); + $this->expectException(FileSystemException::class); $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); + + $this->filesystemMock->method('getDirectoryWrite')->willReturn( + $this->localDirectoryMock + ); + $this->localDriverMock->expects(self::exactly(2)) + ->method('isExists') + ->with('nonexistent.file') + ->willReturn(true); + $file = 'nonexistent.file'; $this->object->getMimeType($file); } @@ -42,10 +94,18 @@ public function testGetMimeTypeNonexistentFileException() * * @dataProvider getMimeTypeDataProvider */ - public function testGetMimeType($file, $expectedType) + public function testGetMimeType($file, $expectedType): void { + $this->filesystemMock->method('getDirectoryWrite')->willReturn( + $this->localDirectoryMock + ); + $this->localDriverMock->expects(self::exactly(2)) + ->method('isExists') + ->with($file) + ->willReturn(true); + $actualType = $this->object->getMimeType($file); - $this->assertSame($expectedType, $actualType); + self::assertSame($expectedType, $actualType); } /** @@ -61,4 +121,29 @@ public function getMimeTypeDataProvider(): array 'tmp file mime type' => [__DIR__ . '/_files/magento', 'image/jpeg'], ]; } + + /** + * @param string $file + * @param string $expectedType + * + * @dataProvider getMimeTypeDataProvider + */ + public function testGetMimeTypeRemote($file, $expectedType): void + { + $this->filesystemMock->method('getDirectoryWrite')->willReturnOnConsecutiveCalls( + $this->localDirectoryMock, + $this->remoteDirectoryMock + ); + $this->localDriverMock->method('isExists') + ->willReturn(false); + $this->remoteDriverMock->expects(self::once()) + ->method('isExists') + ->with($file) + ->willReturn(true); + $this->remoteDriverMock->method('getMetadata') + ->willReturn(['mimetype' => $expectedType]); + + $actualType = $this->object->getMimeType($file); + self::assertSame($expectedType, $actualType); + } } diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index f707ec6dd5461..c3246bfbf1e48 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -8,6 +8,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\TargetDirectory; use Magento\Framework\Filesystem\DriverInterface; use Magento\Framework\Filesystem\DriverPool; use Magento\Framework\Validation\ValidationException; @@ -19,6 +21,7 @@ * validation by protected file extension list to extended class * * @SuppressWarnings(PHPMD.TooManyFields) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api * @since 100.0.2 @@ -43,8 +46,7 @@ class Uploader /** * Upload type. Used to right handle $_FILES array. - * - * @var \Magento\Framework\File\Uploader::SINGLE_STYLE|\Magento\Framework\File\Uploader::MULTIPLE_STYLE + * @var Uploader::SINGLE_STYLE|\Magento\Framework\File\Uploader::MULTIPLE_STYLE * @access protected */ protected $_uploadType; @@ -180,6 +182,11 @@ class Uploader */ private $fileDriver; + /** + * @var TargetDirectory + */ + private $targetDirectory; + /** * Init upload * @@ -187,15 +194,17 @@ class Uploader * @param \Magento\Framework\File\Mime|null $fileMime * @param DirectoryList|null $directoryList * @param DriverPool|null $driverPool + * @param TargetDirectory|null $targetDirectory * @throws \DomainException */ public function __construct( $fileId, Mime $fileMime = null, DirectoryList $directoryList = null, - DriverPool $driverPool = null + DriverPool $driverPool = null, + TargetDirectory $targetDirectory = null ) { - $this->directoryList= $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); + $this->directoryList = $directoryList ?: ObjectManager::getInstance()->get(DirectoryList::class); $this->_setUploadFileId($fileId); if (!file_exists($this->_file['tmp_name'])) { @@ -205,7 +214,8 @@ public function __construct( $this->_fileExists = true; } $this->fileMime = $fileMime ?: ObjectManager::getInstance()->get(Mime::class); - $this->driverPool = $driverPool; + $this->driverPool = $driverPool ?: ObjectManager::getInstance()->get(DriverPool::class); + $this->targetDirectory = $targetDirectory ?: ObjectManager::getInstance()->get(TargetDirectory::class); } /** @@ -292,7 +302,10 @@ private function validateDestination(string $destinationFolder): void { if ($this->_allowCreateFolders) { $this->createDestinationFolder($destinationFolder); - } elseif (!$this->getFileDriver()->isWritable($destinationFolder)) { + } elseif (!$this->getTargetDirectory() + ->getDirectoryWrite(DirectoryList::ROOT) + ->isWritable($destinationFolder) + ) { throw new FileSystemException(__('Destination folder is not writable or does not exists.')); } } @@ -315,15 +328,53 @@ protected function chmod($file) * * @param string $tmpPath * @param string $destPath - * @return bool|void + * @return bool + * @throws FileSystemException */ protected function _moveFile($tmpPath, $destPath) { - if (is_uploaded_file($tmpPath)) { - return move_uploaded_file($tmpPath, $destPath); - } elseif (is_file($tmpPath)) { - return rename($tmpPath, $destPath); + $rootCode = DirectoryList::PUB; + + if (strpos($destPath, $this->getDirectoryList()->getPath($rootCode)) !== 0) { + $rootCode = DirectoryList::ROOT; } + + $destPath = str_replace($this->getDirectoryList()->getPath($rootCode), '', $destPath); + $directory = $this->getTargetDirectory()->getDirectoryWrite($rootCode); + + return $this->getFileDriver()->rename( + $tmpPath, + $directory->getAbsolutePath($destPath), + $directory->getDriver() + ); + } + + /** + * Retrieves target directory. + * + * @return TargetDirectory + */ + private function getTargetDirectory(): TargetDirectory + { + if (!isset($this->targetDirectory)) { + $this->targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); + } + + return $this->targetDirectory; + } + + /** + * Retrieves directory list. + * + * @return DirectoryList + */ + private function getDirectoryList(): DirectoryList + { + if (!isset($this->directoryList)) { + $this->directoryList = ObjectManager::getInstance()->get(DirectoryList::class); + } + + return $this->directoryList; } /** @@ -667,7 +718,7 @@ private function validateFileId(array $fileId): void * Create destination folder * * @param string $destinationFolder - * @return \Magento\Framework\File\Uploader + * @return Uploader * @throws FileSystemException */ private function createDestinationFolder(string $destinationFolder) @@ -680,8 +731,10 @@ private function createDestinationFolder(string $destinationFolder) $destinationFolder = substr($destinationFolder, 0, -1); } - if (!$this->getFileDriver()->isDirectory($destinationFolder)) { - $result = $this->getFileDriver()->createDirectory($destinationFolder); + $rootDirectory = $this->getTargetDirectory()->getDirectoryWrite(DirectoryList::ROOT); + + if (!$rootDirectory->isDirectory($destinationFolder)) { + $result = $rootDirectory->getDriver()->createDirectory($destinationFolder); if (!$result) { throw new FileSystemException(__('Unable to create directory %1.', $destinationFolder)); } @@ -698,20 +751,24 @@ private function createDestinationFolder(string $destinationFolder) */ public static function getNewFileName($destinationFile) { + /** @var Filesystem $fileSystem */ + $fileSystem = ObjectManager::getInstance()->get(Filesystem::class); + $local = $fileSystem->getDirectoryRead(DirectoryList::ROOT); + /** @var TargetDirectory $targetDirectory */ + $targetDirectory = ObjectManager::getInstance()->get(TargetDirectory::class); + $remote = $targetDirectory->getDirectoryRead(DirectoryList::ROOT); + + $fileExists = function ($path) use ($local, $remote) { + return $local->isExist($path) || $remote->isExist($path); + }; + $fileInfo = pathinfo($destinationFile); - if (file_exists($destinationFile)) { - $index = 1; - $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension']; - while (file_exists($fileInfo['dirname'] . '/' . $baseName)) { - $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension']; - $index++; - } - $destFileName = $baseName; - } else { - return $fileInfo['basename']; + $index = 1; + while ($fileExists($fileInfo['dirname'] . '/' . $fileInfo['basename'])) { + $fileInfo['basename'] = $fileInfo['filename'] . '_' . $index++ . '.' . $fileInfo['extension']; } - return $destFileName; + return $fileInfo['basename']; } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php index 25a290455dc46..33b025389945a 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadFactory.php @@ -7,6 +7,9 @@ use Magento\Framework\Filesystem\DriverPool; +/** + * The factory of the filesystem directory instances for read operations. + */ class ReadFactory { /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php b/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php new file mode 100644 index 0000000000000..836eb680c24f7 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Directory/TargetDirectory.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Directory; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; + +/** + * A target directory for remote filesystems. + */ +class TargetDirectory +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $driverCode; + + /** + * @param Filesystem $filesystem + * @param string $driverCode + */ + public function __construct(Filesystem $filesystem, $driverCode = Filesystem\DriverPool::FILE) + { + $this->filesystem = $filesystem; + $this->driverCode = $driverCode; + } + + /** + * Create an instance of directory with write permissions. + * + * @param string $directoryCode + * @return WriteInterface + * @throws FileSystemException + */ + public function getDirectoryWrite(string $directoryCode): WriteInterface + { + return $this->filesystem->getDirectoryWrite($directoryCode, $this->driverCode); + } + + /** + * Create an instance of directory with read permissions. + * + * @param string $directoryCode + * @return ReadInterface + */ + public function getDirectoryRead(string $directoryCode): ReadInterface + { + return $this->filesystem->getDirectoryRead($directoryCode, $this->driverCode); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php index 1d60b7ce879bf..4d4bb3b6c7f5e 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Write.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Write.php @@ -116,7 +116,7 @@ public function renameFile($path, $newPath, WriteInterface $targetDirectory = nu } $absolutePath = $this->driver->getAbsolutePath($this->path, $path); $absoluteNewPath = $targetDirectory->getAbsolutePath($newPath); - return $this->driver->rename($absolutePath, $absoluteNewPath, $targetDirectory->driver); + return $this->driver->rename($absolutePath, $absoluteNewPath, $targetDirectory->getDriver()); } /** @@ -349,7 +349,11 @@ public function openFile($path, $mode = 'w') */ public function writeFile($path, $content, $mode = 'w+') { - return $this->openFile($path, $mode)->write($content); + $file = $this->openFile($path, $mode); + $result = $file->write($content); + $file->close(); + + return $result; } /** diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php index ff14b12f62047..1cb642c5c3a2a 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteFactory.php @@ -7,6 +7,9 @@ use Magento\Framework\Filesystem\DriverPool; +/** + * The factory of the filesystem directory instances for write operations. + */ class WriteFactory { /** diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index 1affad5521372..bc08f67228849 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -5,7 +5,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\Framework\Filesystem\Driver; use Magento\Framework\Exception\FileSystemException; @@ -257,7 +256,7 @@ public function readDirectory($path) $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - + $iterator = new \FilesystemIterator($path, $flags); $result = []; /** @var \FilesystemIterator $file */ @@ -300,12 +299,13 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) { $result = false; $targetDriver = $targetDriver ?: $this; - if (get_class($targetDriver) == get_class($this)) { + if (get_class($targetDriver) === get_class($this)) { $result = @rename($this->getScheme() . $oldPath, $newPath); + $this->changePermissions($newPath, 0777 & ~umask()); } else { $content = $this->fileGetContents($oldPath); if (false !== $targetDriver->filePutContents($newPath, $content)) { - $result = $this->deleteFile($newPath); + $result = $this->isFile($oldPath) ? $this->deleteFile($oldPath) : true; } } if (!$result) { @@ -331,7 +331,7 @@ public function rename($oldPath, $newPath, DriverInterface $targetDriver = null) public function copy($source, $destination, DriverInterface $targetDriver = null) { $targetDriver = $targetDriver ?: $this; - if (get_class($targetDriver) == get_class($this)) { + if (get_class($targetDriver) === get_class($this)) { $result = @copy($this->getScheme() . $source, $destination); } else { $content = $this->fileGetContents($source); @@ -952,7 +952,7 @@ public function readDirectoryRecursively($path = null) $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; - + try { $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($path, $flags), diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php b/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php new file mode 100644 index 0000000000000..4634e3dd1018d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File/Mime.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Driver\File; + +use Magento\Framework\Exception\FileSystemException; + +/** + * Mime type resolver. + */ +class Mime +{ + /** + * Mime types + * + * @var array + */ + private $mimeTypes = [ + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'swf' => 'application/x-shockwave-flash', + 'flv' => 'video/x-flv', + + // images + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + + // archives + 'zip' => 'application/zip', + 'rar' => 'application/x-rar-compressed', + 'exe' => 'application/x-msdownload', + 'msi' => 'application/x-msdownload', + 'cab' => 'application/vnd.ms-cab-compressed', + + // audio/video + 'mp3' => 'audio/mpeg', + 'qt' => 'video/quicktime', + 'mov' => 'video/quicktime', + + // adobe + 'pdf' => 'application/pdf', + 'psd' => 'image/vnd.adobe.photoshop', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + ]; + + /** + * List of mime types that can be defined by file extension. + * + * @var array + */ + private $defineByExtensionList = [ + 'txt' => 'text/plain', + 'htm' => 'text/html', + 'html' => 'text/html', + 'php' => 'text/html', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'xml' => 'application/xml', + 'svg' => 'image/svg+xml', + ]; + + /** + * List of generic MIME types + * + * The file mime type should be detected by the file's extension if the native mime type is one of the listed below. + * + * @var array + */ + private $genericMimeTypes = [ + 'application/x-empty', + 'inode/x-empty', + ]; + + /** + * Get mime type of a file + * + * @param string $path Absolute file path + * @return string + * @throws FileSystemException + */ + public function getMimeType(string $path): string + { + if (!file_exists($path)) { + throw new FileSystemException(__("File '$path' doesn't exist")); + } + + $result = null; + $extension = $this->getFileExtension($path); + + if (function_exists('mime_content_type')) { + $result = $this->getNativeMimeType($path); + } else { + $imageInfo = getimagesize($path); + $result = $imageInfo['mime']; + } + + if (null === $result && isset($this->mimeTypes[$extension])) { + $result = $this->mimeTypes[$extension]; + } elseif (null === $result) { + $result = 'application/octet-stream'; + } + + return $result; + } + + /** + * Get mime type by the native mime_content_type function. + * + * Search for extended mime type if mime_content_type() returned 'application/octet-stream' or 'text/plain' + * + * @param string $file + * @return string + */ + private function getNativeMimeType(string $file): string + { + $extension = $this->getFileExtension($file); + $result = mime_content_type($file); + if (isset($this->mimeTypes[$extension], $this->defineByExtensionList[$extension]) + && ( + strpos($result, 'text/') === 0 + || strpos($result, 'image/svg') === 0 + || in_array($result, $this->genericMimeTypes, true) + ) + ) { + $result = $this->mimeTypes[$extension]; + } + + return $result; + } + + /** + * Get file extension by file name. + * + * @param string $path + * @return string + */ + private function getFileExtension(string $path): string + { + return strtolower(pathinfo($path, PATHINFO_EXTENSION)); + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php new file mode 100644 index 0000000000000..d99b285be0f67 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/DriverPoolInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem; + +/** + * A pool of stream wrappers. + */ +interface DriverPoolInterface +{ + /** + * Gets a driver instance by code + * + * @param string $code + * @return DriverInterface + */ + public function getDriver($code): DriverInterface; +} diff --git a/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php new file mode 100644 index 0000000000000..c2643d7c54e79 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/ExtendedDriverInterface.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem; + +use Magento\Framework\Exception\FileSystemException; + +/** + * Provides extension for Driver interface. + * + * @see DriverInterface + * + * @deprecated Method will be moved to DriverInterface + * @see DriverInterface + */ +interface ExtendedDriverInterface extends DriverInterface +{ + /** + * Retrieve file metadata. + * + * Implementation must return associative array with next keys: + * + * ``` + * [ + * 'path', + * 'dirname', + * 'basename', + * 'extension', + * 'filename', + * 'timestamp', + * 'size', + * 'mimetype', + * 'extra' => [ + * 'image-width', + * 'image-height' + * ] + * ]; + * + * @param string $path Absolute path to file + * @return array + * @throws FileSystemException + * + * @deprecated Method will be moved to DriverInterface + */ + public function getMetadata(string $path): array; +} diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php new file mode 100644 index 0000000000000..4e34d497d86af --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/MimeTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Filesystem\Test\Unit\Driver\File; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\Driver\File\Mime; +use PHPUnit\Framework\TestCase; + +/** + * @see Mime + */ +class MimeTest extends TestCase +{ + /** + * @var Mime + */ + private $mime; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->mime = new Mime(); + } + + public function testGetMimeTypeNonexistentFileException(): void + { + $this->expectException(FileSystemException::class); + $this->expectExceptionMessage('File \'nonexistent.file\' doesn\'t exist'); + + $file = 'nonexistent.file'; + $this->mime->getMimeType($file); + } + + /** + * @param string $file + * @param string $expectedType + * @throws FileSystemException + * + * @dataProvider getMimeTypeDataProvider + */ + public function testGetMimeType(string $file, string $expectedType): void + { + $actualType = $this->mime->getMimeType($file); + self::assertSame($expectedType, $actualType); + } + + /** + * @return array + */ + public function getMimeTypeDataProvider(): array + { + return [ + 'javascript' => [__DIR__ . '/_files/javascript.js', 'application/javascript'], + 'weird extension' => [__DIR__ . '/_files/file.weird', 'application/octet-stream'], + 'weird uppercase extension' => [__DIR__ . '/_files/UPPERCASE.WEIRD', 'application/octet-stream'], + 'generic mime type' => [__DIR__ . '/_files/blank.html', 'text/html'], + 'tmp file mime type' => [__DIR__ . '/_files/magento', 'image/jpeg'], + ]; + } +} diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD new file mode 100644 index 0000000000000..b361f47e9c25d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/UPPERCASE.WEIRD @@ -0,0 +1 @@ +� diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html new file mode 100644 index 0000000000000..2b699a9062611 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/blank.html @@ -0,0 +1,6 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird new file mode 100644 index 0000000000000..b361f47e9c25d --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/file.weird @@ -0,0 +1 @@ +� diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js new file mode 100644 index 0000000000000..d168db0daa734 --- /dev/null +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/javascript.js @@ -0,0 +1,5 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +var test = 10; diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/File/_files/magento differ diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php index 13d6e7b72d89f..fab7eb93aabf8 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Request.php @@ -5,6 +5,7 @@ */ namespace Magento\Framework\HTTP\PhpEnvironment; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Stdlib\Cookie\CookieReaderInterface; use Magento\Framework\Stdlib\StringUtils; use Laminas\Http\Header\HeaderInterface; @@ -794,7 +795,7 @@ public function setRequestUri($requestUri = null) public function getBaseUrl() { $url = urldecode(parent::getBaseUrl()); - $url = str_replace('\\', '/', $url); + $url = str_replace(['\\', '/' . DirectoryList::PUB .'/'], '/', $url); return $url; } diff --git a/lib/internal/Magento/Framework/Image.php b/lib/internal/Magento/Framework/Image.php index b3867c0197b79..5b49e9f303ca0 100644 --- a/lib/internal/Magento/Framework/Image.php +++ b/lib/internal/Magento/Framework/Image.php @@ -48,10 +48,6 @@ public function open() { $this->_adapter->checkDependencies(); - if (!file_exists($this->_fileName)) { - throw new \Exception("File '{$this->_fileName}' does not exist."); - } - $this->_adapter->open($this->_fileName); } @@ -94,7 +90,7 @@ public function rotate($angle) /** * Crop an image. * - * @param int $top Default value is 0 + * @param int $top Default value is 0 * @param int $left Default value is 0 * @param int $right Default value is 0 * @param int $bottom Default value is 0 @@ -204,9 +200,6 @@ public function watermark( $watermarkImageOpacity = 30, $repeat = false ) { - if (!file_exists($watermarkImage)) { - throw new \Exception("Required file '{$watermarkImage}' does not exists."); - } $this->_adapter->watermark($watermarkImage, $positionX, $positionY, $watermarkImageOpacity, $repeat); } @@ -238,7 +231,7 @@ public function getImageType() * @access public * @return void */ - public function process() + public function process() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } @@ -248,7 +241,7 @@ public function process() * @access public * @return void */ - public function instruction() + public function instruction() //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock { } diff --git a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php index 8b983809e643f..dc34fb3fcddc9 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php +++ b/lib/internal/Magento/Framework/Image/Adapter/AbstractAdapter.php @@ -675,12 +675,10 @@ protected function _prepareDestination($destination = null, $newName = null) { if (empty($destination)) { $destination = $this->_fileSrcPath; - } else { - if (empty($newName)) { - $info = pathinfo((string) $destination); - $newName = $info['basename']; - $destination = $info['dirname']; - } + } elseif (empty($newName)) { + $info = pathinfo((string) $destination); + $newName = $info['basename']; + $destination = $info['dirname']; } if (empty($newName)) { @@ -751,4 +749,24 @@ public function validateUploadFile($filePath) return $this->getImageType() !== null; } + + /** + * Get file source path + * + * @return string + */ + public function getFileSrcPath(): string + { + return $this->_fileSrcPath ?? ''; + } + + /** + * Get file source name + * + * @return string + */ + public function getFileSrcName(): string + { + return $this->_fileSrcName ?? ''; + } } diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index c37cb89c30587..bebf64c56596a 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -6,6 +6,9 @@ namespace Magento\Framework\Image\Adapter; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Phrase; + /** * Gd2 adapter. * @@ -56,10 +59,15 @@ protected function _reset() * * @param string $filename * @return void - * @throws \OverflowException + * @throws \OverflowException|FileSystemException */ public function open($filename) { + if (!file_exists($filename)) { + throw new FileSystemException( + new Phrase('File "%1" does not exist.', [$this->_fileName]) + ); + } if (!$filename || filesize($filename) === 0 || !$this->validateURLScheme($filename)) { throw new \InvalidArgumentException('Wrong file'); } @@ -436,8 +444,6 @@ public function rotate($angle) * @param int $opacity * @param bool $tile * @return void - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -452,53 +458,42 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $merged = false; + $watermark = $this->createWatermarkBasedOnPosition($watermark, $positionX, $positionY, $merged, $tile); + + imagedestroy($watermark); + $this->refreshImageDimensions(); + } + + /** + * Create watermark based on it's image position. + * + * @param resource $watermark + * @param int $positionX + * @param int $positionY + * @param bool $merged + * @param bool $tile + * @return false|resource + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + private function createWatermarkBasedOnPosition( + $watermark, + int $positionX, + int $positionY, + bool $merged, + bool $tile + ) { if ($this->getWatermarkWidth() && $this->getWatermarkHeight() && $this->getWatermarkPosition() != self::POSITION_STRETCH ) { - $newWatermark = imagecreatetruecolor($this->getWatermarkWidth(), $this->getWatermarkHeight()); - imagealphablending($newWatermark, false); - $col = imagecolorallocate($newWatermark, 255, 255, 255); - imagecolortransparent($newWatermark, $col); - imagefilledrectangle($newWatermark, 0, 0, $this->getWatermarkWidth(), $this->getWatermarkHeight(), $col); - imagesavealpha($newWatermark, true); - imagecopyresampled( - $newWatermark, - $watermark, - 0, - 0, - 0, - 0, - $this->getWatermarkWidth(), - $this->getWatermarkHeight(), - imagesx($watermark), - imagesy($watermark) - ); - $watermark = $newWatermark; + $watermark = $this->createWaterMark($watermark, $this->getWatermarkWidth(), $this->getWatermarkHeight()); } if ($this->getWatermarkPosition() == self::POSITION_TILE) { $tile = true; } elseif ($this->getWatermarkPosition() == self::POSITION_STRETCH) { - $newWatermark = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight); - imagealphablending($newWatermark, false); - $col = imagecolorallocate($newWatermark, 255, 255, 255); - imagecolortransparent($newWatermark, $col); - imagefilledrectangle($newWatermark, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight, $col); - imagesavealpha($newWatermark, true); - imagecopyresampled( - $newWatermark, - $watermark, - 0, - 0, - 0, - 0, - $this->_imageSrcWidth, - $this->_imageSrcHeight, - imagesx($watermark), - imagesy($watermark) - ); - $watermark = $newWatermark; + $watermark = $this->createWaterMark($watermark, $this->_imageSrcWidth, $this->_imageSrcHeight); } elseif ($this->getWatermarkPosition() == self::POSITION_CENTER) { $positionX = $this->_imageSrcWidth / 2 - imagesx($watermark) / 2; $positionY = $this->_imageSrcHeight / 2 - imagesy($watermark) / 2; @@ -602,8 +597,39 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = } } - imagedestroy($watermark); - $this->refreshImageDimensions(); + return $watermark; + } + + /** + * Create watermark. + * + * @param resource $watermark + * @param string $width + * @param string $height + * @return false|resource + */ + private function createWaterMark($watermark, string $width, string $height) + { + $newWatermark = imagecreatetruecolor($width, $height); + imagealphablending($newWatermark, false); + $col = imagecolorallocate($newWatermark, 255, 255, 255); + imagecolortransparent($newWatermark, $col); + imagefilledrectangle($newWatermark, 0, 0, $width, $height, $col); + imagesavealpha($newWatermark, true); + imagecopyresampled( + $newWatermark, + $watermark, + 0, + 0, + 0, + 0, + $width, + $height, + imagesx($watermark), + imagesy($watermark) + ); + + return $newWatermark; } /** diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 7e36cdb334eb2..31793d281ac52 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -6,7 +6,9 @@ namespace Magento\Framework\Image\Adapter; +use Magento\Framework\Exception\FileSystemException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Phrase; /** * Image adapter from ImageMagick. @@ -88,6 +90,11 @@ public function backgroundColor($color = null) */ public function open($filename) { + if (!file_exists($filename)) { + throw new FileSystemException( + new Phrase('File "%1" does not exist.', [$this->_fileName]) + ); + } if (!empty($filename) && !$this->validateURLScheme($filename)) { throw new \InvalidArgumentException('Wrong file'); } diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php index f21101f099200..355a221c5d368 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -80,7 +80,7 @@ public function watermarkDataProvider(): array { return [ ['', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], - [__DIR__ . '/not_exists', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], + ['not_exist', ImageMagick::ERROR_WATERMARK_IMAGE_ABSENT], [ __DIR__ . '/_files/invalid_image.jpg', ImageMagick::ERROR_WRONG_IMAGE @@ -105,6 +105,8 @@ public function testSaveWithException() */ public function testOpenInvalidUrl() { + require_once __DIR__ . '/_files/global_php_mock.php'; + $this->expectException(\InvalidArgumentException::class); $this->imageMagic->open('bar://foo.bar'); diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php index a62909b495ab4..034e9c32c6954 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/_files/global_php_mock.php @@ -48,6 +48,16 @@ function filesize($file) return Gd2Test::$imageSize; } +/** + * @param $file + * @return bool + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +function file_exists($file) +{ + return !($file === 'not_exist'); +} + /** * @param $real * @return int diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php index c84f3bfb0f52c..46ea82b887db6 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Topology/Config/QueueConfigItem/DataMapperTest.php @@ -1,13 +1,16 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Framework\MessageQueue\Test\Unit\Topology\Config\QueueConfigItem; use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\MessageQueue\Rpc\ResponseQueueNameBuilder; use Magento\Framework\MessageQueue\Topology\Config\Data; use Magento\Framework\MessageQueue\Topology\Config\QueueConfigItem\DataMapper; @@ -17,34 +20,46 @@ class DataMapperTest extends TestCase { /** - * @var MockObject + * @var Data|MockObject */ - private $configData; + private $configDataMock; /** - * @var MockObject + * @var CommunicationConfig|MockObject */ - private $communicationConfig; + private $communicationConfigMock; /** - * @var MockObject + * @var ResponseQueueNameBuilder|MockObject */ - private $queueNameBuilder; + private $queueNameBuilderMock; /** * @var DataMapper */ private $model; + /** + * @return void + */ protected function setUp(): void { - $this->configData = $this->createMock(Data::class); - $this->communicationConfig = $this->createMock(CommunicationConfig::class); - $this->queueNameBuilder = $this->createMock(ResponseQueueNameBuilder::class); - $this->model = new DataMapper($this->configData, $this->communicationConfig, $this->queueNameBuilder); + $this->configDataMock = $this->createMock(Data::class); + $this->communicationConfigMock = $this->createMock(CommunicationConfig::class); + $this->queueNameBuilderMock = $this->createMock(ResponseQueueNameBuilder::class); + $this->model = new DataMapper( + $this->configDataMock, + $this->communicationConfigMock, + $this->queueNameBuilderMock + ); } - public function testGetMappedData() + /** + * @return void + * + * @throws LocalizedException + */ + public function testGetMappedData(): void { $data = [ 'ex01' => [ @@ -100,9 +115,11 @@ public function testGetMappedData() ['topic02', ['name' => 'topic02', 'is_synchronous' => false]], ]; - $this->communicationConfig->expects($this->exactly(2))->method('getTopic')->willReturnMap($communicationMap); - $this->configData->expects($this->once())->method('get')->willReturn($data); - $this->queueNameBuilder->expects($this->once()) + $this->communicationConfigMock->expects($this->exactly(2)) + ->method('getTopic') + ->willReturnMap($communicationMap); + $this->configDataMock->expects($this->once())->method('get')->willReturn($data); + $this->queueNameBuilderMock->expects($this->once()) ->method('getQueueName') ->with('topic01') ->willReturn('responseQueue.topic01'); @@ -114,23 +131,27 @@ public function testGetMappedData() 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'some.queue--amqp' => [ 'name' => 'some.queue', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], ]; $this->assertEquals($expectedResult, $actualResult); } /** + * @return void + * + * @throws LocalizedException + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function testGetMappedDataForWildcard() + public function testGetMappedDataForWildcard(): void { $data = [ 'ex01' => [ @@ -200,13 +221,15 @@ public function testGetMappedDataForWildcard() 'topic08.part2.some.test' => ['name' => 'topic08.part2.some.test', 'is_synchronous' => true], ]; - $this->communicationConfig->expects($this->once()) + $this->communicationConfigMock->expects($this->once()) ->method('getTopic') ->with('topic01') ->willReturn(['name' => 'topic01', 'is_synchronous' => true]); - $this->communicationConfig->expects($this->any())->method('getTopics')->willReturn($communicationData); - $this->configData->expects($this->once())->method('get')->willReturn($data); - $this->queueNameBuilder->expects($this->any()) + $this->communicationConfigMock->expects($this->any()) + ->method('getTopics') + ->willReturn($communicationData); + $this->configDataMock->expects($this->once())->method('get')->willReturn($data); + $this->queueNameBuilderMock->expects($this->any()) ->method('getQueueName') ->willReturnCallback(function ($value) { return 'responseQueue.' . $value; @@ -219,49 +242,49 @@ public function testGetMappedDataForWildcard() 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'some.queue--amqp' => [ 'name' => 'some.queue', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic02--amqp' => [ 'name' => 'responseQueue.topic02', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic03--amqp' => [ 'name' => 'responseQueue.topic03', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic04.04.04--amqp' => [ 'name' => 'responseQueue.topic04.04.04', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic05.05--amqp' => [ 'name' => 'responseQueue.topic05.05', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ], 'responseQueue.topic08.part2.some.test--amqp' => [ 'name' => 'responseQueue.topic08.part2.some.test', 'connection' => 'amqp', 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => ['some' => 'arguments'], ] ]; $this->assertEquals($expectedResult, $actualResult); diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php index 7e8d35fb0940f..912aa4a6b0fb1 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/Config/QueueConfigItem/DataMapper.php @@ -1,14 +1,18 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Framework\MessageQueue\Topology\Config\QueueConfigItem; -use Magento\Framework\MessageQueue\Topology\Config\Data; use Magento\Framework\Communication\ConfigInterface as CommunicationConfig; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\MessageQueue\Rpc\ResponseQueueNameBuilder; +use Magento\Framework\MessageQueue\Topology\Config\Data; use Magento\Framework\Phrase; /** @@ -59,21 +63,29 @@ public function __construct( * Get mapped config data. * * @return array + * @throws LocalizedException */ - public function getMappedData() + public function getMappedData(): array { if (null === $this->mappedData) { - $this->mappedData = []; + $mappedData = []; foreach ($this->configData->get() as $exchange) { $connection = $exchange['connection']; foreach ($exchange['bindings'] as $binding) { if ($binding['destinationType'] === 'queue') { - $queueItems = $this->createQueueItems($binding['destination'], $binding['topic'], $connection); - $this->mappedData = array_merge($this->mappedData, $queueItems); + $queueItems = $this->createQueueItems( + (string)$binding['destination'], + (string)$binding['topic'], + (array)$binding['arguments'], + (string)$connection + ); + $mappedData[] = $queueItems; } } } + $this->mappedData = array_merge([], ...$mappedData); } + return $this->mappedData; } @@ -82,10 +94,12 @@ public function getMappedData() * * @param string $name * @param string $topic + * @param array $arguments * @param string $connection * @return array + * @throws LocalizedException */ - private function createQueueItems($name, $topic, $connection) + private function createQueueItems(string $name, string $topic, array $arguments, string $connection): array { $output = []; $synchronousTopics = []; @@ -103,7 +117,7 @@ private function createQueueItems($name, $topic, $connection) 'connection' => $connection, 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => $arguments, ]; } @@ -112,8 +126,9 @@ private function createQueueItems($name, $topic, $connection) 'connection' => $connection, 'durable' => true, 'autoDelete' => false, - 'arguments' => [], + 'arguments' => $arguments, ]; + return $output; } @@ -124,15 +139,14 @@ private function createQueueItems($name, $topic, $connection) * @return bool * @throws LocalizedException */ - private function isSynchronousTopic($topicName) + private function isSynchronousTopic(string $topicName): bool { try { $topic = $this->communicationConfig->getTopic($topicName); - $isSync = (bool)$topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; - } catch (LocalizedException $e) { + return (bool)$topic[CommunicationConfig::TOPIC_IS_SYNCHRONOUS]; + } catch (LocalizedException $exception) { throw new LocalizedException(new Phrase('Error while checking if topic is synchronous')); } - return $isSync; } /** @@ -141,7 +155,7 @@ private function isSynchronousTopic($topicName) * @param string $wildcard * @return array */ - private function matchSynchronousTopics($wildcard) + private function matchSynchronousTopics(string $wildcard): array { $topicDefinitions = array_filter( $this->communicationConfig->getTopics(), @@ -152,11 +166,13 @@ function ($item) { $topics = []; $pattern = $this->buildWildcardPattern($wildcard); + foreach (array_keys($topicDefinitions) as $topicName) { if (preg_match($pattern, $topicName)) { $topics[$topicName] = $topicName; } } + return $topics; } @@ -166,11 +182,10 @@ function ($item) { * @param string $wildcardKey * @return string */ - private function buildWildcardPattern($wildcardKey) + private function buildWildcardPattern(string $wildcardKey): string { $pattern = '/^' . str_replace('.', '\.', $wildcardKey); - $pattern = str_replace('#', '.+', $pattern); - $pattern = str_replace('*', '[^\.]+', $pattern); + $pattern = str_replace(['#', '*'], ['.+', '[^\.]+'], $pattern); $pattern .= strpos($wildcardKey, '#') === strlen($wildcardKey) ? '/' : '$/'; return $pattern; } diff --git a/lib/internal/Magento/Framework/Setup/FilePermissions.php b/lib/internal/Magento/Framework/Setup/FilePermissions.php index af0db6498144e..8003f2241f22a 100644 --- a/lib/internal/Magento/Framework/Setup/FilePermissions.php +++ b/lib/internal/Magento/Framework/Setup/FilePermissions.php @@ -93,12 +93,16 @@ public function getInstallationWritableDirectories() $data = [ DirectoryList::CONFIG, DirectoryList::VAR_DIR, - DirectoryList::MEDIA, - DirectoryList::STATIC_VIEW, + DirectoryList::MEDIA ]; if ($this->state->getMode() !== State::MODE_PRODUCTION) { $data[] = DirectoryList::GENERATED; + /** + * Static files may be pre-generated on separate machine. + */ + $data[] = DirectoryList::STATIC_VIEW; } + foreach ($data as $code) { $this->installationWritableDirectories[$code] = $this->directoryList->getPath($code); } @@ -260,6 +264,7 @@ public function getMissingWritablePathsForInstallation($associative = false) if ($associative) { $missingPaths[$missingPath] = $this->nonWritablePathsInDirectories[$missingPath]; } else { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $missingPaths = array_merge( $missingPaths, $this->nonWritablePathsInDirectories[$missingPath] diff --git a/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php b/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php index e3428c411130c..6e2b83887561d 100644 --- a/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php +++ b/lib/internal/Magento/Framework/Setup/Test/Unit/FilePermissionsTest.php @@ -76,8 +76,8 @@ public function testGetInstallationWritableDirectories($mageMode) BP . '/app/etc', BP . '/var', BP . '/pub/media', + BP . '/generated', BP . '/pub/static', - BP . '/generated' ]; $this->assertEquals($expected, $this->filePermissions->getInstallationWritableDirectories()); @@ -94,7 +94,6 @@ public function testGetInstallationWritableDirectoriesInProduction() BP . '/app/etc', BP . '/var', BP . '/pub/media', - BP . '/pub/static' ]; $this->assertEquals($expected, $this->filePermissions->getInstallationWritableDirectories()); @@ -188,8 +187,8 @@ public function testGetMissingWritableDirectoriesAndPathsForInstallation($mageMo $expected = [ BP . '/var', BP . '/pub/media', + BP . '/generated', BP . '/pub/static', - BP . '/generated' ]; $this->assertEquals( @@ -213,8 +212,7 @@ public function testGetMissingWritableDirectoriesAndPathsForInstallationInProduc $expected = [ BP . '/var', - BP . '/pub/media', - BP . '/pub/static' + BP . '/pub/media' ]; $this->assertEquals( @@ -283,10 +281,15 @@ public function setUpDirectoryListInstallation() { $this->setUpDirectoryListInstallationInProduction(); $this->directoryListMock - ->expects($this->at(4)) + ->expects($this->at(3)) ->method('getPath') ->with(DirectoryList::GENERATED) ->willReturn(BP . '/generated'); + $this->directoryListMock + ->expects($this->at(4)) + ->method('getPath') + ->with(DirectoryList::STATIC_VIEW) + ->willReturn(BP . '/pub/static'); } public function setUpDirectoryListInstallationInProduction() @@ -306,11 +309,6 @@ public function setUpDirectoryListInstallationInProduction() ->method('getPath') ->with(DirectoryList::MEDIA) ->willReturn(BP . '/pub/media'); - $this->directoryListMock - ->expects($this->at(3)) - ->method('getPath') - ->with(DirectoryList::STATIC_VIEW) - ->willReturn(BP . '/pub/static'); } public function setUpDirectoryWriteInstallation() @@ -348,24 +346,6 @@ public function setUpDirectoryWriteInstallation() ->expects($this->at(6)) ->method('isDirectory') ->willReturn(false); - - // STATIC_VIEW - $this->directoryWriteMock - ->expects($this->at(7)) - ->method('isExist') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(8)) - ->method('isDirectory') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(9)) - ->method('isReadable') - ->willReturn(true); - $this->directoryWriteMock - ->expects($this->at(10)) - ->method('isWritable') - ->willReturn(false); } /** diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php index 48935913e0561..76c659791308e 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Design/Theme/ImageTest.php @@ -13,8 +13,10 @@ use Magento\Framework\App\Area; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filesystem\Directory\Write; use Magento\Framework\Image\Factory; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Design\Theme\Image; use Magento\Framework\View\Design\Theme\Image\Uploader; @@ -70,8 +72,20 @@ class ImageTest extends TestCase */ protected $imagePathMock; + private function setupObjectManagerForCheckImageExist($return) + { + $objectManagerMock = $this->getMockForAbstractClass(ObjectManagerInterface::class); + $mockFileSystem = $this->createMock(Filesystem::class); + $mockRead = $this->createMock(ReadInterface::class); + $objectManagerMock->method($this->logicalOr('get', 'create'))->willReturn($mockFileSystem); + $mockFileSystem->method('getDirectoryRead')->willReturn($mockRead); + $mockRead->method('isExist')->willReturn($return); + \Magento\Framework\App\ObjectManager::setInstance($objectManagerMock); + } + protected function setUp(): void { + $this->setupObjectManagerForCheckImageExist(false); $this->_mediaDirectoryMock = $this->createPartialMock( Write::class, ['isExist', 'copyFile', 'getRelativePath', 'delete'] diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index de40e3afa40ab..ae8dad5865709 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -204,12 +204,24 @@ define([ * @returns {float} */ function resolveModulo(qty, qtyIncrements) { + var divideEpsilon = 10000, + epsilon, + remainder; + while (qtyIncrements < 1) { qty *= 10; qtyIncrements *= 10; } - return qty % qtyIncrements; + epsilon = qtyIncrements / divideEpsilon; + remainder = qty % qtyIncrements; + + if (Math.abs(remainder - qtyIncrements) < epsilon || + Math.abs(remainder) < epsilon) { + remainder = 0; + } + + return remainder; } /** diff --git a/nginx.conf.sample b/nginx.conf.sample index ead80ccb22ece..2dbba68c39c39 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -26,6 +26,9 @@ ## ## In production mode, you should uncomment the 'expires' directive in the /static/ location block +# Modules can be loaded only at the very beginning of the Nginx config file, please move the line below to the main config file +# load_module /etc/nginx/modules/ngx_http_image_filter_module.so; + root $MAGE_ROOT/pub; index index.php; @@ -134,6 +137,29 @@ location /static/ { } location /media/ { + +## The following section allows to offload image resizing from Magento instance to the Nginx. +## Catalog image URL format should be set accordingly. +## See https://docs.magento.com/m2/ee/user_guide/configuration/general/web.html#url-options +# location ~* ^/media/catalog/.* { +# +# # Replace placeholders and uncomment the line below to serve product images from public S3 +# # See examples of S3 authentication at https://github.com/anomalizer/ngx_aws_auth +# # resolver 8.8.8.8; +# # proxy_pass https://<bucket-name>.<region-name>.amazonaws.com; +# +# set $width "-"; +# set $height "-"; +# if ($arg_width != '') { +# set $width $arg_width; +# } +# if ($arg_height != '') { +# set $height $arg_height; +# } +# image_filter resize $width $height; +# image_filter_jpeg_quality 90; +# } + try_files $uri $uri/ /get.php$is_args$args; location ~ ^/media/theme_customization/.*\.xml { diff --git a/pub/.htaccess b/pub/.htaccess index 6a97a6d14dc00..d30951ee22ca5 100644 --- a/pub/.htaccess +++ b/pub/.htaccess @@ -22,6 +22,11 @@ ## cgi.fix_pathinfo = 1 ## If it still doesn't work, rename php.ini to php5.ini +############################################ +## Enable usage of methods arguments in backtrace + + #SetEnv MAGE_DEBUG_SHOW_ARGS 1 + ############################################ ## This line is specific for 1and1 hosting @@ -33,24 +38,6 @@ DirectoryIndex index.php -<IfModule mod_php5.c> -############################################ -## Adjust memory limit - - php_value memory_limit 756M - php_value max_execution_time 18000 - -############################################ -## Disable automatic session start -## before autoload was initialized - - php_flag session.auto_start off - -############################################ -# Disable user agent verification to not break multiple image upload - - php_flag suhosin.session.cryptua off -</IfModule> <IfModule mod_php7.c> ############################################ ## Adjust memory limit @@ -75,7 +62,6 @@ php_flag suhosin.session.cryptua off </IfModule> - <IfModule mod_security.c> ########################################### # Disable POST processing to not break multiple image upload @@ -93,7 +79,7 @@ # Insert filter on all content ###SetOutputFilter DEFLATE # Insert filter on selected content types only - #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript + #AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/x-javascript application/json image/svg+xml # Netscape 4.x has some problems... #BrowserMatch ^Mozilla/4 gzip-only-text/html @@ -121,6 +107,13 @@ </IfModule> +############################################ +## Workaround for Apache 2.4.6 CentOS build when working via ProxyPassMatch with HHVM (or any other) +## Please, set it on virtual host configuration level + +## SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1 +############################################ + <IfModule mod_rewrite.c> ############################################ @@ -147,6 +140,13 @@ RewriteCond %{REQUEST_METHOD} ^TRAC[EK] RewriteRule .* - [L,R=405] +############################################ +## Redirect for mobile user agents + + #RewriteCond %{REQUEST_URI} !^/mobiledirectoryhere/.*$ + #RewriteCond %{HTTP_USER_AGENT} "android|blackberry|ipad|iphone|ipod|iemobile|opera mobile|palmos|webos|googlebot-mobile" [NC] + #RewriteRule ^(.*)$ /mobiledirectoryhere/ [L,R=302] + ############################################ ## Never rewrite for existing files, directories and links @@ -168,6 +168,7 @@ AddDefaultCharset Off #AddDefaultCharset UTF-8 + AddType 'text/html; charset=UTF-8' html <IfModule mod_expires.c> @@ -193,18 +194,15 @@ Require all denied </IfVersion> </Files> - -# For 404s and 403s that aren't handled by the application, show plain 404 response -ErrorDocument 404 /errors/404.php -ErrorDocument 403 /errors/404.php - -############################################ -## If running in cluster environment, uncomment this -## http://developer.yahoo.com/performance/rules.html#etags - - #FileETag none - -########################################### + <Files .htaccess> + <IfVersion < 2.4> + order allow,deny + deny from all + </IfVersion> + <IfVersion >= 2.4> + Require all denied + </IfVersion> + </Files> ## Deny access to cron.php <Files cron.php> <IfVersion < 2.4> @@ -226,8 +224,48 @@ ErrorDocument 403 /errors/404.php </IfVersion> </Files> +# For 404s and 403s that aren't handled by the application, show plain 404 response +ErrorDocument 404 /errors/404.php +ErrorDocument 403 /errors/404.php + +################################ +## If running in cluster environment, uncomment this +## http://developer.yahoo.com/performance/rules.html#etags + + #FileETag none + +# ###################################################################### +# # INTERNET EXPLORER # +# ###################################################################### + +# ---------------------------------------------------------------------- +# | Document modes | +# ---------------------------------------------------------------------- + +# Force Internet Explorer 8/9/10 to render pages in the highest mode +# available in the various cases when it may not. +# +# https://hsivonen.fi/doctype/#ie8 +# +# (!) Starting with Internet Explorer 11, document modes are deprecated. +# If your business still relies on older web apps and services that were +# designed for older versions of Internet Explorer, you might want to +# consider enabling `Enterprise Mode` throughout your company. +# +# https://msdn.microsoft.com/en-us/library/ie/bg182625.aspx#docmode +# http://blogs.msdn.com/b/ie/archive/2014/04/02/stay-up-to-date-with-enterprise-mode-for-internet-explorer-11.aspx + <IfModule mod_headers.c> ############################################ + Header set X-UA-Compatible "IE=edge" + + # `mod_headers` cannot match based on the content-type, however, + # the `X-UA-Compatible` response header should be send only for + # HTML documents and not for the other resources. + <FilesMatch "\.(appcache|atom|bbaw|bmp|crx|css|cur|eot|f4[abpv]|flv|geojson|gif|htc|ico|jpe?g|js|json(ld)?|m4[av]|manifest|map|mp4|oex|og[agv]|opus|otf|pdf|png|rdf|rss|safariextz|svgz?|swf|topojson|tt[cf]|txt|vcard|vcf|vtt|webapp|web[mp]|webmanifest|woff2?|xloc|xml|xpi)$"> + Header unset X-UA-Compatible + </FilesMatch> + ## Prevent clickjacking Header set X-Frame-Options SAMEORIGIN </IfModule> diff --git a/pub/get.php b/pub/get.php index 215a83b74fbca..c59365c98727c 100644 --- a/pub/get.php +++ b/pub/get.php @@ -43,13 +43,16 @@ // Serve file if it's materialized if ($mediaDirectory) { - if (!$isAllowed($relativePath, $allowedResources)) { + $fileAbsolutePath = __DIR__ . '/' . $relativePath; + $fileRelativePath = str_replace(rtrim($mediaDirectory, '/') . '/', '', $fileAbsolutePath); + + if (!$isAllowed($fileRelativePath, $allowedResources)) { require_once 'errors/404.php'; exit; } - $mediaAbsPath = $mediaDirectory . '/' . $relativePath; - if (is_readable($mediaAbsPath)) { - if (is_dir($mediaAbsPath)) { + + if (is_readable($fileAbsolutePath)) { + if (is_dir($fileAbsolutePath)) { require_once 'errors/404.php'; exit; } @@ -57,7 +60,7 @@ new \Magento\Framework\HTTP\PhpEnvironment\Response(), new \Magento\Framework\File\Mime() ); - $transfer->send($mediaAbsPath); + $transfer->send($fileAbsolutePath); exit; } } diff --git a/pub/index.php b/pub/index.php index 612e190719053..9e91f3bfa5488 100644 --- a/pub/index.php +++ b/pub/index.php @@ -7,7 +7,6 @@ */ use Magento\Framework\App\Bootstrap; -use Magento\Framework\App\Filesystem\DirectoryList; try { require __DIR__ . '/../app/bootstrap.php'; @@ -24,17 +23,7 @@ exit(1); } -$params = $_SERVER; -$params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = array_replace_recursive( - $params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] ?? [], - [ - DirectoryList::PUB => [DirectoryList::URL_PATH => ''], - DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], - DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], - DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], - ] -); -$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $params); +$bootstrap = Bootstrap::create(BP, $_SERVER); /** @var \Magento\Framework\App\Http $app */ $app = $bootstrap->createApplication(\Magento\Framework\App\Http::class); $bootstrap->run($app); diff --git a/pub/media/sitemap/.htaccess b/pub/media/sitemap/.htaccess new file mode 100644 index 0000000000000..187517e43efb2 --- /dev/null +++ b/pub/media/sitemap/.htaccess @@ -0,0 +1 @@ +Allow From All diff --git a/setup/src/Magento/Setup/Console/CommandList.php b/setup/src/Magento/Setup/Console/CommandList.php index ab31a3add07ed..ae65e82bba12b 100644 --- a/setup/src/Magento/Setup/Console/CommandList.php +++ b/setup/src/Magento/Setup/Console/CommandList.php @@ -66,10 +66,6 @@ protected function getCommandsClasses() \Magento\Setup\Console\Command\ModuleStatusCommand::class, \Magento\Setup\Console\Command\ModuleUninstallCommand::class, \Magento\Setup\Console\Command\ModuleConfigStatusCommand::class, - \Magento\Setup\Console\Command\MaintenanceAllowIpsCommand::class, - \Magento\Setup\Console\Command\MaintenanceDisableCommand::class, - \Magento\Setup\Console\Command\MaintenanceEnableCommand::class, - \Magento\Setup\Console\Command\MaintenanceStatusCommand::class, \Magento\Setup\Console\Command\RollbackCommand::class, \Magento\Setup\Console\Command\UpgradeCommand::class, \Magento\Setup\Console\Command\UninstallCommand::class, diff --git a/setup/src/Magento/Setup/Fixtures/ImagesFixture.php b/setup/src/Magento/Setup/Fixtures/ImagesFixture.php index 1878a48977156..cd403897de07a 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesFixture.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesFixture.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\ValidatorException; +use Magento\MediaStorage\Service\ImageResize; use Symfony\Component\Console\Output\OutputInterface; /** @@ -106,6 +107,10 @@ class ImagesFixture extends Fixture * @var array */ private $tableCache = []; + /** + * @var ImageResize + */ + private $imageResize; /** * @param FixtureModel $fixtureModel @@ -117,6 +122,7 @@ class ImagesFixture extends Fixture * @param \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory * @param \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool + * @param ImageResize $imageResize */ public function __construct( FixtureModel $fixtureModel, @@ -127,7 +133,8 @@ public function __construct( \Magento\Eav\Model\AttributeRepository $attributeRepository, \Magento\Framework\DB\Sql\ColumnValueExpressionFactory $expressionFactory, \Magento\Setup\Model\BatchInsertFactory $batchInsertFactory, - \Magento\Framework\EntityManager\MetadataPool $metadataPool + \Magento\Framework\EntityManager\MetadataPool $metadataPool, + ImageResize $imageResize ) { parent::__construct($fixtureModel); @@ -139,6 +146,7 @@ public function __construct( $this->expressionFactory = $expressionFactory; $this->batchInsertFactory = $batchInsertFactory; $this->metadataPool = $metadataPool; + $this->imageResize = $imageResize; } /** @@ -147,9 +155,10 @@ public function __construct( */ public function execute() { - if (!$this->checkIfImagesExists()) { + if (!$this->checkIfImagesExists() && $this->getImagesToGenerate()) { $this->createImageEntities(); $this->assignImagesToProducts(); + iterator_to_array($this->imageResize->resizeFromThemes(), false); } } diff --git a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php index cfcdebd4ac373..dc730b69f8775 100644 --- a/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php +++ b/setup/src/Magento/Setup/Fixtures/ImagesGenerator/ImagesGenerator.php @@ -36,9 +36,9 @@ public function __construct( /** * Generates image from $data and puts its to /tmp folder - * - * @param string $config + * @param array $config * @return string $imagePath + * @throws \Exception */ public function generate($config) { @@ -70,9 +70,15 @@ public function generate($config) $relativePathToMedia = $mediaDirectory->getRelativePath($this->mediaConfig->getBaseTmpMediaPath()); $mediaDirectory->create($relativePathToMedia); - $absolutePathToMedia = $mediaDirectory->getAbsolutePath($this->mediaConfig->getBaseTmpMediaPath()); - $imagePath = $absolutePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; - imagejpeg($image, $imagePath, 100); + $imagePath = $relativePathToMedia . DIRECTORY_SEPARATOR . $config['image-name']; + $imagePath = preg_replace('|/{2,}|', '/', $imagePath); + $memory = fopen('php://memory', 'r+'); + if(!imagejpeg($image, $memory)) { + throw new \Exception('Could not create picture ' . $imagePath); + } + $mediaDirectory->writeFile($imagePath, stream_get_contents($memory, -1, 0)); + fclose($memory); + imagedestroy($image); // phpcs:enable return $imagePath; diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php index e838dbee33603..0e9cc65f17bd9 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php @@ -15,6 +15,7 @@ /** * Deployment configuration options for the folders. + * @deprecared Magento always uses the pub directory */ class Directory implements ConfigOptionsListInterface { @@ -70,7 +71,7 @@ public function getOptions() $this->selectOptions, self::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, 'Flag to show is Pub is on root, can be true or false only', - false + true ), ]; }