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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+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 @@
+
+
+
+
+
+
+ -
+
- aws-s3
+ - AWS S3
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ - Magento\AwsS3\Driver\AwsS3Factory
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
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 @@
+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('' . implode('' . PHP_EOL . '', $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(
'Set exempt IP-addresses: ' . (implode(', ', $this->maintenanceMode->getAddressInfo()) ?: 'none')
. ''
);
}
- 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('' . implode('' . PHP_EOL . '', $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('Set exempt IP-addresses: none');
}
- 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 'Disabled maintenance mode';
}
@@ -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 'Enabled maintenance mode';
}
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(
'Status: maintenance mode is ' .
@@ -56,6 +58,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
$addressInfo = $this->maintenanceMode->getAddressInfo();
$addresses = implode(' ', $addressInfo);
$output->writeln('List of exempt IP-addresses: ' . ($addresses ? $addresses : 'none') . '');
- 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 @@
+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 @@
+
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml
index 2469151337bfe..b87b92e86528c 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireAdminSessionTest.xml
@@ -29,7 +29,7 @@
-
+
diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml
index 0e3bf07d32441..b2b71c4ad3eca 100644
--- a/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml
+++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminExpireCustomerSessionTest.xml
@@ -40,7 +40,7 @@
-
+
diff --git a/app/code/Magento/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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+Magento\Backend\Console\Command\CacheFlushCommand
- Magento\Backend\Console\Command\CacheCleanCommand
- Magento\Backend\Console\Command\CacheStatusCommand
+ - Magento\Backend\Console\Command\MaintenanceAllowIpsCommand
+ - Magento\Backend\Console\Command\MaintenanceDisableCommand
+ - Magento\Backend\Console\Command\MaintenanceDisableCommand
+ - Magento\Backend\Console\Command\MaintenanceStatusCommand
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 100.00
+
+
+ 50.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
+
+
+
+
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 @@
+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 @@
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ Set "Enable Qty Increments" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ Fills in the "Qty Increments" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.
+
+
+
+
+
+
+
+
+
+
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 @@
New Bundle Product Name
This is the description
+
+ 10
+ In Stock
+
+
+ 0
+ Out of Stock
+
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 @@
+
+
+
+
+
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 @@
-
+
@@ -126,7 +126,7 @@
-
+
diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCatalogNavigationMenuUIDesktopTest.xml
index 2a59be6306a30..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 @@
-
-
+
+
+
+
@@ -87,8 +89,9 @@
-
-
+
+
+
@@ -167,8 +170,9 @@
-
-
+
+
+
@@ -203,8 +207,9 @@
-
-
+
+
+
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 @@
-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 @@
-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 @@
+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 @@
+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 @@
-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 @@
-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 @@
+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 @@
+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 @@
Magento\Catalog\Model\Config\Source\LayoutList
+
+
+
+ Magento\Catalog\Model\Config\Source\Web\CatalogMediaUrlFormat
+ Learn more about catalog URL formats.
Warning! If you switch back to legacy mode, you must use the CLI to regenerate images.]]>
+
+
separator-top
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 @@
tmp
- media/catalog/product/cache/
catalog
custom_options
@@ -83,6 +82,11 @@
stretch
+
+
+ hash
+
+
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 @@
getCustomAttributes() as $name => $value): ?>
- = $escaper->escapeHtmlAttr($name) ?>="= $escaper->escapeHtmlAttr($value) ?>"
+ = $escaper->escapeHtmlAttr($name) ?>="= $escaper->escapeHtml($value) ?>"
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;
getCustomAttributes() as $name => $value): ?>
- = $escaper->escapeHtmlAttr($name) ?>="= $escaper->escapeHtmlAttr($value) ?>"
+ = $escaper->escapeHtmlAttr($name) ?>="= $escaper->escapeHtml($value) ?>"
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 @@
+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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+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 @@
-
-
+
+
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 @@
-
-
+
+
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 @@
-
-
+
+
+
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 @@
-
-
+
+
@@ -71,8 +71,9 @@
-
-
+
+
+
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 @@
-
-
+
+
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 @@
+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 `` 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(
- '~[/\\\]+~',
+ '~[/\\\]+(?_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('~[/\\\]+(?_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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ Navigate to the Admin Blocks Grid page.
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
+
+
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 @@
sales25off everything!
0
+
+ Test Block
+ ActiveTestBlock
+ All Store Views
+ Test Block content
+ 1
+
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 @@
+
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 @@
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $grabTotalRecordsCmsPagesBeforeClickSearchButton
+ $grabTotalRecordsCmsPagesAfterClickSearchButton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $grabTotalRecordsBlocksBeforeClickSearchButton
+ $grabTotalRecordsBlocksAfterClickSearchButton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+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 @@
wysiwyg
+ .thumbs
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 @@
-
-
- true
- - pub[/\\]+media[/\\]+captcha[/\\]*$
+ - media[/\\]+captcha[/\\]*$
-
- true
- - pub[/\\]+media[/\\]+catalog[/\\]+product[/\\]*$
+ - media[/\\]+catalog[/\\]+product[/\\]*$
-
- true
- - pub[/\\]+media[/\\]+customer[/\\]*$
+ - media[/\\]+customer[/\\]*$
-
- true
- - pub[/\\]+media[/\\]+downloadable[/\\]*$
+ - media[/\\]+downloadable[/\\]*$
-
- true
- - pub[/\\]+media[/\\]+import[/\\]*$
+ - media[/\\]+import[/\\]*$
-
- true
- - pub[/\\]+media[/\\]+theme[/\\]*$
+ - media[/\\]+theme[/\\]*$
-
- true
- - pub[/\\]+media[/\\]+theme_customization[/\\]*$
+ - media[/\\]+theme_customization[/\\]*$
-
- true
- - pub[/\\]+media[/\\]+tmp[/\\]*$
+ - media[/\\]+tmp[/\\]*$
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 @@
-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 @@
+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 @@
+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 @@
-
-
+
+
+
@@ -142,8 +143,9 @@
-
-
+
+
+
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 @@
+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 @@
+
+
+
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 @@
Magento\Catalog\Model\ResourceModel\Product\Indexer\TemporaryTableStrategy
indexer
+ Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price\BaseStockStatusSelectProcessor
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 @@
-
-
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+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 @@
^[0-9]{4}$
+ ^[a-zA-z]{1}[0-9]{4}[a-zA-z]{3}$
@@ -228,6 +229,7 @@
^[0-9]{3}-[0-9]{3}$
+ ^[0-9]{5}$
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 @@
+
+
+
+
+
+
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 @@
+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 @@
+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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ - Magento\GroupedProduct\Model\Inventory\ParentItemProcessor
+
+
+
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 @@
Magento\Framework\Serialize\Serializer\Json
+
Magento\Framework\Filesystem\Driver\File
+
Magento\Framework\Filesystem\Driver\File
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 @@
+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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-->
-
+
-
+
+
+ Use AdminMediaGalleryCatalogUiEditCategoryFromGridPageTest instead
+
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 @@
-->
-
+
-
+
+
+ Use AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest instead
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
1000
1000
+
+
+ .renditions
+
+
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 @@
Edit image from the View Details panel
-
+
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 @@
-
+
+
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 @@
-
-
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-->
-
+
-
+
+
+ Use AdminMediaGalleryEditImageDetailsFromGridTest instead
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-->
-
+
-
+
+
+ Use AdminStandaloneMediaGalleryEditImageDetailsFromDialogTest instead
+
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 @@
-
-
-
-
+
+
+
+
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 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page for 'Payment Methods'. Disables PayPal Express Checkout solution. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
+
Goes to the 'Configuration' page for 'Payment Methods'. Fills in the provided Sample PayPal credentials and other details. Clicks on Save.
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 @@
-
+
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 @@
-
-
+
+
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.
*/
?>
-getCustomerId()) :?>
-
- getLinkAttributes()?>>= $block->escapeHtml(__('Not you?'));?>
-
-
-
-
-
-
-
+
+
+
+
+
+