From d8137ca1806fd49729c508b6f9486df6660b5081 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Tue, 28 May 2024 20:33:16 +0700 Subject: [PATCH 01/72] add module and init project --- .github/workflows/build.yml | 76 +++++++++++++++++++++++++++++++++++++ .php-cs-fixer.dist.php | 68 +++++++++++++-------------------- composer.json | 7 +++- 3 files changed, 108 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000000..b192df838aeee --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,76 @@ +# 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" # 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" + 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: 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/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 30fcfc86de059..3c9d99935f78d 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,47 +1,31 @@ + * Tran Ngoc Duc */ -$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'); +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('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; +return eval($config); diff --git a/composer.json b/composer.json index 46e307a577781..9ddbe1f1e957a 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,10 @@ "magento/*": true, "php-http/discovery": true }, - "preferred-install": "dist", + "preferred-install": { + "diepxuan/*": "source", + "*": "dist" + }, "sort-packages": true }, "require": { @@ -96,6 +99,7 @@ "allure-framework/allure-phpunit": "^2", "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", "dg/bypass-finals": "^1.4", + "diepxuan/module-autologin": "*", "friendsofphp/php-cs-fixer": "^3.22", "lusitanian/oauth": "^0.8", "magento/magento-coding-standard": "*", @@ -401,5 +405,6 @@ "Magento\\PhpStan\\": "dev/tests/static/framework/Magento/PhpStan/" } }, + "minimum-stability": "dev", "prefer-stable": true } From 18604100222d11edd9866276b53be1c0675510b4 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Tue, 28 May 2024 20:41:53 +0700 Subject: [PATCH 02/72] ISSUES - package is not present in the lock file --- .github/workflows/build.yml | 4 +++- pub/errors/local.xml | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 pub/errors/local.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b192df838aeee..bd16841b26bae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,9 @@ jobs: ini-values: "post_max_size=256M" #optional - name: Install dependencies - run: composer install + run: | + rm composer.lock + composer install - name: Setup Node.js if: false diff --git a/pub/errors/local.xml b/pub/errors/local.xml new file mode 100644 index 0000000000000..c5c35559bd6d0 --- /dev/null +++ b/pub/errors/local.xml @@ -0,0 +1,48 @@ + + + + default + + + print + + Store Debug Information + + + + leave + + 0 + + From a7eebe86051a806d79778dcaaf8e653a921f94b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Ng=E1=BB=8Dc=20=C4=90=E1=BB=A9c?= Date: Tue, 28 May 2024 22:02:31 +0700 Subject: [PATCH 03/72] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd16841b26bae..3a8e2cac4111c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ on: paths-ignore: - "**.md" schedule: - - cron: "30 * * * 0" # run every @hourly + - cron: "*/30 * * * 0" # run every @hourly # Allows you to run this workflow manually from the Actions tab workflow_dispatch: From 4134415746c20a6e3a601c488d9175a994306a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Ng=E1=BB=8Dc=20=C4=90=E1=BB=A9c?= Date: Tue, 28 May 2024 22:13:31 +0700 Subject: [PATCH 04/72] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a8e2cac4111c..904759d8e7f35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ on: paths-ignore: - "**.md" schedule: - - cron: "*/30 * * * 0" # run every @hourly + - cron: "*/30 * * * *" # run every @hourly # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -46,7 +46,7 @@ jobs: - name: Install dependencies run: | - rm composer.lock + rm composer.lock || true composer install - name: Setup Node.js From b9246a5618361401a20e499816606ffb5c4eeb1e Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Tue, 28 May 2024 20:33:16 +0700 Subject: [PATCH 05/72] add module and init project --- .github/workflows/build.yml | 76 +++++++++++++++++++++++++++++++++++++ .php-cs-fixer.dist.php | 68 +++++++++++++-------------------- composer.json | 7 +++- 3 files changed, 108 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000000..b192df838aeee --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,76 @@ +# 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" # 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" + 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: 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/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 30fcfc86de059..3c9d99935f78d 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,47 +1,31 @@ + * Tran Ngoc Duc */ -$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'); +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('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; +return eval($config); diff --git a/composer.json b/composer.json index 46e307a577781..9ddbe1f1e957a 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,10 @@ "magento/*": true, "php-http/discovery": true }, - "preferred-install": "dist", + "preferred-install": { + "diepxuan/*": "source", + "*": "dist" + }, "sort-packages": true }, "require": { @@ -96,6 +99,7 @@ "allure-framework/allure-phpunit": "^2", "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", "dg/bypass-finals": "^1.4", + "diepxuan/module-autologin": "*", "friendsofphp/php-cs-fixer": "^3.22", "lusitanian/oauth": "^0.8", "magento/magento-coding-standard": "*", @@ -401,5 +405,6 @@ "Magento\\PhpStan\\": "dev/tests/static/framework/Magento/PhpStan/" } }, + "minimum-stability": "dev", "prefer-stable": true } From fa1e15c8ebb69ba1ed462aba4f46d7639c53cbd6 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Tue, 28 May 2024 20:41:53 +0700 Subject: [PATCH 06/72] ISSUES - package is not present in the lock file --- .github/workflows/build.yml | 4 +++- pub/errors/local.xml | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 pub/errors/local.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b192df838aeee..bd16841b26bae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,9 @@ jobs: ini-values: "post_max_size=256M" #optional - name: Install dependencies - run: composer install + run: | + rm composer.lock + composer install - name: Setup Node.js if: false diff --git a/pub/errors/local.xml b/pub/errors/local.xml new file mode 100644 index 0000000000000..c5c35559bd6d0 --- /dev/null +++ b/pub/errors/local.xml @@ -0,0 +1,48 @@ + + + + default + + + print + + Store Debug Information + + + + leave + + 0 + + From a46cb04ef2ce95b3e88d04bd6defd3a347833009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Ng=E1=BB=8Dc=20=C4=90=E1=BB=A9c?= Date: Tue, 28 May 2024 22:02:31 +0700 Subject: [PATCH 07/72] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd16841b26bae..3a8e2cac4111c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ on: paths-ignore: - "**.md" schedule: - - cron: "30 * * * 0" # run every @hourly + - cron: "*/30 * * * 0" # run every @hourly # Allows you to run this workflow manually from the Actions tab workflow_dispatch: From 01d1cc8a6fdba9c3d4c6eba7a5b598e39cb2ab32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Ng=E1=BB=8Dc=20=C4=90=E1=BB=A9c?= Date: Tue, 28 May 2024 22:13:31 +0700 Subject: [PATCH 08/72] Update build.yml --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a8e2cac4111c..904759d8e7f35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ on: paths-ignore: - "**.md" schedule: - - cron: "*/30 * * * 0" # run every @hourly + - cron: "*/30 * * * *" # run every @hourly # Allows you to run this workflow manually from the Actions tab workflow_dispatch: @@ -46,7 +46,7 @@ jobs: - name: Install dependencies run: | - rm composer.lock + rm composer.lock || true composer install - name: Setup Node.js From 96a9eceb67ae35dcd3d78bb20bec03d81ce535ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Ng=E1=BB=8Dc=20=C4=90=E1=BB=A9c?= Date: Fri, 31 May 2024 07:51:51 +0700 Subject: [PATCH 09/72] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 904759d8e7f35..74b0b2b49f300 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ on: paths-ignore: - "**.md" schedule: - - cron: "*/30 * * * *" # run every @hourly + - cron: "0 * * * *" # run every @hourly # Allows you to run this workflow manually from the Actions tab workflow_dispatch: From 35affbbc90bdfb9e563d85fbd3636b56d45b5ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=E1=BA=A7n=20Ng=E1=BB=8Dc=20=C4=90=E1=BB=A9c?= Date: Fri, 31 May 2024 07:52:15 +0700 Subject: [PATCH 10/72] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 74b0b2b49f300..904759d8e7f35 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ on: paths-ignore: - "**.md" schedule: - - cron: "0 * * * *" # run every @hourly + - cron: "*/30 * * * *" # run every @hourly # Allows you to run this workflow manually from the Actions tab workflow_dispatch: From a34161ef3dfc936c5ff35fad02e63227659f2716 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Fri, 31 May 2024 10:27:23 +0700 Subject: [PATCH 11/72] add diepxuan packages --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 9ddbe1f1e957a..39e6d14212bf3 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "colinmollenhour/credis": "^1.15", "colinmollenhour/php-redis-session-abstract": "^1.5", "composer/composer": "^2.0, !=2.2.16", + "diepxuan/module-magento": "*", "elasticsearch/elasticsearch": "~7.17.0 || ~8.5.0", "ezyang/htmlpurifier": "^4.17", "guzzlehttp/guzzle": "^7.5", From b82258abb8e536f82aaa1cdc266dcec44d5e4165 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Sat, 22 Jun 2024 07:31:47 +0700 Subject: [PATCH 12/72] add opensearch docker --- docker-compose.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000..c4f53d082b7ff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3' +services: + opensearch: + container_name: opensearch + image: opensearchproject/opensearch:latest + environment: + - discovery.type=single-node + - bootstrap.memory_lock=true + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" + - "DISABLE_INSTALL_DEMO_CONFIG=true" + - "DISABLE_SECURITY_PLUGIN=true" + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD} + ulimits: + # memlock: + # soft: -1 + # hard: -1 + nofile: + soft: 65536 + hard: 65536 + volumes: + - opensearch-data:/usr/share/opensearch/data + env_file: + - .env + ports: + - 9200:9200 + - 9600:9600 + restart: always +volumes: + opensearch-data: From 803b316879e42f6eeba3f7fdc999f7966dc2ccc1 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Fri, 28 Jun 2024 19:14:47 +0700 Subject: [PATCH 13/72] create module githubaction --- .github/workflows/module.yml | 29 +++++++++++++++++++++++++++++ .gitignore | 2 ++ .phpactor.json | 6 ++++++ composer.json | 6 ++++++ 4 files changed, 43 insertions(+) create mode 100644 .github/workflows/module.yml create mode 100644 .phpactor.json diff --git a/.github/workflows/module.yml b/.github/workflows/module.yml new file mode 100644 index 0000000000000..8da8d42344ac2 --- /dev/null +++ b/.github/workflows/module.yml @@ -0,0 +1,29 @@ +# 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: + +permissions: read-all + +# 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: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/magento-module-update.yml@main diff --git a/.gitignore b/.gitignore index f3784ee198240..7116cf70dd888 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 diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 0000000000000..bf7a32bbf7ab0 --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,6 @@ +{ + "$schema": "/phpactor.schema.json", + "language_server_phpstan.enabled": true, + "language_server_php_cs_fixer.enabled": true, + "php_code_sniffer.enabled": true +} \ No newline at end of file diff --git a/composer.json b/composer.json index 39e6d14212bf3..b52b7385aafb6 100644 --- a/composer.json +++ b/composer.json @@ -406,6 +406,12 @@ "Magento\\PhpStan\\": "dev/tests/static/framework/Magento/PhpStan/" } }, + "repositories": [ + { + "type": "composer", + "url": "https://repo.magento.com/" + } + ], "minimum-stability": "dev", "prefer-stable": true } From 5cef79100cacd3558dfcadef84327673e55083a0 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Duc Date: Fri, 28 Jun 2024 19:30:30 +0700 Subject: [PATCH 14/72] add module source --- app/code/Diepxuan/Autologin/.editorconfig | 24 + .../Autologin/.github/workflows/build.yml | 29 + .../Autologin/.github/workflows/test.yml | 27 + .../Diepxuan/Autologin/.php-cs-fixer.dist.php | 31 + app/code/Diepxuan/Autologin/LICENSE | 21 + app/code/Diepxuan/Autologin/Model/Auth.php | 312 +++++ .../Config/Source/AuthenticationUser.php | 90 ++ app/code/Diepxuan/Autologin/Model/Context.php | 197 ++++ .../Autologin/Plugin/Backend/Model/Auth.php | 83 ++ .../Autologin/Plugin/User/Model/User.php | 85 ++ app/code/Diepxuan/Autologin/README.md | 41 + app/code/Diepxuan/Autologin/composer.json | 20 + .../Diepxuan/Autologin/etc/adminhtml/di.xml | 15 + .../Autologin/etc/adminhtml/system.xml | 27 + app/code/Diepxuan/Autologin/etc/config.xml | 20 + app/code/Diepxuan/Autologin/etc/module.xml | 15 + app/code/Diepxuan/Autologin/registration.php | 11 + .../EAVCleaner/.github/workflows/build.yml | 16 + .../EAVCleaner/.github/workflows/test.yml | 18 + app/code/Diepxuan/EAVCleaner/.gitignore | 45 + .../EAVCleaner/.php-cs-fixer.dist.php | 31 + .../Command/EavAttributesRestoreDefault.php | 111 ++ app/code/Diepxuan/EAVCleaner/LICENSE | 21 + app/code/Diepxuan/EAVCleaner/README.md | 26 + app/code/Diepxuan/EAVCleaner/composer.json | 20 + app/code/Diepxuan/EAVCleaner/etc/di.xml | 10 + app/code/Diepxuan/EAVCleaner/etc/module.xml | 5 + app/code/Diepxuan/EAVCleaner/registration.php | 20 + .../Images/.github/workflows/build.yml | 16 + .../Images/.github/workflows/test.yml | 18 + app/code/Diepxuan/Images/.gitignore | 45 + .../Diepxuan/Images/.php-cs-fixer.dist.php | 31 + .../Images/Backend/Block/Media/Uploader.php | 54 + .../Product/Attribute/Backend/Image.php | 96 ++ .../Config/Model/Config/Backend/Image.php | 72 ++ .../Model/Config/Backend/Image/Favicon.php | 75 ++ .../Images/Framework/Api/ImageProcessor.php | 63 ++ .../Image/Adapter/AbstractAdapter.php | 194 ++++ .../Images/Framework/Image/Adapter/Gd2.php | 1007 +++++++++++++++++ app/code/Diepxuan/Images/LICENSE | 21 + app/code/Diepxuan/Images/Model/Extension.php | 250 ++++ .../Product/Helper/Form/Gallery/Content.php | 45 + .../Product/Gallery/MimeTypeExtensionMap.php | 45 + .../Model/Product/Gallery/Processor.php | 212 ++++ .../Adminhtml/Wysiwyg/Directive.php | 116 ++ .../Adminhtml/Wysiwyg/Images/Thumbnail.php | 60 + .../Images/Plugin/Framework/File/Uploader.php | 49 + .../Plugin/Framework/Filesystem/Io/File.php | 68 ++ .../Model/GenerateRenditions.php | 216 ++++ .../Ui/Component/ImageUploader.php | 42 + .../MediaStorage/Model/File/Uploader.php | 53 + .../Images/Plugin/Theme/Helper/Storage.php | 50 + .../Theme/Model/Design/Backend/Favicon.php | 42 + .../Theme/Model/Design/Backend/Image.php | 42 + .../Theme/Model/Design/Backend/Logo.php | 42 + app/code/Diepxuan/Images/README.md | 22 + app/code/Diepxuan/Images/composer.json | 20 + app/code/Diepxuan/Images/etc/adminhtml/di.xml | 34 + app/code/Diepxuan/Images/etc/config.xml | 14 + app/code/Diepxuan/Images/etc/di.xml | 86 ++ app/code/Diepxuan/Images/etc/module.xml | 14 + app/code/Diepxuan/Images/registration.php | 20 + .../Images/view/adminhtml/requirejs-config.js | 9 + .../ui_component/design_config_form.xml | 27 + .../adminhtml/web/js/media-uploader-mixin.js | 172 +++ .../Magento/.github/workflows/build.yml | 16 + .../Magento/.github/workflows/test.yml | 18 + .../Diepxuan/Magento/.php-cs-fixer.dist.php | 31 + .../Magento/Block/Product/ProductsList.php | 149 +++ .../Magento/Controller/Index/Index.php | 23 + app/code/Diepxuan/Magento/LICENSE | 21 + app/code/Diepxuan/Magento/README.md | 17 + app/code/Diepxuan/Magento/composer.json | 20 + .../Diepxuan/Magento/etc/adminhtml/config.xml | 11 + .../Diepxuan/Magento/etc/adminhtml/system.xml | 11 + app/code/Diepxuan/Magento/etc/config.xml | 56 + app/code/Diepxuan/Magento/etc/di.xml | 10 + app/code/Diepxuan/Magento/etc/frontend/di.xml | 18 + .../Diepxuan/Magento/etc/frontend/events.xml | 10 + .../Magento/etc/frontend/page_types.xml | 4 + .../Diepxuan/Magento/etc/frontend/routes.xml | 8 + app/code/Diepxuan/Magento/etc/module.xml | 21 + app/code/Diepxuan/Magento/etc/widget.xml | 62 + app/code/Diepxuan/Magento/i18n/vi_VN.csv | 22 + app/code/Diepxuan/Magento/registration.php | 7 + .../Magento/view/frontend/layout/default.xml | 30 + .../frontend/layout/lienhe_index_index.xml | 16 + 87 files changed, 5394 insertions(+) create mode 100644 app/code/Diepxuan/Autologin/.editorconfig create mode 100644 app/code/Diepxuan/Autologin/.github/workflows/build.yml create mode 100644 app/code/Diepxuan/Autologin/.github/workflows/test.yml create mode 100644 app/code/Diepxuan/Autologin/.php-cs-fixer.dist.php create mode 100644 app/code/Diepxuan/Autologin/LICENSE create mode 100644 app/code/Diepxuan/Autologin/Model/Auth.php create mode 100644 app/code/Diepxuan/Autologin/Model/Config/Source/AuthenticationUser.php create mode 100644 app/code/Diepxuan/Autologin/Model/Context.php create mode 100644 app/code/Diepxuan/Autologin/Plugin/Backend/Model/Auth.php create mode 100644 app/code/Diepxuan/Autologin/Plugin/User/Model/User.php create mode 100644 app/code/Diepxuan/Autologin/README.md create mode 100644 app/code/Diepxuan/Autologin/composer.json create mode 100644 app/code/Diepxuan/Autologin/etc/adminhtml/di.xml create mode 100644 app/code/Diepxuan/Autologin/etc/adminhtml/system.xml create mode 100644 app/code/Diepxuan/Autologin/etc/config.xml create mode 100644 app/code/Diepxuan/Autologin/etc/module.xml create mode 100644 app/code/Diepxuan/Autologin/registration.php create mode 100644 app/code/Diepxuan/EAVCleaner/.github/workflows/build.yml create mode 100644 app/code/Diepxuan/EAVCleaner/.github/workflows/test.yml create mode 100644 app/code/Diepxuan/EAVCleaner/.gitignore create mode 100644 app/code/Diepxuan/EAVCleaner/.php-cs-fixer.dist.php create mode 100644 app/code/Diepxuan/EAVCleaner/Console/Command/EavAttributesRestoreDefault.php create mode 100644 app/code/Diepxuan/EAVCleaner/LICENSE create mode 100644 app/code/Diepxuan/EAVCleaner/README.md create mode 100644 app/code/Diepxuan/EAVCleaner/composer.json create mode 100644 app/code/Diepxuan/EAVCleaner/etc/di.xml create mode 100644 app/code/Diepxuan/EAVCleaner/etc/module.xml create mode 100644 app/code/Diepxuan/EAVCleaner/registration.php create mode 100644 app/code/Diepxuan/Images/.github/workflows/build.yml create mode 100644 app/code/Diepxuan/Images/.github/workflows/test.yml create mode 100644 app/code/Diepxuan/Images/.gitignore create mode 100644 app/code/Diepxuan/Images/.php-cs-fixer.dist.php create mode 100644 app/code/Diepxuan/Images/Backend/Block/Media/Uploader.php create mode 100644 app/code/Diepxuan/Images/Catalog/Model/ResourceModel/Product/Attribute/Backend/Image.php create mode 100644 app/code/Diepxuan/Images/Config/Model/Config/Backend/Image.php create mode 100644 app/code/Diepxuan/Images/Config/Model/Config/Backend/Image/Favicon.php create mode 100644 app/code/Diepxuan/Images/Framework/Api/ImageProcessor.php create mode 100644 app/code/Diepxuan/Images/Framework/Image/Adapter/AbstractAdapter.php create mode 100644 app/code/Diepxuan/Images/Framework/Image/Adapter/Gd2.php create mode 100644 app/code/Diepxuan/Images/LICENSE create mode 100644 app/code/Diepxuan/Images/Model/Extension.php create mode 100644 app/code/Diepxuan/Images/Plugin/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php create mode 100644 app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/MimeTypeExtensionMap.php create mode 100644 app/code/Diepxuan/Images/Plugin/Catalog/Model/Product/Gallery/Processor.php create mode 100644 app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Directive.php create mode 100644 app/code/Diepxuan/Images/Plugin/Controller/Adminhtml/Wysiwyg/Images/Thumbnail.php create mode 100644 app/code/Diepxuan/Images/Plugin/Framework/File/Uploader.php create mode 100644 app/code/Diepxuan/Images/Plugin/Framework/Filesystem/Io/File.php create mode 100644 app/code/Diepxuan/Images/Plugin/MediaGalleryRenditions/Model/GenerateRenditions.php create mode 100644 app/code/Diepxuan/Images/Plugin/MediaGalleryUi/Ui/Component/ImageUploader.php create mode 100644 app/code/Diepxuan/Images/Plugin/MediaStorage/Model/File/Uploader.php create mode 100644 app/code/Diepxuan/Images/Plugin/Theme/Helper/Storage.php create mode 100644 app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Favicon.php create mode 100644 app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Image.php create mode 100644 app/code/Diepxuan/Images/Plugin/Theme/Model/Design/Backend/Logo.php create mode 100644 app/code/Diepxuan/Images/README.md create mode 100644 app/code/Diepxuan/Images/composer.json create mode 100644 app/code/Diepxuan/Images/etc/adminhtml/di.xml create mode 100644 app/code/Diepxuan/Images/etc/config.xml create mode 100644 app/code/Diepxuan/Images/etc/di.xml create mode 100644 app/code/Diepxuan/Images/etc/module.xml create mode 100644 app/code/Diepxuan/Images/registration.php create mode 100644 app/code/Diepxuan/Images/view/adminhtml/requirejs-config.js create mode 100644 app/code/Diepxuan/Images/view/adminhtml/ui_component/design_config_form.xml create mode 100644 app/code/Diepxuan/Images/view/adminhtml/web/js/media-uploader-mixin.js create mode 100644 app/code/Diepxuan/Magento/.github/workflows/build.yml create mode 100644 app/code/Diepxuan/Magento/.github/workflows/test.yml create mode 100644 app/code/Diepxuan/Magento/.php-cs-fixer.dist.php create mode 100644 app/code/Diepxuan/Magento/Block/Product/ProductsList.php create mode 100644 app/code/Diepxuan/Magento/Controller/Index/Index.php create mode 100644 app/code/Diepxuan/Magento/LICENSE create mode 100644 app/code/Diepxuan/Magento/README.md create mode 100644 app/code/Diepxuan/Magento/composer.json create mode 100644 app/code/Diepxuan/Magento/etc/adminhtml/config.xml create mode 100644 app/code/Diepxuan/Magento/etc/adminhtml/system.xml create mode 100644 app/code/Diepxuan/Magento/etc/config.xml create mode 100644 app/code/Diepxuan/Magento/etc/di.xml create mode 100644 app/code/Diepxuan/Magento/etc/frontend/di.xml create mode 100644 app/code/Diepxuan/Magento/etc/frontend/events.xml create mode 100644 app/code/Diepxuan/Magento/etc/frontend/page_types.xml create mode 100644 app/code/Diepxuan/Magento/etc/frontend/routes.xml create mode 100644 app/code/Diepxuan/Magento/etc/module.xml create mode 100644 app/code/Diepxuan/Magento/etc/widget.xml create mode 100644 app/code/Diepxuan/Magento/i18n/vi_VN.csv create mode 100644 app/code/Diepxuan/Magento/registration.php create mode 100644 app/code/Diepxuan/Magento/view/frontend/layout/default.xml create mode 100644 app/code/Diepxuan/Magento/view/frontend/layout/lienhe_index_index.xml 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/.github/workflows/build.yml b/app/code/Diepxuan/Autologin/.github/workflows/build.yml new file mode 100644 index 0000000000000..f8cf5f0673859 --- /dev/null +++ b/app/code/Diepxuan/Autologin/.github/workflows/build.yml @@ -0,0 +1,29 @@ +# 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" # run every @hourly + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +permissions: read-all + +# 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: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-build.yml@main diff --git a/app/code/Diepxuan/Autologin/.github/workflows/test.yml b/app/code/Diepxuan/Autologin/.github/workflows/test.yml new file mode 100644 index 0000000000000..27014cb39b221 --- /dev/null +++ b/app/code/Diepxuan/Autologin/.github/workflows/test.yml @@ -0,0 +1,27 @@ +# Runs tests and verifies that the package can be built. +name: Test + +# 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 * */2 * *" # 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" + test: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-test.yml@main 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(' + */ + +namespace Diepxuan\Autologin\Model; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Exception\Plugin\AuthenticationException as PluginAuthenticationException; +use Magento\Framework\Phrase; + +/** + * Backend Auth model + * + * @see \Magento\Backend\Model\Auth + */ +class Auth +{ + const ENABLE = 'admin/autologin/enable'; + const USERNAME = 'admin/autologin/username'; + const ALLOWS = 'admin/autologin/allows'; + + /** + * @var \Diepxuan\Autologin\Model\Context + */ + protected $_context; + + /** + * @var \Psr\Log\LoggerInterface + */ + protected $_logger; + + /** + * @var \Magento\Framework\App\Request\Http + */ + protected $_request; + + /** + * 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; + + public function __construct( + \Diepxuan\Autologin\Model\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 (\Magento\Framework\Exception\LocalizedException $e) { + return $this->getAuthStorage()->isLoggedIn(); + } + + return $this->getAuthStorage()->isLoggedIn(); + } + + /** + * Perform login process + * + * @param string $username + * @param string $password + * @return void + * @throws \Magento\Framework\Exception\AuthenticationException + */ + public function autoAuthentication() + { + 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 (\Magento\Framework\Exception\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 \Magento\User\Model\User $user + * @param bool $result + * + * @return bool + * @throws \Magento\Framework\Exception\AuthenticationException + */ + public function verifyIdentity(\Magento\User\Model\User $user, $result=false) + { + if (!$this->isEnable()) { + return $result; + } + + if ($user->getUserName() == $this->getAdminUserName()) { + $result = true; + } + + return $result; + } + + /** + * @return boolean + */ + 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 + * + * @return void + */ + public function logout() + { + $this->getAuthStorage()->processLogout(); + } + + /** + * Throws specific Backend Authentication \Exception + * + * @param \Magento\Framework\Phrase $msg + * @return void + * @throws \Magento\Framework\Exception\AuthenticationException + * @static + */ + public static function throwException(Phrase $msg = null) + { + if ($msg === null) { + $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 \Magento\Backend\Model\Auth\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(); + } + + /** + * Initialize credential storage from configuration + * + * @return void + */ + protected function _initCredentialStorage() + { + $this->_credentialStorage = $this->_modelFactory->create( + \Magento\User\Model\User::class + ); + } + + /** + * Return credential storage object + * + * @return null|\Magento\Backend\Model\Auth\Credential\StorageInterface + * @codeCoverageIgnore + */ + public function getCredentialStorage() + { + return $this->_credentialStorage; + } + + /** + * @return void + */ + protected function _autoAuthentication() + { + if (!$this->isEnable()) { + return; + } + + return $this->autoAuthentication(); + } + + /** + * @return boolean + */ + protected function _isEnable() + { + if ($this->getAuthStorage()->isLoggedIn()) { + return false; + } + + $enable = $this->_coreConfig->getValue(self::ENABLE); + if (!$enable) { + return false; + } + + return true; + } + + /** + * @return \Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->_logger; + } +} 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..0924afe0a1761 --- /dev/null +++ b/app/code/Diepxuan/Autologin/composer.json @@ -0,0 +1,20 @@ +{ + "name": "diepxuan/module-autologin", + "description": "Autologin module for Magento 2", + "type": "magento2-module", + "license": "MIT", + "authors": [ + { + "name": "Tran Ngoc Duc", + "email": "ductn@diepxuan.com" + } + ], + "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..7ca62c99bec20 --- /dev/null +++ b/app/code/Diepxuan/Autologin/etc/adminhtml/system.xml @@ -0,0 +1,27 @@ + + + + +
+ + + + + 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..36a1d208b7f80 --- /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..17f2de913bd9d --- /dev/null +++ b/app/code/Diepxuan/Autologin/etc/module.xml @@ -0,0 +1,15 @@ + + + + + + + + + + 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/EAVCleaner/.github/workflows/build.yml b/app/code/Diepxuan/EAVCleaner/.github/workflows/build.yml new file mode 100644 index 0000000000000..caf1d1d42f928 --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/.github/workflows/build.yml @@ -0,0 +1,16 @@ +name: Build +on: + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + workflow_dispatch: +jobs: + build: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-build.yml@main diff --git a/app/code/Diepxuan/EAVCleaner/.github/workflows/test.yml b/app/code/Diepxuan/EAVCleaner/.github/workflows/test.yml new file mode 100644 index 0000000000000..bbdc399b9dc5b --- /dev/null +++ b/app/code/Diepxuan/EAVCleaner/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Test +on: + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + schedule: + - cron: "*/5 * * * *" + workflow_dispatch: +jobs: + test: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-test.yml@main 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/Images/.github/workflows/build.yml b/app/code/Diepxuan/Images/.github/workflows/build.yml new file mode 100644 index 0000000000000..caf1d1d42f928 --- /dev/null +++ b/app/code/Diepxuan/Images/.github/workflows/build.yml @@ -0,0 +1,16 @@ +name: Build +on: + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + workflow_dispatch: +jobs: + build: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-build.yml@main diff --git a/app/code/Diepxuan/Images/.github/workflows/test.yml b/app/code/Diepxuan/Images/.github/workflows/test.yml new file mode 100644 index 0000000000000..9260f18663f86 --- /dev/null +++ b/app/code/Diepxuan/Images/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Test +on: + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + schedule: + - cron: "*/30 * */2 * *" # run every @hourly + workflow_dispatch: +jobs: + test: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-test.yml@main diff --git a/app/code/Diepxuan/Images/.gitignore b/app/code/Diepxuan/Images/.gitignore new file mode 100644 index 0000000000000..abe6d79fedbbd --- /dev/null +++ b/app/code/Diepxuan/Images/.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/Images/.php-cs-fixer.dist.php b/app/code/Diepxuan/Images/.php-cs-fixer.dist.php new file mode 100644 index 0000000000000..3c9d99935f78d --- /dev/null +++ b/app/code/Diepxuan/Images/.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-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..5ae95d4a3260c --- /dev/null +++ b/app/code/Diepxuan/Images/Config/Model/Config/Backend/Image.php @@ -0,0 +1,72 @@ + + * @author Tran Ngoc Duc + * + * @lastupdate 2024-06-27 07:22:08 + */ + +namespace Diepxuan\Images\Config\Model\Config\Backend; + +use Diepxuan\Images\Model\Extension; +use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; +use Magento\Config\Model\Config\Backend\Image as OriginImage; +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 Image extends OriginImage +{ + 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->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/.github/workflows/build.yml b/app/code/Diepxuan/Magento/.github/workflows/build.yml new file mode 100644 index 0000000000000..caf1d1d42f928 --- /dev/null +++ b/app/code/Diepxuan/Magento/.github/workflows/build.yml @@ -0,0 +1,16 @@ +name: Build +on: + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + workflow_dispatch: +jobs: + build: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-build.yml@main diff --git a/app/code/Diepxuan/Magento/.github/workflows/test.yml b/app/code/Diepxuan/Magento/.github/workflows/test.yml new file mode 100644 index 0000000000000..9260f18663f86 --- /dev/null +++ b/app/code/Diepxuan/Magento/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Test +on: + push: + branches: [main] + paths-ignore: + - "**.md" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + schedule: + - cron: "*/30 * */2 * *" # run every @hourly + workflow_dispatch: +jobs: + test: + permissions: + contents: write + uses: diepxuan/.github/.github/workflows/php-package-test.yml@main diff --git a/app/code/Diepxuan/Magento/.php-cs-fixer.dist.php b/app/code/Diepxuan/Magento/.php-cs-fixer.dist.php new file mode 100644 index 0000000000000..3c9d99935f78d --- /dev/null +++ b/app/code/Diepxuan/Magento/.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-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/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/composer.json b/app/code/Diepxuan/Magento/composer.json new file mode 100644 index 0000000000000..dffc27f31f69d --- /dev/null +++ b/app/code/Diepxuan/Magento/composer.json @@ -0,0 +1,20 @@ +{ + "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" + } + ], + "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..dee06657f1381 --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/adminhtml/system.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/code/Diepxuan/Magento/etc/config.xml b/app/code/Diepxuan/Magento/etc/config.xml new file mode 100644 index 0000000000000..913d1197c79ee --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/config.xml @@ -0,0 +1,56 @@ + + + + + + + 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..5fc11f625daae --- /dev/null +++ b/app/code/Diepxuan/Magento/etc/di.xml @@ -0,0 +1,10 @@ + + + + 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..9628847075f6b --- /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.]]> +
+
+ + + +
+ + +