diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..2c9ec0d --- /dev/null +++ b/.distignore @@ -0,0 +1,28 @@ +# Paths (relative to the plugin dir) excluded from the distributable ZIP and the WP.org SVN deploy +# Read by bin/build-zip.php; CI deploys the extracted ZIP, so this file defines what reaches WP.org. +/.git +/.github +/.vscode +/.wordpress-org +/node_modules +/vendor +/tests +/build +.distignore +.gitignore +.gitattributes +.editorconfig +.DS_Store +Thumbs.db +README.md +composer.json +composer.lock +package.json +package-lock.json +phpcs.xml +phpcs.xml.dist +.phpcs.xml.dist +phpstan.neon +phpstan.neon.dist +*.log +*.map diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5f60d4d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# WordPress coding standards: tabs, LF, UTF-8. +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab + +[*.{yml,yaml,json}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index 2468acf..5d6ca39 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,11 @@ +# Line endings: always LF (WordPress Coding Standards; CI runs on Linux). +* text=auto eol=lf +*.png binary +*.jpg binary +*.gif binary +*.pdf binary +*.zip binary + # Export only the plugin directory in GitHub-generated source zips # 1) Exclude everything by default diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..541d7aa --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,53 @@ +# GitHub Actions Workflows + +Два workflow-а: **QA** проверява всяка промяна, **Complete Release Pipeline** пуска версия само ако QA мине. + +## `ci.yml` — QA + +**Кога:** push в `main`/`dev`, PR към `main`/`dev`, ръчно, или извикан от `release.yml`. + +| Job | Какво проверява | +|---|---| +| Version consistency | `Version:` в header-а = `Stable tag` в readme = `BIZZIO_SYNC_GENCLOUD_VERSION` = fallback в core класа; има changelog запис; при таг — тагът съвпада (`bin/check-versions.php`) | +| PHP 7.4 … 8.5 syntax | `php -l` на всеки PHP файл за всяка поддържана версия | +| PHPCS + PHPStan | WordPress Coding Standards + PHPCompatibilityWP (`phpcs.xml.dist`), PHPStan level 5 с WP/WC stubs (`phpstan.neon.dist`); грешките излизат като анотации в PR-а | +| Plugin Check | Официалният WP.org Plugin Check (strict + experimental) върху **билднатия ZIP**, т.е. точно това, което ще се качи | + +Версиите на инструментите са заключени в `composer.lock`, затова локално и в CI резултатът е еднакъв. + +## `release.yml` — Complete Release Pipeline + +``` +QA (ci.yml) ──► Build ZIP ──┬─► GitHub Release + └─► WordPress.org SVN (trunk + tags/) +``` + +**Как се пуска версия:** +```bash +# 1. Вдигни версията: header + константа + fallback в core класа + readme (Stable tag + changelog) +composer qa # локално: PHPCS + PHPStan + Plugin Check (LocalWP) +php bin/check-versions.php # трябва да отпечата новата версия +# 2. merge в main, после: +git tag v1.2.3 && git push origin v1.2.3 +``` + +- При **таг `v*`**: QA → build → GitHub Release → deploy в WordPress.org. +- **Ръчно** (Actions → Complete Release Pipeline → Run workflow): QA → build → GitHub Release; deploy в WP.org само ако `deploy-to-wporg = true`. +- Всичко се билдва от тагнатия комит, а не от `main`. +- ZIP-ът се прави с `bin/build-zip.php` по `.distignore` (в корена на репото). В WP.org се качва съдържанието на същия ZIP. +- Банери/иконки за WP.org: сложи ги в `.wordpress-org/` в корена — 10up action-ът ги качва в `assets/`. + +**Тайни:** `WP_SVN_USERNAME`, `WP_SVN_PASSWORD`. +**Environment:** deploy job-ът ползва environment `wordpress.org`. В Settings → Environments може да се добави задължително одобрение преди качване. + +## Локално (същите проверки) + +| Команда | Какво прави | +|---|---| +| `composer lint` / `composer lint:fix` | PHPCS / PHPCBF | +| `composer analyse` | PHPStan | +| `composer plugin-check` | Plugin Check в LocalWP сайта от `wp-cli.local.yml` | +| `composer build` | `build/bizzio-sync-for-woocommerce.zip` | +| `composer qa` | версии + lint + analyse + plugin-check | + +Тестовият сайт се настройва с `bin/setup-local-site.sh` (виж коментара в скрипта). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a015323 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,112 @@ +# Quality gate for the free WP.org plugin. Runs on every push/PR and is called by release.yml, +# so nothing is built or deployed unless all of these pass. +name: QA + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + workflow_dispatch: + workflow_call: + +permissions: + contents: read + +concurrency: + group: qa-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PLUGIN_SLUG: bizzio-sync-for-woocommerce + +jobs: + versions: + name: Version consistency + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + + - name: Plugin header, readme, constants and changelog agree + run: | + tag="" + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then tag="${GITHUB_REF_NAME}"; fi + php bin/check-versions.php "$tag" + + php-lint: + name: PHP ${{ matrix.php }} syntax + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # readme.txt "Requires PHP: 7.4" up to the newest supported release. + php: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5"] + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: php -l + run: | + failed=0 + while IFS= read -r -d '' file; do + out=$(php -l "$file" 2>&1) || { echo "$out"; failed=1; } + done < <(find "$PLUGIN_SLUG" -name '*.php' -print0) + exit "$failed" + + static-analysis: + name: PHPCS (WPCS) + PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + tools: cs2pr + + - uses: ramsey/composer-install@v3 + + - name: PHPCS – WordPress Coding Standards + PHPCompatibilityWP + run: vendor/bin/phpcs --no-cache --report-full --report-checkstyle=./phpcs-report.xml + + - name: PHPCS annotations + if: ${{ always() && hashFiles('phpcs-report.xml') != '' }} + run: cs2pr ./phpcs-report.xml --graceful-warnings + + - name: PHPStan + run: vendor/bin/phpstan analyse --memory-limit=2G --no-progress --error-format=github + + plugin-check: + name: Plugin Check (WP.org) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + coverage: none + + # Check exactly what will be shipped: the ZIP built with .distignore, not the repo folder. + - name: Build distributable + run: | + php bin/build-zip.php "$PLUGIN_SLUG" build + mkdir -p build/dist + unzip -q "build/$PLUGIN_SLUG.zip" -d build/dist + + - name: Run Plugin Check + uses: wordpress/plugin-check-action@v1 + with: + build-dir: build/dist/${{ env.PLUGIN_SLUG }} + include-experimental: true + strict: true diff --git a/.github/workflows/deploy-assets.yml b/.github/workflows/deploy-assets.yml deleted file mode 100644 index 94ae8ba..0000000 --- a/.github/workflows/deploy-assets.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Deploy Assets to WordPress.org - -on: - workflow_dispatch: - inputs: - version: - description: 'Version to tag (e.g. 1.2.3)' - required: true - type: string - -jobs: - deploy-code: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Deploy Plugin Code - uses: 10up/action-wordpress-plugin-deploy@v2 - with: - svn-username: ${{ secrets.WP_SVN_USERNAME }} - svn-password: ${{ secrets.WP_SVN_PASSWORD }} - slug: bizzio-sync-for-woocommerce - build-dir: bizzio-sync-gencloud - skip-asset-copy: true - version: ${{ github.event.inputs.version }} diff --git a/.github/workflows/deploy-wporg.yml b/.github/workflows/deploy-wporg.yml deleted file mode 100644 index 8ff6c4a..0000000 --- a/.github/workflows/deploy-wporg.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Deploy to WordPress.org - -on: - workflow_dispatch: - inputs: - dry-run: - description: 'Dry run (skip actual SVN deployment)' - required: false - default: 'false' - type: choice - options: - - 'true' - - 'false' - -jobs: - deploy-wporg: - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - continue-on-error: false - with: - fetch-depth: 0 - - - name: Validate plugin files exist - continue-on-error: false - run: | - echo "=== Validating Plugin Structure ===" - - if [ ! -f "bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php" ]; then - echo "❌ Main plugin file not found!" - exit 1 - fi - - if [ ! -f "bizzio-sync-for-woocommerce/readme.txt" ]; then - echo "❌ readme.txt not found!" - exit 1 - fi - - echo "✅ All required files present" - - - name: Extract and validate versions - id: versions - continue-on-error: false - run: | - if [ "${{ steps.readme.outputs.readme_version }}" != "${{ steps.plugin.outputs.plugin_version }}" ]; then - echo "❌ Version mismatch! readme.txt = ${{ steps.readme.outputs.readme_version }}, bizzio-sync-for-woocommerce.php = ${{ steps.plugin.outputs.plugin_version }}" - exit 1 - fi - - echo "✅ Versions match: $readme_version" - - - name: Deploy Plugin Code - uses: 10up/action-wordpress-plugin-deploy@stable - with: - generate-zip: false - env: - SLUG: bizzio-sync-for-woocommerce - BUILD_DIR: bizzio-sync-for-woocommerce - ASSETS_DIR: .wordpress-org - VERSION: ${{ steps.versions.outputs.readme_version }} - SVN_USERNAME: ${{ secrets.WP_SVN_USERNAME }} - SVN_PASSWORD: ${{ secrets.WP_SVN_PASSWORD }} - - - name: Dry run summary - if: inputs.dry-run == 'true' - run: | - echo "🔍 DRY RUN MODE - No deployment performed" - echo "Version that would be deployed: ${{ steps.versions.outputs.readme_version }}" - echo "Plugin slug: bizzio-sync-for-woocommerce" - - - name: Deployment summary - if: always() - run: | - echo "## WordPress.org Deployment Summary" >> $GITHUB_STEP_SUMMARY - echo "- **Plugin Slug:** bizzio-sync-for-woocommerce" >> $GITHUB_STEP_SUMMARY - echo "- **Version:** ${{ steps.versions.outputs.readme_version }}" >> $GITHUB_STEP_SUMMARY - echo "- **Dry Run:** ${{ inputs.dry-run }}" >> $GITHUB_STEP_SUMMARY - - if [ "${{ inputs.dry-run }}" == "true" ]; then - echo "- **Status:** 🔍 Dry run completed (no actual deployment)" >> $GITHUB_STEP_SUMMARY - else - echo "- **Status:** ✅ Deployed to WordPress.org" >> $GITHUB_STEP_SUMMARY - echo "- **Plugin URL:** https://wordpress.org/plugins/bizzio-sync-for-woocommerce/" >> $GITHUB_STEP_SUMMARY - fi diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index d239988..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,142 +0,0 @@ -name: Build and Deploy WordPress Plugin - -on: - workflow_dispatch: # Позволява ръчно стартиране от GitHub UI - push: - tags: - - "v*" # Стартира се при всеки таг от вида v1.0.0 - -jobs: - build-and-deploy: - runs-on: ubuntu-latest - env: - PLUGIN_SLUG: bizzio-sync-for-woocommerce - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - continue-on-error: false - with: - # Винаги checkout от main branch, независимо от тага - fetch-depth: 0 - - - name: Validate plugin structure - continue-on-error: false - run: | - composer global config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true - composer global require dealerdirect/phpcodesniffer-composer-installer - composer global require wp-coding-standards/wpcs:^3.2 - composer global require wp-coding-standards/wpcs - /home/runner/.composer/vendor/bin/phpcs --config-set installed_paths \ - /home/runner/.composer/vendor/wp-coding-standards/wpcs,\ - /home/runner/.composer/vendor/phpcsstandards/phpcsutils,\ - /home/runner/.composer/vendor/phpcsstandards/phpcsextra - - - name: Auto-fix code style issues (continue even if some remain) - run: /home/runner/.composer/vendor/bin/phpcbf ./bizzio-sync-for-woocommerce || true - - - name: Extract version from files - id: version - continue-on-error: false - run: | - readme_version=$(grep -i "Stable tag" bizzio-sync-for-woocommerce/readme.txt | awk '{print $3}' | tr -d '\r\n ') - plugin_version=$(grep -i "Version:" bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php | head -n 1 | awk '{print $3}' | tr -d '\r\n ') - - echo "readme_version=$readme_version" >> $GITHUB_OUTPUT - echo "plugin_version=$plugin_version" >> $GITHUB_OUTPUT - - echo "Version from readme.txt: $readme_version" - echo "Version from plugin file: $plugin_version" - - if [ "$readme_version" != "$plugin_version" ]; then - echo "❌ Version mismatch detected!" - exit 1 - fi - - echo "✅ Versions match: $readme_version" - echo "✅ Versions match: $readme_version" - - - name: Build plugin ZIP - continue-on-error: false - run: | - mkdir -p build - - echo "=== Build Information ===" - echo "Git Branch: $(git branch --show-current)" - echo "Git Commit: $(git rev-parse HEAD)" - echo "Git Tag: $(git describe --tags --exact-match 2>/dev/null || echo 'No tag')" - echo "Plugin Version: ${{ steps.version.outputs.readme_version }}" - echo "=========================" - - # Създаваме ZIP с папката bizzio-sync-for-woocommerce и цялото й съдържание - zip -r build/${{ env.PLUGIN_SLUG }}.zip bizzio-sync-for-woocommerce/ -x \ - "bizzio-sync-for-woocommerce/.git*" \ - "bizzio-sync-for-woocommerce/.DS_Store" \ - "bizzio-sync-for-woocommerce/node_modules/*" - - # Показваме структурата - echo "=== ZIP Archive Contents (first 30 files) ===" - unzip -l build/${{ env.PLUGIN_SLUG }}.zip | head -30 - echo "" - echo "=== Build Directory ===" - ls -lh build/ - echo "✅ ZIP created successfully" - - - name: Upload ZIP artifact - uses: actions/upload-artifact@v4 - continue-on-error: false - with: - name: ${{ env.PLUGIN_SLUG }}-${{ steps.version.outputs.readme_version }} - path: build/${{ env.PLUGIN_SLUG }}.zip - retention-days: 90 - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/v') - continue-on-error: true - with: - files: build/${{ env.PLUGIN_SLUG }}.zip - name: Release ${{ github.ref_name }} - body: | - ## WordPress Plugin Release ${{ steps.version.outputs.readme_version }} - - Built from `main` branch at commit `${{ github.sha }}` - - ### Download - - [bizzio-sync-for-woocommerce.zip](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/bizzio-sync-for-woocommerce.zip) - - ### Installation - 1. Download the ZIP file above - 2. Go to WordPress Admin → Plugins → Add New → Upload Plugin - 3. Upload the ZIP file and activate - - ### Changelog - See [readme.txt](https://github.com/${{ github.repository }}/blob/main/bizzio-sync-for-woocommerce/readme.txt#L1) for full changelog. - draft: false - prerelease: false - make_latest: true - generate_release_notes: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Summary - if: always() - run: | - echo "## Build Summary" >> $GITHUB_STEP_SUMMARY - echo "- **Plugin:** ${{ env.PLUGIN_SLUG }}" >> $GITHUB_STEP_SUMMARY - echo "- **Version:** ${{ steps.version.outputs.readme_version }}" >> $GITHUB_STEP_SUMMARY - echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Tag:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY - - if [ -f "build/${{ env.PLUGIN_SLUG }}.zip" ]; then - size=$(ls -lh build/${{ env.PLUGIN_SLUG }}.zip | awk '{print $5}') - echo "- **ZIP Size:** $size" >> $GITHUB_STEP_SUMMARY - echo "✅ **Status:** Build successful" >> $GITHUB_STEP_SUMMARY - else - echo "❌ **Status:** Build failed" >> $GITHUB_STEP_SUMMARY - fi - # uses: 10up/action-wordpress-plugin-deploy@v2 - # with: - # plugin_slug: ${{ env.PLUGIN_SLUG }} - # svn_username: ${{ secrets.WPORG_USERNAME }} - # svn_password: ${{ secrets.WPORG_PASSWORD }} diff --git a/.github/workflows/deploy_wporg.yml b/.github/workflows/deploy_wporg.yml deleted file mode 100644 index 54cc52e..0000000 --- a/.github/workflows/deploy_wporg.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Deploy TRUNK (code only) - -on: - workflow_dispatch: - -jobs: - trunk: - runs-on: ubuntu-latest - env: - SLUG: bizzio-sync-for-woocommerce # wp.org plugin slug - BUILD_DIR: bizzio-sync-for-woocommerce # папката на плъгина в GitHub - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} - - steps: - - uses: actions/checkout@v4 - - - name: Install Subversion - run: | - sudo apt-get update -y - sudo apt-get install -y subversion rsync - - - name: Validate source - run: | - test -d "$BUILD_DIR" || { echo "Missing BUILD_DIR: $BUILD_DIR"; exit 1; } - test -f "$BUILD_DIR/readme.txt" || { echo "Missing readme.txt in $BUILD_DIR"; exit 1; } - - - name: Checkout WP.org SVN (trunk only) - id: svn - run: | - set -e - SVN_URL="https://plugins.svn.wordpress.org/${SLUG}" - SVN_DIR="${RUNNER_TEMP}/svn-${SLUG}" - echo "SVN_DIR=$SVN_DIR" >> "$GITHUB_OUTPUT" - svn checkout --depth immediates "$SVN_URL" "$SVN_DIR" - svn update --set-depth infinity "$SVN_DIR/trunk" - - - name: Sync code to trunk (no tags) - run: | - SVN_DIR="${{ steps.svn.outputs.SVN_DIR }}" - rsync -rc "$BUILD_DIR/" "$SVN_DIR/trunk/" --delete --delete-excluded - - - name: SVN add/remove and commit - run: | - SVN_DIR="${{ steps.svn.outputs.SVN_DIR }}" - cd "$SVN_DIR" - svn add . --force > /dev/null - svn status | awk '/^\!/{print $2}' | xargs -r svn rm > /dev/null - svn commit -m "Sync trunk from GitHub" --no-auth-cache --non-interactive \ - --username "$SVN_USERNAME" --password "$SVN_PASSWORD" diff --git a/.github/workflows/plugin-check.yml b/.github/workflows/plugin-check.yml deleted file mode 100644 index b2e79e2..0000000 --- a/.github/workflows/plugin-check.yml +++ /dev/null @@ -1,26 +0,0 @@ -# .github/workflows/plugin-check.yml -name: Plugin Code Quality Check - -on: - pull_request: - branches: - - main # Trigger only when PR is made *towards* main - paths: - - "**.php" - - "**/plugin/**" - -jobs: - plugin-check: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Run Plugin Check - uses: wordpress/plugin-check-action@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - build-dir: "./bizzio-sync-for-woocommerce" # или ./my-plugin ако е в подпапка - strict: true - include-experimental: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 243c787..a2b8673 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,195 +1,141 @@ +# Release: QA gate -> build ZIP -> GitHub Release -> WordPress.org SVN. +# +# 1. Bump the version (plugin header, constant, core-class fallback, readme Stable tag + changelog). +# 2. Merge to main, then: git tag v1.2.3 && git push origin v1.2.3 +# +# Every step builds from the tagged commit; deploy only happens if all QA jobs pass. name: Complete Release Pipeline on: + push: + tags: + - "v*" workflow_dispatch: inputs: deploy-to-wporg: - description: 'Deploy to WordPress.org SVN' + description: "Deploy to WordPress.org SVN" required: false - default: 'true' + default: "false" type: choice options: - - 'true' - - 'false' - push: - tags: - - "v*" + - "true" + - "false" + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + PLUGIN_SLUG: bizzio-sync-for-woocommerce jobs: - validate: - name: Validate Plugin + qa: + name: QA + uses: ./.github/workflows/ci.yml + + build: + name: Build plugin ZIP + needs: qa runs-on: ubuntu-latest outputs: - version: ${{ steps.versions.outputs.readme_version }} - + version: ${{ steps.version.outputs.version }} steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 with: - ref: main - fetch-depth: 0 - - - name: Validate structure - run: | - echo "🔍 Validating plugin structure..." - - required_files=( - "bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php" - "bizzio-sync-for-woocommerce/readme.txt" - "bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud.php" - "bizzio-sync-for-woocommerce/admin/class-bizzio-sync-gencloud-admin.php" - ) - - for file in "${required_files[@]}"; do - if [ ! -f "$file" ]; then - echo "❌ Missing: $file" - exit 1 - fi - echo "✅ Found: $file" - done - - - name: Extract versions - id: versions + php-version: "8.3" + coverage: none + + - name: Resolve version + id: version run: | - readme_version=$(grep -i "Stable tag:" bizzio-sync-for-woocommerce/readme.txt | awk '{print $3}' | tr -d '\r\n ') - plugin_version=$(grep -i "Version:" bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php | head -n 1 | awk '{print $3}' | tr -d '\r\n ') - - echo "readme_version=$readme_version" >> $GITHUB_OUTPUT - echo "plugin_version=$plugin_version" >> $GITHUB_OUTPUT - - echo "📋 readme.txt: $readme_version" - echo "📋 Plugin file: $plugin_version" - - if [ "$readme_version" != "$plugin_version" ]; then - echo "❌ Version mismatch!" - exit 1 - fi - - echo "✅ Version validated: $readme_version" + tag="" + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then tag="${GITHUB_REF_NAME}"; fi + echo "version=$(php bin/check-versions.php "$tag")" >> "$GITHUB_OUTPUT" - build: - name: Build Plugin ZIP - runs-on: ubuntu-latest - needs: validate - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: main - - - name: Build ZIP + - name: Build ZIP (.distignore) run: | - echo "📦 Building plugin ZIP..." - mkdir -p build - - cd bizzio-sync-for-woocommerce - zip -r ../build/bizzio-sync-for-woocommerce.zip . \ - -x ".git*" ".DS_Store" "*.log" "node_modules/*" "tests/*" - cd .. - - ls -lh build/ - echo "✅ ZIP created" - - - name: Upload artifact - uses: actions/upload-artifact@v4 + php bin/build-zip.php "$PLUGIN_SLUG" build + unzip -l "build/$PLUGIN_SLUG.zip" + + - uses: actions/upload-artifact@v4 with: - name: plugin-zip-${{ needs.validate.outputs.version }} - path: build/bizzio-sync-for-woocommerce.zip + name: plugin-zip + path: build/${{ env.PLUGIN_SLUG }}.zip retention-days: 90 github-release: - name: Create GitHub Release + name: GitHub Release + needs: build runs-on: ubuntu-latest - needs: [validate, build] - if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' - + permissions: + contents: write steps: - - name: Download artifact - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4 with: - name: plugin-zip-${{ needs.validate.outputs.version }} - path: ./ - - - name: Create Release - uses: softprops/action-gh-release@v2 + name: plugin-zip + + - uses: softprops/action-gh-release@v2 with: - files: bizzio-sync-for-woocommerce.zip - tag_name: ${{ github.ref_name != '' && github.ref_name || format('v{0}', needs.validate.outputs.version) }} - name: Release v${{ needs.validate.outputs.version }} + files: ${{ env.PLUGIN_SLUG }}.zip + tag_name: v${{ needs.build.outputs.version }} + target_commitish: ${{ github.sha }} + name: Bizzio Sync for WooCommerce v${{ needs.build.outputs.version }} body: | - ## Bizzio Sync for WooCommerce v${{ needs.validate.outputs.version }} - - ### Download - - [bizzio-sync-for-woocommerce.zip](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/bizzio-sync-for-woocommerce.zip) - - ### Installation - 1. Download the ZIP file - 2. WordPress Admin → Plugins → Add New → Upload Plugin - 3. Upload and activate - - ### Support - - [Documentation](https://github.com/${{ github.repository }}) - - [WordPress.org](https://wordpress.org/plugins/bizzio-sync-for-woocommerce/) - draft: false - prerelease: false + Built from `${{ github.sha }}` after PHPCS (WPCS), PHPStan, PHP 7.4–8.5 lint and Plugin Check passed. + + Changelog: see [readme.txt](https://github.com/${{ github.repository }}/blob/v${{ needs.build.outputs.version }}/${{ env.PLUGIN_SLUG }}/readme.txt). make_latest: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} wporg-deploy: name: Deploy to WordPress.org + needs: build + if: github.event_name == 'push' || inputs.deploy-to-wporg == 'true' runs-on: ubuntu-latest - needs: [validate, build] - if: | - (github.event_name == 'workflow_dispatch' && inputs.deploy-to-wporg == 'true') || - (startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push') timeout-minutes: 30 - + environment: + name: wordpress.org + url: https://wordpress.org/plugins/bizzio-sync-for-woocommerce/ steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: main - fetch-depth: 0 - - - name: Deploy to WP.org - uses: 10up/action-wordpress-plugin-deploy@stable + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 with: - generate-zip: false + name: plugin-zip + + # Deploy exactly the QA-checked ZIP contents. With BUILD_DIR set the 10up action + # rsyncs that folder as-is, so .distignore has already been applied by the build. + - name: Extract build + run: | + mkdir -p build/dist + unzip -q "$PLUGIN_SLUG.zip" -d build/dist + + - uses: 10up/action-wordpress-plugin-deploy@stable env: - SLUG: bizzio-sync-for-woocommerce - BUILD_DIR: bizzio-sync-for-woocommerce - VERSION: ${{ needs.validate.outputs.version }} + SLUG: ${{ env.PLUGIN_SLUG }} + VERSION: ${{ needs.build.outputs.version }} + BUILD_DIR: build/dist/${{ env.PLUGIN_SLUG }} + ASSETS_DIR: .wordpress-org SVN_USERNAME: ${{ secrets.WP_SVN_USERNAME }} SVN_PASSWORD: ${{ secrets.WP_SVN_PASSWORD }} - - - name: Notify success - if: success() - run: | - echo "✅ Successfully deployed to WordPress.org" - echo "Plugin URL: https://wordpress.org/plugins/bizzio-sync-for-woocommerce/" summary: - name: Release Summary - runs-on: ubuntu-latest - needs: [validate, build, github-release, wporg-deploy] + name: Summary + needs: [qa, build, github-release, wporg-deploy] if: always() - + runs-on: ubuntu-latest steps: - - name: Generate summary - run: | - echo "## 🚀 Release Pipeline Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ needs.validate.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "**Plugin:** bizzio-sync-for-woocommerce" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Pipeline Status" >> $GITHUB_STEP_SUMMARY - echo "- Validate: ${{ needs.validate.result }}" >> $GITHUB_STEP_SUMMARY - echo "- Build: ${{ needs.build.result }}" >> $GITHUB_STEP_SUMMARY - echo "- GitHub Release: ${{ needs.github-release.result }}" >> $GITHUB_STEP_SUMMARY - echo "- WordPress.org Deploy: ${{ needs.wporg-deploy.result }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Links" >> $GITHUB_STEP_SUMMARY - echo "- [GitHub Releases](https://github.com/${{ github.repository }}/releases)" >> $GITHUB_STEP_SUMMARY - echo "- [WordPress.org Plugin](https://wordpress.org/plugins/bizzio-sync-for-woocommerce/)" >> $GITHUB_STEP_SUMMARY + - run: | + { + echo "## Release v${{ needs.build.outputs.version }}" + echo "| Step | Result |" + echo "|---|---|" + echo "| QA | ${{ needs.qa.result }} |" + echo "| Build | ${{ needs.build.result }} |" + echo "| GitHub Release | ${{ needs.github-release.result }} |" + echo "| WordPress.org | ${{ needs.wporg-deploy.result }} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 529c896..80e3e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,12 @@ *-p .vscode node_modules -composer.lock vendor bizzio-sync-for-woocommerce-pro Bizzio E-commerce API.pdf + +# QA tooling +/build +.phpcs-cache +.phpstan-cache +wp-cli.local.yml diff --git a/bin/build-zip.php b/bin/build-zip.php new file mode 100644 index 0000000..2675a3c --- /dev/null +++ b/bin/build-zip.php @@ -0,0 +1,84 @@ +open( $zip_path, ZipArchive::CREATE ) ) { + fwrite( STDERR, "Cannot create $zip_path\n" ); + exit( 1 ); +} + +$it = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + new RecursiveDirectoryIterator( $src, FilesystemIterator::SKIP_DOTS ), + static function ( SplFileInfo $file ) use ( $src, $patterns ) { + $rel = ltrim( str_replace( '\\', '/', substr( $file->getPathname(), strlen( $src ) ) ), '/' ); + return ! bizzio_build_is_ignored( $rel, $patterns ); + } + ) +); + +$count = 0; +foreach ( $it as $file ) { + $rel = ltrim( str_replace( '\\', '/', substr( $file->getPathname(), strlen( $src ) ) ), '/' ); + $zip->addFile( $file->getPathname(), $slug . '/' . $rel ); + ++$count; +} +$zip->close(); + +echo "Built $zip_path ($count files)\n"; diff --git a/bin/check-versions.php b/bin/check-versions.php new file mode 100644 index 0000000..409704a --- /dev/null +++ b/bin/check-versions.php @@ -0,0 +1,53 @@ + preg_match( '/^\s*\*\s*Version:\s*(\S+)/mi', $main, $m ) ? $m[1] : null, + 'readme.txt Stable tag' => preg_match( '/^Stable tag:\s*(\S+)/mi', $readme, $m ) ? $m[1] : null, + 'BIZZIO_SYNC_GENCLOUD_VERSION' => preg_match( "/define\(\s*'BIZZIO_SYNC_GENCLOUD_VERSION',\s*'([^']+)'/", $main, $m ) ? $m[1] : null, + 'core class fallback version' => preg_match( "/\\\$this->version = '([^']+)'/", $core, $m ) ? $m[1] : null, +); +$version = $found['plugin header Version']; +$errors = array(); + +foreach ( $found as $where => $value ) { + if ( null === $value ) { + $errors[] = "$where: not found"; + } elseif ( $value !== $version ) { + $errors[] = "$where is $value, expected $version"; + } +} + +if ( $version && ! preg_match( '/^= ' . preg_quote( $version, '/' ) . '\b/m', $readme ) ) { + $errors[] = "readme.txt has no changelog entry \"= $version ... =\""; +} + +if ( '' !== $tag && ltrim( $tag, 'v' ) !== $version ) { + $errors[] = "git tag $tag does not match plugin version $version"; +} + +if ( $errors ) { + fwrite( STDERR, "Version check failed:\n - " . implode( "\n - ", $errors ) . "\n" ); + exit( 1 ); +} + +echo $version . PHP_EOL; diff --git a/bin/mu-plugins/bizzio-qa-no-self-update.php b/bin/mu-plugins/bizzio-qa-no-self-update.php new file mode 100644 index 0000000..30a0167 --- /dev/null +++ b/bin/mu-plugins/bizzio-qa-no-self-update.php @@ -0,0 +1,17 @@ +response['bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php'] ) ) { + unset( $transient->response['bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php'] ); + } + return $transient; + } +); diff --git a/bin/setup-local-site.sh b/bin/setup-local-site.sh new file mode 100644 index 0000000..d5d8bce --- /dev/null +++ b/bin/setup-local-site.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Prepare a fresh LocalWP site as the QA/test site for the free plugin. +# +# DB_PASSWORD=... bin/setup-local-site.sh +# e.g. DB_PASSWORD=... bin/setup-local-site.sh "D:/LocalWP-bizzio-free/app/public" http://bizzio-free.local +# +# Points the site at the shared external MariaDB (own table prefix), installs the latest +# WordPress + WooCommerce + Plugin Check, removes everything else, links the plugin from +# this repo via a junction and writes wp-cli.local.yml so `wp` / `composer plugin-check` +# from the repo root target this site. +set -euo pipefail + +SITE="${1:?site public dir required (…/app/public)}" +URL="${2:?site url required (http://xxx.local)}" +DB_HOST="${DB_HOST:-192.168.1.93:3306}" +DB_NAME="${DB_NAME:-local}" +DB_USER="${DB_USER:-localwp}" +DB_PASSWORD="${DB_PASSWORD:?DB_PASSWORD env var required}" +DB_PREFIX="${DB_PREFIX:-bzfree_}" +SLUG="bizzio-sync-for-woocommerce" + +REPO="$(cd "$(dirname "$0")/.." && pwd)" +wp() { command wp --path="$SITE" "$@"; } + +echo "==> wp-config: external DB ${DB_HOST}/${DB_NAME}, prefix ${DB_PREFIX}" +wp config set DB_HOST "$DB_HOST" +wp config set DB_NAME "$DB_NAME" +wp config set DB_USER "$DB_USER" +wp config set DB_PASSWORD "$DB_PASSWORD" --quiet +wp config set table_prefix "$DB_PREFIX" --type=variable +wp config set WP_DEBUG true --raw +wp config set WP_DEBUG_LOG true --raw +wp config set WP_DEBUG_DISPLAY false --raw +wp config set SCRIPT_DEBUG true --raw +wp config set WP_ENVIRONMENT_TYPE local + +if ! wp core is-installed 2>/dev/null; then + echo "==> Installing WordPress into prefix ${DB_PREFIX}" + wp core install --url="$URL" --title="Bizzio Free QA" \ + --admin_user=admin --admin_password=admin --admin_email=admin@example.test --skip-email +fi + +echo "==> Updating core to latest" +wp core update +wp core update-db +wp language core install bg_BG || true + +echo "==> Plugins: WooCommerce + Plugin Check only" +wp plugin delete akismet hello 2>/dev/null || true +wp plugin install woocommerce plugin-check --activate +wp plugin update woocommerce plugin-check +wp option update woocommerce_onboarding_profile '{"skipped":true}' --format=json +wp option update woocommerce_coming_soon no +wp option update woocommerce_currency BGN + +echo "==> Themes: keep only the latest default theme" +DEFAULT_THEME="$(wp theme list --field=name --status=active)" +for theme in $(wp theme list --field=name --status=inactive); do + wp theme delete "$theme" +done +wp theme update --all +echo " active theme: ${DEFAULT_THEME}" + +echo "==> mu-plugin: block WP.org updates of the junctioned plugin (would overwrite the repo)" +MU_DIR="$(wp eval 'echo WPMU_PLUGIN_DIR;')" +mkdir -p "$MU_DIR" +cp "$REPO/bin/mu-plugins/bizzio-qa-no-self-update.php" "$MU_DIR/" + +echo "==> Linking ${SLUG} from repo (junction)" +PLUGINS_DIR="$(wp eval 'echo WP_PLUGIN_DIR;')" +TARGET="${PLUGINS_DIR}/${SLUG}" +if [ ! -e "$TARGET" ]; then + powershell -NoProfile -Command "New-Item -ItemType Junction -Path '$(cygpath -w "$TARGET")' -Target '$(cygpath -w "$REPO/$SLUG")' | Out-Null" +fi +wp plugin activate "$SLUG" + +echo "==> Writing ${REPO}/wp-cli.local.yml" +printf 'path: %s\n' "$SITE" > "$REPO/wp-cli.local.yml" + +wp core version --extra +wp plugin list diff --git a/bizzio-sync-for-woocommerce/admin/class-bizzio-sync-gencloud-admin.php b/bizzio-sync-for-woocommerce/admin/class-bizzio-sync-gencloud-admin.php index ea054de..a986684 100644 --- a/bizzio-sync-for-woocommerce/admin/class-bizzio-sync-gencloud-admin.php +++ b/bizzio-sync-for-woocommerce/admin/class-bizzio-sync-gencloud-admin.php @@ -9,6 +9,10 @@ * @subpackage Bizzio_Sync_Gencloud/admin */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * The admin-specific functionality of the plugin. * @@ -158,6 +162,11 @@ public function setup_sections() { ); } + /** + * Render the description of the API settings section. + * + * @since 1.0.0 + */ public function section_callback() { echo '

' . esc_html__( 'Enter your Bizzio API credentials and settings below.', 'bizzio-sync-for-woocommerce' ) . '

'; } @@ -217,13 +226,19 @@ public function setup_fields() { ); $sanitize_callback = array( $this, 'sanitize_setting' ); - if ( isset( $field['type'] ) && 'password' === $field['type'] ) { + if ( 'password' === $field['type'] ) { $sanitize_callback = array( $this, 'sanitize_password' ); } register_setting( $this->plugin_name, $field['uid'], array( 'sanitize_callback' => $sanitize_callback ) ); } } + /** + * Render a settings field. + * + * @since 1.0.0 + * @param array $arguments Field arguments: uid, type, placeholder, helper. + */ public function field_callback( $arguments ) { $uid = esc_attr( $arguments['uid'] ); $type = esc_attr( isset( $arguments['type'] ) ? $arguments['type'] : 'text' ); @@ -287,12 +302,22 @@ public function sanitize_setting( $input ) { /** * Sanitize a password field from user input. * + * The field is always rendered empty, so an empty submission keeps the stored password + * instead of wiping it. The value is not run through sanitize_text_field() because that + * would alter valid password characters; it is XML-escaped when sent to the API. + * * @since 1.0.2 - * @param string $input The input string. - * @return string The sanitized string. + * @param mixed $input The submitted value. + * @return string The password to store. */ public function sanitize_password( $input ) { - return trim( $input ); + $input = is_string( $input ) ? trim( $input ) : ''; + + if ( '' === $input ) { + return (string) get_option( 'bizzio_api_password', '' ); + } + + return $input; } /** @@ -305,7 +330,7 @@ public function import_products_callback() { check_ajax_referer( 'bizzio_import_products_nonce', 'security' ); $api_client = new Bizzio_Sync_Gencloud_Api_Get_Articles(); - $articles_to_import = $api_client->fetch(); + $articles_to_import = $api_client->fetch( array( 'currency' => get_woocommerce_currency() ) ); if ( is_wp_error( $articles_to_import ) ) { wp_send_json_error( @@ -319,7 +344,7 @@ public function import_products_callback() { $total_articles = count( $articles_to_import ); // Store articles and reset progress counters. - update_option( 'bizzio_sync_gencloud_articles_to_import', $articles_to_import ); + update_option( 'bizzio_sync_gencloud_articles_to_import', $articles_to_import, false ); update_option( 'bizzio_sync_gencloud_total_articles', $total_articles ); update_option( 'bizzio_sync_gencloud_import_progress', 0 ); update_option( 'bizzio_sync_gencloud_imported_count', 0 ); @@ -383,7 +408,7 @@ public function process_product_batch_callback() { $product_barcode = $article_data['Barcode']; $product_price = $article_data['P_Sale']; $product_qty = $article_data['Qty']; - $product_description = $article_data['Description']; + $product_description = wp_kses_post( $article_data['Description'] ); // Check for existing product by SKU, which is more efficient and HPOS-compatible. $product_id = wc_get_product_id_by_sku( $product_barcode ); @@ -398,7 +423,7 @@ public function process_product_batch_callback() { $product = $product_id ? wc_get_product( $product_id ) : new WC_Product_Simple(); if ( ! $product ) { - $new_product_id = new WP_Error( 'product_not_found', 'Product not found' ); + $new_product_id = new WP_Error( 'product_not_found', __( 'Product not found.', 'bizzio-sync-for-woocommerce' ) ); } else { $product->set_name( $product_name ); $product->set_description( $product_description ); @@ -406,7 +431,7 @@ public function process_product_batch_callback() { $product->set_sku( $product_barcode ); $product->set_price( $product_price ); $product->set_regular_price( $product_price ); - $product->set_manage_stock( 'yes' ); + $product->set_manage_stock( true ); $product->set_stock_quantity( $product_qty ); $product->set_stock_status( $product_qty > 0 ? 'instock' : 'outofstock' ); $product->update_meta_data( '_bizzio_barcode', $product_barcode ); @@ -477,7 +502,7 @@ public function get_import_progress_callback() { if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'bizzio-sync-for-woocommerce' ) ), 403 ); } - check_ajax_referer( 'bizzio_import_products_nonce', 'security' ); // Using the same nonce for simplicity + check_ajax_referer( 'bizzio_import_products_nonce', 'security' ); $current_progress = get_option( 'bizzio_sync_gencloud_import_progress', 0 ); $total_articles = get_option( 'bizzio_sync_gencloud_total_articles', 0 ); @@ -516,7 +541,7 @@ public function test_connection_callback() { wp_send_json_error( array( 'message' => $response->get_error_message(), - 'response' => is_callable( array( $response, 'get_error_data' ) ) ? $response->get_error_data() : '', + 'response' => $response->get_error_data(), ) ); } @@ -548,7 +573,7 @@ public function import_categories_callback() { $total_categories = count( $categories_to_import ); // Store categories and reset progress counters. - update_option( 'bizzio_sync_gencloud_categories_to_import', $categories_to_import ); + update_option( 'bizzio_sync_gencloud_categories_to_import', $categories_to_import, false ); update_option( 'bizzio_sync_gencloud_total_categories', $total_categories ); update_option( 'bizzio_sync_gencloud_category_import_progress', 0 ); update_option( 'bizzio_sync_gencloud_category_imported_count', 0 ); @@ -604,7 +629,7 @@ public function process_category_batch_callback() { $category_name = $category_data['Name']; $category_id = $category_data['ID']; $parent_id = $category_data['ID_Parent']; - $category_description = $category_data['Note']; + $category_description = wp_kses_post( $category_data['Note'] ); $category_image_url = $category_data['Image']; $parent_term_id = 0; @@ -645,7 +670,7 @@ public function process_category_batch_callback() { if ( ! is_wp_error( $term_id_result ) ) { ++$imported_count; - $new_term_id = is_array( $term_id_result ) ? $term_id_result['term_id'] : $term_id_result; + $new_term_id = (int) $term_id_result['term_id']; // Download and set category thumbnail using Image Helper. if ( ! empty( $category_image_url ) ) { diff --git a/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-admin-display.php b/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-admin-display.php index 0471d52..886056d 100644 --- a/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-admin-display.php +++ b/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-admin-display.php @@ -1,14 +1,8 @@
@@ -37,4 +35,4 @@
-
\ No newline at end of file + diff --git a/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-categories-display.php b/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-categories-display.php index 9d12e95..f2ef0c7 100644 --- a/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-categories-display.php +++ b/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-categories-display.php @@ -1,14 +1,8 @@
@@ -32,4 +30,4 @@
0%
- \ No newline at end of file + diff --git a/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-products-display.php b/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-products-display.php index eedc777..d5ce384 100644 --- a/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-products-display.php +++ b/bizzio-sync-for-woocommerce/admin/partials/bizzio-sync-gencloud-products-display.php @@ -1,19 +1,8 @@
@@ -36,4 +29,4 @@
0%
- \ No newline at end of file + diff --git a/bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php b/bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php index bf4efd5..868585d 100644 --- a/bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php +++ b/bizzio-sync-for-woocommerce/bizzio-sync-for-woocommerce.php @@ -14,21 +14,22 @@ * @wordpress-plugin * Plugin Name: Bizzio Sync for WooCommerce * Description: Sync products and categories from Bizzio ERP to WooCommerce store. - * Version: 1.1.0 - * Author:gencloud + * Version: 1.1.1 + * Requires at least: 6.5 + * Requires PHP: 7.4 + * Author: gencloud * Author URI: https://gencloud.bg/ - * License: GPL-2.0+ - * License URI: http://www.gnu.org/licenses/gpl-2.0.txt + * License: GPL-2.0-or-later + * License URI: https://www.gnu.org/licenses/gpl-2.0.html * Text Domain: bizzio-sync-for-woocommerce - * Requires Plugins: woocommerce + * Requires Plugins: woocommerce * - * WC requires at least: 8.3.0 - * WC tested up to: 10.1.2 + * WC requires at least: 8.3 + * WC tested up to: 11.2 */ // If this file is called directly, abort. if ( ! defined( 'ABSPATH' ) ) { - // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative exit; } @@ -52,7 +53,7 @@ function () { * * @since 1.0.0 */ -define( 'BIZZIO_SYNC_GENCLOUD_VERSION', '1.1.0' ); +define( 'BIZZIO_SYNC_GENCLOUD_VERSION', '1.1.1' ); /** * Define BIZZIO_SYNC_GENCLOUD_DEBUG_LOG to true in wp-config.php to enable full debug logging. diff --git a/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-client.php b/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-client.php index e11ba3a..d504fda 100644 --- a/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-client.php +++ b/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-client.php @@ -8,6 +8,20 @@ * @package Bizzio_Sync_Gencloud * @subpackage Bizzio_Sync_Gencloud/includes/api */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * The base API client class. + * + * Holds the credentials and the SOAP transport shared by all Bizzio API methods. + * + * @since 1.1.0 + * @package Bizzio_Sync_Gencloud + * @subpackage Bizzio_Sync_Gencloud/includes/api + */ class Bizzio_Sync_Gencloud_Api_Client { /** @@ -61,26 +75,45 @@ class Bizzio_Sync_Gencloud_Api_Client { * @since 1.1.0 */ public function __construct() { - $this->api_database = get_option( 'bizzio_api_database' ); - $this->api_username = get_option( 'bizzio_api_username' ); - $this->api_password = get_option( 'bizzio_api_password' ); - $this->id_site = get_option( 'bizzio_id_site' ); + $this->api_database = (string) get_option( 'bizzio_api_database', '' ); + $this->api_username = (string) get_option( 'bizzio_api_username', '' ); + $this->api_password = (string) get_option( 'bizzio_api_password', '' ); + $this->id_site = (string) get_option( 'bizzio_id_site', '' ); } /** - * Helper function to make SOAP requests + * Build the SOAP envelope with the authentication header around a request body. * - * @param string $soap_action The SOAPAction header. - * @param string $request_body The XML request body. - * @return array|WP_Error The response array or WP_Error on failure. + * All values are XML-escaped so credentials containing &, < or > cannot break the request. + * + * @since 1.1.1 + * @param string $body_xml The inner content. + * @return string The full SOAP envelope. */ - protected function _make_soap_request( $soap_action, $request_body ) { - if ( defined( 'BIZZIO_SYNC_GENCLOUD_DEBUG_LOG' ) && BIZZIO_SYNC_GENCLOUD_DEBUG_LOG ) { - $sanitized_request_body = preg_replace( '/()(.*)(<\/biz:Database>)/i', '$1[REDACTED]$3', $request_body ); - $sanitized_request_body = preg_replace( '/()(.*)(<\/biz:Username>)/i', '$1[REDACTED]$3', $sanitized_request_body ); - $sanitized_request_body = preg_replace( '/()(.*)(<\/biz:Password>)/i', '$1[REDACTED]$3', $sanitized_request_body ); - } + protected function build_envelope( $body_xml ) { + return ' + + + ' . esc_xml( $this->api_database ) . ' + ' . esc_xml( $this->api_username ) . ' + ' . esc_xml( $this->api_password ) . ' + + + + ' . $body_xml . ' + + '; + } + /** + * Helper function to make SOAP requests. + * + * @since 1.1.0 + * @param string $soap_action The SOAPAction header. + * @param string $request_body The XML request body. + * @return array|WP_Error The response array or WP_Error on failure. + */ + protected function make_soap_request( $soap_action, $request_body ) { $args = array( 'body' => $request_body, 'headers' => array( @@ -88,32 +121,38 @@ protected function _make_soap_request( $soap_action, $request_body ) { 'SOAPAction' => $soap_action, ), 'method' => 'POST', - 'timeout' => 60, // seconds + 'timeout' => 60, ); - $response = wp_remote_post( $this->endpoint, $args ); - - return $response; + return wp_remote_post( $this->endpoint, $args ); } /** * Process Bizzio API response and handle common errors. * - * @param SimpleXMLElement|WP_Error $xml The SimpleXMLElement object or WP_Error on failure. - * @param string $context Context for error messages (e.g., 'categories', 'products'). - * @return SimpleXMLElement|WP_Error SimpleXMLElement on success, WP_Error on failure. + * @since 1.1.0 + * @param SimpleXMLElement|WP_Error $xml The SimpleXMLElement object or WP_Error on failure. + * @param string $context Context for error messages (e.g., 'categories', 'products'). + * @return SimpleXMLElement|WP_Error SimpleXMLElement on success, WP_Error on failure. */ - protected function _process_bizzio_response( $xml, $context ) { + protected function process_bizzio_response( $xml, $context ) { if ( is_wp_error( $xml ) ) { - /* translators: 1: context, 2: error message */ - return new WP_Error( 'bizzio_api_error', sprintf( esc_html__( 'Error fetching %1$s: %2$s', 'bizzio-sync-for-woocommerce' ), $context, $xml->get_error_message() ) ); + return new WP_Error( + 'bizzio_api_error', + sprintf( + /* translators: 1: context, 2: error message */ + esc_html__( 'Error fetching %1$s: %2$s', 'bizzio-sync-for-woocommerce' ), + esc_html( $context ), + esc_html( $xml->get_error_message() ) + ) + ); } $namespaces = $xml->getNamespaces( true ); - $body = $xml->children( $namespaces['s'] )->Body; + $body = $xml->children( $namespaces['s'] )->Body; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- SOAP element name. $response_container = $body->children( $namespaces[''] ); - // Determine the correct response element based on context + // Determine the correct response element based on context. $response_element_name = ''; if ( 'categories' === $context || 'connection_test' === $context ) { $response_element_name = 'GetSiteGroupsResponse'; @@ -121,31 +160,39 @@ protected function _process_bizzio_response( $xml, $context ) { $response_element_name = 'GetArticlesResponse'; } - $responseElement = $response_container->{$response_element_name}; + $response_element = $response_container->{$response_element_name}; - if ( ! $responseElement ) { - /* translators: %s: context */ - return new WP_Error( 'bizzio_invalid_response', sprintf( esc_html__( 'Invalid response element for %s.', 'bizzio-sync-for-woocommerce' ), $context ) ); + if ( ! $response_element ) { + return new WP_Error( + 'bizzio_invalid_response', + sprintf( + /* translators: %s: context */ + esc_html__( 'Invalid response element for %s.', 'bizzio-sync-for-woocommerce' ), + esc_html( $context ) + ) + ); } - $error_code = (string) $responseElement->ErrorCode; - $error_message = (string) $responseElement->ErrorMessage; - $error_type = (string) $responseElement->ErrorType; + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Property names mirror the Bizzio SOAP schema. + $error_code = (string) $response_element->ErrorCode; + $error_message = (string) $response_element->ErrorMessage; + $error_type = (string) $response_element->ErrorType; + // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase if ( '0' !== $error_code || 'Success' !== $error_type ) { return new WP_Error( 'bizzio_api_error', sprintf( /* translators: 1: context, 2: error message, 3: error code */ - esc_html__( 'API Error fetching %1$s: %2$s (Code: %3$s).
Please contact Bizzio administrator for assistance.', 'bizzio-sync-for-woocommerce' ), - $context, - $error_message, - $error_code + esc_html__( 'API Error fetching %1$s: %2$s (Code: %3$s). Please contact Bizzio administrator for assistance.', 'bizzio-sync-for-woocommerce' ), + esc_html( $context ), + esc_html( $error_message ), + esc_html( $error_code ) ), array( 'response' => $xml->asXML() ) ); } - return $responseElement; + return $response_element; } } diff --git a/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-articles.php b/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-articles.php index 21e9dbd..92179bc 100644 --- a/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-articles.php +++ b/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-articles.php @@ -8,6 +8,20 @@ * @package Bizzio_Sync_Gencloud * @subpackage Bizzio_Sync_Gencloud/includes/api */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Property names mirror the Bizzio SOAP schema. + +/** + * The GetArticles API class. + * + * @since 1.1.0 + * @package Bizzio_Sync_Gencloud + * @subpackage Bizzio_Sync_Gencloud/includes/api + */ class Bizzio_Sync_Gencloud_Api_Get_Articles extends Bizzio_Sync_Gencloud_Api_Client { /** @@ -30,9 +44,9 @@ public function fetch( $args = array() ) { $args = wp_parse_args( $args, $defaults ); $soap_action = 'http://tempuri.org/IRiznShopExtService/GetArticles'; - $request_body = $this->_build_get_articles_request_body( $args ); + $request_body = $this->build_get_articles_request_body( $args ); - $response = $this->_make_soap_request( $soap_action, $request_body ); + $response = $this->make_soap_request( $soap_action, $request_body ); if ( is_wp_error( $response ) ) { return $response; @@ -45,59 +59,54 @@ public function fetch( $args = array() ) { return new WP_Error( 'bizzio_xml_error', __( 'Failed to parse XML response for products.', 'bizzio-sync-for-woocommerce' ) ); } - $response_element = $this->_process_bizzio_response( $xml, 'products' ); + $response_element = $this->process_bizzio_response( $xml, 'products' ); if ( is_wp_error( $response_element ) ) { return $response_element; } - return $this->_parse_articles_response( $xml, $response_element ); + return $this->parse_articles_response( $xml, $response_element ); } /** * Build the GetArticlesRequest XML body. * - * @since 1.2.0 + * @since 1.1.0 * @param array $args Request arguments. * @return string The XML request body. */ - private function _build_get_articles_request_body( $args ) { + private function build_get_articles_request_body( $args ) { $available_only = $args['available_only'] ? 'true' : 'false'; $is_cars = $args['is_cars'] ? 'true' : 'false'; $is_files = $args['is_files'] ? 'true' : 'false'; $is_qty_by_warehouses = $args['is_qty_by_warehouses'] ? 'true' : 'false'; - return ' - - - ' . $this->api_database . ' - ' . $this->api_username . ' - ' . $this->api_password . ' - - - - + $currency = empty( $args['currency'] ) + ? '' + : '' . esc_xml( $args['currency'] ) . ''; + + return $this->build_envelope( + ' ' . $available_only . ' - - ' . $this->id_site . ' + ' . $currency . ' + ' . esc_xml( $this->id_site ) . ' ' . $is_cars . ' ' . $is_files . ' ' . $is_qty_by_warehouses . ' - - - '; + ' + ); } /** * Parse the GetArticles XML response into a structured array. * - * @since 1.2.0 + * @since 1.1.0 * @param SimpleXMLElement $xml The full XML response. * @param SimpleXMLElement $response_element The GetArticlesResponse element. * @return array Array of parsed articles. */ - private function _parse_articles_response( $xml, $response_element ) { + private function parse_articles_response( $xml, $response_element ) { $namespaces = $xml->getNamespaces( true ); $articles = array(); diff --git a/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-site-groups.php b/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-site-groups.php index 064d7f9..1405fab 100644 --- a/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-site-groups.php +++ b/bizzio-sync-for-woocommerce/includes/api/class-bizzio-sync-gencloud-api-get-site-groups.php @@ -8,6 +8,20 @@ * @package Bizzio_Sync_Gencloud * @subpackage Bizzio_Sync_Gencloud/includes/api */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Property names mirror the Bizzio SOAP schema. + +/** + * The GetSiteGroups API class. + * + * @since 1.1.0 + * @package Bizzio_Sync_Gencloud + * @subpackage Bizzio_Sync_Gencloud/includes/api + */ class Bizzio_Sync_Gencloud_Api_Get_Site_Groups extends Bizzio_Sync_Gencloud_Api_Client { /** @@ -21,7 +35,7 @@ public function fetch( $with_files = true ) { $soap_action = 'http://tempuri.org/IRiznShopExtService/GetSiteGroups'; $request_body = $this->build_get_site_groups_request_body( $with_files ); - $response = $this->_make_soap_request( $soap_action, $request_body ); + $response = $this->make_soap_request( $soap_action, $request_body ); if ( is_wp_error( $response ) ) { return $response; @@ -34,7 +48,7 @@ public function fetch( $with_files = true ) { return new WP_Error( 'bizzio_xml_error', __( 'Failed to parse XML response for categories.', 'bizzio-sync-for-woocommerce' ) ); } - $response_element = $this->_process_bizzio_response( $xml, 'categories' ); + $response_element = $this->process_bizzio_response( $xml, 'categories' ); if ( is_wp_error( $response_element ) ) { return $response_element; @@ -53,11 +67,17 @@ public function test_connection() { $soap_action = 'http://tempuri.org/IRiznShopExtService/GetSiteGroups'; $request_body = $this->build_get_site_groups_request_body( false ); - $response = $this->_make_soap_request( $soap_action, $request_body ); + $response = $this->make_soap_request( $soap_action, $request_body ); if ( is_wp_error( $response ) ) { - /* translators: %s: error message */ - return new WP_Error( 'connection_failed', sprintf( esc_html__( 'Connection test failed: %s', 'bizzio-sync-for-woocommerce' ), $response->get_error_message() ) ); + return new WP_Error( + 'connection_failed', + sprintf( + /* translators: %s: error message */ + esc_html__( 'Connection test failed: %s', 'bizzio-sync-for-woocommerce' ), + esc_html( $response->get_error_message() ) + ) + ); } $body = wp_remote_retrieve_body( $response ); @@ -67,7 +87,7 @@ public function test_connection() { return new WP_Error( 'xml_parsing_failed', esc_html__( 'Connection test failed: Failed to parse XML response.', 'bizzio-sync-for-woocommerce' ) ); } - $result = $this->_process_bizzio_response( $xml, 'connection_test' ); + $result = $this->process_bizzio_response( $xml, 'connection_test' ); if ( is_wp_error( $result ) ) { return $result; @@ -79,34 +99,25 @@ public function test_connection() { /** * Build the GetSiteGroupsRequest XML body. * - * @since 1.2.0 + * @since 1.1.0 * @param bool $with_files Whether to include files. * @return string The XML request body. */ private function build_get_site_groups_request_body( $with_files ) { $is_files = $with_files ? 'true' : 'false'; - return ' - - - ' . $this->api_database . ' - ' . $this->api_username . ' - ' . $this->api_password . ' - - - - - ' . $this->id_site . ' + return $this->build_envelope( + ' + ' . esc_xml( $this->id_site ) . ' ' . $is_files . ' - - - '; + ' + ); } /** * Parse the GetSiteGroups XML response into a structured array. * - * @since 1.2.0 + * @since 1.1.0 * @param SimpleXMLElement $xml The full XML response. * @param SimpleXMLElement $response_element The GetSiteGroupsResponse element. * @return array Array of parsed categories. diff --git a/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-image-helper.php b/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-image-helper.php index a70d03f..89d32a6 100644 --- a/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-image-helper.php +++ b/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-image-helper.php @@ -5,18 +5,22 @@ * Handles all image-related operations for products and categories. * * @link https://gencloud.bg - * @since 1.2.0 + * @since 1.1.0 * * @package Bizzio_Sync_Gencloud * @subpackage Bizzio_Sync_Gencloud/includes */ +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + /** * The image helper class. * * This class defines all image processing functionality. * - * @since 1.2.0 + * @since 1.1.0 * @package Bizzio_Sync_Gencloud * @subpackage Bizzio_Sync_Gencloud/includes * @author gencloud @@ -24,24 +28,23 @@ class Bizzio_Sync_Gencloud_Image_Helper { /** - * Find an attachment ID by the Bizzio image ID stored in post meta. + * Find an attachment ID by a meta value stored on the attachment. * - * @since 1.2.0 - * @param string $bizzio_id The Bizzio image ID. + * @since 1.1.1 + * @param string $meta_key The attachment meta key. + * @param string $meta_value The meta value to look for. * @return int|null The attachment ID if found, otherwise null. */ - public static function find_attachment_by_bizzio_id( $bizzio_id ) { + private static function find_attachment_by_meta( $meta_key, $meta_value ) { global $wpdb; - // Use direct database query for better performance. + // Direct query: a meta_query through WP_Query is far slower for this exact-match lookup on large media libraries. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $attachment_id = $wpdb->get_var( $wpdb->prepare( - "SELECT post_id FROM {$wpdb->postmeta} - WHERE meta_key = '_bizzio_image_id' - AND meta_value = %s - LIMIT 1", - $bizzio_id + "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s LIMIT 1", + $meta_key, + $meta_value ) ); @@ -49,23 +52,43 @@ public static function find_attachment_by_bizzio_id( $bizzio_id ) { } /** - * Sideloads an image from Bizzio, attaches it to a product, and sets meta data. + * Find an attachment ID by the Bizzio image ID stored in post meta. * - * @since 1.2.0 - * @param int $product_id The product ID. - * @param array $image_data Array containing image 'ID', 'Name', and 'Uri'. - * @return int|WP_Error The new attachment ID or a WP_Error on failure. + * @since 1.1.0 + * @param string $bizzio_id The Bizzio image ID. + * @return int|null The attachment ID if found, otherwise null. */ - public static function sideload_bizzio_image( $product_id, $image_data ) { + public static function find_attachment_by_bizzio_id( $bizzio_id ) { + return self::find_attachment_by_meta( '_bizzio_image_id', (string) $bizzio_id ); + } + + /** + * Load the WordPress media functions needed for sideloading outside of wp-admin requests. + * + * @since 1.1.1 + * @return void + */ + private static function load_media_functions() { if ( ! function_exists( 'media_handle_sideload' ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; } + } - $image_url = $image_data['Uri']; + /** + * Download a remote image into the media library. + * + * @since 1.1.1 + * @param string $image_url The remote image URL. + * @param string $file_name The file name to store the image under. + * @param int $parent_id The post the attachment belongs to (0 for none). + * @return int|WP_Error The new attachment ID or a WP_Error on failure. + */ + private static function sideload( $image_url, $file_name, $parent_id ) { + self::load_media_functions(); - if ( empty( $image_url ) || ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) { + if ( empty( $image_url ) || ! wp_http_validate_url( $image_url ) ) { return new WP_Error( 'invalid_image_url', __( 'Invalid image URL provided.', 'bizzio-sync-for-woocommerce' ) ); } @@ -75,19 +98,35 @@ public static function sideload_bizzio_image( $product_id, $image_data ) { } $file_array = array( - 'name' => basename( $image_data['Name'] ), + 'name' => sanitize_file_name( basename( $file_name ) ), 'tmp_name' => $tmp, ); - $attach_id = media_handle_sideload( $file_array, $product_id ); + $attach_id = media_handle_sideload( $file_array, $parent_id ); if ( is_wp_error( $attach_id ) ) { - wp_delete_file( $file_array['tmp_name'] ); - return $attach_id; + wp_delete_file( $tmp ); } - // Store the Bizzio image ID to prevent duplicates. - update_post_meta( $attach_id, '_bizzio_image_id', $image_data['ID'] ); + return $attach_id; + } + + /** + * Sideloads an image from Bizzio, attaches it to a product, and sets meta data. + * + * @since 1.1.0 + * @param int $product_id The product ID. + * @param array $image_data Array containing image 'ID', 'Name', and 'Uri'. + * @return int|WP_Error The new attachment ID or a WP_Error on failure. + */ + public static function sideload_bizzio_image( $product_id, $image_data ) { + $file_name = ! empty( $image_data['Name'] ) ? $image_data['Name'] : wp_parse_url( $image_data['Uri'], PHP_URL_PATH ); + $attach_id = self::sideload( $image_data['Uri'], (string) $file_name, (int) $product_id ); + + if ( ! is_wp_error( $attach_id ) ) { + // Store the Bizzio image ID to prevent duplicates. + update_post_meta( $attach_id, '_bizzio_image_id', $image_data['ID'] ); + } return $attach_id; } @@ -95,7 +134,7 @@ public static function sideload_bizzio_image( $product_id, $image_data ) { /** * Process and attach images to a product. * - * @since 1.2.0 + * @since 1.1.0 * @param int $product_id The product ID. * @param array $files Array of file data from the API. * @return void @@ -116,15 +155,13 @@ public static function process_product_images( $product_id, $files ) { continue; } - // Check if image already exists. + // Reuse the attachment if this Bizzio image was imported before. $attach_id = self::find_attachment_by_bizzio_id( $file_data['ID'] ); - // If not, sideload it. if ( ! $attach_id ) { $attach_id = self::sideload_bizzio_image( $product_id, $file_data ); } - // If we have a valid attachment ID, add it to our gallery array. if ( ! is_wp_error( $attach_id ) ) { $gallery_ids[] = $attach_id; } @@ -148,45 +185,31 @@ public static function process_product_images( $product_id, $files ) { /** * Set WooCommerce category thumbnail from a URL. * - * @since 1.2.0 + * The source URL is stored on the attachment, so re-running the import reuses the + * existing attachment instead of downloading the same image again. + * + * @since 1.1.0 * @param int $term_id The term ID of the category. * @param string $image_url The URL of the image. * @return void */ public static function set_category_thumbnail( $term_id, $image_url ) { - if ( ! function_exists( 'media_handle_sideload' ) ) { - require_once ABSPATH . 'wp-admin/includes/image.php'; - require_once ABSPATH . 'wp-admin/includes/file.php'; - require_once ABSPATH . 'wp-admin/includes/media.php'; - } - - // Check if the image URL is valid. - if ( empty( $image_url ) || ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) { + if ( empty( $image_url ) || ! wp_http_validate_url( $image_url ) ) { return; } - // Need to sideload the image. - $tmp = download_url( $image_url ); - if ( is_wp_error( $tmp ) ) { - return; - } - - $file_array = array( - 'name' => basename( $image_url ), - 'tmp_name' => $tmp, - ); + $attach_id = self::find_attachment_by_meta( '_bizzio_image_url', $image_url ); - // If error storing temporarily, unlink. - if ( is_wp_error( $tmp ) ) { - wp_delete_file( $file_array['tmp_name'] ); - $file_array['tmp_name'] = ''; - } + if ( ! $attach_id ) { + $attach_id = self::sideload( $image_url, (string) wp_parse_url( $image_url, PHP_URL_PATH ), 0 ); - // Do the validation and storage stuff. - $attach_id = media_handle_sideload( $file_array, 0 ); + if ( is_wp_error( $attach_id ) ) { + return; + } - if ( ! is_wp_error( $attach_id ) ) { - update_term_meta( $term_id, 'thumbnail_id', $attach_id ); + update_post_meta( $attach_id, '_bizzio_image_url', $image_url ); } + + update_term_meta( $term_id, 'thumbnail_id', $attach_id ); } } diff --git a/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-loader.php b/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-loader.php index a5321c2..92c076f 100644 --- a/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-loader.php +++ b/bizzio-sync-for-woocommerce/includes/class-bizzio-sync-gencloud-loader.php @@ -1,4 +1,18 @@ version = BIZZIO_SYNC_GENCLOUD_VERSION; } else { - $this->version = '1.1.0'; + $this->version = '1.1.1'; } $this->plugin_name = 'bizzio-sync-gencloud'; diff --git a/bizzio-sync-for-woocommerce/readme.txt b/bizzio-sync-for-woocommerce/readme.txt index 8b1911c..2a2ac45 100644 --- a/bizzio-sync-for-woocommerce/readme.txt +++ b/bizzio-sync-for-woocommerce/readme.txt @@ -1,9 +1,9 @@ === Bizzio Sync for WooCommerce === Contributors: gencloud Tags: woocommerce, bizzio, erp, sync, import -Requires at least: 5.0 -Tested up to: 6.8 -Stable tag: 1.1.0 +Requires at least: 6.5 +Tested up to: 7.1 +Stable tag: 1.1.1 Requires PHP: 7.4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -24,17 +24,18 @@ Note: This plugin requires valid API credentials provided by GenCloud Ltd. and a == External Services == -This plugin communicates with the Bizzio ERP system (by GenCloud Ltd.) to fetch product and category data into WooCommerce. +This plugin connects to the Bizzio ERP API, a service operated by GenCloud Ltd., to import product and category data into WooCommerce. The plugin cannot work without it. -It use only the provided authentication credentials (Database, Username, Password, and Site ID) when the store administrator initiates synchronization. +* **What is sent:** the API credentials entered on the settings page (Database, Username, Password and Site ID) and, for product imports, the store currency code. +* **When:** only when a store administrator clicks "Test Connection", "Import Categories" or "Import Products". Nothing is sent automatically, in the background, or on front-end page views. +* **Where:** `https://bizzio.gencloud.bg/Services/Extensions/RiznShopExtService.svc`. Product and category images returned by the API are downloaded from the image addresses it provides. +* **What is not sent:** no customer, order, visitor or other site data. -No visitor or site data is shared. - -The Bizzio ERP service is operated by GenCloud Ltd. — https://www.gencloud.bg +The Bizzio ERP service is provided by GenCloud Ltd. — https://gencloud.bg/ (contact: web@gencloud.bg). == Installation == -1. Upload the `bizzio-sync-gencloud` folder to the `/wp-content/plugins/` directory. +1. Upload the `bizzio-sync-for-woocommerce` folder to the `/wp-content/plugins/` directory. 2. Activate the plugin through the 'Plugins' menu in WordPress. 3. Go to the **Bizzio Sync** menu in your WordPress admin area. 4. Enter your Bizzio API credentials in the **Settings** tab. @@ -59,6 +60,18 @@ For support, please contact us at [web@gencloud.bg](mailto:web@gencloud.bg). == Changelog == += 1.1.1 - 2026-10-28 = +* Tested up to WordPress 7.1 and WooCommerce 11.2. Requires WordPress 6.5+ (needed for the "Requires Plugins" header). +* Fix: saving the settings no longer clears the stored API password when the password field is left empty. +* Fix: API credentials and Site ID are XML-escaped, so values containing &, < or > no longer break requests. +* Fix: category images are reused instead of being downloaded again on every import; temporary files are removed when an image import fails. +* Fix: "Manage stock" is set correctly on imported products. +* Security: messages and HTML received from the API are escaped/sanitized before display or storage. +* Product prices are requested in the store currency. +* Large import queues are no longer autoloaded on every page load. +* Uninstall now also removes the created/updated counters. +* Code now passes WordPress Coding Standards, PHPStan and Plugin Check. + = 1.1.0 - 2025-11-11 = * Major code refactoring for improved architecture and extensibility. * Added dedicated API layer with separate classes for GetArticles and GetSiteGroups methods. diff --git a/bizzio-sync-for-woocommerce/uninstall.php b/bizzio-sync-for-woocommerce/uninstall.php index a24cae8..6cd0764 100644 --- a/bizzio-sync-for-woocommerce/uninstall.php +++ b/bizzio-sync-for-woocommerce/uninstall.php @@ -10,7 +10,6 @@ // If uninstall not called from WordPress, then exit. if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { - // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative exit; } @@ -24,6 +23,8 @@ 'bizzio_sync_gencloud_total_articles', 'bizzio_sync_gencloud_import_progress', 'bizzio_sync_gencloud_imported_count', + 'bizzio_sync_gencloud_created_count', + 'bizzio_sync_gencloud_updated_count', 'bizzio_sync_gencloud_failed_count', 'bizzio_sync_gencloud_import_status', 'bizzio_sync_gencloud_categories_to_import', diff --git a/composer.json b/composer.json index db20f81..4418b2e 100644 --- a/composer.json +++ b/composer.json @@ -4,18 +4,47 @@ "type": "project", "license": "GPL-2.0-or-later", "require-dev": { - "squizlabs/php_codesniffer": "^3.10", - "wp-coding-standards/wpcs": "^3.0", - "phpcompatibility/php-compatibility": "^9.3", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + "squizlabs/php_codesniffer": "^3.13.5", + "wp-coding-standards/wpcs": "^3.4", + "phpcompatibility/phpcompatibility-wp": "^2.1", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "phpstan/phpstan": "^2.1", + "szepeviktor/phpstan-wordpress": "^2.0", + "php-stubs/woocommerce-stubs": "*", + "phpstan/extension-installer": "^1.4" }, "config": { "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + }, + "sort-packages": true }, "scripts": { - "lint": "vendor/bin/phpcs", - "lint-fix": "vendor/bin/phpcbf" + "lint": "phpcs", + "lint:fix": "phpcbf", + "lint:report": "phpcs --report=source", + "analyse": "phpstan analyse --memory-limit=2G", + "plugin-check": "wp plugin check bizzio-sync-for-woocommerce --include-experimental", + "plugin-check:strict": "wp plugin check bizzio-sync-for-woocommerce --include-experimental --severity=5", + "build": "@php bin/build-zip.php bizzio-sync-for-woocommerce build", + "version-check": "@php bin/check-versions.php", + "qa": [ + "@version-check", + "@lint", + "@analyse", + "@plugin-check" + ] + }, + "scripts-descriptions": { + "lint": "PHPCS: WordPress Coding Standards + PHPCompatibilityWP", + "lint:fix": "PHPCBF: auto-fix of the fixable PHPCS issues", + "lint:report": "PHPCS: summary grouped by sniff", + "analyse": "PHPStan with WordPress + WooCommerce stubs", + "plugin-check": "Plugin Check (PCP) via WP-CLI against the LocalWP test site (wp-cli.local.yml)", + "plugin-check:strict": "Plugin Check, only severity >= 5 (what blocks WP.org review)", + "build": "Build the distributable ZIP into ./build using .distignore", + "version-check": "Header/readme/constant/changelog versions agree (pass a tag to also compare it)", + "qa": "version-check + lint + analyse + plugin-check" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..2d30af0 --- /dev/null +++ b/composer.lock @@ -0,0 +1,919 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "bf2b06f583637bf537d161f26b41d7b2", + "packages": [], + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-05-06T08:26:05+00:00" + }, + { + "name": "php-stubs/woocommerce-stubs", + "version": "v11.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/woocommerce-stubs.git", + "reference": "fb590c1c2a26c47aa5da2c6c51a707130e8a389c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/woocommerce-stubs/zipball/fb590c1c2a26c47aa5da2c6c51a707130e8a389c", + "reference": "fb590c1c2a26c47aa5da2c6c51a707130e8a389c", + "shasum": "" + }, + "require": { + "php-stubs/wordpress-stubs": "^5.3 || ^6.0 || ^7.0" + }, + "require-dev": { + "php": "~7.1 || ~8.0", + "php-stubs/generator": "^0.9.0" + }, + "suggest": { + "symfony/polyfill-php73": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WooCommerce function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/woocommerce-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "woocommerce", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/woocommerce-stubs/issues", + "source": "https://github.com/php-stubs/woocommerce-stubs/tree/v11.2.0" + }, + "time": "2026-10-07T19:02:59+00:00" + }, + { + "name": "php-stubs/wordpress-stubs", + "version": "v7.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "6926c5156431a3ff909d43faa16a92a0bc0c2428" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6926c5156431a3ff909d43faa16a92a0bc0c2428", + "reference": "6926c5156431a3ff909d43faa16a92a0bc0c2428", + "shasum": "" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "5.6.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^5.5", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.9", + "phpdocumentor/reflection-docblock": "^6.0", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.2", + "wp-coding-standards/wpcs": "3.4.1 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v7.1.2" + }, + "time": "2026-10-06T06:59:21+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "1.3.4", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-09-19T17:43:28+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "2.1.8", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^9.0", + "phpcompatibility/phpcompatibility-paragonie": "^1.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-10-18T00:05:59+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/39467533fdb742446d68c1d10ac33d625ee0311c", + "reference": "39467533fdb742446d68c1d10ac33d625ee0311c", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.3", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-07-27T11:13:17+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/5f35d9408c54d7b529501f3c688b6eae562aea1f", + "reference": "5f35d9408c54d7b529501f3c688b6eae562aea1f", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-07-27T10:28:41+00:00" + }, + { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/phpstan/extension-installer.git", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/extension-installer/zipball/85e90b3942d06b2326fba0403ec24fe912372936", + "reference": "85e90b3942d06b2326fba0403ec24fe912372936", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^7.2 || ^8.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "require-dev": { + "composer/composer": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2.0", + "phpstan/phpstan-strict-rules": "^0.11 || ^0.12 || ^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Composer plugin for automatic installation of PHPStan extensions", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpstan/extension-installer/issues", + "source": "https://github.com/phpstan/extension-installer/tree/1.4.3" + }, + "time": "2024-09-04T20:21:43+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.3.3", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4b57dc8ca7456b635d1661845f44bc060b02c6b0", + "reference": "4b57dc8ca7456b635d1661845f44bc060b02c6b0", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-10-27T14:06:26+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.6", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-08-06T00:17:32+00:00" + }, + { + "name": "szepeviktor/phpstan-wordpress", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/szepeviktor/phpstan-wordpress.git", + "reference": "84577e2ea1e4c2a95537fe5ea2ac8e18f4ceab1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/84577e2ea1e4c2a95537fe5ea2ac8e18f4ceab1a", + "reference": "84577e2ea1e4c2a95537fe5ea2ac8e18f4ceab1a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "php-stubs/wordpress-stubs": ">=6.6.2", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "composer/composer": "^2.1.14", + "composer/semver": "^3.4", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.0", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" + }, + "type": "phpstan-extension", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "SzepeViktor\\PHPStan\\WordPress\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress extensions for PHPStan", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v2.0.4" + }, + "time": "2026-05-22T16:22:09+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "ec2ff942335f33683a5957a85d138753876a05cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/ec2ff942335f33683a5957a85d138753876a05cf", + "reference": "ec2ff942335f33683a5957a85d138753876a05cf", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.1", + "phpcsstandards/phpcsutils": "^1.2.3", + "squizlabs/php_codesniffer": "^3.13.5" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2026-07-27T11:53:23+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 3afb2b3..eb6717e 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,65 +1,65 @@ - - bizzio-sync-for-woocommerce + + WordPress Coding Standards + PHP compatibility for the free WP.org plugin. - */vendor/* - */node_modules/* - */.git/* - */.github/* - */.vscode/* - */assets/* + bizzio-sync-for-woocommerce - - + */vendor/* + */node_modules/* + */build/* + *.min.js + *.min.css - - - + + + + + + - - + + + - - - - - + + + - - - + + + + + + + - - - - - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - + - - - - diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..3c5873c --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,23 @@ +# PHPStan for the free plugin. WordPress stubs come from szepeviktor/phpstan-wordpress +# (auto-registered by phpstan/extension-installer); WooCommerce stubs are added below. +parameters: + level: 5 + phpVersion: + min: 70400 + max: 80599 + paths: + - bizzio-sync-for-woocommerce + excludePaths: + - bizzio-sync-for-woocommerce/vendor (?) + - bizzio-sync-for-woocommerce/build (?) + bootstrapFiles: + - vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php + dynamicConstantNames: + - BIZZIO_SYNC_GENCLOUD_DEBUG_LOG + tmpDir: .phpstan-cache + ignoreErrors: + # Admin view templates are included from Bizzio_Sync_Gencloud_Admin methods, where $this is defined. + - + identifier: variable.undefined + message: '#^Variable \$this might not be defined\.$#' + path: bizzio-sync-for-woocommerce/admin/partials/* diff --git a/wp-cli.local.yml.example b/wp-cli.local.yml.example new file mode 100644 index 0000000..bfc43d5 --- /dev/null +++ b/wp-cli.local.yml.example @@ -0,0 +1,3 @@ +# Copy to wp-cli.local.yml (git-ignored) and point it at the LocalWP test site. +# Then `wp ...` and `composer plugin-check` run from the repo root target that site. +path: D:/LocalWP-bizzio-free/app/public