diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example new file mode 100644 index 0000000..bfd891a --- /dev/null +++ b/.devcontainer/.env.example @@ -0,0 +1,20 @@ +MAGENTO_PUBLIC_KEY= +MAGENTO_PRIVATE_KEY= +MAGENTO_VERSION=2.4.* +MAGENTO_BASE_URL=http://localhost:8080/ + +MAGENTO_DB_HOST=db +MAGENTO_DB_NAME=magento +MAGENTO_DB_USER=magento +MAGENTO_DB_PASSWORD=magento + +MAGENTO_SEARCH_ENGINE=opensearch +MAGENTO_OPENSEARCH_HOST=opensearch +MAGENTO_OPENSEARCH_PORT=9200 + +MAGENTO_ADMIN_FIRSTNAME=Admin +MAGENTO_ADMIN_LASTNAME=User +MAGENTO_ADMIN_EMAIL=admin@example.com +MAGENTO_ADMIN_USER=admin +MAGENTO_ADMIN_PASSWORD=Admin123!Admin123! +MAGENTO_ADMIN_URI=admin diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..c230ba5 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,93 @@ +FROM php:8.2-fpm-bullseye + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + git \ + curl \ + procps \ + rsync \ + unzip \ + zip \ + ca-certificates \ + default-mysql-client \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + libzip-dev \ + libicu-dev \ + libxml2-dev \ + libxslt1-dev \ + libonig-dev \ + libcurl4-openssl-dev \ + libssl-dev \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j"$(nproc)" \ + bcmath \ + curl \ + ftp \ + gd \ + intl \ + mbstring \ + mysqli \ + pdo_mysql \ + soap \ + sockets \ + xsl \ + zip \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug \ + && rm -rf /var/lib/apt/lists/* + +ENV NVM_DIR=/usr/local/nvm +ENV NODE_VERSION=20 + +RUN mkdir -p "$NVM_DIR" \ + && curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash \ + && . "$NVM_DIR/nvm.sh" \ + && nvm install "$NODE_VERSION" \ + && nvm alias default "$NODE_VERSION" \ + && nvm use default + +ENV NODE_PATH="$NVM_DIR/versions/node/v${NODE_VERSION}/lib/node_modules" +ENV PATH="$NVM_DIR/versions/node/v${NODE_VERSION}/bin:${PATH}" + +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +ENV COMPOSER_HOME=/composer +ENV PATH="/composer/vendor/bin:${PATH}" + +RUN mkdir -p /composer \ + && git config --global --add safe.directory /var/www/magento/app/code/ImageKit/ImageKitMagento \ + && composer --version \ + && composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true \ + && composer global require --no-interaction --prefer-dist \ + squizlabs/php_codesniffer:^3.9 \ + phpcompatibility/php-compatibility:^9.3 \ + magento/magento-coding-standard:^35 \ + phpstan/phpstan:^1.12 \ + phpunit/phpunit:^10.5 \ + && phpcs --config-set installed_paths "/composer/vendor/magento/magento-coding-standard,/composer/vendor/phpcompatibility/php-compatibility" + +ARG MAGENTO_PUBLIC_KEY +ARG MAGENTO_PRIVATE_KEY +ARG MAGENTO_VERSION=2.4.* + +RUN composer config --global --auth http-basic.repo.magento.com "${MAGENTO_PUBLIC_KEY}" "${MAGENTO_PRIVATE_KEY}" \ + && COMPOSER_MEMORY_LIMIT=-1 composer create-project --repository-url=https://repo.magento.com/ \ + magento/project-community-edition="${MAGENTO_VERSION}" /var/www/magento \ + && rm -f /composer/auth.json + +RUN { \ + echo "xdebug.mode=develop,debug"; \ + echo "xdebug.start_with_request=yes"; \ + echo "xdebug.discover_client_host=0"; \ + echo "xdebug.client_host=host.docker.internal"; \ + echo "xdebug.client_port=9003"; \ + } > /usr/local/etc/php/conf.d/99-xdebug.ini \ + && echo "memory_limit = -1" > /usr/local/etc/php/conf.d/99-memory.ini + +WORKDIR /var/www/magento/app/code/ImageKit/ImageKitMagento + +CMD ["php-fpm", "-F"] diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 0000000..ee72958 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,69 @@ +# Dev Container (Magento Latest 2.4 E2E) + +This dev container provides an end-to-end Magento 2.4 environment for this module. + +## What is included + +- PHP 8.2 + PHP-FPM +- Nginx +- MariaDB 10.6 +- OpenSearch 2.x +- Xdebug +- Composer +- PHP_CodeSniffer + Magento Coding Standard +- PHPStan +- PHPUnit + +## Prerequisites + +To download Magento source on first bootstrap, export your Adobe Commerce Marketplace keys: + +```bash +export MAGENTO_PUBLIC_KEY="" +export MAGENTO_PRIVATE_KEY="" +``` + +You can also create `.devcontainer/.env` from `.devcontainer/.env.example` and keep credentials there. + +## First-time setup + +1. Open this repository in VS Code. +2. Run **Dev Containers: Reopen in Container**. +3. In the container terminal, run: + +```bash +.devcontainer/scripts/bootstrap-magento.sh +``` + +When done: + +- Storefront: `http://localhost:8080/` +- Admin: `http://localhost:8080/admin` +- Default admin: `admin / Admin123!Admin123!` +- OpenSearch API: `http://localhost:9200/` + +## Daily workflow + +After editing this module, sync it into Magento and run upgrade: + +```bash +.devcontainer/scripts/install-module.sh +``` + +## Useful commands + +Run from inside `/var/www/magento`: + +```bash +php bin/magento cache:flush +php bin/magento indexer:reindex +php bin/magento setup:di:compile +``` + +Run static analysis tools from the container: + +```bash +phpcs --severity=9 --standard=Magento2 --ignore=*/vendor/* . +phpstan --version +phpunit --version +``` diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..028ba3d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +{ + "name": "ImageKit Magento Latest E2E", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/var/www/magento/app/code/ImageKit/ImageKitMagento", + "shutdownAction": "stopCompose", + "forwardPorts": [ + 8080, + 3306, + 9200 + ], + "portsAttributes": { + "8080": { + "label": "Magento", + "onAutoForward": "notify" + }, + "3306": { + "label": "MariaDB", + "onAutoForward": "silent" + }, + "9200": { + "label": "OpenSearch", + "onAutoForward": "silent" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "bmewburn.vscode-intelephense-client", + "xdebug.php-debug", + "ms-azuretools.vscode-docker" + ], + "settings": { + "php.validate.executablePath": "/usr/local/bin/php", + "editor.formatOnSave": false, + "intelephense.environment.phpVersion": "8.2.0" + } + } + }, + "containerEnv": { + "MAGENTO_ROOT": "/var/www/magento" + }, + "postCreateCommand": "bash .devcontainer/scripts/post-create.sh" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..c0c5b8b --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,80 @@ +services: + app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + args: + MAGENTO_PUBLIC_KEY: ${MAGENTO_PUBLIC_KEY:-} + MAGENTO_PRIVATE_KEY: ${MAGENTO_PRIVATE_KEY:-} + MAGENTO_VERSION: ${MAGENTO_VERSION:-2.4.*} + environment: + MAGENTO_ROOT: /var/www/magento + PHP_IDE_CONFIG: serverName=imagekit-magento + XDEBUG_MODE: debug,develop + XDEBUG_CONFIG: client_host=host.docker.internal client_port=9003 idekey=VSCODE + MAGENTO_PUBLIC_KEY: ${MAGENTO_PUBLIC_KEY:-} + MAGENTO_PRIVATE_KEY: ${MAGENTO_PRIVATE_KEY:-} + MAGENTO_VERSION: ${MAGENTO_VERSION:-2.4.*} + MAGENTO_BASE_URL: ${MAGENTO_BASE_URL:-http://localhost:8080/} + MAGENTO_DB_HOST: ${MAGENTO_DB_HOST:-db} + MAGENTO_DB_NAME: ${MAGENTO_DB_NAME:-magento} + MAGENTO_DB_USER: ${MAGENTO_DB_USER:-magento} + MAGENTO_DB_PASSWORD: ${MAGENTO_DB_PASSWORD:-magento} + MAGENTO_SEARCH_ENGINE: ${MAGENTO_SEARCH_ENGINE:-opensearch} + MAGENTO_OPENSEARCH_HOST: ${MAGENTO_OPENSEARCH_HOST:-opensearch} + MAGENTO_OPENSEARCH_PORT: ${MAGENTO_OPENSEARCH_PORT:-9200} + MAGENTO_ADMIN_FIRSTNAME: ${MAGENTO_ADMIN_FIRSTNAME:-Admin} + MAGENTO_ADMIN_LASTNAME: ${MAGENTO_ADMIN_LASTNAME:-User} + MAGENTO_ADMIN_EMAIL: ${MAGENTO_ADMIN_EMAIL:-admin@example.com} + MAGENTO_ADMIN_USER: ${MAGENTO_ADMIN_USER:-admin} + MAGENTO_ADMIN_PASSWORD: ${MAGENTO_ADMIN_PASSWORD:-Admin123!Admin123!} + MAGENTO_ADMIN_URI: ${MAGENTO_ADMIN_URI:-admin} + volumes: + - magento-data:/var/www/magento + - ..:/var/www/magento/app/code/ImageKit/ImageKitMagento + depends_on: + - db + - opensearch + + web: + image: nginx:1.26-alpine + depends_on: + - app + ports: + - "8080:80" + volumes: + - magento-data:/var/www/magento + - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + + db: + image: mariadb:10.6 + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: magento + MYSQL_USER: magento + MYSQL_PASSWORD: magento + ports: + - "3306:3306" + volumes: + - mariadb-data:/var/lib/mysql + + opensearch: + image: opensearchproject/opensearch:2.19.1 + environment: + discovery.type: single-node + DISABLE_INSTALL_DEMO_CONFIG: "true" + DISABLE_SECURITY_PLUGIN: "true" + OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m" + ports: + - "9200:9200" + ulimits: + memlock: + soft: -1 + hard: -1 + volumes: + - opensearch-data:/usr/share/opensearch/data + +volumes: + magento-data: + mariadb-data: + opensearch-data: diff --git a/.devcontainer/nginx/default.conf b/.devcontainer/nginx/default.conf new file mode 100644 index 0000000..588afbb --- /dev/null +++ b/.devcontainer/nginx/default.conf @@ -0,0 +1,125 @@ +upstream fastcgi_backend { + server app:9000; +} + +server { + listen 80; + server_name localhost; + + set $MAGE_ROOT /var/www/magento; + root $MAGE_ROOT/pub; + + index index.php; + autoindex off; + charset UTF-8; + error_page 404 403 = /errors/404.php; + + location /.user.ini { + deny all; + } + + location / { + try_files $uri $uri/ /index.php$is_args$args; + } + + location /pub/ { + location ~ ^/pub/media/(downloadable|customer|import|custom_options|theme_customization/.*\.xml) { + deny all; + } + alias $MAGE_ROOT/pub/; + add_header X-Frame-Options "SAMEORIGIN"; + } + + location /static/ { + location ~ ^/static/version\d*/ { + rewrite ^/static/version\d*/(.*)$ /static/$1 last; + } + + location ~* \.(ico|jpg|jpeg|png|gif|svg|svgz|webp|avif|avifs|js|css|eot|ttf|otf|woff|woff2|html|json|webmanifest)$ { + add_header Cache-Control "public"; + add_header X-Frame-Options "SAMEORIGIN"; + expires +1y; + + if (!-f $request_filename) { + rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; + } + } + + location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { + add_header Cache-Control "no-store"; + add_header X-Frame-Options "SAMEORIGIN"; + expires off; + + if (!-f $request_filename) { + rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; + } + } + + if (!-f $request_filename) { + rewrite ^/static/(version\d*/)?(.*)$ /static.php?resource=$2 last; + } + add_header X-Frame-Options "SAMEORIGIN"; + } + + location /media/ { + try_files $uri $uri/ /get.php$is_args$args; + + location ~ ^/media/theme_customization/.*\.xml { + deny all; + } + + location ~* \.(ico|jpg|jpeg|png|gif|svg|svgz|webp|avif|avifs|js|css|eot|ttf|otf|woff|woff2)$ { + add_header Cache-Control "public"; + add_header X-Frame-Options "SAMEORIGIN"; + expires +1y; + try_files $uri $uri/ /get.php$is_args$args; + } + + location ~* \.(zip|gz|gzip|bz2|csv|xml)$ { + add_header Cache-Control "no-store"; + add_header X-Frame-Options "SAMEORIGIN"; + expires off; + try_files $uri $uri/ /get.php$is_args$args; + } + add_header X-Frame-Options "SAMEORIGIN"; + } + + location /media/customer/ { deny all; } + location /media/downloadable/ { deny all; } + location /media/import/ { deny all; } + location /media/custom_options/ { deny all; } + + location /errors/ { + location ~* \.xml$ { + deny all; + } + } + + location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ { + try_files $uri =404; + fastcgi_pass fastcgi_backend; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + fastcgi_read_timeout 600s; + fastcgi_connect_timeout 600s; + + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param MAGE_RUN_CODE default; + fastcgi_param MAGE_RUN_TYPE store; + include fastcgi_params; + } + + gzip on; + gzip_disable "msie6"; + gzip_comp_level 6; + gzip_min_length 1100; + gzip_buffers 16 8k; + gzip_proxied any; + gzip_types text/plain text/css text/js text/xml text/javascript application/javascript application/x-javascript application/json application/xml application/xml+rss image/svg+xml; + gzip_vary on; + + location ~* (\.php$|\.phtml$|\.htaccess$|\.htpasswd$|\.git) { + deny all; + } +} diff --git a/.devcontainer/scripts/bootstrap-magento.sh b/.devcontainer/scripts/bootstrap-magento.sh new file mode 100755 index 0000000..c3c9e83 --- /dev/null +++ b/.devcontainer/scripts/bootstrap-magento.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +MAGENTO_ROOT="${MAGENTO_ROOT:-/var/www/magento}" +MAGENTO_VERSION="${MAGENTO_VERSION:-2.4.*}" +BASE_URL="${MAGENTO_BASE_URL:-http://localhost:8080/}" + +DB_HOST="${MAGENTO_DB_HOST:-db}" +DB_NAME="${MAGENTO_DB_NAME:-magento}" +DB_USER="${MAGENTO_DB_USER:-magento}" +DB_PASSWORD="${MAGENTO_DB_PASSWORD:-magento}" +SEARCH_ENGINE="${MAGENTO_SEARCH_ENGINE:-opensearch}" +OPENSEARCH_HOST="${MAGENTO_OPENSEARCH_HOST:-opensearch}" +OPENSEARCH_PORT="${MAGENTO_OPENSEARCH_PORT:-9200}" + +ADMIN_FIRSTNAME="${MAGENTO_ADMIN_FIRSTNAME:-Admin}" +ADMIN_LASTNAME="${MAGENTO_ADMIN_LASTNAME:-User}" +ADMIN_EMAIL="${MAGENTO_ADMIN_EMAIL:-admin@example.com}" +ADMIN_USER="${MAGENTO_ADMIN_USER:-admin}" +ADMIN_PASSWORD="${MAGENTO_ADMIN_PASSWORD:-Admin123!}" +ADMIN_URI="${MAGENTO_ADMIN_URI:-admin}" + +MAGENTO_PUBLIC_KEY="${MAGENTO_PUBLIC_KEY:-}" +MAGENTO_PRIVATE_KEY="${MAGENTO_PRIVATE_KEY:-}" +PHP_BIN="php -d memory_limit=-1" + +fix_permissions() { + if ! id -u www-data > /dev/null 2>&1; then + return + fi + + chown -R www-data:www-data "${MAGENTO_ROOT}" + find "${MAGENTO_ROOT}" -type d -exec chmod 2775 {} + + find "${MAGENTO_ROOT}" -type f -exec chmod 664 {} + + chown -R www-data:www-data /composer/ +} + +wait_for_db() { + echo "Waiting for MariaDB at ${DB_HOST}..." + until mysqladmin ping --silent --host="${DB_HOST}" --user="${DB_USER}" --password="${DB_PASSWORD}"; do + sleep 2 + done +} + +wait_for_opensearch() { + if [ "${SEARCH_ENGINE}" != "opensearch" ]; then + return + fi + + echo "Waiting for OpenSearch at ${OPENSEARCH_HOST}:${OPENSEARCH_PORT}..." + until curl --silent --fail "http://${OPENSEARCH_HOST}:${OPENSEARCH_PORT}" > /dev/null; do + sleep 2 + done +} + +cd "${MAGENTO_ROOT}" + +mkdir -p var generated pub/static pub/media app/etc +fix_permissions + +COMPOSER_MEMORY_LIMIT=-1 composer install --no-interaction --prefer-dist + +wait_for_db +wait_for_opensearch + +if [ ! -f "${MAGENTO_ROOT}/app/etc/env.php" ]; then + setup_install_args=( + --base-url="${BASE_URL}" + --db-host="${DB_HOST}" + --db-name="${DB_NAME}" + --db-user="${DB_USER}" + --db-password="${DB_PASSWORD}" + --admin-firstname="${ADMIN_FIRSTNAME}" + --admin-lastname="${ADMIN_LASTNAME}" + --admin-email="${ADMIN_EMAIL}" + --admin-user="${ADMIN_USER}" + --admin-password="${ADMIN_PASSWORD}" + --backend-frontname="${ADMIN_URI}" + --language=en_US + --currency=USD + --timezone=UTC + --use-rewrites=1 + --search-engine="${SEARCH_ENGINE}" + ) + + if [ "${SEARCH_ENGINE}" = "opensearch" ]; then + setup_install_args+=( + --opensearch-host="${OPENSEARCH_HOST}" + --opensearch-port="${OPENSEARCH_PORT}" + --opensearch-enable-auth=0 + ) + fi + + XDEBUG_MODE=off ${PHP_BIN} bin/magento setup:install "${setup_install_args[@]}" +fi + +fix_permissions + +bash /var/www/magento/app/code/ImageKit/ImageKitMagento/.devcontainer/scripts/install-module.sh +bash /var/www/magento/app/code/ImageKit/ImageKitMagento/.devcontainer/scripts/disable-2fa.sh +bash /var/www/magento/app/code/ImageKit/ImageKitMagento/.devcontainer/scripts/install-sampledata.sh +bash /var/www/magento/app/code/ImageKit/ImageKitMagento/.devcontainer/scripts/set-admin-session-timeout.sh + +echo +echo "Magento is ready at: ${BASE_URL}" +echo "Admin URL: ${BASE_URL}${ADMIN_URI}" +echo "Admin user: ${ADMIN_USER}" +echo "Admin password: ${ADMIN_PASSWORD}" diff --git a/.devcontainer/scripts/disable-2fa.sh b/.devcontainer/scripts/disable-2fa.sh new file mode 100644 index 0000000..3d02d69 --- /dev/null +++ b/.devcontainer/scripts/disable-2fa.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +MAGENTO_ROOT="${MAGENTO_ROOT:-/var/www/magento}" +PHP_BIN="php -d memory_limit=-1" + +XDEBUG_MODE=off ${PHP_BIN} bin/magento module:disable Magento_AdminAdobeImsTwoFactorAuth Magento_TwoFactorAuth +XDEBUG_MODE=off ${PHP_BIN} bin/magento cache:flush + +echo "2FA disabled." diff --git a/.devcontainer/scripts/install-module.sh b/.devcontainer/scripts/install-module.sh new file mode 100755 index 0000000..2a05b31 --- /dev/null +++ b/.devcontainer/scripts/install-module.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +MAGENTO_ROOT="${MAGENTO_ROOT:-/var/www/magento}" +PHP_BIN="php -d memory_limit=-1" + +if [ ! -f "${MAGENTO_ROOT}/bin/magento" ]; then + echo "Magento CLI not found at ${MAGENTO_ROOT}/bin/magento" + exit 1 +fi + +cd "${MAGENTO_ROOT}" + +if ! composer show imagekit/imagekit > /dev/null 2>&1; then + COMPOSER_MEMORY_LIMIT=-1 composer require --no-interaction imagekit/imagekit:^4.0 +fi + +XDEBUG_MODE=off ${PHP_BIN} bin/magento module:enable ImageKit_ImageKitMagento +XDEBUG_MODE=off ${PHP_BIN} bin/magento setup:upgrade +XDEBUG_MODE=off ${PHP_BIN} bin/magento setup:di:compile +XDEBUG_MODE=off ${PHP_BIN} bin/magento cache:flush + +echo "ImageKit module installed and enabled." diff --git a/.devcontainer/scripts/install-sampledata.sh b/.devcontainer/scripts/install-sampledata.sh new file mode 100644 index 0000000..9b5d140 --- /dev/null +++ b/.devcontainer/scripts/install-sampledata.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +MAGENTO_ROOT="${MAGENTO_ROOT:-/var/www/magento}" +PHP_BIN="php -d memory_limit=-1" + +XDEBUG_MODE=off ${PHP_BIN} bin/magento sampledata:deploy +XDEBUG_MODE=off ${PHP_BIN} bin/magento module:enable Magento_CatalogSampleData Magento_BundleSampleData Magento_GroupedProductSampleData Magento_DownloadableSampleData Magento_ThemeSampleData Magento_ConfigurableSampleData Magento_ReviewSampleData Magento_OfflineShippingSampleData Magento_CatalogRuleSampleData Magento_TaxSampleData Magento_SalesRuleSampleData Magento_SwatchesSampleData Magento_MsrpSampleData Magento_CustomerSampleData Magento_CmsSampleData Magento_AdminAdobeImsTwoFactorAuth Magento_SalesSampleData Magento_ProductLinksSampleData Magento_WidgetSampleData Magento_WishlistSampleData +XDEBUG_MODE=off ${PHP_BIN} bin/magento setup:upgrade +XDEBUG_MODE=off ${PHP_BIN} bin/magento setup:di:compile +XDEBUG_MODE=off ${PHP_BIN} bin/magento indexer:reindex +XDEBUG_MODE=off ${PHP_BIN} bin/magento cache:flush + +echo "Sample data installed." diff --git a/.devcontainer/scripts/post-create.sh b/.devcontainer/scripts/post-create.sh new file mode 100755 index 0000000..09040fe --- /dev/null +++ b/.devcontainer/scripts/post-create.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Dev container ready." +echo "Next: run '.devcontainer/scripts/bootstrap-magento.sh' to install Magento and module." diff --git a/.devcontainer/scripts/set-admin-session-timeout.sh b/.devcontainer/scripts/set-admin-session-timeout.sh new file mode 100644 index 0000000..a16949f --- /dev/null +++ b/.devcontainer/scripts/set-admin-session-timeout.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +MAGENTO_ROOT="${MAGENTO_ROOT:-/var/www/magento}" +PHP_BIN="php -d memory_limit=-1" + +XDEBUG_MODE=off ${PHP_BIN} bin/magento config:set admin/security/session_lifetime 31536000 + +echo "Admin session timeout set to 1 year." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2b4b9b..df568e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,8 @@ jobs: name: M2 PHPStan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: extdn/github-actions-m2/magento-phpstan@master + - uses: actions/checkout@v4 + - uses: extdn/github-actions-m2/magento-phpstan/8.2@master with: composer_name: imagekit/imagekit-magento @@ -15,17 +15,16 @@ jobs: name: M2 Coding Standard runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP Action - uses: shivammathur/setup-php@2.21.1 + uses: shivammathur/setup-php@2.36.0 with: - php-version: 7.4 + php-version: 8.2 - name: Install dependencies run: | + composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true composer global require magento/magento-coding-standard:* - composer global require phpcompatibility/php-compatibility:* composer install - ~/.composer/vendor/bin/phpcs --config-set installed_paths ../../magento/magento-coding-standard/,../../phpcompatibility/php-compatibility - name: PHPCS run: | ~/.composer/vendor/bin/phpcs --severity=9 --standard=Magento2 --ignore=*/vendor/* . @@ -34,10 +33,10 @@ jobs: name: M2 Zip Package runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: montudor/action-zip@v1 with: - args: zip -r imagekit_imagekit-magento.zip . -x *.git* -x .github* + args: zip -r imagekit_imagekit-magento.zip . -x *.git* -x .github* -x ".devcontainer*" - uses: actions/upload-artifact@v4 with: name: imagekit_imagekit-magento.zip diff --git a/.gitignore b/.gitignore index abe6d79..7f751ae 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ /var/package/* !/var/package/*.xml +.devcontainer/.env diff --git a/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 3b76c1e..eaefc99 100644 --- a/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -42,4 +42,19 @@ public function getUploaderUrl() return $imageUploadUrl; } + + public function getVideoImporterUrl() + { + if (!$this->configuration->isEnabled()) { + return null; + } + + try { + $videoImportUrl = $this->_urlBuilder->addSessionParam()->getUrl('imagekit/ajax/importVideo'); + } catch (\Exception $e) { + $videoImportUrl = $this->_urlBuilder->getUrl('imagekit/ajax/importVideo'); + } + + return $videoImportUrl; + } } diff --git a/Controller/Adminhtml/Ajax/ImportVideo.php b/Controller/Adminhtml/Ajax/ImportVideo.php new file mode 100644 index 0000000..9cb3612 --- /dev/null +++ b/Controller/Adminhtml/Ajax/ImportVideo.php @@ -0,0 +1,267 @@ +resultRawFactory = $resultRawFactory; + $this->mediaConfig = $mediaConfig; + $this->fileSystem = $fileSystem; + $this->imageAdapterFactory = $imageAdapterFactory; + $this->curl = $curl; + $this->fileUtility = $fileUtility; + $this->protocolValidator = $protocolValidator; + $this->extensionValidator = $extensionValidator; + $this->storeManager = $storeManager; + $this->configuration = $configuration; + $this->libraryMapFactory = $libraryMapFactory; + } + + public function execute() + { + try { + $fileData = $this->getRequest()->getParam('file'); + if (!isset($fileData['url'])) { + throw new LocalizedException(__('Missing video URL.')); + } + + $videoUrl = $fileData['url']; + $parsedUrl = parse_url($videoUrl); + if ($parsedUrl === false || !isset($parsedUrl['scheme'], $parsedUrl['host'])) { + throw new LocalizedException(__('Invalid video URL.')); + } + $videoPath = $parsedUrl['path'] ?? ''; + $endpoint = (string) $this->configuration->getEndpoint(); + $parsedEndpoint = parse_url($endpoint) ?: []; + $endpointPath = $parsedEndpoint['path'] ?? ''; + + $videoPathParts = array_values(array_filter(explode('/', ltrim($videoPath, '/')))); + $endpointPathParts = array_values(array_filter(explode('/', ltrim($endpointPath, '/')))); + + // If the URL path starts with the same leading segment as the configured endpoint path + // (e.g. endpoint: //..., asset URL: //file.mp4), strip it so + // the stored mapping path remains relative to the endpoint. + if (!empty($videoPathParts) && !empty($endpointPathParts) && $videoPathParts[0] === $endpointPathParts[0]) { + array_shift($videoPathParts); + } + + $thumbnailUrl = $fileData['thumbnail'] + ?? ($parsedUrl['scheme'] . '://' . $parsedUrl['host'] . rtrim($videoPath, '/') . '/ik-thumbnail.jpg' + . (!empty($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '')); + $videoName = $fileData['name'] ?? basename($videoUrl); + + if (!$this->protocolValidator->isValid($thumbnailUrl)) { + throw new LocalizedException(__("Protocol isn't allowed")); + } + + $baseTmpMediaPath = $this->mediaConfig->getBaseTmpMediaPath(); + $thumbnailName = pathinfo($videoName, PATHINFO_FILENAME) . '.jpg'; + $ikUniqId = $this->configuration->generateIkuniqid(); + $thumbnailName = $this->configuration->addUniquePrefixToBasename($thumbnailName, $ikUniqId); + $localFileName = Uploader::getCorrectFileName($thumbnailName); + $localTmpFile = Uploader::getDispretionPath($localFileName) + . DIRECTORY_SEPARATOR . $localFileName; + $localFilePath = $baseTmpMediaPath . $localTmpFile; + + $destinationFile = $this->appendAbsoluteFileSystemPath($localFilePath); + $fileName = Uploader::getNewFileName($destinationFile); + $fileInfo = pathinfo($localFilePath); + $localFilePath = $fileInfo['dirname'] . DIRECTORY_SEPARATOR . $fileName; + + $this->retrieveRemoteThumbnail($thumbnailUrl, $localFilePath); + + $localFileFullPath = $this->appendAbsoluteFileSystemPath($localFilePath); + $imageAdapter = $this->imageAdapterFactory->create(); + $imageAdapter->validateUploadFile($localFileFullPath); + + $extension = pathinfo($localFileFullPath, PATHINFO_EXTENSION); + if (!$this->extensionValidator->isValid($extension)) { + throw new LocalizedException(__('Disallowed file type.')); + } + + $tmpFileName = $localFilePath; + if (substr($tmpFileName, 0, strlen($baseTmpMediaPath)) == $baseTmpMediaPath) { + $tmpFileName = substr($tmpFileName, strlen($baseTmpMediaPath)); + } + + $result = []; + $result['name'] = basename($localFilePath); + $result['type'] = $imageAdapter->getMimeType(); + $result['error'] = 0; + $result['size'] = filesize($localFileFullPath); + $result['url'] = $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $localFilePath; + $result['tmp_name'] = $localFileFullPath; + $result['file'] = $tmpFileName; + + $result['media_type'] = 'external-video'; + $result['video_provider'] = 'imagekit'; + $result['video_url'] = $videoUrl; + $result['video_title'] = pathinfo($videoName, PATHINFO_FILENAME); + $result['video_description'] = ''; + + $parsedThumbUrl = parse_url($thumbnailUrl); + if ($parsedThumbUrl === false || !isset($parsedThumbUrl['scheme'], $parsedThumbUrl['host'], $parsedThumbUrl['path'])) { + throw new LocalizedException(__('Invalid thumbnail URL.')); + } + $cleanThumbnailUrl = $parsedThumbUrl['scheme'] . '://' . $parsedThumbUrl['host'] + . $parsedThumbUrl['path']; + $this->saveMapping($ikUniqId, $cleanThumbnailUrl, 'video-thumbnail'); + } catch (\Exception $e) { + $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()]; + $fileWriter = $this->fileSystem->getDirectoryWrite(DirectoryList::MEDIA); + if (isset($localFileFullPath) && $fileWriter->isExist($localFileFullPath)) { + $fileWriter->delete($localFileFullPath); + } + } + + $response = $this->resultRawFactory->create(); + $response->setHeader('Content-Type', 'application/json'); + $response->setContents(json_encode($result)); + return $response; + } + + protected function retrieveRemoteThumbnail($fileUrl, $localFilePath, $maxRetries = 5, $retryDelaySec = 3) + { + $attempts = 0; + + while ($attempts < $maxRetries) { + $this->curl->setConfig(['header' => false]); + $this->curl->write('GET', $fileUrl); + $body = $this->curl->read(); + $statusCode = (int) $this->curl->getInfo(CURLINFO_HTTP_CODE); + $this->curl->close(); + + if ($statusCode === 200 && !empty($body)) { + $this->fileUtility->saveFile($localFilePath, $body); + return; + } + + if ($statusCode === 202) { + $attempts++; + if ($attempts < $maxRetries) { + sleep($retryDelaySec); + } + continue; + } + + if (empty($body)) { + throw new LocalizedException( + __('The preview image information is unavailable. Check your connection and try again.') + ); + } + + throw new LocalizedException( + __('Failed to retrieve thumbnail. HTTP status: %1', $statusCode) + ); + } + + throw new LocalizedException( + __('Thumbnail generation timed out after %1 attempts. Please try again later.', $maxRetries) + ); + } + + protected function appendAbsoluteFileSystemPath($localTmpFile) + { + $mediaDirectory = $this->fileSystem->getDirectoryRead(DirectoryList::MEDIA); + return $mediaDirectory->getAbsolutePath() . $localTmpFile; + } + + protected function saveMapping($ikUniqId, $remoteFilePath, $assetType = 'image') + { + return $this->libraryMapFactory->create() + ->setImagePath($ikUniqId) + ->setIkPath($remoteFilePath) + ->setAssetType($assetType) + ->save(); + } +} diff --git a/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php b/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php index 8bd8c4a..16c56a6 100644 --- a/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php +++ b/Controller/Adminhtml/Cms/Wysiwyg/Images/Upload.php @@ -90,7 +90,6 @@ public function __construct( Context $context, Registry $coreRegistry, JsonFactory $resultJsonFactory, - DirectoryResolver $directoryResolver = null, DirectoryList $directoryList, Config $mediaConfig, Filesystem $fileSystem, @@ -101,7 +100,8 @@ public function __construct( NotProtectedExtension $extensionValidator, FileIo $file, LibraryMapFactory $libraryMapFactory, - ConfigurationInterface $configuration + ConfigurationInterface $configuration, + ?DirectoryResolver $directoryResolver = null ) { parent::__construct($context, $coreRegistry, $resultJsonFactory, $directoryResolver); $this->directoryList = $directoryList; @@ -141,11 +141,13 @@ public function execute() $this->validateRemoteFileExtensions($localFilePath); $this->retrieveRemoteImage($this->remoteFileUrl, $localFilePath); - $this->getStorage()->resizeFile($localFilePath, true); - $this->imageAdapter->validateUploadFile($localFilePath); + if ($this->isImageFile($fileData)) { + $this->getStorage()->resizeFile($localFilePath, true); + $this->imageAdapter->validateUploadFile($localFilePath); + } - $result = $this->appendResultSaveRemoteImage($localFilePath); + $result = $this->appendResultSaveRemoteImage($localFilePath, $fileData); $this->saveMapping($ikUniqId, $fileData['filePath']); } catch (\Exception $e) { @@ -157,15 +159,23 @@ public function execute() return $resultJson->setData($result); } + private function isImageFile($fileData) + { + return !isset($fileData['fileType']) || $fileData['fileType'] === 'image'; + } + private function addFallbackExtension($localFileName, $fileData) { - $fileType = $fileData['fileType']; + $pathInfo = $this->file->getPathInfo($localFileName); - if ($fileType === "image") { - $pathInfo = $this->file->getPathInfo($localFileName); + if ($this->isImageFile($fileData)) { if (!isset($pathInfo['extension'])) { $localFileName = $localFileName . ".jpg"; } + } else { + if (!isset($pathInfo['extension'])) { + $localFileName = $localFileName . ".mp4"; + } } return $localFileName; @@ -194,8 +204,11 @@ private function validatePath($path, $directoryConfig = DirectoryList::MEDIA) private function validateRemoteFileExtensions($filePath) { $extension = $this->file->getPathInfo($filePath)['extension']; - $allowedExtensions = (array) $this->getStorage()->getAllowedExtensions($this->getRequest()->getParam('type')); - if (!$this->extensionValidator->isValid($extension) || !in_array($extension, $allowedExtensions)) { + $type = $this->getRequest()->getParam('type'); + $allowedExtensions = (array) $this->getStorage()->getAllowedExtensions($type); + $mediaExtensions = (array) $this->getStorage()->getAllowedExtensions('media'); + $allAllowed = array_unique(array_merge($allowedExtensions, $mediaExtensions)); + if (!$this->extensionValidator->isValid($extension) || !in_array($extension, $allAllowed)) { throw new ValidatorException(__('Disallowed file type.')); } } @@ -213,11 +226,15 @@ protected function retrieveRemoteImage($fileUrl, $localFilePath) $this->fileUtility->saveFile($localFilePath, $image); } - protected function appendResultSaveRemoteImage($filePath) + protected function appendResultSaveRemoteImage($filePath, $fileData = []) { $fileInfo = $this->file->getPathInfo($filePath); $result['name'] = $fileInfo['basename']; - $result['type'] = $this->imageAdapter->getMimeType(); + if ($this->isImageFile($fileData)) { + $result['type'] = $this->imageAdapter->getMimeType(); + } else { + $result['type'] = mime_content_type($filePath) ?: 'application/octet-stream'; + } $result['error'] = 0; $result['size'] = filesize($filePath); // phpcs:ignore Magento2.Functions.DiscouragedFunction.Discouraged $result['url'] = $this->getRequest()->getParam('remote_image'); diff --git a/Core/ImageKitImageProvider.php b/Core/ImageKitImageProvider.php index 375d5f3..3ef66c5 100644 --- a/Core/ImageKitImageProvider.php +++ b/Core/ImageKitImageProvider.php @@ -25,25 +25,33 @@ public function __construct( public function retrieveTransformed(string $image, array $transformations, string $originalUrl) { - if ($transformations === null || empty($transformations)) { + if (empty($transformations)) { $transformations = []; } - preg_match('/(ik_[A-Za-z0-9]{13}_).+$/i', $image, $ikUniqid); - if ($ikUniqid && isset($ikUniqid[1])) { - $mapped = $this->libraryMapFactory - ->create() - ->getCollection() - ->addFieldToFilter('image_path', $ikUniqid) - ->setPageSize(1) - ->getFirstItem(); - if ($mapped->getIkPath()) { - $image = $mapped->getIkPath(); - } elseif (!$this->configuration->isOriginConfigured()) { - return $originalUrl; - } else { - $image = $this->configuration->getPath($image); + $ikPath = $this->resolveImageKitPath($image); + + if ($ikPath !== null && preg_match('#^https?://#i', $ikPath)) { + $endpoint = rtrim((string) $this->configuration->getEndpoint(), '/'); + + if (empty($endpoint) || stripos($ikPath, $endpoint) !== 0) { + return $ikPath; } + + $ikPath = substr($ikPath, strlen($endpoint)); + + // Otherwise it's a native ImageKit asset — apply transformations. + return $this->imageKitClient->getClient()->url( + [ + "path" => $ikPath, + "transformation" => $transformations, + ] + ); + } + + // Mapped relative path — use it directly as the image path. + if ($ikPath !== null) { + $image = $ikPath; } elseif (!$this->configuration->isOriginConfigured()) { return $originalUrl; } else { @@ -53,7 +61,7 @@ public function retrieveTransformed(string $image, array $transformations, strin return $this->imageKitClient->getClient()->url( [ "path" => $image, - "transformation" => $transformations + "transformation" => $transformations, ] ); } @@ -62,4 +70,27 @@ public function retrieve(string $image, string $originalUrl) { return $this->retrieveTransformed($image, [], $originalUrl); } + + /** + * Look up the ImageKit path from the library map. + * + * @return string|null The mapped ikPath, or null if no mapping exists. + */ + private function resolveImageKitPath(string $image): ?string + { + preg_match('/(ik_[A-Za-z0-9]{13}_).+$/i', $image, $ikUniqid); + + if (empty($ikUniqid[1])) { + return null; + } + + $mapped = $this->libraryMapFactory + ->create() + ->getCollection() + ->addFieldToFilter('image_path', $ikUniqid[1]) + ->setPageSize(1) + ->getFirstItem(); + + return $mapped->getIkPath() ?: null; + } } diff --git a/Plugin/AddImagesToGalleryBlock.php b/Plugin/AddImagesToGalleryBlock.php index 2bcda36..d1397bb 100644 --- a/Plugin/AddImagesToGalleryBlock.php +++ b/Plugin/AddImagesToGalleryBlock.php @@ -3,18 +3,55 @@ namespace ImageKit\ImageKitMagento\Plugin; use Magento\Catalog\Block\Product\View\Gallery; +use Magento\Framework\Json\DecoderInterface; +use Magento\Framework\Json\EncoderInterface; class AddImagesToGalleryBlock { + /** + * @var DecoderInterface + */ + private $jsonDecoder; + + /** + * @var EncoderInterface + */ + private $jsonEncoder; + + public function __construct( + DecoderInterface $jsonDecoder, + EncoderInterface $jsonEncoder + ) { + $this->jsonDecoder = $jsonDecoder; + $this->jsonEncoder = $jsonEncoder; + } + public function afterGetGalleryImages(Gallery $subject, $images) { try { - // foreach ($images as $key => $value) { - // print_r($value); - // } return $images; } catch (\Exception $e) { return $images; } } + + public function afterGetMediaGalleryDataJson(Gallery $subject, $result) + { + try { + $mediaGalleryData = $this->jsonDecoder->decode($result); + $galleryImages = $subject->getProduct()->getMediaGalleryImages(); + $i = 0; + + foreach ($galleryImages as $image) { + if (isset($mediaGalleryData[$i]) && $image->getVideoProvider()) { + $mediaGalleryData[$i]['videoProvider'] = $image->getVideoProvider(); + } + $i++; + } + + return $this->jsonEncoder->encode($mediaGalleryData); + } catch (\Exception $e) { + return $result; + } + } } diff --git a/Plugin/Catalog/Block/Product/ImageFactory.php b/Plugin/Catalog/Block/Product/ImageFactory.php index 5fe5244..2de13d5 100644 --- a/Plugin/Catalog/Block/Product/ImageFactory.php +++ b/Plugin/Catalog/Block/Product/ImageFactory.php @@ -72,7 +72,9 @@ public function aroundCreate( ); $imagePath = preg_replace('/\/catalog\/product\/cache\/[a-f0-9]{32}\//', '/', $imagePath); - $image = sprintf('catalog/product%s', $imagePath); + $imagePath = ltrim($imagePath, '/'); + $imagePath = preg_replace('#^catalog/product/#', '', $imagePath); + $image = sprintf('catalog/product/%s', $imagePath); $transformations = $this->createTransformation($imageMiscParams); $generatedImageUrl = $this->imageKitImageProvider->retrieveTransformed( diff --git a/Plugin/Catalog/Model/Product/Image/UrlBuilder.php b/Plugin/Catalog/Model/Product/Image/UrlBuilder.php index e3743ee..4dd82d2 100644 --- a/Plugin/Catalog/Model/Product/Image/UrlBuilder.php +++ b/Plugin/Catalog/Model/Product/Image/UrlBuilder.php @@ -75,7 +75,9 @@ public function aroundGetUrl( ); $imagePath = preg_replace('/\/catalog\/product\/cache\/[a-f0-9]{32}\//', '/', $imagePath); - $image = sprintf('catalog/product%s', $imagePath); + $imagePath = ltrim($imagePath, '/'); + $imagePath = preg_replace('#^catalog/product/#', '', $imagePath); + $image = sprintf('catalog/product/%s', $imagePath); $transformations = $this->createTransformation($imageMiscParams); $url = $this->imagekitImageProvider->retrieveTransformed( diff --git a/Plugin/Helper/Image.php b/Plugin/Helper/Image.php index 6486e36..36e7e64 100644 --- a/Plugin/Helper/Image.php +++ b/Plugin/Helper/Image.php @@ -62,7 +62,10 @@ public function aroundGetUrl(CatalogImageHelper $helper, \Closure $originalMetho } $imagePath = $this->imageFile ?: $this->product->getData($helper->getType()); - $image = sprintf('catalog/product%s', $imagePath); + $imagePath = ltrim($imagePath, '/'); + $imagePath = preg_replace('#^media/#', '', $imagePath); + $imagePath = preg_replace('#^catalog/product/#', '', $imagePath); + $image = sprintf('catalog/product/%s', $imagePath); $this->createTransformation($helper); return $this->imageKitImageProvider->retrieveTransformed($image, [$this->transformations], $originalMethod()); diff --git a/composer.json b/composer.json index c70ff26..29899d7 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "ImageKit 2 Magento Integration", "type": "magento2-module", "license": "MIT", - "version": "1.3.0", + "version": "1.4.0", "authors": [ { "name": "ImageKit Developer", diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml index 7a1a1a9..1581f7a 100644 --- a/etc/adminhtml/di.xml +++ b/etc/adminhtml/di.xml @@ -12,4 +12,23 @@ + + + + + 1 + 1 + 1 + 1 + + + 1 + 1 + 1 + 1 + + + + + \ No newline at end of file diff --git a/etc/csp_whitelist.xml b/etc/csp_whitelist.xml index 4dad3f2..e0cec2a 100644 --- a/etc/csp_whitelist.xml +++ b/etc/csp_whitelist.xml @@ -35,6 +35,12 @@ *.imagekit.io + + + imagekit.io + *.imagekit.io + + unpkg.com diff --git a/etc/db_schema.xml b/etc/db_schema.xml index a2f9bbd..9bb4318 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -4,6 +4,7 @@ + diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json new file mode 100644 index 0000000..49bb251 --- /dev/null +++ b/etc/db_schema_whitelist.json @@ -0,0 +1,14 @@ +{ + "imagekit_library_map": { + "column": { + "id": true, + "image_path": true, + "ik_path": true, + "asset_type": true + }, + "constraint": { + "PRIMARY": true, + "IMAGEKIT_LIBRARY_MAP_IMAGE_PATH": true + } + } +} \ No newline at end of file diff --git a/etc/module.xml b/etc/module.xml index f91982c..b74fb4f 100644 --- a/etc/module.xml +++ b/etc/module.xml @@ -1,4 +1,4 @@ - + diff --git a/view/adminhtml/requirejs-config.js b/view/adminhtml/requirejs-config.js index 43f17b2..d468387 100644 --- a/view/adminhtml/requirejs-config.js +++ b/view/adminhtml/requirejs-config.js @@ -1,5 +1,5 @@ var config = { paths: { - imagekitMediaLibrary: "//unpkg.com/imagekit-media-library-widget@1.0.4/dist/imagekit-media-library-widget.min" + imagekitMediaLibrary: "//unpkg.com/imagekit-media-library-widget@2.4.1/dist/imagekit-media-library-widget.min" } } \ No newline at end of file diff --git a/view/adminhtml/templates/catalog/product/helper/gallery.phtml b/view/adminhtml/templates/catalog/product/helper/gallery.phtml index dbcaf25..b6ef5ec 100644 --- a/view/adminhtml/templates/catalog/product/helper/gallery.phtml +++ b/view/adminhtml/templates/catalog/product/helper/gallery.phtml @@ -64,6 +64,7 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode(): "containerId": ".ik-ml-gallery-modal", "ikMLContainer": "#ik-ml-gallery-modal-content", "imageUploaderUrl": "escapeUrl($block->getUploaderUrl()) ?>", + "videoImporterUrl": "escapeUrl($block->getVideoImporterUrl()) ?>", "triggerSelector": "#media_gallery_content", "triggerEvent": "addItem" } diff --git a/view/adminhtml/web/js/panel.js b/view/adminhtml/web/js/panel.js index ea1c89c..925f6e5 100644 --- a/view/adminhtml/web/js/panel.js +++ b/view/adminhtml/web/js/panel.js @@ -5,16 +5,19 @@ define([ 'Magento_Ui/js/modal/alert', 'mage/backend/notification', 'mage/translate', -], function (Element, $, IKMediaLibraryWidget, uiAlert, notification, $t,) { +], function (Element, $, ikMLWidget, uiAlert, notification, $t) { 'use strict'; + const IKMediaLibraryWidget = ikMLWidget.ImagekitMediaLibraryWidget; + return Element.extend({ defaults: { "containerId": ".imagekit-media-library-modal", "ikMLContainer": "#imagekit-media-library-modal-content", "triggerSelector": null, "triggerEvent": null, - "callback": null + "callback": null, + "videoImporterUrl": null }, initialize: function () { this._super(); @@ -41,8 +44,14 @@ define([ if (event.eventType === "INSERT") { for (const data of event.data) { + var isVideo = data.fileType && data.fileType !== 'image' && + /\.(mp4|webm|ogv|mov)$/i.test(data.name); + var uploadUrl = (isVideo && widget.videoImporterUrl) + ? widget.videoImporterUrl + : widget.imageUploaderUrl; + $.ajax({ - url: widget.imageUploaderUrl, + url: uploadUrl, data: { file: data, param_name: 'image', diff --git a/view/frontend/requirejs-config.js b/view/frontend/requirejs-config.js new file mode 100644 index 0000000..4e6514e --- /dev/null +++ b/view/frontend/requirejs-config.js @@ -0,0 +1,14 @@ +var config = { + map: { + '*': { + videoHtml5: 'ImageKit_ImageKitMagento/js/video-html5' + } + }, + config: { + mixins: { + 'Magento_ProductVideo/js/fotorama-add-video-events': { + 'ImageKit_ImageKitMagento/js/fotorama-add-video-events-mixin': true + } + } + } +}; diff --git a/view/frontend/web/js/fotorama-add-video-events-mixin.js b/view/frontend/web/js/fotorama-add-video-events-mixin.js new file mode 100644 index 0000000..ed8ecd4 --- /dev/null +++ b/view/frontend/web/js/fotorama-add-video-events-mixin.js @@ -0,0 +1,41 @@ +define([ + 'jquery', + 'videoHtml5' +], function ($) { + 'use strict'; + + return function (target) { + var originalCreateVideoData = $.mage.AddFotoramaVideoEvents.prototype._createVideoData; + + $.mage.AddFotoramaVideoEvents.prototype._createVideoData = function (inputData, isJSON) { + var videoData = originalCreateVideoData.call(this, inputData, isJSON), + parsedInput = isJSON ? JSON.parse(inputData) : inputData, + i; + + for (i = 0; i < videoData.length; i++) { + if (parsedInput[i] && parsedInput[i].videoUrl && parsedInput[i].videoProvider === 'imagekit') { + videoData[i].id = parsedInput[i].videoUrl; + videoData[i].provider = 'imagekit'; + videoData[i].videoUrl = parsedInput[i].videoUrl; + } + } + + return videoData; + }; + + // Override productVideoLoader._create to handle ImageKit videos + // with an HTML5