diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000000..34af2f767fef6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,78 @@ +# Runs tests and verifies that the package can be built. +name: Build + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + schedule: + - cron: "30 0 * * *" + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + runs-on: ${{matrix.operating-system}} + strategy: + fail-fast: false + matrix: + # operating-system: [ubuntu-latest] + operating-system: [ubuntu-22.04, ubuntu-20.04] + # php-versions: ['5.3', '5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.3'] + php-versions: ['8.1', '8.2', '8.3'] + name: build and test using PHP ${{ matrix.php-versions }} with ${{ matrix.operating-system }} + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: intl #optional + ini-values: "post_max_size=256M" #optional + + - name: Install dependencies + run: | + rm composer.lock || true + composer install + + - name: Setup Node.js + if: false + uses: actions/setup-node@v3 + with: + node-version: '16' + check-latest: true + + - name: Install NPM dependencies + if: false + run: npm install + + - name: Compile assets for production + if: false + run: npm run build + + - name: Push to repository + if: false + run: | + if [ "$(git status --porcelain=v1 2>/dev/null | wc -l)" != "0" ]; then + # git add src/debian/control + git commit -m "Update packages from ${{ matrix.operating-system }} at $(date +'%d-%m-%y')" && + git push || + git stash && + git pull --rebase && + git stash apply && + git push || true + fi diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml new file mode 100644 index 0000000000000..efebaa013210b --- /dev/null +++ b/.github/workflows/module.yml @@ -0,0 +1,42 @@ +# Runs tests and verifies that the package can be built. +name: Module + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + # schedule: + # - cron: "30 * * * 0" # run every @hourly + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + module: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/magento-module-update.yml@main + secrets: + GPG_KEY: ${{ secrets.GPG_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} + SSH_ID_RSA: ${{ secrets.SSH_ID_RSA }} + theme: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/magento-theme-update.yml@main + secrets: + GPG_KEY: ${{ secrets.GPG_KEY }} + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} + SSH_ID_RSA: ${{ secrets.SSH_ID_RSA }} + diff --git a/.gitignore b/.gitignore index f3784ee198240..c5d0e5cb3d9a3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,10 @@ atlassian* /lib/internal/flex/varien/.settings /node_modules /.grunt +/composer.lock /Gruntfile.js /package.json +/package-lock.json /.php_cs /.php_cs.cache /.php-cs-fixer.php @@ -64,6 +66,8 @@ atlassian* !/pub/media/sitemap/.htaccess /pub/static/* !/pub/static/.htaccess +/pub/media/email/* +!/pub/media/email/.htaccess /var/* !/var/.htaccess @@ -72,3 +76,9 @@ atlassian* /generated/* !/generated/.htaccess .DS_Store + +auth.json +magento.code-workspace +bin/magerun2 +.m2config +.phpactor.json diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 30fcfc86de059..eabd587379b80 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,47 +1,109 @@ + * @author Tran Ngoc Duc + * + * @lastupdate 2025-03-31 12:41:27 */ -$finder = PhpCsFixer\Finder::create() - ->name('*.phtml') - ->exclude('dev/tests/integration/tmp') - ->exclude('dev/tests/integration/var') - ->exclude('lib/internal/Cm') - ->exclude('lib/internal/Credis') - ->exclude('lib/internal/Less') - ->exclude('lib/internal/LinLibertineFont') - ->exclude('pub/media') - ->exclude('pub/static') - ->exclude('setup/vendor') - ->exclude('var'); - -$config = new PhpCsFixer\Config(); -$config->setFinder($finder) - ->setRules([ - '@PSR2' => true, - 'array_syntax' => ['syntax' => 'short'], - 'concat_space' => ['spacing' => 'one'], - 'include' => true, - 'new_with_braces' => true, - 'no_empty_statement' => true, - 'no_extra_blank_lines' => true, - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_multiline_whitespace_around_double_arrow' => true, - 'multiline_whitespace_before_semicolons' => true, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_unused_imports' => true, - 'no_whitespace_in_blank_line' => true, - 'object_operator_without_whitespace' => true, - 'ordered_imports' => true, - 'standardize_not_equals' => true, - 'ternary_operator_spaces' => true, - ]); -return $config; +use PhpCsFixer\Config; +use PhpCsFixer\Finder; + +if (!function_exists('import_url')) { + function import_url($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, 0); + $data = curl_exec($ch); + curl_close($ch); + + return $data; + } +} + +date_default_timezone_set('Asia/Ho_Chi_Minh'); + +try { + $configPath = 'https://raw.githubusercontent.com/diepxuan/php/main/.php-cs-fixer.dist.php'; + $configContent = import_url($configPath); + $tempFile = sys_get_temp_dir() . '/.php-cs-fixer.dist.php'; + file_put_contents($tempFile, $configContent); + return require $tempFile; + // $config = import_url($configPath); + // $config = str_replace('setRiskyAllowed(true) + ->setRules([ + '@PHP74Migration' => true, + '@PHP74Migration:risky' => true, + '@PHPUnit100Migration:risky' => true, + '@PhpCsFixer' => true, + '@PhpCsFixer:risky' => true, + // '@PER-CS2.0' => true, + 'general_phpdoc_annotation_remove' => ['annotations' => ['expectedDeprecation']], // one should use PHPUnit built-in method instead + 'header_comment' => ['header' => sprintf(<<<'EOF' + @copyright © 2019 Dxvn, Inc. + + @author Tran Ngoc Duc + @author Tran Ngoc Duc + + @lastupdate %s + EOF, date('Y-m-d H:i:s'))], + + 'modernize_strpos' => true, // needs PHP 8+ or polyfill + 'no_useless_concat_operator' => false, // TODO switch back on when the `src/Console/Application.php` no longer needs the concat + 'numeric_literal_separator' => true, + 'string_implicit_backslashes' => true, // https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7786 + + '@PSR2' => true, + 'array_syntax' => ['syntax' => 'short'], + 'concat_space' => ['spacing' => 'one'], + 'include' => true, + 'new_with_braces' => true, + 'no_empty_statement' => true, + // 'no_extra_consecutive_blank_lines' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + // 'multiline_whitespace_before_semicolons' => false, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_alias_language_construct_call' => true, + 'assign_null_coalescing_to_coalesce_equal' => true, + 'no_unused_imports' => true, + 'no_whitespace_in_blank_line' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => true, + 'standardize_not_equals' => true, + 'ternary_operator_spaces' => true, + 'binary_operator_spaces' => [ + 'default' => 'at_least_single_space', + 'operators' => [ + '=>' => 'align_single_space_minimal', + '=' => 'align_single_space_minimal', + ]], + 'operator_linebreak' => ['only_booleans' => true], + ]) + ->setFinder( + (new Finder()) + ->ignoreDotFiles(false) + ->ignoreVCSIgnored(true) + ->exclude([ + 'dev-tools/phpstan', + 'tests/Fixtures', + 'lib/internal/Magento/Framework', + 'app/code/Magento', + ]) + ->in(__DIR__) + ) + ; +} diff --git a/app/code/Diepxuan/Autologin/.editorconfig b/app/code/Diepxuan/Autologin/.editorconfig new file mode 100644 index 0000000000000..80caf2930639c --- /dev/null +++ b/app/code/Diepxuan/Autologin/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[docker-compose.yml] +indent_size = 4 + +[changelog] +indent_size = 2 + +[CHANGELOG.md] +indent_size = 2 + +[*.{yml,yaml,json}] +indent_size = 2 diff --git a/app/code/Diepxuan/Autologin/.php-cs-fixer.dist.php b/app/code/Diepxuan/Autologin/.php-cs-fixer.dist.php new file mode 100644 index 0000000000000..3c9d99935f78d --- /dev/null +++ b/app/code/Diepxuan/Autologin/.php-cs-fixer.dist.php @@ -0,0 +1,31 @@ + + * Tran Ngoc Duc + */ + +if (!function_exists('import_url')) { + function import_url($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, 0); + $data = curl_exec($ch); + curl_close($ch); + + return $data; + } +} + +date_default_timezone_set('Asia/Ho_Chi_Minh'); +$config = import_url('https://raw.githubusercontent.com/diepxuan/php/main/.php-cs-fixer.dist.php'); +$config = str_replace(' + * @author Tran Ngoc Duc + * + * @lastupdate 2025-04-01 13:08:44 + */ + +namespace Diepxuan\Autologin\Model; + +use Magento\Backend\Helper\Data; +use Magento\Backend\Model\Auth\StorageInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Data\Collection\ModelFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\Plugin\AuthenticationException as PluginAuthenticationException; +use Magento\Framework\Phrase; +use Magento\User\Model\User; +use Psr\Log\LoggerInterface; + +/** + * Backend Auth model. + * + * @see \Magento\Backend\Model\Auth + */ +class Auth +{ + const ENABLE = 'diepxuan_autologin/autologin/enable'; + const USERNAME = 'diepxuan_autologin/autologin/username'; + const ALLOWS = 'diepxuan_autologin/autologin/allows'; + + /** + * @var Context + */ + protected $_context; + + /** + * @var LoggerInterface + */ + protected $_logger; + + /** + * @var Http + */ + protected $_request; + + /** + * Core event manager proxy. + * + * @var ManagerInterface + */ + protected $_eventManager; + + /** + * Backend data. + * + * @var Data + */ + protected $_backendData; + + /** + * @var \Magento\Backend\Model\Auth + */ + protected $_auth; + + /** + * @var StorageInterface + */ + protected $_authStorage; + + /** + * @var \Magento\Backend\Model\Auth\Credential\StorageInterface + */ + protected $_credentialStorage; + + /** + * @var ScopeConfigInterface + */ + protected $_coreConfig; + + /** + * @var ModelFactory + */ + protected $_modelFactory; + + public function __construct( + Context $context + ) { + $this->_context = $context; + $this->_logger = $context->getLogger(); + $this->_request = $context->getRequest(); + $this->_eventManager = $context->getEventManager(); + $this->_backendData = $context->getBackendData(); + $this->_auth = $context->getAuth(); + $this->_authStorage = $context->getAuthStorage(); + $this->_coreConfig = $context->getCoreConfig(); + $this->_modelFactory = $context->getModelFactory(); + } + + /** + * Check if current user is logged in. + * + * @return bool + */ + public function isLoggedIn() + { + try { + $this->_autoAuthentication(); + } catch (LocalizedException $e) { + return $this->getAuthStorage()->isLoggedIn(); + } + + return $this->getAuthStorage()->isLoggedIn(); + } + + /** + * Perform login process. + * + * @throws AuthenticationException + */ + public function autoAuthentication(): void + { + try { + $this->_initCredentialStorage(); + $this->getCredentialStorage()->loadByUsername($this->getAdminUserName()); + if ($this->getCredentialStorage()->getId()) { + $this->getAuth()->login($this->getCredentialStorage()->getUserName(), $this->getCredentialStorage()->getPassword()); + + $this->_eventManager->dispatch( + 'backend_auth_user_login_success', + ['user' => $this->getCredentialStorage()] + ); + } + if (!$this->getAuthStorage()->getUser()) { + self::throwException(__('You did not sign in correctly or your account is temporarily disabled.')); + } + } catch (PluginAuthenticationException $e) { + $this->_eventManager->dispatch( + 'backend_auth_user_login_failed', + ['user_name' => $this->getAdminUserName(), 'exception' => $e] + ); + + throw $e; + } catch (LocalizedException $e) { + $this->_eventManager->dispatch( + 'backend_auth_user_login_failed', + ['user_name' => $this->getAdminUserName(), 'exception' => $e] + ); + self::throwException( + __($e->getMessage() ?: 'You did not sign in correctly or your account is temporarily disabled.') + ); + } + } + + /** + * Check if the current user account is active. + * + * @param bool $result + * + * @return bool + * + * @throws AuthenticationException + */ + public function verifyIdentity(User $user, $result = false) + { + if (!$this->isEnable()) { + return $result; + } + + if ($user->getUserName() === $this->getAdminUserName()) { + $result = true; + } + + return $result; + } + + /** + * @return bool + */ + public function isEnable() + { + return $this->_isEnable(); + } + + /** + * @return string + * + * @throws \Exception + */ + public function getAdminUserName() + { + $username = $this->_coreConfig->getValue(self::USERNAME); + + if (empty($username)) { + self::throwException(__('Autologin/Authentication | Your admin username was not found!')); + } + + return $username; + } + + /** + * Perform logout process. + */ + public function logout(): void + { + $this->getAuthStorage()->processLogout(); + } + + /** + * Throws specific Backend Authentication \Exception. + * + * @throws AuthenticationException + * + * @static + */ + public static function throwException(?Phrase $msg = null): void + { + if (null === $msg) { + $msg = __('Authentication error occurred.'); + } + + throw new AuthenticationException($msg); + } + + /** + * @return \Magento\Backend\Model\Auth + * + * @codeCoverageIgnore + */ + public function getAuth() + { + return $this->_auth; + } + + /** + * Return auth storage. + * If auth storage was not defined outside - returns default object of auth storage. + * + * @return StorageInterface + * + * @codeCoverageIgnore + */ + public function getAuthStorage() + { + return $this->_authStorage; + } + + /** + * Return current (successfully authenticated) user, + * an instance of \Magento\Backend\Model\Auth\Credential\StorageInterface. + * + * @return \Magento\Backend\Model\Auth\Credential\StorageInterface + */ + public function getUser() + { + return $this->getAuthStorage()->getUser(); + } + + /** + * Return credential storage object. + * + * @return null|\Magento\Backend\Model\Auth\Credential\StorageInterface + * + * @codeCoverageIgnore + */ + public function getCredentialStorage() + { + return $this->_credentialStorage; + } + + /** + * @return LoggerInterface + */ + public function getLogger() + { + return $this->_logger; + } + + /** + * Initialize credential storage from configuration. + */ + protected function _initCredentialStorage(): void + { + $this->_credentialStorage = $this->_modelFactory->create( + User::class + ); + } + + protected function _autoAuthentication() + { + if (!$this->isEnable()) { + return; + } + + return $this->autoAuthentication(); + } + + /** + * @return bool + */ + protected function _isEnable() + { + if ($this->getAuthStorage()->isLoggedIn()) { + return false; + } + + $enable = $this->_coreConfig->getValue(self::ENABLE); + if (!$enable) { + return false; + } + + return true; + } +} diff --git a/app/code/Diepxuan/Autologin/Model/Config/Source/AuthenticationUser.php b/app/code/Diepxuan/Autologin/Model/Config/Source/AuthenticationUser.php new file mode 100644 index 0000000000000..ecd188992e216 --- /dev/null +++ b/app/code/Diepxuan/Autologin/Model/Config/Source/AuthenticationUser.php @@ -0,0 +1,90 @@ + + */ + +namespace Diepxuan\Autologin\Model\Config\Source; + +class AuthenticationUser implements \Magento\Framework\Option\ArrayInterface +{ + /** + * @var \Magento\User\Model\ResourceModel\User\Collection + */ + private $_userCollection; + + /** + * @var \Psr\Log\LoggerInterface + */ + protected $_logger; + + /** + * @var array + */ + private $users; + + public function __construct( + \Magento\User\Model\ResourceModel\User\Collection $userCollection, + \Psr\Log\LoggerInterface $logger + ) { + $this->_userCollection = $userCollection; + $this->_logger = $logger; + $this->users = []; + } + + /** + * @return array + */ + public function toArray($includeEmptyChoice = true) + { + if (!is_null($this->users) && !empty($this->users)) { + return $this->users; + } + + if ($includeEmptyChoice) { + $this->users[''] = __('-- Default --'); + } + + $this->getCollection()->addFieldToFilter('is_active', true); + $this->getCollection()->addOrder('username', \Magento\Framework\Data\Collection::SORT_ORDER_ASC); + + foreach ($this->getCollection() as $user) { + $this->users[$user->getUsername()] = $user->getName() . ' (' . $user->getUsername() . ')'; + } + + return $this->users; + } + + /** + * Options getter + * + * @return array + */ + public function toOptionArray() + { + $users = []; + foreach ($this->toArray() as $userName => $name) { + $users[] = [ + 'value' => $userName, + 'label' => $name, + ]; + } + return $users; + } + + /** + * @return \Magento\User\Model\ResourceModel\User\Collection + */ + public function getCollection() + { + return $this->_userCollection; + } + + /** + * @return \Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->_logger; + } +} diff --git a/app/code/Diepxuan/Autologin/Model/Context.php b/app/code/Diepxuan/Autologin/Model/Context.php new file mode 100644 index 0000000000000..4443de7a2d50e --- /dev/null +++ b/app/code/Diepxuan/Autologin/Model/Context.php @@ -0,0 +1,197 @@ + + */ + +namespace Diepxuan\Autologin\Model; + +/** + * Autologin model + * + * @api + * @see \Diepxuan\Autologin\Model\Context + */ +class Context +{ + /** + * @var \Magento\Config\Model\ResourceModel\Config + */ + protected $_resourceConfig; + + /** + * @var \Magento\Framework\App\Request\Http + */ + protected $_request; + + /** + * @var \Psr\Log\LoggerInterface + */ + protected $_logger; + + /** + * Core event manager proxy + * + * @var \Magento\Framework\Event\ManagerInterface + */ + protected $_eventManager; + + /** + * Backend data + * + * @var \Magento\Backend\Helper\Data + */ + protected $_backendData; + + /** + * @var \Magento\Backend\Model\Auth + */ + protected $_auth; + + /** + * @var \Magento\Backend\Model\Auth\StorageInterface + */ + protected $_authStorage; + + /** + * @var \Magento\Backend\Model\Auth\Credential\StorageInterface + */ + protected $_credentialStorage; + + /** + * @var \Magento\Framework\App\Config\ScopeConfigInterface + */ + protected $_coreConfig; + + /** + * @var \Magento\Framework\Data\Collection\ModelFactory + */ + protected $_modelFactory; + + /** + * @param \Magento\Config\Model\ResourceModel\Config $resourceConfig + * @param \Magento\Framework\App\Request\Http $request + * @param \Psr\Log\LoggerInterface $logger + * @param \Magento\Framework\Event\ManagerInterface $eventManager + * @param \Magento\Backend\Helper\Data $backendData + * @param \Magento\Backend\Model\Auth $auth + * @param \Magento\Backend\Model\Auth\StorageInterface $authStorage + * @param \Magento\Backend\Model\Auth\Credential\StorageInterface $credentialStorage + * @param \Magento\Framework\App\Config\ScopeConfigInterface $coreConfig + * @param \Magento\Framework\Data\Collection\ModelFactory $modelFactory + */ + public function __construct( + \Magento\Config\Model\ResourceModel\Config $resourceConfig, + \Magento\Framework\App\Request\Http $request, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Backend\Helper\Data $backendData, + \Magento\Backend\Model\Auth $auth, + \Magento\Backend\Model\Auth\StorageInterface $authStorage, + \Magento\Backend\Model\Auth\Credential\StorageInterface $credentialStorage, + \Magento\Framework\App\Config\ScopeConfigInterface $coreConfig, + \Magento\Framework\Data\Collection\ModelFactory $modelFactory + ) { + $this->_resourceConfig = $resourceConfig; + $this->_request = $request; + $this->_logger = $logger; + $this->_eventManager = $eventManager; + $this->_backendData = $backendData; + $this->_auth = $auth; + $this->_authStorage = $authStorage; + $this->_credentialStorage = $credentialStorage; + $this->_coreConfig = $coreConfig; + $this->_modelFactory = $modelFactory; + } + + /** + * @return \Magento\Config\Model\ResourceModel\Config + */ + public function getResourceConfig() + { + return $this->_resourceConfig; + } + + /** + * @return \Magento\Framework\App\Request\Http + */ + public function getRequest() + { + return $this->_request; + } + + /** + * @return \Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->_logger; + } + + /** + * @return \Magento\Framework\Event\ManagerInterface + */ + public function getEventManager() + { + return $this->_eventManager; + } + + /** + * @return \Magento\Backend\Helper\Data + */ + public function getBackendData() + { + return $this->_backendData; + } + + /** + * Return auth storage. + * If auth storage was not defined outside - returns default object of auth storage + * + * @return \Magento\Backend\Model\Auth\StorageInterface + * @codeCoverageIgnore + */ + public function getAuthStorage() + { + return $this->_authStorage; + } + + /** + * Return auth. + * + * @return \Magento\Backend\Model\Auth + * @codeCoverageIgnore + */ + public function getAuth() + { + return $this->_auth; + } + + /** + * Return credential storage object + * + * @return null|\Magento\Backend\Model\Auth\Credential\StorageInterface + * @codeCoverageIgnore + */ + public function getCredentialStorage() + { + return $this->_credentialStorage; + } + + /** + * @return \Magento\Framework\App\Config\ScopeConfigInterface + */ + public function getCoreConfig() + { + return $this->_coreConfig; + } + + /** + * @return \Magento\Framework\Data\Collection\ModelFactory + */ + public function getModelFactory() + { + return $this->_modelFactory; + } + +} diff --git a/app/code/Diepxuan/Autologin/Plugin/Backend/Model/Auth.php b/app/code/Diepxuan/Autologin/Plugin/Backend/Model/Auth.php new file mode 100644 index 0000000000000..49a5c662b105c --- /dev/null +++ b/app/code/Diepxuan/Autologin/Plugin/Backend/Model/Auth.php @@ -0,0 +1,83 @@ + + */ + +namespace Diepxuan\Autologin\Plugin\Backend\Model; + +class Auth +{ + /** + * @var \Psr\Log\LoggerInterface + */ + protected $_logger; + + /** + * @var \Diepxuan\Autologin\Model\Auth + */ + protected $_auth; + + /** + * @var \Magento\Framework\App\Request\Http + */ + protected $_request; + + /** + * @param \Psr\Log\LoggerInterface $logger + * @param \Diepxuan\Autologin\Model\Auth $auth + * @param \Magento\Framework\App\Request\Http $request + */ + public function __construct( + \Psr\Log\LoggerInterface $logger, + \Diepxuan\Autologin\Model\Auth $auth, + \Magento\Framework\App\Request\Http $request + ) { + $this->_logger = $logger; + $this->_auth = $auth; + $this->_request = $request; + } + + /** + * Authenticate user + * @param \Magento\Backend\Model\Auth $auth + * @param bool $result + * + * @return bool + */ + public function afterIsLoggedIn( + \Magento\Backend\Model\Auth $auth, + bool $result + ) { + if (!$result) { + return $this->getAuth()->isLoggedIn(); + } + + return $result; + } + + /** + * @return \Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->_logger; + } + + /** + * @return \Diepxuan\Autologin\Model\Auth + */ + public function getAuth() + { + return $this->_auth; + } + + /** + * @return \Magento\Framework\App\Request\Http + */ + public function getRequest() + { + return $this->_request; + } +} diff --git a/app/code/Diepxuan/Autologin/Plugin/User/Model/User.php b/app/code/Diepxuan/Autologin/Plugin/User/Model/User.php new file mode 100644 index 0000000000000..1ca76e06b4fb9 --- /dev/null +++ b/app/code/Diepxuan/Autologin/Plugin/User/Model/User.php @@ -0,0 +1,85 @@ + + */ + +namespace Diepxuan\Autologin\Plugin\User\Model; + +class User +{ + /** + * @var \Psr\Log\LoggerInterface + */ + protected $_logger; + + /** + * @var \Diepxuan\Autologin\Model\Auth + */ + protected $_auth; + + /** + * @var \Magento\Framework\App\Request\Http + */ + protected $_request; + + /** + * @param \Psr\Log\LoggerInterface $logger + * @param \Diepxuan\Autologin\Model\Auth $auth + * @param \Magento\Framework\App\Request\Http $request + */ + public function __construct( + \Psr\Log\LoggerInterface $logger, + \Diepxuan\Autologin\Model\Auth $auth, + \Magento\Framework\App\Request\Http $request + ) { + $this->_logger = $logger; + $this->_auth = $auth; + $this->_request = $request; + } + + /** + * @param \Magento\User\Model\User $user + * @param bool $result + * @return bool + */ + public function afterVerifyIdentity( + \Magento\User\Model\User $user, + bool $result + ): bool { + if (!$result) { + try { + return $this->getAuth()->verifyIdentity($user, $result); + } catch (\Exception $exception) { + return $result; + } + } + + return $result; + } + + /** + * @return \Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->_logger; + } + + /** + * @return \Diepxuan\Autologin\Model\Auth + */ + public function getAuth() + { + return $this->_auth; + } + + /** + * @return \Magento\Framework\App\Request\Http + */ + public function getRequest() + { + return $this->_request; + } +} diff --git a/app/code/Diepxuan/Autologin/README.md b/app/code/Diepxuan/Autologin/README.md new file mode 100644 index 0000000000000..d9fe3728f8131 --- /dev/null +++ b/app/code/Diepxuan/Autologin/README.md @@ -0,0 +1,41 @@ +Autologin module for Magento 2 +================== +[![Packagist](https://img.shields.io/packagist/v/diepxuan/module-autologin)](https://packagist.org/packages/diepxuan/module-autologin) +[![Magento 2](https://img.shields.io/badge/Magento-%3E=2.4-blue.svg)](https://github.com/magento/magento2) +[![Downloads](https://img.shields.io/packagist/dt/diepxuan/module-autologin)](https://packagist.org/packages/diepxuan/module-autologin) +[![License](https://img.shields.io/packagist/l/diepxuan/module-autologin)](https://packagist.org/packages/diepxuan/module-autologin) + +- **Auto-login:** Provides automatic logins for administrators. + + +Autologin +-------------- + +This extension amend the default authentication of Magento, it use for developer, whom do not want use time for login to administrators + + +Installation +------------ + +### Step 1 : Download Magento 2 Autologin Extension + +The easiest way to install the extension is to use [Composer](https://getcomposer.org/) + +Run the following commands: + +``` +composer require diepxuan/module-autologin +bin/magento module:enable Diepxuan_Autologin +bin/magento setup:upgrade && bin/magento setup:static-content:deploy +``` + +### Step 2: General configuration + +`Login to Magento admin > Stores > Configuration > ADVANCED > Admin > Auto Login > Enable Autologin in Admin > Choose Yes to enable the module.` + +After you finish configuring, save and clear the cache. +Run the following command: + +``` +php bin/magento cache:clean +``` diff --git a/app/code/Diepxuan/Autologin/composer.json b/app/code/Diepxuan/Autologin/composer.json new file mode 100644 index 0000000000000..d77c50a898497 --- /dev/null +++ b/app/code/Diepxuan/Autologin/composer.json @@ -0,0 +1,23 @@ +{ + "name": "diepxuan/module-autologin", + "description": "Autologin module for Magento 2", + "type": "magento2-module", + "license": "MIT", + "authors": [ + { + "name": "Tran Ngoc Duc", + "email": "ductn@diepxuan.com" + } + ], + "require": { + "diepxuan/module-magento": "*" + }, + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Diepxuan\\Autologin\\": "" + } + } +} diff --git a/app/code/Diepxuan/Autologin/etc/adminhtml/di.xml b/app/code/Diepxuan/Autologin/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..f6d61adbc2d49 --- /dev/null +++ b/app/code/Diepxuan/Autologin/etc/adminhtml/di.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/code/Diepxuan/Autologin/etc/adminhtml/system.xml b/app/code/Diepxuan/Autologin/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..4a407709d419d --- /dev/null +++ b/app/code/Diepxuan/Autologin/etc/adminhtml/system.xml @@ -0,0 +1,31 @@ + + + + +
+ + diepxuan + Diepxuan_Autologin::config + + + + + + Magento\Config\Model\Config\Source\Yesno + + + + Diepxuan\Autologin\Model\Config\Source\AuthenticationUser + + 1 + + + +
+
+
diff --git a/app/code/Diepxuan/Autologin/etc/config.xml b/app/code/Diepxuan/Autologin/etc/config.xml new file mode 100644 index 0000000000000..c3cf8bc7d7d53 --- /dev/null +++ b/app/code/Diepxuan/Autologin/etc/config.xml @@ -0,0 +1,20 @@ + + + + + + + 1 + admin + + + 5184000 + + + + diff --git a/app/code/Diepxuan/Autologin/etc/module.xml b/app/code/Diepxuan/Autologin/etc/module.xml new file mode 100644 index 0000000000000..52881998c0078 --- /dev/null +++ b/app/code/Diepxuan/Autologin/etc/module.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/app/code/Diepxuan/Autologin/registration.php b/app/code/Diepxuan/Autologin/registration.php new file mode 100644 index 0000000000000..2fee36cadde4d --- /dev/null +++ b/app/code/Diepxuan/Autologin/registration.php @@ -0,0 +1,11 @@ + + */ + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Diepxuan_Autologin', + __DIR__ +); diff --git a/app/code/Diepxuan/Dbunsupport/LICENSE b/app/code/Diepxuan/Dbunsupport/LICENSE new file mode 100644 index 0000000000000..f288702d2fa16 --- /dev/null +++ b/app/code/Diepxuan/Dbunsupport/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/app/code/Diepxuan/Dbunsupport/README.md b/app/code/Diepxuan/Dbunsupport/README.md new file mode 100644 index 0000000000000..54a691db25538 --- /dev/null +++ b/app/code/Diepxuan/Dbunsupport/README.md @@ -0,0 +1,24 @@ +Allow un-support for Magento 2 +================== +[![Packagist](https://img.shields.io/packagist/v/diepxuan/module-dbunsupport)](https://packagist.org/packages/diepxuan/module-dbunsupport) +[![Magento 2](https://img.shields.io/badge/Magento-%3E=2.4-blue.svg)](https://github.com/magento/magento2) +[![Downloads](https://img.shields.io/packagist/dt/diepxuan/module-dbunsupport)](https://packagist.org/packages/diepxuan/module-dbunsupport) +[![License](https://img.shields.io/packagist/l/diepxuan/module-dbunsupport)](https://packagist.org/packages/diepxuan/module-dbunsupport) + + +Allow Magento 2 un-support +-------------- + +Override Module to allow MariaDB 10.5 to 10.9 support for development + + +Installation +------------ + +The easiest way to install the extension is to use [Composer](https://getcomposer.org/) + +Run the following commands: + +- ```$ composer require diepxuan/module-dbunsupport``` +- ```$ bin/magento module:enable Diepxuan_Dbunsupport``` +- ```$ bin/magento setup:upgrade && bin/magento setup:static-content:deploy``` diff --git a/app/code/Diepxuan/Dbunsupport/composer.json b/app/code/Diepxuan/Dbunsupport/composer.json new file mode 100644 index 0000000000000..6050cef797052 --- /dev/null +++ b/app/code/Diepxuan/Dbunsupport/composer.json @@ -0,0 +1,24 @@ +{ + "name": "diepxuan/module-dbunsupport", + "description": "Override Module to allow MariaDB 10.11 support for development", + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "authors": [ + { + "name": "Tran Ngoc Duc", + "email": "ductn@diepxuan.com" + } + ], + "require": {}, + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Diepxuan\\Dbunsupport\\": "" + } + } +} diff --git a/app/code/Diepxuan/Dbunsupport/etc/di.xml b/app/code/Diepxuan/Dbunsupport/etc/di.xml new file mode 100644 index 0000000000000..47f26100b5b7e --- /dev/null +++ b/app/code/Diepxuan/Dbunsupport/etc/di.xml @@ -0,0 +1,13 @@ + + + + + + ^8\.0\. + ^5\.7\. + ^10\.[2-9]\. + ^10\.1[0-1]\. + + + + diff --git a/app/code/Diepxuan/Dbunsupport/etc/module.xml b/app/code/Diepxuan/Dbunsupport/etc/module.xml new file mode 100644 index 0000000000000..0a1613e90d1bc --- /dev/null +++ b/app/code/Diepxuan/Dbunsupport/etc/module.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/code/Diepxuan/Dbunsupport/registration.php b/app/code/Diepxuan/Dbunsupport/registration.php new file mode 100644 index 0000000000000..72088fb869793 --- /dev/null +++ b/app/code/Diepxuan/Dbunsupport/registration.php @@ -0,0 +1,21 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-12-04 23:36:01 + */ + +use Magento\Framework\Component\ComponentRegistrar; + +// Standard Registration Module +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Diepxuan_Dbunsupport', + __DIR__ +); diff --git a/app/code/Diepxuan/EAVCleaner/.gitignore b/app/code/Diepxuan/EAVCleaner/.gitignore new file mode 100644 index 0000000000000..abe6d79fedbbd --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/.gitignore @@ -0,0 +1,45 @@ +#--------------------------# +# Magento Default Files # +#--------------------------# + +/PATCH_*.sh + +/app/etc/local.xml + +/media/* +!/media/.htaccess + +!/media/customer +/media/customer/* +!/media/customer/.htaccess + +!/media/dhl +/media/dhl/* +!/media/dhl/logo.jpg + +!/media/downloadable +/media/downloadable/* +!/media/downloadable/.htaccess + +!/media/xmlconnect +/media/xmlconnect/* + +!/media/xmlconnect/custom +/media/xmlconnect/custom/* +!/media/xmlconnect/custom/ok.gif + +!/media/xmlconnect/original +/media/xmlconnect/original/* +!/media/xmlconnect/original/ok.gif + +!/media/xmlconnect/system +/media/xmlconnect/system/* +!/media/xmlconnect/system/ok.gif + +/var/* +!/var/.htaccess + +!/var/package +/var/package/* +!/var/package/*.xml + diff --git a/app/code/Diepxuan/EAVCleaner/.php-cs-fixer.dist.php b/app/code/Diepxuan/EAVCleaner/.php-cs-fixer.dist.php new file mode 100644 index 0000000000000..3c9d99935f78d --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/.php-cs-fixer.dist.php @@ -0,0 +1,31 @@ + + * Tran Ngoc Duc + */ + +if (!function_exists('import_url')) { + function import_url($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, 0); + $data = curl_exec($ch); + curl_close($ch); + + return $data; + } +} + +date_default_timezone_set('Asia/Ho_Chi_Minh'); +$config = import_url('https://raw.githubusercontent.com/diepxuan/php/main/.php-cs-fixer.dist.php'); +$config = str_replace(' + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-28 17:33:40 + */ + +namespace Diepxuan\EAVCleaner\Console\Command; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class EavAttributesRestoreDefault extends Command +{ + /** + * @var AdapterInterface + */ + protected $connection; + + /** + * @var InputInterface + */ + protected $input; + + /** + * @var OutputInterface + */ + protected $output; + + public function __construct( + ResourceConnection $resourceConnection, + ) { + $this->connection = $resourceConnection->getConnection(); + parent::__construct(); + } + + /** + * Executes the current command. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return void; + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + array_map(function ($tableName): void { + // $this->output("[✔] Imported {$sCategory->category->sku} ({$index}/{$total})"); + $this->output("[i] Clean from table {$tableName}"); + $this->tableCleaner($tableName); + }, ['varchar', 'int', 'decimal', 'text', 'datetime']); + + return 0; + } + + /** + * Console output. + * + * @param mixed $str + */ + public function output(string $str): void + { + $this->output->writeln($str); + } + + /** + * Init command. + */ + protected function configure(): void + { + $this + ->setName('eav:attributes:restoredefault') + ->setDescription("Restore product(s) attribute 'Use Default Value'.") + ; + } + + private function cleanerAttribute($fullTableName, $attribute): void + { + extract($attribute); + $this->connection->query("DELETE FROM {$fullTableName} WHERE value_id = {$value_id}"); + $this->output( + <<✔] Deleted value {$value_id} + * value {$value} + * attribute {$attribute_id} + * table {$fullTableName} + EOF + ); + } + + private function tableCleaner($tableName): void + { + $fullTableName = $this->connection->getTableName('catalog_product_entity_' . $tableName); + $rows = $this->connection->fetchAll('SELECT * FROM ' . $fullTableName . ' WHERE store_id != 0'); + + array_map(function ($attribute) use ($fullTableName): void { + $this->cleanerAttribute($fullTableName, $attribute); + }, $rows); + } +} diff --git a/app/code/Diepxuan/EAVCleaner/LICENSE b/app/code/Diepxuan/EAVCleaner/LICENSE new file mode 100644 index 0000000000000..b7a98a09a86e6 --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 DXVN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/code/Diepxuan/EAVCleaner/README.md b/app/code/Diepxuan/EAVCleaner/README.md new file mode 100644 index 0000000000000..ae150adaf51b7 --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/README.md @@ -0,0 +1,26 @@ +Magento 2 module +================== +[![Magento 2](https://img.shields.io/badge/Magento-%3E=2.4-blue.svg)](https://github.com/magento/magento2) +[![Packagist](https://img.shields.io/packagist/v/diepxuan/module-eavcleaner)](https://packagist.org/packages/diepxuan/module-eavcleaner) +[![Downloads](https://img.shields.io/packagist/dt/diepxuan/module-eavcleaner)](https://packagist.org/packages/diepxuan/module-eavcleaner) +[![License](https://img.shields.io/packagist/l/diepxuan/module-eavcleaner)](https://packagist.org/packages/diepxuan/module-eavcleaner) + +EAV Cleaner Console +------------------- +* Provide EAV cleanup. + +Commands +-------- + +* `eav:attributes:restoredefault` Remove the storeview product attribute values. + +Installation +------------ + +The easiest way to install the extension is to use [Composer](https://getcomposer.org/) + +Run the following commands: + +- ```$ composer require diepxuan/module-eavcleaner``` +- ```$ bin/magento module:enable Diepxuan_EAVCleaner``` +- ```$ bin/magento setup:upgrade && bin/magento setup:static-content:deploy``` \ No newline at end of file diff --git a/app/code/Diepxuan/EAVCleaner/composer.json b/app/code/Diepxuan/EAVCleaner/composer.json new file mode 100644 index 0000000000000..3856ace2ccedb --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/composer.json @@ -0,0 +1,20 @@ +{ + "name": "diepxuan/module-eavcleaner", + "description": "Magento 2 EAV Cleaner", + "type": "magento2-module", + "license": "MIT", + "authors": [ + { + "name": "Tran Ngoc Duc", + "email": "ductn@diepxuan.com" + } + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Diepxuan\\EAVCleaner\\": "" + } + } +} diff --git a/app/code/Diepxuan/EAVCleaner/etc/di.xml b/app/code/Diepxuan/EAVCleaner/etc/di.xml new file mode 100644 index 0000000000000..1cc1c6535f9c2 --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/etc/di.xml @@ -0,0 +1,10 @@ + + + + + + Diepxuan\EAVCleaner\Console\Command\EavAttributesRestoreDefault + + + + diff --git a/app/code/Diepxuan/EAVCleaner/etc/module.xml b/app/code/Diepxuan/EAVCleaner/etc/module.xml new file mode 100644 index 0000000000000..6dbb85f1a984c --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/etc/module.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/code/Diepxuan/EAVCleaner/registration.php b/app/code/Diepxuan/EAVCleaner/registration.php new file mode 100644 index 0000000000000..34692c7ecc61d --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/registration.php @@ -0,0 +1,20 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-28 09:32:01 + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Diepxuan_EAVCleaner', + __DIR__ +); diff --git a/app/code/Diepxuan/GoogleMaps/.gitignore b/app/code/Diepxuan/GoogleMaps/.gitignore new file mode 100644 index 0000000000000..abe6d79fedbbd --- /dev/null +++ b/app/code/Diepxuan/GoogleMaps/.gitignore @@ -0,0 +1,45 @@ +#--------------------------# +# Magento Default Files # +#--------------------------# + +/PATCH_*.sh + +/app/etc/local.xml + +/media/* +!/media/.htaccess + +!/media/customer +/media/customer/* +!/media/customer/.htaccess + +!/media/dhl +/media/dhl/* +!/media/dhl/logo.jpg + +!/media/downloadable +/media/downloadable/* +!/media/downloadable/.htaccess + +!/media/xmlconnect +/media/xmlconnect/* + +!/media/xmlconnect/custom +/media/xmlconnect/custom/* +!/media/xmlconnect/custom/ok.gif + +!/media/xmlconnect/original +/media/xmlconnect/original/* +!/media/xmlconnect/original/ok.gif + +!/media/xmlconnect/system +/media/xmlconnect/system/* +!/media/xmlconnect/system/ok.gif + +/var/* +!/var/.htaccess + +!/var/package +/var/package/* +!/var/package/*.xml + diff --git a/app/code/Diepxuan/GoogleMaps/.php-cs-fixer.dist.php b/app/code/Diepxuan/GoogleMaps/.php-cs-fixer.dist.php new file mode 100644 index 0000000000000..3c9d99935f78d --- /dev/null +++ b/app/code/Diepxuan/GoogleMaps/.php-cs-fixer.dist.php @@ -0,0 +1,31 @@ + + * Tran Ngoc Duc + */ + +if (!function_exists('import_url')) { + function import_url($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, 0); + $data = curl_exec($ch); + curl_close($ch); + + return $data; + } +} + +date_default_timezone_set('Asia/Ho_Chi_Minh'); +$config = import_url('https://raw.githubusercontent.com/diepxuan/php/main/.php-cs-fixer.dist.php'); +$config = str_replace(' + + + + + + diff --git a/app/code/Diepxuan/GoogleMaps/etc/module.xml b/app/code/Diepxuan/GoogleMaps/etc/module.xml new file mode 100644 index 0000000000000..15ea7f4d60b4b --- /dev/null +++ b/app/code/Diepxuan/GoogleMaps/etc/module.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Diepxuan/GoogleMaps/registration.php b/app/code/Diepxuan/GoogleMaps/registration.php new file mode 100644 index 0000000000000..5f174f14d116a --- /dev/null +++ b/app/code/Diepxuan/GoogleMaps/registration.php @@ -0,0 +1,7 @@ + + * Tran Ngoc Duc + */ + +if (!function_exists('import_url')) { + function import_url($url) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, 0); + $data = curl_exec($ch); + curl_close($ch); + + return $data; + } +} + +date_default_timezone_set('Asia/Ho_Chi_Minh'); +$config = import_url('https://raw.githubusercontent.com/diepxuan/php/main/.php-cs-fixer.dist.php'); +$config = str_replace(' + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 12:49:36 + */ + +namespace Diepxuan\Images\Backend\Block\Media; + +use Magento\Backend\Block\Media\Uploader as OriginUploader; + +/** + * Adminhtml media library uploader. + * + * @api + * + * @since 100.0.2 + */ +class Uploader extends OriginUploader +{ + /** + * Initialize block. + */ + protected function _construct(): void + { + parent::_construct(); + + $this->setId($this->getId() . '_Uploader'); + + $uploadUrl = $this->_urlBuilder->getUrl('adminhtml/*/upload'); + $this->getConfig()->setUrl($uploadUrl); + $this->getConfig()->setParams(['form_key' => $this->getFormKey()]); + $this->getConfig()->setFileField('file'); + $this->getConfig()->setFilters( + [ + 'images' => [ + 'label' => __('Images (.gif, .jpg, .png .svg .webp)'), + 'files' => ['*.gif', '*.jpg', '*.png', '*.svg', '*.webp'], + ], + 'media' => [ + 'label' => __('Media (.avi, .flv, .swf)'), + 'files' => ['*.avi', '*.flv', '*.swf'], + ], + 'all' => ['label' => __('All Files'), 'files' => ['*.*']], + ] + ); + } +} diff --git a/app/code/Diepxuan/Images/Catalog/Model/ResourceModel/Product/Attribute/Backend/Image.php b/app/code/Diepxuan/Images/Catalog/Model/ResourceModel/Product/Attribute/Backend/Image.php new file mode 100644 index 0000000000000..1e63a6187e519 --- /dev/null +++ b/app/code/Diepxuan/Images/Catalog/Model/ResourceModel/Product/Attribute/Backend/Image.php @@ -0,0 +1,96 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 14:32:21 + */ + +namespace Diepxuan\Images\Catalog\Model\ResourceModel\Product\Attribute\Backend; + +use Diepxuan\Images\Model\Extension; +use Magento\Catalog\Model\ResourceModel\Product\Attribute\Backend\Image as OriginImage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\DataObject; +use Magento\Framework\Filesystem; +use Magento\MediaStorage\Model\File\Uploader; +use Magento\MediaStorage\Model\File\UploaderFactory; + +class Image extends OriginImage +{ + /** + * Filesystem facade. + * + * @var Filesystem + */ + protected $_filesystem; + + /** + * File Uploader factory. + * + * @var UploaderFactory + */ + protected $_fileUploaderFactory; + + /** + * @var Extension + */ + private $extension; + + public function __construct( + Filesystem $filesystem, + UploaderFactory $fileUploaderFactory, + Extension $extension + ) { + parent::__construct($filesystem, $fileUploaderFactory); + $this->extension = $extension; + } + + /** + * After save. + * + * @param DataObject $object + * + * @return $this|void + */ + public function afterSave($object) + { + $value = $object->getData($this->getAttribute()->getName()); + + if (\is_array($value) && !empty($value['delete'])) { + $object->setData($this->getAttribute()->getName(), ''); + $this->getAttribute()->getEntity()->saveAttribute($object, $this->getAttribute()->getName()); + + return; + } + + try { + /** @var Uploader $uploader */ + $uploader = $this->_fileUploaderFactory->create(['fileId' => $this->getAttribute()->getName()]); + $uploader->setAllowedExtensions(array_merge(['jpg', 'jpeg', 'gif', 'png'], $this->extension->getAllowedExtensions())); + $uploader->setAllowRenameFiles(true); + $uploader->setFilesDispersion(true); + } catch (\Exception $e) { + return $this; + } + $path = $this->_filesystem->getDirectoryRead( + DirectoryList::MEDIA + )->getAbsolutePath( + 'catalog/product/' + ); + $uploader->save($path); + + $fileName = $uploader->getUploadedFileName(); + if ($fileName) { + $object->setData($this->getAttribute()->getName(), $fileName); + $this->getAttribute()->getEntity()->saveAttribute($object, $this->getAttribute()->getName()); + } + + return $this; + } +} diff --git a/app/code/Diepxuan/Images/Config/Model/Config/Backend/Image.php b/app/code/Diepxuan/Images/Config/Model/Config/Backend/Image.php new file mode 100644 index 0000000000000..b39db63863ee4 --- /dev/null +++ b/app/code/Diepxuan/Images/Config/Model/Config/Backend/Image.php @@ -0,0 +1,42 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-12-04 22:34:14 + */ + +namespace Diepxuan\Images\Config\Model\Config\Backend; + +use Diepxuan\Images\Model\Extension; +use Magento\Config\Model\Config\Backend\Image as OriginImage; + +class Image extends OriginImage +{ + private $extension; + + protected function getExtension(): Extension + { + $this->extension = $this->extension ?: new Extension(); + + return $this->extension; + } + + /** + * Getter for allowed extensions of uploaded files. + * + * @return string[] + */ + protected function _getAllowedExtensions() + { + return array_merge( + parent::_getAllowedExtensions(), + $this->getExtension()->getAllowedExtensions(), + ); + } +} diff --git a/app/code/Diepxuan/Images/Config/Model/Config/Backend/Image/Favicon.php b/app/code/Diepxuan/Images/Config/Model/Config/Backend/Image/Favicon.php new file mode 100644 index 0000000000000..f5236496e2e0f --- /dev/null +++ b/app/code/Diepxuan/Images/Config/Model/Config/Backend/Image/Favicon.php @@ -0,0 +1,75 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 16:50:10 + */ + +namespace Diepxuan\Images\Config\Model\Config\Backend\Image; + +use Diepxuan\Images\Model\Extension; +use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; +use Magento\Config\Model\Config\Backend\Image\Favicon as OriginFavicon; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Filesystem; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\MediaStorage\Model\File\UploaderFactory; + +class Favicon extends OriginFavicon +{ + /** + * @var Extension + */ + private $extension; + + public function __construct( + Context $context, + Registry $registry, + ScopeConfigInterface $config, + TypeListInterface $cacheTypeList, + UploaderFactory $uploaderFactory, + RequestDataInterface $requestData, + Filesystem $filesystem, + ?AbstractResource $resource, + ?AbstractDb $resourceCollection, + array $data, + Extension $extension + ) { + parent::__construct( + $context, + $registry, + $config, + $cacheTypeList, + $uploaderFactory, + $requestData, + $filesystem, + $resource, + $resourceCollection, + $data + ); + $this->extension = $extension; + } + + /** + * Getter for allowed extensions of uploaded files. + * + * @return string[] + */ + protected function _getAllowedExtensions() + { + return array_merge( + parent::_getAllowedExtensions(), + $this->extension->getVectorExtensions(), + ); + } +} diff --git a/app/code/Diepxuan/Images/Framework/Api/ImageProcessor.php b/app/code/Diepxuan/Images/Framework/Api/ImageProcessor.php new file mode 100644 index 0000000000000..f5308a797f469 --- /dev/null +++ b/app/code/Diepxuan/Images/Framework/Api/ImageProcessor.php @@ -0,0 +1,63 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 16:08:48 + */ + +namespace Diepxuan\Images\Framework\Api; + +use Diepxuan\Images\Model\Extension; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Api\ImageContentValidatorInterface; +use Magento\Framework\Api\ImageProcessor as OriginImageProcessor; +use Magento\Framework\Api\Uploader; +use Magento\Framework\Filesystem; +use Psr\Log\LoggerInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ImageProcessor extends OriginImageProcessor +{ + /** + * @var Extension + */ + protected $extension; + + public function __construct( + Filesystem $fileSystem, + ImageContentValidatorInterface $contentValidator, + DataObjectHelper $dataObjectHelper, + LoggerInterface $logger, + Uploader $uploader, + Extension $extension + ) { + parent::__construct( + $fileSystem, + $contentValidator, + $dataObjectHelper, + $logger, + $uploader + ); + $this->extension = $extension; + } + + /** + * Get mime type extension. + * + * @param string $mimeType + * + * @return string + */ + protected function getMimeTypeExtension($mimeType) + { + return $this->extension->getAllowedMimeType($mimeType) ?? ''; + } +} diff --git a/app/code/Diepxuan/Images/Framework/Image/Adapter/AbstractAdapter.php b/app/code/Diepxuan/Images/Framework/Image/Adapter/AbstractAdapter.php new file mode 100644 index 0000000000000..2669837ce03a9 --- /dev/null +++ b/app/code/Diepxuan/Images/Framework/Image/Adapter/AbstractAdapter.php @@ -0,0 +1,194 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-23 16:15:52 + */ + +namespace Diepxuan\Images\Framework\Image\Adapter; + +use Diepxuan\Images\Model\Extension; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Psr\Log\LoggerInterface; + +/** + * Image abstract adapter. + * + * @api + * + * @SuppressWarnings(PHPMD.TooManyFields) + */ +abstract class AbstractAdapter extends \Magento\Framework\Image\Adapter\AbstractAdapter +{ + /** + * @var Extension + */ + protected $extension; + + /** + * Initialize default values. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function __construct( + Filesystem $filesystem, + LoggerInterface $logger, + Extension $extension, + array $data = [], + ) { + $this->_filesystem = $filesystem; + $this->logger = $logger; + $this->directoryWrite = $this->_filesystem->getDirectoryWrite(DirectoryList::ROOT); + $this->extension = $extension; + } + + /** + * Open image for processing. + * + * @param string $fileName + */ + abstract public function open($fileName): void; + + /** + * Save image to specific path. + * + * If some folders of the path do not exist they will be created. + * + * @param null|string $destination + * @param null|string $newName + * + * @throws \Exception If destination path is not writable + */ + abstract public function save($destination = null, $newName = null): void; + + /** + * Render image and return its binary contents. + * + * @return string + */ + abstract public function getImage(); + + /** + * Change the image size. + * + * @param null|int $width + * @param null|int $height + */ + abstract public function resize($width = null, $height = null): void; + + /** + * Rotate image on specific angle. + * + * @param int $angle + */ + abstract public function rotate($angle): void; + + /** + * Crop image. + * + * @param int $top + * @param int $left + * @param int $right + * @param int $bottom + * + * @return bool + */ + abstract public function crop($top = 0, $left = 0, $right = 0, $bottom = 0); + + /** + * Add watermark to image. + * + * @param string $imagePath + * @param int $positionX + * @param int $positionY + * @param int $opacity + * @param bool $tile + */ + abstract public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = 30, $tile = false): void; + + /** + * Checks required dependencies. + * + * @throws \Exception If some of dependencies are missing + */ + abstract public function checkDependencies(): void; + + /** + * Create Image from string. + * + * @param string $text + * @param string $font Path to font file + * + * @return AbstractAdapter + */ + abstract public function createPngFromString($text, $font = ''); + + /** + * Reassign image dimensions. + */ + abstract public function refreshImageDimensions(): void; + + /** + * Returns rgba array of the specified pixel. + * + * @param int $x + * @param int $y + * + * @return array + */ + abstract public function getColorAt($x, $y); + + /** + * Return supported image formats. + * + * @return string[] + */ + public function getSupportedFormats() + { + return array_merge(parent::getSupportedFormats(), $this->extension->getAllowedExtensions()); + } + + /** + * Check - is this file an image. + * + * @param string $filePath + * + * @return bool + */ + public function validateUploadFile($filePath) + { + return $this->extension->isVectorImage($filePath) || parent::validateUploadFile($filePath); + } + + /** + * Adapt resize values based on image configuration. + * + * @param int $frameWidth + * @param int $frameHeight + * + * @return array + * + * @throws \Exception + */ + protected function _adaptResizeValues($frameWidth, $frameHeight) + { + $dims = parent::_adaptResizeValues($frameWidth, $frameHeight); + foreach ($dims as $dimKey => $dim) { + foreach ($dim as $sizeKey => $size) { + $dims[$dimKey][$sizeKey] = (int) $size; + } + } + + return $dims; + } +} diff --git a/app/code/Diepxuan/Images/Framework/Image/Adapter/Gd2.php b/app/code/Diepxuan/Images/Framework/Image/Adapter/Gd2.php new file mode 100644 index 0000000000000..7162b67326b71 --- /dev/null +++ b/app/code/Diepxuan/Images/Framework/Image/Adapter/Gd2.php @@ -0,0 +1,1007 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-23 15:27:06 + */ + +namespace Diepxuan\Images\Framework\Image\Adapter; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Phrase; + +/** + * Gd2 adapter. + * + * Class is a copy of \Magento\Framework\Image\Adapter\Gd2 + * var $_callbacks add IMAGETYPE_WEBP + * function _getTransparency IMAGETYPE_WEBP isTrueColor + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ +class Gd2 extends AbstractAdapter +{ + /** + * @var array + */ + protected $_requiredExtensions = ['gd']; + + /** + * Whether image was resized or not. + * + * @var bool + */ + protected $_resized = false; + + /** + * Image output callbacks by type. + * + * @var array + */ + private static $_callbacks = [ + IMAGETYPE_GIF => ['output' => 'imagegif', 'create' => 'imagecreatefromgif'], + IMAGETYPE_JPEG => ['output' => 'imagejpeg', 'create' => 'imagecreatefromjpeg'], + IMAGETYPE_PNG => ['output' => 'imagepng', 'create' => 'imagecreatefrompng'], + IMAGETYPE_XBM => ['output' => 'imagexbm', 'create' => 'imagecreatefromxbm'], + IMAGETYPE_WBMP => ['output' => 'imagewbmp', 'create' => 'imagecreatefromxbm'], + IMAGETYPE_WEBP => ['output' => 'imagewebp', 'create' => 'imagecreatefromwebp'], + ]; + + /** + * Standard destructor. Destroy stored information about image. + */ + public function __destruct() + { + $this->imageDestroy(); + } + + /** + * Open image for processing. + * + * @param string $filename + * + * @throws FileSystemException|\OverflowException + */ + public function open($filename): void + { + if (null === $filename || !file_exists($filename)) { + throw new FileSystemException( + new Phrase('File "%1" does not exist.', [$filename]) + ); + } + if (!$filename || 0 === filesize($filename) || !$this->validateURLScheme($filename)) { + throw new \InvalidArgumentException('Wrong file'); + } + $this->_fileName = $filename; + $this->_reset(); + $this->getMimeType(); + $this->_getFileAttributes(); + if ($this->_isMemoryLimitReached()) { + throw new \OverflowException('Memory limit has been reached.'); + } + $this->imageDestroy(); + $this->_imageHandler = \call_user_func( + $this->_getCallback('create', null, sprintf('Unsupported image format. File: %s', $this->_fileName)), + $this->_fileName + ); + } + + /** + * Save image to specific path. + * + * If some folders of path does not exist they will be created + * + * @param null|string $destination + * @param null|string $newName + * + * @throws \Exception If destination path is not writable + */ + public function save($destination = null, $newName = null): void + { + $fileName = $this->_prepareDestination($destination, $newName); + + if (!$this->_resized) { + // keep alpha transparency + $isAlpha = false; + $isTrueColor = false; + $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha, $isTrueColor); + if ($isAlpha) { + if ($isTrueColor) { + $newImage = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight); + } else { + $newImage = imagecreate($this->_imageSrcWidth, $this->_imageSrcHeight); + } + $this->fillBackgroundColor($newImage); + imagecopy($newImage, $this->_imageHandler, 0, 0, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight); + $this->imageDestroy(); + $this->_imageHandler = $newImage; + } + } + + // Enable interlace + imageinterlace($this->_imageHandler, true); + + // Set image quality value + switch ($this->_fileType) { + case IMAGETYPE_PNG: + $quality = 9; // For PNG files compression level must be from 0 (no compression) to 9. + + break; + + case IMAGETYPE_JPEG: + $quality = $this->quality(); + + break; + + default: + $quality = null; // No compression. + } + + // Prepare callback method parameters + $functionParameters = [$this->_imageHandler, $fileName]; + if ($quality) { + $functionParameters[] = $quality; + } + + \call_user_func_array($this->_getCallback('output'), $functionParameters); + } + + /** + * Render image and return its binary contents. + * + * @see \Magento\Framework\Image\Adapter\AbstractAdapter::getImage + * + * @return string + */ + public function getImage() + { + ob_start(); + \call_user_func($this->_getCallback('output'), $this->_imageHandler); + + return ob_get_clean(); + } + + /** + * Gives true for a PNG with alpha, false otherwise. + * + * @param string $fileName + * + * @return bool + */ + public function checkAlpha($fileName) + { + return (\ord(file_get_contents((string) $fileName, false, null, 25, 1)) & 6 & 4) === 4; + } + + /** + * Change the image size. + * + * @param null|int $frameWidth + * @param null|int $frameHeight + */ + public function resize($frameWidth = null, $frameHeight = null): void + { + $dims = $this->_adaptResizeValues($frameWidth, $frameHeight); + + // create new image + $isAlpha = false; + $isTrueColor = false; + $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha, $isTrueColor); + if ($isTrueColor) { + $newImage = imagecreatetruecolor($dims['frame']['width'], $dims['frame']['height']); + } else { + $newImage = imagecreate($dims['frame']['width'], $dims['frame']['height']); + } + + if ($isAlpha) { + $this->_saveAlpha($newImage); + } + + // fill new image with required color + $this->fillBackgroundColor($newImage); + + if ($this->_imageHandler) { + // resample source image and copy it into new frame + imagecopyresampled( + $newImage, + $this->_imageHandler, + $dims['dst']['x'], + $dims['dst']['y'], + $dims['src']['x'], + $dims['src']['y'], + $dims['dst']['width'], + $dims['dst']['height'], + $this->_imageSrcWidth, + $this->_imageSrcHeight + ); + } + $this->imageDestroy(); + $this->_imageHandler = $newImage; + $this->refreshImageDimensions(); + $this->_resized = true; + } + + /** + * Rotate image on specific angle. + * + * @param int $angle + */ + public function rotate($angle): void + { + $rotatedImage = imagerotate($this->_imageHandler, $angle, $this->imageBackgroundColor); + $this->imageDestroy(); + $this->_imageHandler = $rotatedImage; + $this->refreshImageDimensions(); + } + + /** + * Add watermark to image. + * + * @param string $imagePath + * @param int $positionX + * @param int $positionY + * @param int $opacity + * @param bool $tile + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = 30, $tile = false): void + { + [$watermarkSrcWidth, $watermarkSrcHeight, $watermarkFileType] = $this->_getImageOptions($imagePath); + $this->_getFileAttributes(); + $watermark = \call_user_func( + $this->_getCallback('create', $watermarkFileType, 'Unsupported watermark image format.'), + $imagePath + ); + + $merged = false; + + $watermark = $this->createWatermarkBasedOnPosition($watermark, $positionX, $positionY, $merged, $tile); + + imagedestroy($watermark); + $this->refreshImageDimensions(); + } + + /** + * Crop image. + * + * @param int $top + * @param int $left + * @param int $right + * @param int $bottom + * + * @return bool + */ + public function crop($top = 0, $left = 0, $right = 0, $bottom = 0) + { + if (0 === $left && 0 === $top && 0 === $right && 0 === $bottom) { + return false; + } + + $newWidth = $this->_imageSrcWidth - $left - $right; + $newHeight = $this->_imageSrcHeight - $top - $bottom; + + $canvas = imagecreatetruecolor($newWidth, $newHeight); + + if (IMAGETYPE_PNG === $this->_fileType) { + $this->_saveAlpha($canvas); + } + + imagecopyresampled( + $canvas, + $this->_imageHandler, + 0, + 0, + $left, + $top, + $newWidth, + $newHeight, + $newWidth, + $newHeight + ); + $this->imageDestroy(); + $this->_imageHandler = $canvas; + $this->refreshImageDimensions(); + + return true; + } + + /** + * Checks required dependencies. + * + * @throws \RuntimeException If some of dependencies are missing + */ + public function checkDependencies(): void + { + foreach ($this->_requiredExtensions as $value) { + if (!\extension_loaded($value)) { + throw new \RuntimeException("Required PHP extension '{$value}' was not loaded."); + } + } + } + + /** + * Reassign image dimensions. + */ + public function refreshImageDimensions(): void + { + $this->_imageSrcWidth = imagesx($this->_imageHandler); + $this->_imageSrcHeight = imagesy($this->_imageHandler); + } + + /** + * Returns rgba array of the specified pixel. + * + * @param int $x + * @param int $y + * + * @return array + */ + public function getColorAt($x, $y) + { + $colorIndex = imagecolorat($this->_imageHandler, $x, $y); + + return imagecolorsforindex($this->_imageHandler, $colorIndex); + } + + /** + * Create Image from string. + * + * @param string $text + * @param string $font + * + * @return \Magento\Framework\Image\Adapter\AbstractAdapter + */ + public function createPngFromString($text, $font = '') + { + $error = false; + $this->_resized = true; + + try { + $this->_createImageFromTtfText($text, $font); + } catch (\Exception $e) { + $error = true; + } + + if ($error || empty($this->_imageHandler)) { + $this->_createImageFromText($text); + } + + return $this; + } + + /** + * For properties reset, e.g. mimeType caching. + */ + protected function _reset(): void + { + $this->_fileMimeType = null; + $this->_fileType = null; + } + + /** + * Checks whether memory limit is reached. + * + * @return bool + */ + protected function _isMemoryLimitReached() + { + $limit = $this->_convertToByte(\ini_get('memory_limit')); + $requiredMemory = $this->_getImageNeedMemorySize($this->_fileName); + if (-1 === $limit) { + // A limit of -1 means no limit: http://www.php.net/manual/en/ini.core.php#ini.memory-limit + return false; + } + + return memory_get_usage(true) + $requiredMemory > $limit; + } + + /** + * Get image needed memory size. + * + * @param string $file + * + * @return float|int + */ + protected function _getImageNeedMemorySize($file) + { + $imageInfo = getimagesize($file); + if (!isset($imageInfo[0]) || !isset($imageInfo[1])) { + return 0; + } + if (!isset($imageInfo['channels'])) { + // if there is no info about this parameter lets set it for maximum + $imageInfo['channels'] = 4; + } + if (!isset($imageInfo['bits'])) { + // if there is no info about this parameter lets set it for maximum + $imageInfo['bits'] = 8; + } + + return round( + ($imageInfo[0] * $imageInfo[1] * $imageInfo['bits'] * $imageInfo['channels'] / 8 + 2 ** 16) * 1.65 + ); + } + + /** + * Converts memory value (e.g. 64M, 129K) to bytes. + * + * Case insensitive value might be used. + * + * @param string $memoryValue + * + * @return int + */ + protected function _convertToByte($memoryValue) + { + if (false !== stripos($memoryValue, 'G')) { + return (int) $memoryValue * 1_024 ** 3; + } + if (false !== stripos($memoryValue, 'M')) { + return (int) $memoryValue * 1_024 * 1_024; + } + if (false !== stripos($memoryValue, 'K')) { + return (int) $memoryValue * 1_024; + } + + return (int) $memoryValue; + } + + /** + * Create Image using standard font. + * + * @param string $text + */ + protected function _createImageFromText($text): void + { + $width = imagefontwidth($this->_fontSize) * \strlen((string) $text); + $height = imagefontheight($this->_fontSize); + + $this->_createEmptyImage($width, $height); + + $black = imagecolorallocate($this->_imageHandler, 0, 0, 0); + imagestring($this->_imageHandler, $this->_fontSize, 0, 0, $text, $black); + } + + /** + * Create Image using ttf font. + * + * Note: This function requires both the GD library and the FreeType library + * + * @param string $text + * @param string $font + * + * @throws \InvalidArgumentException + */ + protected function _createImageFromTtfText($text, $font): void + { + $boundingBox = imagettfbbox($this->_fontSize, 0, $font, $text); + $width = abs($boundingBox[4] - $boundingBox[0]); + $height = abs($boundingBox[5] - $boundingBox[1]); + + $this->_createEmptyImage($width, $height); + + $black = imagecolorallocate($this->_imageHandler, 0, 0, 0); + $result = imagettftext( + $this->_imageHandler, + $this->_fontSize, + 0, + 0, + $height - $boundingBox[1], + $black, + $font, + $text + ); + if (false === $result) { + throw new \InvalidArgumentException('Unable to create TTF text'); + } + } + + /** + * Create empty image with transparent background. + * + * @param int $width + * @param int $height + */ + protected function _createEmptyImage($width, $height): void + { + $this->_fileType = IMAGETYPE_PNG; + $image = imagecreatetruecolor($width, $height); + $colorWhite = imagecolorallocatealpha($image, 255, 255, 255, 127); + + imagealphablending($image, true); + imagesavealpha($image, true); + + imagefill($image, 0, 0, $colorWhite); + $this->imageDestroy(); + $this->_imageHandler = $image; + } + + /** + * Checks for invalid URL schema if it exists. + */ + private function validateURLScheme(string $filename): bool + { + $allowed_schemes = ['ftp', 'ftps', 'http', 'https']; + $url = parse_url($filename); + if ($url && isset($url['scheme']) && !\in_array($url['scheme'], $allowed_schemes, true)) { + return false; + } + + return true; + } + + /** + * Obtain function name, basing on image type and callback type. + * + * @param string $callbackType + * @param null|int $fileType + * @param string $unsupportedText + * + * @return string + * + * @throws \InvalidArgumentException + * @throws \BadFunctionCallException + */ + private function _getCallback($callbackType, $fileType = null, $unsupportedText = 'Unsupported image format.') + { + if (null === $fileType) { + $fileType = $this->_fileType; + } + if (empty(self::$_callbacks[$fileType])) { + throw new \InvalidArgumentException($unsupportedText); + } + if (empty(self::$_callbacks[$fileType][$callbackType])) { + throw new \BadFunctionCallException('Callback not found.'); + } + + return self::$_callbacks[$fileType][$callbackType]; + } + + /** + * Fill image with main background color. + * + * Returns a color identifier. + * + * @param resource &$imageResourceTo + * + * @throws \InvalidArgumentException + */ + private function fillBackgroundColor(&$imageResourceTo): void + { + // try to keep transparency, if any + if ($this->_keepTransparency) { + $isAlpha = false; + $transparentIndex = $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha); + + try { + // fill true color png with alpha transparency + if ($isAlpha) { + $this->applyAlphaTransparency($imageResourceTo); + + return; + } + + if (false !== $transparentIndex) { + $this->applyTransparency($imageResourceTo, $transparentIndex); + + return; + } + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + } catch (\Exception $e) { + // fallback to default background color + } + } + [$red, $green, $blue] = $this->_backgroundColor ?: [0, 0, 0]; + $color = imagecolorallocate($imageResourceTo, $red, $green, $blue); + + if (!imagefill($imageResourceTo, 0, 0, $color)) { + throw new \InvalidArgumentException("Failed to fill image background with color {$red} {$green} {$blue}."); + } + } + + /** + * Method to apply alpha transparency for image. + * + * @param resource $imageResourceTo + * + * @SuppressWarnings(PHPMD.LongVariable) + */ + private function applyAlphaTransparency(&$imageResourceTo): void + { + if (!imagealphablending($imageResourceTo, false)) { + throw new \InvalidArgumentException('Failed to set alpha blending for PNG image.'); + } + $transparentAlphaColor = imagecolorallocatealpha($imageResourceTo, 0, 0, 0, 127); + + if (false === $transparentAlphaColor) { + throw new \InvalidArgumentException('Failed to allocate alpha transparency for PNG image.'); + } + + if (!imagefill($imageResourceTo, 0, 0, $transparentAlphaColor)) { + throw new \InvalidArgumentException('Failed to fill PNG image with alpha transparency.'); + } + + if (!imagesavealpha($imageResourceTo, true)) { + throw new \InvalidArgumentException('Failed to save alpha transparency into PNG image.'); + } + } + + /** + * Method to apply transparency for image. + * + * @param resource $imageResourceTo + * @param int $transparentIndex + */ + private function applyTransparency(&$imageResourceTo, $transparentIndex): void + { + // fill image with indexed non-alpha transparency + $transparentColor = false; + + if ($transparentIndex >= 0 && $transparentIndex < imagecolorstotal($this->_imageHandler)) { + try { + $colorsForIndex = imagecolorsforindex($this->_imageHandler, $transparentIndex); + [$red, $green, $blue] = array_values($colorsForIndex); + $transparentColor = imagecolorallocate($imageResourceTo, (int) $red, (int) $green, (int) $blue); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch + } catch (\ValueError $e) { + } + } + if (false === $transparentColor) { + throw new \InvalidArgumentException('Failed to allocate transparent color for image.'); + } + if (!imagefill($imageResourceTo, 0, 0, $transparentColor)) { + throw new \InvalidArgumentException('Failed to fill image with transparency.'); + } + imagecolortransparent($imageResourceTo, $transparentColor); + } + + /** + * Checks if image has alpha transparency. + * + * @param resource $imageResource + * @param int $fileType + * @param bool $isAlpha + * @param bool $isTrueColor + * + * @return bool + * + * @SuppressWarnings(PHPMD.BooleanGetMethodName) + */ + private function _getTransparency($imageResource, $fileType, &$isAlpha = false, &$isTrueColor = false) + { + $isAlpha = false; + $isTrueColor = false; + // assume that transparency is supported by gif/png only + if (IMAGETYPE_GIF === $fileType || IMAGETYPE_PNG === $fileType) { + // check for specific transparent color + $transparentIndex = imagecolortransparent($imageResource); + if ($transparentIndex >= 0 && $transparentIndex < imagecolorstotal($imageResource)) { + return $transparentIndex; + } + if (IMAGETYPE_PNG === $fileType) { + // assume that truecolor PNG has transparency + $isAlpha = $this->checkAlpha($this->_fileName); + $isTrueColor = true; + + // -1 + return $transparentIndex; + } + } + if (IMAGETYPE_JPEG === $fileType || IMAGETYPE_WEBP === $fileType) { + $isTrueColor = true; + } + + return false; + } + + /** + * Create watermark based on it's image position. + * + * @param resource $watermark + * + * @return false|resource + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function createWatermarkBasedOnPosition( + $watermark, + int $positionX, + int $positionY, + bool $merged, + bool $tile + ) { + if ($this->getWatermarkWidth() + && $this->getWatermarkHeight() + && self::POSITION_STRETCH !== $this->getWatermarkPosition() + ) { + $watermark = $this->createWaterMark($watermark, $this->getWatermarkWidth(), $this->getWatermarkHeight()); + } + + /* + * Fixes issue with watermark with transparent background and an image that is not truecolor (e.g GIF). + * blending mode is allowed for truecolor images only. + * @see imagealphablending() + */ + if (!imageistruecolor($this->_imageHandler)) { + $newImage = $this->createTruecolorImageCopy(); + $this->imageDestroy(); + $this->_imageHandler = $newImage; + } + + if (self::POSITION_TILE === $this->getWatermarkPosition()) { + $tile = true; + } elseif (self::POSITION_STRETCH === $this->getWatermarkPosition()) { + $watermark = $this->createWaterMark($watermark, $this->_imageSrcWidth, $this->_imageSrcHeight); + } elseif (self::POSITION_CENTER === $this->getWatermarkPosition()) { + $positionX = (int) ($this->_imageSrcWidth / 2 - imagesx($watermark) / 2); + $positionY = (int) ($this->_imageSrcHeight / 2 - imagesy($watermark) / 2); + $this->imagecopymergeWithAlphaFix( + $this->_imageHandler, + $watermark, + $positionX, + $positionY, + 0, + 0, + imagesx($watermark), + imagesy($watermark), + $this->getWatermarkImageOpacity() + ); + } elseif (self::POSITION_TOP_RIGHT === $this->getWatermarkPosition()) { + $positionX = $this->_imageSrcWidth - imagesx($watermark); + $this->imagecopymergeWithAlphaFix( + $this->_imageHandler, + $watermark, + $positionX, + $positionY, + 0, + 0, + imagesx($watermark), + imagesy($watermark), + $this->getWatermarkImageOpacity() + ); + } elseif (self::POSITION_TOP_LEFT === $this->getWatermarkPosition()) { + $this->imagecopymergeWithAlphaFix( + $this->_imageHandler, + $watermark, + $positionX, + $positionY, + 0, + 0, + imagesx($watermark), + imagesy($watermark), + $this->getWatermarkImageOpacity() + ); + } elseif (self::POSITION_BOTTOM_RIGHT === $this->getWatermarkPosition()) { + $positionX = $this->_imageSrcWidth - imagesx($watermark); + $positionY = $this->_imageSrcHeight - imagesy($watermark); + $this->imagecopymergeWithAlphaFix( + $this->_imageHandler, + $watermark, + $positionX, + $positionY, + 0, + 0, + imagesx($watermark), + imagesy($watermark), + $this->getWatermarkImageOpacity() + ); + } elseif (self::POSITION_BOTTOM_LEFT === $this->getWatermarkPosition()) { + $positionY = $this->_imageSrcHeight - imagesy($watermark); + $this->imagecopymergeWithAlphaFix( + $this->_imageHandler, + $watermark, + $positionX, + $positionY, + 0, + 0, + imagesx($watermark), + imagesy($watermark), + $this->getWatermarkImageOpacity() + ); + } + + if (false === $tile && false === $merged) { + $this->imagecopymergeWithAlphaFix( + $this->_imageHandler, + $watermark, + $positionX, + $positionY, + 0, + 0, + imagesx($watermark), + imagesy($watermark), + $this->getWatermarkImageOpacity() + ); + } else { + $offsetX = $positionX; + $offsetY = $positionY; + while ($offsetY <= $this->_imageSrcHeight + imagesy($watermark)) { + while ($offsetX <= $this->_imageSrcWidth + imagesx($watermark)) { + $this->imagecopymergeWithAlphaFix( + $this->_imageHandler, + $watermark, + $offsetX, + $offsetY, + 0, + 0, + imagesx($watermark), + imagesy($watermark), + $this->getWatermarkImageOpacity() + ); + $offsetX += imagesx($watermark); + } + $offsetX = $positionX; + $offsetY += imagesy($watermark); + } + } + + return $watermark; + } + + /** + * Create watermark. + * + * @param resource $watermark + * + * @return false|resource + */ + private function createWaterMark($watermark, string $width, string $height) + { + $newWatermark = imagecreatetruecolor($width, $height); + imagealphablending($newWatermark, false); + $col = imagecolorallocate($newWatermark, 255, 255, 255); + imagecolortransparent($newWatermark, $col); + imagefilledrectangle($newWatermark, 0, 0, $width, $height, $col); + imagesavealpha($newWatermark, true); + imagecopyresampled( + $newWatermark, + $watermark, + 0, + 0, + 0, + 0, + $width, + $height, + imagesx($watermark), + imagesy($watermark) + ); + + return $newWatermark; + } + + /** + * Helper function to free up memory associated with _imageHandler resource. + */ + private function imageDestroy(): void + { + if (\is_resource($this->_imageHandler)) { + imagedestroy($this->_imageHandler); + } + } + + /** + * Fixes saving PNG alpha channel. + * + * @param resource $imageHandler + */ + private function _saveAlpha($imageHandler): void + { + $background = imagecolorallocate($imageHandler, 0, 0, 0); + imagecolortransparent($imageHandler, $background); + imagealphablending($imageHandler, false); + imagesavealpha($imageHandler, true); + } + + /** + * Fix an issue with the usage of imagecopymerge where the alpha channel is lost. + * + * @param resource $dst_im + * @param resource $src_im + * @param int $dst_x + * @param int $dst_y + * @param int $src_x + * @param int $src_y + * @param int $src_w + * @param int $src_h + * @param int $pct + * + * @return bool + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function imagecopymergeWithAlphaFix( + $dst_im, + $src_im, + $dst_x, + $dst_y, + $src_x, + $src_y, + $src_w, + $src_h, + $pct + ) { + if ($pct >= 100) { + if (false === imagealphablending($dst_im, true)) { + return false; + } + + return imagecopy($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); + } + + if ($pct < 0) { + return false; + } + + $sizeX = imagesx($src_im); + $sizeY = imagesy($src_im); + if (false === $sizeX || false === $sizeY) { + return false; + } + + $tmpImg = imagecreatetruecolor($src_w, $src_h); + if (false === $tmpImg) { + return false; + } + + if (false === imagealphablending($tmpImg, false)) { + return false; + } + + if (false === imagesavealpha($tmpImg, true)) { + return false; + } + + if (false === imagecopy($tmpImg, $src_im, 0, 0, 0, 0, $sizeX, $sizeY)) { + return false; + } + + $transparency = (int) (127 - (($pct * 127) / 100)); + if (false === imagefilter($tmpImg, IMG_FILTER_COLORIZE, 0, 0, 0, $transparency)) { + return false; + } + + if (false === imagealphablending($dst_im, true)) { + return false; + } + + if (false === imagesavealpha($dst_im, true)) { + return false; + } + + $result = imagecopy($dst_im, $tmpImg, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h); + imagedestroy($tmpImg); + + return $result; + } + + /** + * Create truecolor image copy of current image. + * + * @return resource + */ + private function createTruecolorImageCopy() + { + $this->_getTransparency($this->_imageHandler, $this->_fileType, $isAlpha); + + $newImage = imagecreatetruecolor($this->_imageSrcWidth, $this->_imageSrcHeight); + + if ($isAlpha) { + $this->_saveAlpha($newImage); + } + + imagecopy($newImage, $this->_imageHandler, 0, 0, 0, 0, $this->_imageSrcWidth, $this->_imageSrcHeight); + + return $newImage; + } +} diff --git a/app/code/Diepxuan/Images/LICENSE b/app/code/Diepxuan/Images/LICENSE new file mode 100644 index 0000000000000..2e71fc73043a2 --- /dev/null +++ b/app/code/Diepxuan/Images/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 DXVN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/code/Diepxuan/Images/Model/Extension.php b/app/code/Diepxuan/Images/Model/Extension.php new file mode 100644 index 0000000000000..6df60a9188384 --- /dev/null +++ b/app/code/Diepxuan/Images/Model/Extension.php @@ -0,0 +1,250 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 16:07:26 + */ + +namespace Diepxuan\Images\Model; + +class Extension +{ + const ACCEPT_FILE_TYPES = '/(\.|\/)(gif|jpe?g|png|svg|webp)$/i'; + + /** + * @var array + */ + protected $webMimeTypes = [ + 'webp' => 'image/webp', + ]; + + /** + * @var array + */ + protected $vectorMimeTypes = [ + 'svg' => 'image/svg+xml', + ]; + + /** + * @var array + */ + protected $defaultMimeTypes = [ + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + ]; + + /** + * @var array + */ + protected $allowedMimeTypes = []; + + public function __construct( + ) { + $this->allowedMimeTypes = array_merge($this->webMimeTypes, $this->vectorMimeTypes, $this->defaultMimeTypes); + } + + /** + * Getter for allowed extensions of uploaded files. + * + * @return string[] + */ + public function getAllowedExtensions() + { + return array_keys($this->allowedMimeTypes); + } + + public function getAllowedExtensionsString() + { + return implode(' ', $this->getAllowedExtensions()); + } + + public function getAllowedExtensionsRegex() + { + return static::ACCEPT_FILE_TYPES; + } + + public function getWebExtensions() + { + return array_keys($this->webMimeTypes); + } + + public function getVectorExtensions() + { + return array_keys($this->vectorMimeTypes); + } + + public function getAllowedMimeType($mimeType) + { + try { + return array_flip($this->getAllowedExtensions())[$mimeType]; + } catch (\Throwable $th) { + // throw $th; + } finally { + return ''; + } + } + + /** + * File is a vector image. + * + * @param mixed $filePath + * + * @return bool + */ + public function isVectorImage($filePath) + { + $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + if (empty($extension) && file_exists($filePath)) { + $mimeType = mime_content_type($filePath); + $extension = str_replace('image/', '', $mimeType); + } + + if (!\in_array($extension, $this->getVectorExtensions(), true)) { + return false; + } + + if (!is_file($filePath)) { + return false; + } + + try { + $xmlReader = new \XMLReader(); + $xmlReader->open($filePath); + if (\XMLReader::ELEMENT === $xmlReader->moveToElement() && 'svg' === strtolower($xmlReader->name)) { + return true; + } + + return false; + } catch (\Throwable $th) { + // throw $th; + + return false; + } finally { + $xmlReader->close(); + } + } + + /** + * File is a webp image. + * + * @param mixed $filePath + * + * @return bool + */ + public function isWebpImage($filePath) + { + $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION)); + if (empty($extension) && file_exists($filePath)) { + $mimeType = mime_content_type($filePath); + $extension = str_replace('image/', '', $mimeType); + } + + if (!\in_array($extension, $this->getWebExtensions(), true)) { + return false; + } + if (!is_file($filePath)) { + return false; + } + + try { + $fp = fopen(realpath($filePath), 'r'); + if (!$fp) { + return false; + } + + $data = fread($fp, 90); + $header_format = 'A4RIFF/' . // get n string + 'I1FILESIZE/' . // get integer (file size but not actual size) + 'A4WEBP/' . // get n string + 'A4VP/' . // get n string + 'A74chunk'; + $header = unpack($header_format, $data); + + if (!isset($header['RIFF']) || 'RIFF' !== strtoupper($header['RIFF'])) { + return false; + } + if (!isset($header['WEBP']) || 'WEBP' !== strtoupper($header['WEBP'])) { + return false; + } + if (!isset($header['VP']) || !str_contains(strtoupper($header['VP']), 'VP8')) { + return false; + } + + if ( + str_contains(strtoupper($header['chunk']), 'ANIM') + || str_contains(strtoupper($header['chunk']), 'ANMF') + ) { + $header['ANIMATION'] = true; + } else { + $header['ANIMATION'] = false; + } + + // check for transparent. + if (str_contains(strtoupper($header['chunk']), 'ALPH')) { + $header['ALPHA'] = true; + } else { + if (str_contains(strtoupper($header['VP']), 'VP8L')) { + // if it is VP8L. + // @link https://developers.google.com/speed/webp/docs/riff_container#simple_file_format_lossless Reference. + $header['ALPHA'] = (bool) ((bool) (\ord($data[24]) & 0x00_00_00_10)); + } elseif (str_contains(strtoupper($header['VP']), 'VP8X')) { + // if it is VP8X. + // @link https://developers.google.com/speed/webp/docs/riff_container#extended_file_format Reference. + // @link https://stackoverflow.com/a/61242086/128761 Original source code. + $header['ALPHA'] = (bool) ((bool) (\ord($data[20]) & 0x00_00_00_10)); + } else { + $header['ALPHA'] = false; + } + } + + // get width & height. + // @link https://developer.wordpress.org/reference/functions/wp_get_webp_info/ Original source code. + if ('VP8' === strtoupper($header['VP'])) { + $parts = unpack('v2', substr($data, 26, 4)); + $header['WIDTH'] = (int) ($parts[1] & 0x3F_FF); + $header['HEIGHT'] = (int) ($parts[2] & 0x3F_FF); + } elseif ('VP8L' === strtoupper($header['VP'])) { + $parts = unpack('C4', substr($data, 21, 4)); + $header['WIDTH'] = (int) (($parts[1] | (($parts[2] & 0x3F) << 8)) + 1); + $header['HEIGHT'] = (int) (((($parts[2] & 0xC0) >> 6) | ($parts[3] << 2) | (($parts[4] & 0x03) << 10)) + 1); + } elseif ('VP8X' === strtoupper($header['VP'])) { + // Pad 24-bit int. + $width = unpack('V', substr($data, 24, 3) . "\x00"); + $header['WIDTH'] = (int) ($width[1] & 0xFF_FF_FF) + 1; + // Pad 24-bit int. + $height = unpack('V', substr($data, 27, 3) . "\x00"); + $header['HEIGHT'] = (int) ($height[1] & 0xFF_FF_FF) + 1; + } + + // return $header; + + return true; + } catch (\Throwable $th) { + // throw $th; + + return false; + } finally { + fclose($fp); + } + } + + /** + * File is a Web image. + * + * @param mixed $filePath + * + * @return bool + */ + public function isWebImage($filePath) + { + return $this->isWebpImage($filePath) || $this->isVectorImage($filePath); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Diepxuan/Images/Plugin/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php new file mode 100644 index 0000000000000..bb16c893197e1 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -0,0 +1,45 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 19:07:46 + */ + +namespace Diepxuan\Images\Plugin\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery; + +use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content as OriginContent; +use Magento\Framework\View\Element\AbstractBlock; + +class Content +{ + /** + * Set layout object. + * + * @return $this + */ + public function afterSetLayout(OriginContent $subject, AbstractBlock $block) + { + try { + $block->getUploader()->getConfig()->setFilters( + [ + 'images' => [ + 'label' => __('Images (.gif, .jpg, .png, .svg, .webp)'), + 'files' => ['*.gif', '*.jpg', '*.jpeg', '*.png', '*.svg' . '*.webp'], + ], + ], + ); + } catch (\Throwable $th) { + // throw $th; + } finally { + return $block; + } + + return $block; + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/MimeTypeExtensionMap.php b/app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/MimeTypeExtensionMap.php new file mode 100644 index 0000000000000..d7cd58198423e --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/MimeTypeExtensionMap.php @@ -0,0 +1,45 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-26 22:08:58 + */ + +namespace Diepxuan\Images\Plugin\Catalog\Model\Product\Gallery; + +use Diepxuan\Images\Model\Extension; +use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap as Origin; + +class MimeTypeExtensionMap +{ + /** + * @var Extension + */ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * @param string $mimeType + * + * @return string + */ + public function aroundGetMimeTypeExtension(Origin $subject, callable $proceed, $mimeType) + { + if (($extension = $this->extension->getAllowedMimeType($mimeType)) !== '') { + return $extension; + } + + return $proceed($mimeType); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/Processor.php b/app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/Processor.php new file mode 100644 index 0000000000000..68b87938a6178 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/Processor.php @@ -0,0 +1,212 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 14:49:59 + */ + +namespace Diepxuan\Images\Plugin\Catalog\Model\Product\Gallery; + +use Diepxuan\Images\Model\Extension; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap as Origin; +use Magento\Catalog\Model\Product\Gallery\Processor as OriginProcessor; +use Magento\Catalog\Model\Product\Media\Config; +use Magento\Catalog\Model\ResourceModel\Product\Gallery; +use Magento\Framework\Api\Data\ImageContentInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\MediaStorage\Model\File\Uploader; + +class Processor extends OriginProcessor +{ /** + * @var Mime + */ + private $mime; + + /** + * @var Extension + */ + private $extension; + + /** + * @throws FileSystemException + */ + public function __construct( + ProductAttributeRepositoryInterface $attributeRepository, + Database $fileStorageDb, + Config $mediaConfig, + Filesystem $filesystem, + Gallery $resourceModel, + ?Mime $mime, + Extension $extension + ) { + $this->mime = $mime ?: ObjectManager::getInstance()->get(Mime::class); + parent::__construct( + $attributeRepository, + $fileStorageDb, + $mediaConfig, + $filesystem, + $resourceModel, + $this->mime + ); + $this->extension = $extension; + } + + public function aroundAddImage( + Origin $subject, + callable $proceed, + Product $product, + $file, + $mediaAttribute = null, + $move = false, + $exclude = true + ) { + try { + $fileName = $this->_addImage( + $product, + $file, + $mediaAttribute, + $move, + $exclude + ); + if (!$fileName) { + throw new LocalizedException(__("The image doesn't exist.")); + } + + return $fileName; + } catch (\Throwable $th) { + // throw $th; + return $proceed( + $product, + $file, + $mediaAttribute, + $move, + $exclude + ); + } + } + + /** + * Add image to media gallery and return new filename. + * + * @param string $file file path of image in file system + * @param string|string[] $mediaAttribute code of attribute with type 'media_image', + * leave blank if image should be only in gallery + * @param bool $move if true, it will move source file + * @param bool $exclude mark image as disabled in product page view + * + * @return string + * + * @throws LocalizedException + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @since 101.0.0 + */ + private function _addImage( + Product $product, + $file, + $mediaAttribute = null, + $move = false, + $exclude = true + ) { + $file = $this->mediaDirectory->getRelativePath($file); + if (!$this->mediaDirectory->isFile($file)) { + throw new LocalizedException(__("The image doesn't exist.")); + } + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $pathinfo = pathinfo($file); + $imgExtensions = array_merge(['jpg', 'jpeg', 'gif', 'png'], $this->extension->getAllowedExtensions()); + if (!isset($pathinfo['extension']) || !\in_array(strtolower($pathinfo['extension']), $imgExtensions, true)) { + throw new LocalizedException( + __('The image type for the file is invalid. Enter the correct image type and try again.') + ); + } + + $fileName = Uploader::getCorrectFileName($pathinfo['basename']); + $dispersionPath = Uploader::getDispersionPath($fileName); + $fileName = $dispersionPath . '/' . $fileName; + + $fileName = $this->getNotDuplicatedFilename($fileName, $dispersionPath); + + $destinationFile = $this->mediaConfig->getTmpMediaPath($fileName); + + try { + /** @var Database $storageHelper */ + $storageHelper = $this->fileStorageDb; + if ($move) { + $this->mediaDirectory->renameFile($file, $destinationFile); + + // If this is used, filesystem should be configured properly + $storageHelper->saveFile($this->mediaConfig->getTmpMediaShortUrl($fileName)); + } else { + $this->mediaDirectory->copyFile($file, $destinationFile); + + $storageHelper->saveFile($this->mediaConfig->getTmpMediaShortUrl($fileName)); + } + } catch (\Exception $e) { + throw new LocalizedException(__('The "%1" file couldn\'t be moved.', $e->getMessage())); + } + + $fileName = str_replace('\\', '/', $fileName); + + $attrCode = $this->getAttribute()->getAttributeCode(); + $mediaGalleryData = $product->getData($attrCode); + $position = 0; + + $absoluteFilePath = $this->mediaDirectory->getAbsolutePath($destinationFile); + $imageMimeType = $this->mime->getMimeType($absoluteFilePath); + $imageContent = $this->mediaDirectory->readFile($absoluteFilePath); + $imageBase64 = base64_encode($imageContent); + $imageName = $pathinfo['filename']; + + if (!\is_array($mediaGalleryData)) { + $mediaGalleryData = ['images' => []]; + } + + foreach ($mediaGalleryData['images'] as &$image) { + if (isset($image['position']) && $image['position'] > $position) { + $position = $image['position']; + } + } + + ++$position; + $mediaGalleryData['images'][] = [ + 'file' => $fileName, + 'position' => $position, + 'label' => '', + 'disabled' => (int) $exclude, + 'media_type' => 'image', + 'types' => $mediaAttribute, + 'content' => [ + 'data' => [ + ImageContentInterface::NAME => $imageName, + ImageContentInterface::BASE64_ENCODED_DATA => $imageBase64, + ImageContentInterface::TYPE => $imageMimeType, + ], + ], + ]; + + $product->setData($attrCode, $mediaGalleryData); + + if (null !== $mediaAttribute) { + $this->setMediaAttribute($product, $mediaAttribute, $fileName); + } + + return $fileName; + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Directive.php b/app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Directive.php new file mode 100644 index 0000000000000..e5e8690ba4057 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Directive.php @@ -0,0 +1,116 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-26 16:34:08 + */ + +namespace Diepxuan\Images\Plugin\Controller\Adminhtml\Wysiwyg; + +use Diepxuan\Images\Model\Extension; +use Magento\Cms\Controller\Adminhtml\Wysiwyg\Directive as OriginDirective; +use Magento\Cms\Model\Template\Filter; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Controller\Result\Raw; +use Magento\Framework\Controller\Result\RawFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Image\AdapterFactory; +use Magento\Framework\Url\DecoderInterface; + +class Directive +{ + /** + * @var DecoderInterface + */ + private $urlDecoder; + + /** + * @var Filter + */ + private $filter; + + /** + * @var RawFactory + */ + private $resultRawFactory; + + /** + * @var AdapterFactory + */ + private $adapterFactory; + + /** + * @var Extension + */ + private $extension; + + /** + * @var null|Filesystem + */ + private $filesystem; + + /** + * DirectivePlugin constructor. + */ + public function __construct( + DecoderInterface $urlDecoder, + Filter $filter, + RawFactory $resultRawFactory, + ?AdapterFactory $adapterFactory, + Extension $extension, + ?Filesystem $filesystem = null + ) { + $this->urlDecoder = $urlDecoder; + $this->filter = $filter; + $this->resultRawFactory = $resultRawFactory; + $this->adapterFactory = $adapterFactory ?: ObjectManager::getInstance()->get(AdapterFactory::class); + $this->extension = $extension; + $this->filesystem = $filesystem ?: ObjectManager::getInstance()->get(Filesystem::class); + } + + /** + * Handle vector images for media storage thumbnails. + * + * @return Raw + */ + public function aroundExecute(OriginDirective $subject, callable $proceed) + { + try { + $directive = $subject->getRequest()->getParam('___directive'); + $directive = $this->urlDecoder->decode($directive); + + /** @var Filter $filter */ + $imagePath = $this->filter->filter($directive); + + if (!$this->extension->isWebImage($imagePath)) { + throw new LocalizedException(__('This image type is not a Web')); + } + + /** @var AdapterInterface $image */ + $image = $this->adapterFactory->create(); + $image->open($imagePath); + + $mimeType = $image->getMimeType(); + $content = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA)->getDriver() + ->fileGetContents($imagePath) + ; + + /** @var Raw $resultRaw */ + $resultRaw = $this->resultRawFactory->create(); + $resultRaw->setHeader('Content-Type', $mimeType); + $resultRaw->setContents($content); + + return $resultRaw; + } catch (\Exception $e) { + return $proceed(); + } + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Images/Thumbnail.php b/app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Images/Thumbnail.php new file mode 100644 index 0000000000000..de8bddd3783f2 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Images/Thumbnail.php @@ -0,0 +1,60 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-26 16:54:08 + */ + +namespace Diepxuan\Images\Plugin\Wysiwyg\Images; + +use Diepxuan\Images\Model\Extension; +use Magento\Cms\Model\Wysiwyg\Images\Storage; + +class Thumbnail +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * Skip resizing vector images. + * + * @param bool $keepRatio + * @param mixed $source + * + * @return mixed + */ + public function aroundResizeFile(Storage $storage, callable $proceed, $source, $keepRatio = true) + { + if ($this->extension->isVectorImage($source)) { + return $source; + } + + return $proceed($source, $keepRatio); + } + + /** + * Return original file path as thumbnail for vector images. + * + * @param false $checkFile + * @param mixed $filePath + */ + public function aroundGetThumbnailPath(Storage $storage, callable $proceed, $filePath, $checkFile = false) + { + if ($this->extension->isVectorImage($filePath)) { + return $filePath; + } + + return $proceed($filePath, $checkFile); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Framework/File/Uploader.php b/app/code/Diepxuan/Images/Plugin/Framework/File/Uploader.php new file mode 100644 index 0000000000000..790f90e6a8107 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Framework/File/Uploader.php @@ -0,0 +1,49 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-26 16:39:39 + */ + +namespace Diepxuan\Images\Plugin\Framework\File; + +use Diepxuan\Images\Model\Extension; +use Magento\Framework\App\Action\Action; +use Magento\Framework\App\Action\Context; +use Magento\Framework\File\Uploader as OriginUploader; + +class Uploader extends Action +{ + private $extension; + + public function __construct( + Context $context, + Extension $extension + ) { + $this->extension = $extension; + parent::__construct($context); + } + + /** + * Set allowed extensions. + * + * @param string[] $extensions + * + * @return $this + */ + public function beforeSetAllowedExtensions(OriginUploader $uploader, $extensions = []) + { + return array_merge( + (array) $extensions, + $this->extension->getAllowedExtensions() + ); + } + + public function execute(): void {} +} diff --git a/app/code/Diepxuan/Images/Plugin/Framework/Filesystem/Io/File.php b/app/code/Diepxuan/Images/Plugin/Framework/Filesystem/Io/File.php new file mode 100644 index 0000000000000..7f3a36c973ec6 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Framework/Filesystem/Io/File.php @@ -0,0 +1,68 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 15:55:02 + */ + +namespace Diepxuan\Images\Plugin\Framework\Filesystem\Io; + +use Diepxuan\Images\Model\Extension; +use Magento\Framework\Filesystem\Io\File as OriginFile; + +class File +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * Get list of cwd subdirectories and files. + * + * Suggestions (from moshe): + * - Use filemtime instead of filectime for performance + * - Change $grep to $flags and use binary flags + * - LS_DIRS = 1 + * - LS_FILES = 2 + * - LS_ALL = 3 + * + * @param mixed $list + * + * @return array + * + * @throws LocalizedException + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ShortMethodName) + */ + public function afterLs(OriginFile $subject, $list = []) + { + try { + $list = array_map(function ($listItem) { + $fullPath = $listItem['id']; + $pathInfo = pathinfo($fullPath); + + if ($this->extension->isWebImage($fullPath)) { + $listItem['is_image'] = true; + $listItem['filetype'] = $pathInfo['extension']; + } + + return $listItem; + }, $list); + } catch (\Throwable $th) { + // throw $th; + } finally { + return $list; + } + } +} diff --git a/app/code/Diepxuan/Images/Plugin/MediaGalleryRenditions/Model/GenerateRenditions.php b/app/code/Diepxuan/Images/Plugin/MediaGalleryRenditions/Model/GenerateRenditions.php new file mode 100644 index 0000000000000..c67620e537d7f --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/MediaGalleryRenditions/Model/GenerateRenditions.php @@ -0,0 +1,216 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 12:11:41 + */ + +namespace Diepxuan\Images\Plugin\MediaGalleryRenditions\Model; + +use Diepxuan\Images\Model\Extension; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Image\AdapterFactory; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryRenditions\Model\Config; +use Magento\MediaGalleryRenditions\Model\GenerateRenditions as Origin; +use Magento\MediaGalleryRenditionsApi\Api\GetRenditionPathInterface; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class GenerateRenditions +{ + private const IMAGE_FILE_NAME_PATTERN = '#\.(jpg|jpeg|gif|png|svg|webp)$# i'; + + /** + * @var File + */ + private $driver; + + /** + * @var GetRenditionPathInterface + */ + private $getRenditionPath; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var Config + */ + private $config; + + /** + * @var Extension + */ + private $extension; + + /** + * @var AdapterFactory + */ + private $imageFactory; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + public function __construct( + AdapterFactory $imageFactory, + Config $config, + GetRenditionPathInterface $getRenditionPath, + Filesystem $filesystem, + File $driver, + IsPathExcludedInterface $isPathExcluded, + Extension $extension + ) { + $this->imageFactory = $imageFactory; + $this->config = $config; + $this->getRenditionPath = $getRenditionPath; + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->isPathExcluded = $isPathExcluded; + $this->extension = $extension; + } + + /** + * Handle web images for media gallery renditions. + * + * @return Raw + */ + public function aroundExecute(Origin $subject, callable $proceed, array $paths) + { + return $proceed($paths); + $failedPaths = []; + + try { + foreach ($paths as $path) { + try { + if (!$this->extension->isWebImage($path)) { + throw new LocalizedException(__('This image type is not a Web')); + } + $this->generateRendition($path); + } catch (\Exception $exception) { + $failedPaths[] = $path; + } + } + } catch (\Exception $e) { + return $proceed($failedPaths); + } + } + + /** + * Generate rendition for media asset path. + * + * @throws FileSystemException + * @throws LocalizedException + * @throws \Exception + */ + private function generateRendition(string $path): void + { + $this->validateAsset($path); + + $renditionPath = $this->getRenditionPath->execute($path); + $this->createDirectory($renditionPath); + + $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); + + if ($this->shouldFileBeResized($absolutePath)) { + $this->createResizedRendition( + $absolutePath, + $this->getMediaDirectory()->getAbsolutePath($renditionPath) + ); + } else { + $this->getMediaDirectory()->copyFile($path, $renditionPath); + } + } + + /** + * Ensure valid media asset path is provided for renditions generation. + * + * @throws FileSystemException + * @throws LocalizedException + */ + private function validateAsset(string $path): void + { + if (!$this->getMediaDirectory()->isFile($path)) { + throw new LocalizedException(__('Media asset file %path does not exist!', ['path' => $path])); + } + + if ($this->isPathExcluded->execute($path)) { + throw new LocalizedException( + __('Could not create rendition for image, path is restricted: %path', ['path' => $path]) + ); + } + + if (!preg_match(self::IMAGE_FILE_NAME_PATTERN, $path)) { + throw new LocalizedException( + __('Could not create rendition for image, unsupported file type: %path.', ['path' => $path]) + ); + } + } + + /** + * Create directory for rendition file. + * + * @throws LocalizedException + */ + private function createDirectory(string $path): void + { + try { + $this->getMediaDirectory()->create($this->driver->getParentDirectory($path)); + } catch (\Exception $exception) { + throw new LocalizedException(__('Cannot create directory for rendition %path', ['path' => $path])); + } + } + + /** + * Create rendition file. + * + * @throws \Exception + */ + private function createResizedRendition(string $absolutePath, string $absoluteRenditionPath): void + { + $image = $this->imageFactory->create(); + $image->open($absolutePath); + $image->keepAspectRatio(true); + $image->resize($this->config->getWidth(), $this->config->getHeight()); + $image->save($absoluteRenditionPath); + } + + /** + * Check if image needs to resize or not. + */ + private function shouldFileBeResized(string $absolutePath): bool + { + [$width, $height] = getimagesizefromstring($this->getMediaDirectory()->readFile($absolutePath)); + + return $width > $this->config->getWidth() || $height > $this->config->getHeight(); + } + + /** + * Retrieve a media directory instance with write permissions. + * + * @throws FileSystemException + */ + private function getMediaDirectory(): WriteInterface + { + return $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/MediaGalleryUi/Ui/Component/ImageUploader.php b/app/code/Diepxuan/Images/Plugin/MediaGalleryUi/Ui/Component/ImageUploader.php new file mode 100644 index 0000000000000..2d84ea3d2869b --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/MediaGalleryUi/Ui/Component/ImageUploader.php @@ -0,0 +1,42 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 15:17:22 + */ + +namespace Diepxuan\Images\Plugin\MediaGalleryUi\Ui\Component; + +use Diepxuan\Images\Model\Extension; +use Magento\MediaGalleryUi\Ui\Component\ImageUploader as OriginImageUploader; + +class ImageUploader +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + public function afterPrepare(OriginImageUploader $uploader): void + { + $uploader->setData( + 'config', + array_replace_recursive( + (array) $uploader->getData('config'), + [ + 'acceptFileTypes' => $this->extension->getAllowedExtensionsRegex(), + 'allowedExtensions' => $this->extension->getAllowedExtensionsString(), + ] + ) + ); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/MediaStorage/Model/File/Uploader.php b/app/code/Diepxuan/Images/Plugin/MediaStorage/Model/File/Uploader.php new file mode 100644 index 0000000000000..0f8dd9d4f34aa --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/MediaStorage/Model/File/Uploader.php @@ -0,0 +1,53 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-26 16:39:47 + */ + +namespace Diepxuan\Images\Plugin\MediaStorage\Model\File; + +use Diepxuan\Images\Model\Extension; +use Magento\MediaStorage\Model\File\Uploader as OriginUploader; + +class Uploader +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * Set allowed extensions. + * + * @param string[] $extensions + * + * @return $this + */ + public function beforeSetAllowedExtensions(OriginUploader $uploader, $extensions = []) + { + return array_merge( + (array) $extensions, + $this->extension->getAllowedExtensions() + ); + } + + /** + * Check if specified extension is allowed. + * + * @return bool + */ + public function afterCheckAllowedExtension(OriginUploader $uploader, bool $flag) + { + return $flag || \in_array(strtolower($uploader->getFileExtension()), $this->extension->getAllowedExtensions(), true); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Theme/Helper/Storage.php b/app/code/Diepxuan/Images/Plugin/Theme/Helper/Storage.php new file mode 100644 index 0000000000000..b94f1fe10b902 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Theme/Helper/Storage.php @@ -0,0 +1,50 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 14:58:49 + */ + +namespace Diepxuan\Images\Plugin\MediaStorage\Model\File; + +use Diepxuan\Images\Model\Extension; +use Magento\Framework\Exception\LocalizedException; +use Magento\Theme\Helper\Storage as OriginStorage; +use Magento\Theme\Model\Wysiwyg\Storage as WysiwygStorage; + +class Storage extends OriginStorage +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * Get allowed extensions by type. + * + * @return string[] + * + * @throws LocalizedException + */ + public function aroundGetAllowedExtensionsByType(OriginStorage $subject, callable $proceed) + { + try { + return WysiwygStorage::TYPE_FONT === $this->getStorageType() + ? ['ttf', 'otf', 'eot', 'svg', 'woff'] + : array_merge(['jpg', 'jpeg', 'gif', 'png', 'xbm', 'wbmp'], $this->extension->getAllowedExtensions()); + } catch (\Throwable $th) { + // throw $th; + + return $proceed(); + } + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Favicon.php b/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Favicon.php new file mode 100644 index 0000000000000..91c457378dbd1 --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Favicon.php @@ -0,0 +1,42 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 07:25:06 + */ + +namespace Diepxuan\Images\Plugin\Theme\Model\Design\Backend; + +use Diepxuan\Images\Model\Extension; + +class Favicon +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * Getter for allowed extensions of uploaded files. + * + * @param mixed $extensions + * + * @return string[] + */ + public function afterGetAllowedExtensions($extensions) + { + return array_merge( + (array) $extensions, + $this->extension->getVectorExtensions(), + ); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Image.php b/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Image.php new file mode 100644 index 0000000000000..fecd00595c12c --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Image.php @@ -0,0 +1,42 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 07:10:40 + */ + +namespace Diepxuan\Images\Plugin\Theme\Model\Design\Backend; + +use Diepxuan\Images\Model\Extension; + +class Image +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * Getter for allowed extensions of uploaded files. + * + * @param mixed $extensions + * + * @return string[] + */ + public function afterGetAllowedExtensions($extensions) + { + return array_merge( + (array) $extensions, + $this->extension->getAllowedExtensions(), + ); + } +} diff --git a/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Logo.php b/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Logo.php new file mode 100644 index 0000000000000..3e5e9c531b32e --- /dev/null +++ b/app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Logo.php @@ -0,0 +1,42 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 07:11:28 + */ + +namespace Diepxuan\Images\Plugin\Theme\Model\Design\Backend; + +use Diepxuan\Images\Model\Extension; + +class Logo +{ + private $extension; + + public function __construct( + Extension $extension + ) { + $this->extension = $extension; + } + + /** + * Getter for allowed extensions of uploaded files. + * + * @param mixed $extensions + * + * @return string[] + */ + public function afterGetAllowedExtensions($extensions) + { + return array_merge( + (array) $extensions, + $this->extension->getAllowedExtensions(), + ); + } +} diff --git a/app/code/Diepxuan/Images/README.md b/app/code/Diepxuan/Images/README.md new file mode 100644 index 0000000000000..5e16398cac4d2 --- /dev/null +++ b/app/code/Diepxuan/Images/README.md @@ -0,0 +1,22 @@ +Magento 2 module +================== +[![Magento 2](https://img.shields.io/badge/Magento-%3E=2.4-blue.svg)](https://github.com/magento/magento2) +[![Packagist](https://img.shields.io/packagist/v/diepxuan/module-images)](https://packagist.org/packages/diepxuan/module-images) +[![Downloads](https://img.shields.io/packagist/dt/diepxuan/module-images)](https://packagist.org/packages/diepxuan/module-images) +[![License](https://img.shields.io/packagist/l/diepxuan/module-images)](https://packagist.org/packages/diepxuan/module-images) + +Web images in Magento 2 +----------------------- + +* Support svg and webp + +Installation +------------ + +The easiest way to install the extension is to use [Composer](https://getcomposer.org/) + +Run the following commands: + +- ```$ composer require diepxuan/module-images``` +- ```$ bin/magento module:enable Diepxuan_Images``` +- ```$ bin/magento setup:upgrade && bin/magento setup:static-content:deploy``` diff --git a/app/code/Diepxuan/Images/composer.json b/app/code/Diepxuan/Images/composer.json new file mode 100644 index 0000000000000..3e5dd53034e71 --- /dev/null +++ b/app/code/Diepxuan/Images/composer.json @@ -0,0 +1,20 @@ +{ + "name": "diepxuan/module-images", + "description": "Web images in Magento 2", + "type": "magento2-module", + "license": "MIT", + "authors": [ + { + "name": "Tran Ngoc Duc", + "email": "ductn@diepxuan.com" + } + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Diepxuan\\Images\\": "" + } + } +} diff --git a/app/code/Diepxuan/Images/etc/adminhtml/di.xml b/app/code/Diepxuan/Images/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..3b87998a5f443 --- /dev/null +++ b/app/code/Diepxuan/Images/etc/adminhtml/di.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/code/Diepxuan/Images/etc/config.xml b/app/code/Diepxuan/Images/etc/config.xml new file mode 100644 index 0000000000000..d3d211c341402 --- /dev/null +++ b/app/code/Diepxuan/Images/etc/config.xml @@ -0,0 +1,14 @@ + + + + + + + + Diepxuan\Images\Framework\Image\Adapter\Gd2 + + + + + + diff --git a/app/code/Diepxuan/Images/etc/di.xml b/app/code/Diepxuan/Images/etc/di.xml new file mode 100644 index 0000000000000..bd7438af19f1b --- /dev/null +++ b/app/code/Diepxuan/Images/etc/di.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + image/webp + + + image/svg+xml + image/webp + + + + + + + + + svg + webp + + + + + + + image/svg+xml + image/webp + + + + + + + + svg + webp + + + image/svg+xml + image/webp + + + + + + + 100 + + svg + webp + + + + diff --git a/app/code/Diepxuan/Images/etc/module.xml b/app/code/Diepxuan/Images/etc/module.xml new file mode 100644 index 0000000000000..0fec85ea0aca1 --- /dev/null +++ b/app/code/Diepxuan/Images/etc/module.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/code/Diepxuan/Images/registration.php b/app/code/Diepxuan/Images/registration.php new file mode 100644 index 0000000000000..99f9d774d3b67 --- /dev/null +++ b/app/code/Diepxuan/Images/registration.php @@ -0,0 +1,20 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-21 11:31:54 + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Diepxuan_Images', + __DIR__ +); diff --git a/app/code/Diepxuan/Images/view/adminhtml/requirejs-config.js b/app/code/Diepxuan/Images/view/adminhtml/requirejs-config.js new file mode 100644 index 0000000000000..4121c87da1ea8 --- /dev/null +++ b/app/code/Diepxuan/Images/view/adminhtml/requirejs-config.js @@ -0,0 +1,9 @@ +var config = { + config: { + mixins: { + 'Magento_Backend/js/media-uploader': { + 'Diepxuan_Images/js/media-uploader-mixin': true + } + } + } +}; diff --git a/app/code/Diepxuan/Images/view/adminhtml/ui_component/design_config_form.xml b/app/code/Diepxuan/Images/view/adminhtml/ui_component/design_config_form.xml new file mode 100644 index 0000000000000..02523b36948f5 --- /dev/null +++ b/app/code/Diepxuan/Images/view/adminhtml/ui_component/design_config_form.xml @@ -0,0 +1,27 @@ + +
+
+
+ + + + + jpg jpeg gif png ico apng svg + + + + +
+
+ + + + + jpg jpeg gif png svg webp + + + + +
+
+
diff --git a/app/code/Diepxuan/Images/view/adminhtml/web/js/media-uploader-mixin.js b/app/code/Diepxuan/Images/view/adminhtml/web/js/media-uploader-mixin.js new file mode 100644 index 0000000000000..941e6e3a24257 --- /dev/null +++ b/app/code/Diepxuan/Images/view/adminhtml/web/js/media-uploader-mixin.js @@ -0,0 +1,172 @@ +define([ + 'jquery', + 'mage/template', + 'Magento_Ui/js/modal/alert', + 'Magento_Ui/js/form/element/file-uploader', + 'mage/translate', + 'jquery/uppy-core', +], function ($, mageTemplate, alert, FileUploader) { + 'use strict'; + + return function (widget) { + let fileUploader = new FileUploader({ + dataScope: '', + isMultipleFiles: true + }); + + fileUploader.initUploader(); + + $.widget('mage.mediaUploader', widget, { + /** + * + * @private + */ + _create: function () { + let self = this, + arrayFromObj = Array.from, + progressTmpl = mageTemplate('[data-template="uploader"]'), + uploaderElement = '#fileUploader', + targetElement = this.element.find('.fileinput-button.form-buttons')[0], + uploadUrl = $(uploaderElement).attr('data-url'), + fileId = null, + allowedExt = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'webp'], + allowedResize = false, + options = { + proudlyDisplayPoweredByUppy: false, + target: targetElement, + hideUploadButton: true, + hideRetryButton: true, + hideCancelButton: true, + inline: true, + debug: true, + showRemoveButtonAfterComplete: true, + showProgressDetails: false, + showSelectedFiles: false, + hideProgressAfterFinish: true + }; + + $(document).on('click', uploaderElement, function () { + $(uploaderElement).closest('.fileinput-button.form-buttons') + .find('.uppy-Dashboard-browse').trigger('click'); + }); + + const uppy = new Uppy.Uppy({ + autoProceed: true, + + onBeforeFileAdded: (currentFile) => { + let fileSize, + tmpl; + + fileSize = typeof currentFile.size == 'undefined' ? + $.mage.__('We could not detect a size.') : + byteConvert(currentFile.size); + + // check if file is allowed to upload and resize + allowedResize = $.inArray(currentFile.extension, allowedExt) !== -1; + + if (!allowedResize) { + fileUploader.aggregateError(currentFile.name, + $.mage.__('Disallowed file type.')); + fileUploader.onLoadingStop(); + return false; + } + + fileId = Math.random().toString(33).substr(2, 18); + + tmpl = progressTmpl({ + data: { + name: currentFile.name, + size: fileSize, + id: fileId + } + }); + + // code to allow duplicate files from same folder + const modifiedFile = { + ...currentFile, + id: currentFile.id + '-' + fileId, + tempFileId: fileId + }; + + $(tmpl).appendTo(self.element); + return modifiedFile; + }, + + meta: { + 'form_key': window.FORM_KEY, + isAjax: true + } + }); + + // initialize Uppy upload + uppy.use(Uppy.Dashboard, options); + + // Resize Image as per configuration + if (this.options.isResizeEnabled) { + uppy.use(Uppy.Compressor, { + maxWidth: this.options.maxWidth, + maxHeight: this.options.maxHeight, + quality: 0.92, + beforeDraw() { + if (!allowedResize) { + this.abort(); + } + } + }); + } + + // drop area for file upload + uppy.use(Uppy.DropTarget, { + target: targetElement, + onDragOver: () => { + // override Array.from method of legacy-build.min.js file + Array.from = null; + }, + onDragLeave: () => { + Array.from = arrayFromObj; + } + }); + + // upload files on server + uppy.use(Uppy.XHRUpload, { + endpoint: uploadUrl, + fieldName: 'image' + }); + + uppy.on('upload-success', (file, response) => { + if (response.body && !response.body.error) { + self.element.trigger('addItem', response.body); + } else { + fileUploader.aggregateError(file.name, response.body.error); + } + + self.element.find('#' + file.tempFileId).remove(); + }); + + uppy.on('upload-progress', (file, progress) => { + let progressWidth = parseInt(progress.bytesUploaded / progress.bytesTotal * 100, 10), + progressSelector = '#' + file.tempFileId + ' .progressbar-container .progressbar'; + + self.element.find(progressSelector).css('width', progressWidth + '%'); + }); + + uppy.on('upload-error', (error, file) => { + let progressSelector = '#' + file.tempFileId; + + self.element.find(progressSelector).removeClass('upload-progress').addClass('upload-failure') + .delay(2000) + .hide('highlight') + .remove(); + }); + + uppy.on('complete', () => { + fileUploader.uploaderConfig.stop(); + $(window).trigger('reload.MediaGallery'); + Array.from = arrayFromObj; + }); + + } + }); + return $.mage.mediaUploader; + }; +}); diff --git a/app/code/Diepxuan/Magento/Block/Product/ProductsList.php b/app/code/Diepxuan/Magento/Block/Product/ProductsList.php new file mode 100644 index 0000000000000..87438b96bbf5f --- /dev/null +++ b/app/code/Diepxuan/Magento/Block/Product/ProductsList.php @@ -0,0 +1,149 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-20 12:23:50 + */ + +namespace Diepxuan\Magento\Block\Product; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Block\Product\Context; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; +use Magento\CatalogWidget\Model\Rule; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Url\EncoderInterface; +use Magento\Framework\View\LayoutFactory; +use Magento\Rule\Model\Condition\Sql\Builder as SqlBuilder; +use Magento\Widget\Helper\Conditions; + +class ProductsList extends \Magento\CatalogWidget\Block\Product\ProductsList +{ + /** + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + Context $context, + CollectionFactory $productCollectionFactory, + Visibility $catalogProductVisibility, + HttpContext $httpContext, + SqlBuilder $sqlBuilder, + Rule $rule, + Conditions $conditionsHelper, + array $data = [], + ?Json $json = null, + ?LayoutFactory $layoutFactory = null, + ?EncoderInterface $urlEncoder = null, + ?CategoryRepositoryInterface $categoryRepository = null + ) { + parent::__construct( + $context, + $productCollectionFactory, + $catalogProductVisibility, + $httpContext, + $sqlBuilder, + $rule, + $conditionsHelper, + $data, + $json, + $layoutFactory, + $urlEncoder, + $categoryRepository + ); + } + + /** + * Prepare and return product collection. + * + * @return Collection + * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) + * + * @throws LocalizedException + */ + public function createCollection() + { + $collection = parent::createCollection(); + $collection->getSelect()->orderRand(); + + return $collection; + } + + /** + * Prepare and return product collection without visibility filter. + * + * @throws LocalizedException + */ + public function getBaseCollection(): Collection + { + // $collection = parent::getBaseCollection(); + $collection = $this->productCollectionFactory->create(); + if (null !== $this->getData('store_id')) { + $collection->setStoreId($this->getData('store_id')); + } + + /** + * Change sorting attribute to entity_id because created_at can be the same for products fastly created + * one by one and sorting by created_at is indeterministic in this case. + */ + $collection = $this->_addProductAttributesAndPrices($collection) + ->addStoreFilter() + // ->addAttributeToSort('entity_id', 'desc') + ->setPageSize($this->getPageSize()) + ->setCurPage($this->getRequest()->getParam($this->getData('page_var_name'), 1)) + ; + + $conditions = $this->getConditions(); + $conditions->collectValidatedAttributes($collection); + $this->sqlBuilder->attachConditionToCollection($collection, $conditions); + + /* + * Prevent retrieval of duplicate records. This may occur when multiselect product attribute matches + * several allowed values from condition simultaneously + */ + $collection->distinct(true); + + return $collection; + } + + /** + * Get key pieces for caching block content. + * + * @return array + * + * @SuppressWarnings(PHPMD.RequestAwareBlockMethod) + * + * @throws NoSuchEntityException + */ + public function getCacheKeyInfo() + { + return parent::getCacheKeyInfo(); + } + + protected function _beforeToHtml() + { + $this->setProductCollection($this->createCollection()); + + return parent::_beforeToHtml(); + } + + /** + * Internal constructor, that is called from real constructor. + */ + protected function _construct(): void + { + parent::_construct(); + } +} diff --git a/app/code/Diepxuan/Magento/Controller/Index/Index.php b/app/code/Diepxuan/Magento/Controller/Index/Index.php new file mode 100644 index 0000000000000..ea2ef0b6f4d77 --- /dev/null +++ b/app/code/Diepxuan/Magento/Controller/Index/Index.php @@ -0,0 +1,23 @@ +resultFactory->create(ResultFactory::TYPE_PAGE); + } +} diff --git a/app/code/Diepxuan/Magento/LICENSE b/app/code/Diepxuan/Magento/LICENSE new file mode 100644 index 0000000000000..2e71fc73043a2 --- /dev/null +++ b/app/code/Diepxuan/Magento/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 DXVN + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/code/Diepxuan/Magento/Plugin/Cms/Block/Widget/Block.php b/app/code/Diepxuan/Magento/Plugin/Cms/Block/Widget/Block.php new file mode 100644 index 0000000000000..55eb2b12b0aaa --- /dev/null +++ b/app/code/Diepxuan/Magento/Plugin/Cms/Block/Widget/Block.php @@ -0,0 +1,36 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-12-05 18:14:48 + */ + +namespace Diepxuan\Magento\Plugin\Cms\Block\Widget; + +class Block +{ + private $identifiers = ['lienhe']; + + /** + * Override getTemplate() method. + * + * @param string $result + * + * @return string + */ + public function afterGetTemplate(\Magento\Cms\Block\Widget\Block $subject, $result) + { + // Kiểm tra điều kiện và thay đổi template nếu cần + if (\in_array($subject->getData('block_id'), $this->identifiers, true)) { + return 'Diepxuan_Magento::widget/static_block/default.phtml'; + } + + return $result; // Trả về template gốc nếu không có điều kiện nào phù hợp + } +} diff --git a/app/code/Diepxuan/Magento/README.md b/app/code/Diepxuan/Magento/README.md new file mode 100644 index 0000000000000..92dbeb4736ef8 --- /dev/null +++ b/app/code/Diepxuan/Magento/README.md @@ -0,0 +1,17 @@ +Magento 2 module +================== +[![Magento 2](https://img.shields.io/badge/Magento-%3E=2.4-blue.svg)](https://github.com/magento/magento2) +[![Packagist](https://img.shields.io/packagist/v/diepxuan/module-magento)](https://packagist.org/packages/diepxuan/module-magento) +[![Downloads](https://img.shields.io/packagist/dt/diepxuan/module-magento)](https://packagist.org/packages/diepxuan/module-magento) +[![License](https://img.shields.io/packagist/l/diepxuan/module-magento)](https://packagist.org/packages/diepxuan/module-magento) + +Installation +------------ + +The easiest way to install the extension is to use [Composer](https://getcomposer.org/) + +Run the following commands: + +- ```$ composer require diepxuan/module-magento``` +- ```$ bin/magento module:enable Diepxuan_Magento``` +- ```$ bin/magento setup:upgrade && bin/magento setup:static-content:deploy``` diff --git a/app/code/Diepxuan/Magento/Setup/Patch/Data/CreateCategorySimbaAttribute.php b/app/code/Diepxuan/Magento/Setup/Patch/Data/CreateCategorySimbaAttribute.php new file mode 100644 index 0000000000000..3e1041bb2869a --- /dev/null +++ b/app/code/Diepxuan/Magento/Setup/Patch/Data/CreateCategorySimbaAttribute.php @@ -0,0 +1,78 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2025-04-02 09:25:29 + */ + +namespace Diepxuan\Magento\Setup\Patch\Data; + +use Magento\Catalog\Model\Category; +use Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface; +use Magento\Eav\Setup\EavSetup; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; + +class CreateCategorySimbaAttribute implements DataPatchInterface, PatchVersionInterface +{ + private $eavSetupFactory; + private $moduleDataSetup; + + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + EavSetupFactory $eavSetupFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->eavSetupFactory = $eavSetupFactory; + } + + public function apply() + { + // $this->moduleDataSetup->getConnection()->startSetup(); + + /** @var EavSetup $eavSetup */ + $eavSetup = $this->eavSetupFactory->create(['setup' => $this->moduleDataSetup]); + if ($eavSetup->getAttribute(Category::ENTITY, 'simba_category')) { + $eavSetup->removeAttribute(Category::ENTITY, 'simba_category'); + } + $eavSetup->addAttribute( + Category::ENTITY, + 'simba_category', + [ + 'type' => 'varchar', + 'label' => 'Category Simba', + 'input' => 'text', + 'required' => false, + 'sort_order' => 20, + 'global' => ScopedAttributeInterface::SCOPE_STORE, + 'group' => 'General Information', + ] + ); + + return $this; + // $this->moduleDataSetup->getConnection()->endSetup(); + } + + public static function getDependencies() + { + return []; + } + + public static function getVersion() + { + return '1.0.2'; + } + + public function getAliases() + { + return []; + } +} diff --git a/app/code/Diepxuan/Magento/composer.json b/app/code/Diepxuan/Magento/composer.json new file mode 100644 index 0000000000000..8e786e979ab56 --- /dev/null +++ b/app/code/Diepxuan/Magento/composer.json @@ -0,0 +1,25 @@ +{ + "name": "diepxuan/module-magento", + "description": "DiepXuan website base in Magento 2", + "type": "magento2-module", + "license": "MIT", + "authors": [ + { + "name": "Tran Ngoc Duc", + "email": "ductn@diepxuan.com" + } + ], + "require": { + "diepxuan/module-images": "*", + "diepxuan/module-eavcleaner": "*", + "diepxuan/module-multidomain": "*" + }, + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Diepxuan\\Magento\\": "" + } + } +} diff --git a/app/code/Diepxuan/Magento/etc/adminhtml/config.xml b/app/code/Diepxuan/Magento/etc/adminhtml/config.xml new file mode 100644 index 0000000000000..dee06657f1381 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/adminhtml/config.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/code/Diepxuan/Magento/etc/adminhtml/system.xml b/app/code/Diepxuan/Magento/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..377b676187eee --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/adminhtml/system.xml @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/app/code/Diepxuan/Magento/etc/config.xml b/app/code/Diepxuan/Magento/etc/config.xml new file mode 100644 index 0000000000000..72cc05479b7a6 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/config.xml @@ -0,0 +1,59 @@ + + + + + + + home + + + 1 + + + 1 + 1 + + + + + 0 + + + + + 0 + + + + + USD,EUR,VND + VND + VND + + + + + VN + + + vi_VN + kgs + 1 + 0,6 + + + + + + + + + + + + diff --git a/app/code/Diepxuan/Magento/etc/di.xml b/app/code/Diepxuan/Magento/etc/di.xml new file mode 100644 index 0000000000000..bf8b7c851a579 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/di.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/code/Diepxuan/Magento/etc/frontend/di.xml b/app/code/Diepxuan/Magento/etc/frontend/di.xml new file mode 100644 index 0000000000000..60d671d9fa96b --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/frontend/di.xml @@ -0,0 +1,18 @@ + + + + + + + /lienhe/ + + + + + diff --git a/app/code/Diepxuan/Magento/etc/frontend/events.xml b/app/code/Diepxuan/Magento/etc/frontend/events.xml new file mode 100644 index 0000000000000..4fec270825960 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/frontend/events.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/code/Diepxuan/Magento/etc/frontend/page_types.xml b/app/code/Diepxuan/Magento/etc/frontend/page_types.xml new file mode 100644 index 0000000000000..ff25bb95cc590 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/frontend/page_types.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/code/Diepxuan/Magento/etc/frontend/routes.xml b/app/code/Diepxuan/Magento/etc/frontend/routes.xml new file mode 100644 index 0000000000000..de8e140c7616b --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/frontend/routes.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/code/Diepxuan/Magento/etc/module.xml b/app/code/Diepxuan/Magento/etc/module.xml new file mode 100644 index 0000000000000..aa803de80fa46 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/module.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/app/code/Diepxuan/Magento/etc/widget.xml b/app/code/Diepxuan/Magento/etc/widget.xml new file mode 100644 index 0000000000000..f903b1570d114 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/widget.xml @@ -0,0 +1,62 @@ + + + + + + Random list of Products + + + + + + + + + + + + + 5 + + + + 10 + + + + + + + + + + + If not set, equals to 86400 seconds (24 hours). To update widget instantly, go to Cache Management and clear Blocks HTML Output cache. +
Widget will not show products that begin to match the specified conditions until cache is refreshed.]]> +
+
+ + + +
+ + +