From 9598e221ac4d7023124ea7a8c82bf684a708006e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Mar 2024 01:17:47 +0000 Subject: [PATCH 01/36] Bump ramsey/composer-install from 2 to 3 Bumps [ramsey/composer-install](https://github.com/ramsey/composer-install) from 2 to 3. - [Release notes](https://github.com/ramsey/composer-install/releases) - [Commits](https://github.com/ramsey/composer-install/compare/v2...v3) --- updated-dependencies: - dependency-name: ramsey/composer-install dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/cs-lint.yml | 2 +- .github/workflows/integrations.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cs-lint.yml b/.github/workflows/cs-lint.yml index a12f1cf..91c4721 100644 --- a/.github/workflows/cs-lint.yml +++ b/.github/workflows/cs-lint.yml @@ -54,7 +54,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 # Lint PHP. - name: Lint PHP against parse errors diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 365bdfd..1f7181a 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -71,7 +71,7 @@ jobs: run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Install Composer dependencies - uses: ramsey/composer-install@v2 + uses: ramsey/composer-install@v3 - name: Start MySQL Service run: sudo systemctl start mysql.service From 3d39579fc1d64b8fd63669e9871e27b3bb0c12e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 01:08:41 +0000 Subject: [PATCH 02/36] Bump codecov/codecov-action from 3 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/integrations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 1f7181a..0dd91ae 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -93,7 +93,7 @@ jobs: - name: Send coverage report to Codecov if: ${{ success() && matrix.coverage == true }} - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: files: ./clover.xml fail_ci_if_error: true From 5d2aea2cadbed3beb8362e3cc6253987f359eee6 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Fri, 15 Nov 2024 15:21:19 +0000 Subject: [PATCH 03/36] CI: Add codecov token secret reference --- .github/workflows/integrations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 0dd91ae..3fbacc8 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -97,4 +97,5 @@ jobs: with: files: ./clover.xml fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} verbose: true From ed23f9e6d6cb16bd012de7989d7c099037026c63 Mon Sep 17 00:00:00 2001 From: Raam Dev Date: Tue, 3 Dec 2024 09:00:20 -0500 Subject: [PATCH 04/36] Update GitHub action to install SVN before deploy to WordPress.org --- .github/workflows/deploy.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6d65596..1be8501 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,6 +14,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Install SVN ( Subversion ) + run: | + sudo apt-get update + sudo apt-get install subversion + - name: WordPress Plugin Deploy uses: 10up/action-wordpress-plugin-deploy@stable env: From 5b7092f588672316adc089201dd4a95b2df2cd6f Mon Sep 17 00:00:00 2001 From: Raam Dev Date: Tue, 3 Dec 2024 23:11:46 -0500 Subject: [PATCH 05/36] Fix YAML syntax in GitHub Action for deploy to WordPress.org --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1be8501..f9516f9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,8 +14,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install SVN ( Subversion ) - run: | + - name: Install SVN (Subversion) + run: | sudo apt-get update sudo apt-get install subversion From e8fd628ab0934c15c0fb5c356e987e5c7d64772c Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Wed, 5 Nov 2025 18:14:41 +0000 Subject: [PATCH 06/36] Add wp-env configuration for local development MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add .wp-env.json to standardize local development environment with: - PHP 7.4 (plugin minimum) - Latest WordPress (auto-updates) - Query Monitor auto-installed - Debug constants enabled - Test environment with PHP 8.4 + WordPress trunk 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .wp-env.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .wp-env.json diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 0000000..bfe3821 --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,20 @@ +{ + "plugins": [ + "." + ], + "phpVersion": "7.4", + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + }, + "lifecycleScripts": { + "afterStart": "wp-env run cli wp plugin install query-monitor --activate" + }, + "env": { + "tests": { + "phpVersion": "8.4", + "core": "WordPress/WordPress#trunk" + } + } +} From 596bf5884915351d79afa9319fb06d4fbc93c192 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Thu, 6 Nov 2025 17:06:49 +0000 Subject: [PATCH 07/36] Update composer.json Normalize and expand on items like sorting packages, and script descriptions. --- composer.json | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 75d6011..b9d7897 100644 --- a/composer.json +++ b/composer.json @@ -1,15 +1,19 @@ { "name": "automattic/ad-code-manager", - "type": "wordpress-plugin", "description": "Easy ad code management", - "homepage": "https://github.com/Automattic/ad-code-manager/", "license": "GPL-2.0-or-later", + "type": "wordpress-plugin", "authors": [ { "name": "Automattic", "homepage": "https://automattic.com/" } ], + "homepage": "https://github.com/Automattic/ad-code-manager/", + "support": { + "issues": "https://github.com/Automattic/ad-code-manager/issues", + "source": "https://github.com/Automattic/ad-code-manager" + }, "require": { "php": ">=7.4", "composer/installers": "^1.0 || ^2.0" @@ -24,12 +28,10 @@ "allow-plugins": { "composer/installers": true, "dealerdirect/phpcodesniffer-composer-installer": true - } + }, + "sort-packages": true }, "scripts": { - "cbf": [ - "@php ./vendor/bin/phpcbf" - ], "coverage": [ "@php ./vendor/bin/phpunit --coverage-html ./.phpunit.cache/coverage-html" ], @@ -37,8 +39,12 @@ "@php ./vendor/bin/phpunit" ], "cs": [ - "@php ./vendor/bin/phpcs" + "@php ./vendor/bin/phpcs -q" + ], + "cs-fix": [ + "@php ./vendor/bin/phpcbf -q" ], + "i18n": "@php wp i18n make-pot . ./languages/ad-code-manager.pot", "lint": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" ], @@ -56,8 +62,16 @@ "@composer test" ] }, - "support": { - "issues": "https://github.com/Automattic/ad-code-manager/issues", - "source": "https://github.com/Automattic/ad-code-manager" + "scripts-descriptions": { + "coverage": "Run tests with code coverage reporting", + "coverage-ci": "Run tests with code coverage reporting and send results to stdout", + "cs": "Run PHP Code Sniffer", + "cs-fix": "Run PHP Code Sniffer and fix violations", + "i18n": "Generate a POT file for translation", + "lint": "Run PHP linting", + "lint-ci": "Run PHP linting and send results to stdout", + "prepare-ci": "Prepare the environment for integration tests", + "test": "Run all tests for the Ad Code Manager plugin", + "test-ms": "Run integration tests for the Ad Code Manager plugin in multisite mode" } } From 941f72a70b08ce3320caaf39f0267d3fdc6ee18b Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Fri, 7 Nov 2025 01:21:40 +0000 Subject: [PATCH 08/36] Update minimum WordPress version to 6.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the "Requires WP" header from 5.7 to 6.4 to align with current plugin standards and internal documentation. Also updates: - Test matrix to test against WP 6.4 and 6.8 - README to reflect WP 6.4 minimum and 6.8 tested version - PHPCS minimum supported WP version to 6.4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/integrations.yml | 6 +----- .phpcs.xml.dist | 2 +- README.md | 6 +++--- ad-code-manager.php | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 3fbacc8..f9f0caf 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: php: [ '7.4', '8.2' ] - wordpress: [ '5.7', '6.3' ] + wordpress: [ '6.4', '6.8' ] allowed_failure: [false] coverage: [false] include: @@ -47,10 +47,6 @@ jobs: wordpress: 'latest' allowed_failure: false coverage: true - exclude: - # WordPress 5.7 doesn't support PHP 8.2. - - php: '8.2' - wordpress: '5.7' fail-fast: false continue-on-error: ${{ matrix.allowed_failure }} diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 42011f8..02b110c 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -39,7 +39,7 @@ - + diff --git a/README.md b/README.md index 217262a..f991a8e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Ad Code Manager Stable tag: 0.7.1 -Requires at least: 5.7 -Tested up to: 5.9 +Requires at least: 6.4 +Tested up to: 6.8 Requires PHP: 7.4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -25,7 +25,7 @@ Once this configuration is in place, the Ad Code Manager admin interface will al ## Installation -The plugin requires PHP 7.4 or later. It is also tested WordPress 5.7 and later, though it may run on older versions. +The plugin requires PHP 7.4 or later. It is also tested WordPress 6.4 and later, though it may run on older versions. Since the plugin is in its early stages, there are a couple additional configuration steps: diff --git a/ad-code-manager.php b/ad-code-manager.php index 9b56dcc..1a5e93a 100644 --- a/ad-code-manager.php +++ b/ad-code-manager.php @@ -19,7 +19,7 @@ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * GitHub Plugin URI: https://github.com/Automattic/ad-code-manager/ * Requires PHP: 7.4 - * Requires WP: 5.7 + * Requires WP: 6.4 */ declare(strict_types=1); From 06a1a2578883b88cab3d25618a43bdab1378710d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 01:43:00 +0000 Subject: [PATCH 09/36] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/cs-lint.yml | 2 +- .github/workflows/deploy.yml | 2 +- .github/workflows/integrations.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cs-lint.yml b/.github/workflows/cs-lint.yml index 91c4721..43b01ca 100644 --- a/.github/workflows/cs-lint.yml +++ b/.github/workflows/cs-lint.yml @@ -49,7 +49,7 @@ jobs: uses: korelstar/xmllint-problem-matcher@v1 - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f9516f9..5c0f81e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install SVN (Subversion) run: | diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index f9f0caf..4cdb9e7 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -52,7 +52,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 From 6f24cd7dbea20853c6cbcaa2668a7bcecd013e2c Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 9 Nov 2025 23:01:50 +0000 Subject: [PATCH 10/36] ci: migrate integration tests from SVN to wp-env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modernises the integration test infrastructure to use wp-env instead of the legacy SVN-based test setup. GitHub-hosted runners no longer include SVN by default, causing test failures. This migration aligns with WordPress's recommended testing approach and ensures consistency between local development and CI environments. Updates the workflow to install wp-env, removes MySQL service configuration (now provided by wp-env), and eliminates the prepare-ci script. The WordPress matrix now uses concrete version numbers ('6.8') and 'master' instead of 'latest' and 'trunk', as wp-env expects actual Git branch/tag references. Removes the Codecov integration from CI (coverage can still be run locally via composer scripts). Simplifies the matrix by removing coverage-specific configurations. Renames composer scripts to reflect their integration test purpose: test → test-integration, test-ms → test-integration-ms. Removes bin/install-wp-tests.sh (182 lines) which is no longer needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/integrations.yml | 53 +++------ bin/install-wp-tests.sh | 181 ----------------------------- composer.json | 23 +--- phpunit.xml.dist | 2 +- 4 files changed, 22 insertions(+), 237 deletions(-) delete mode 100755 bin/install-wp-tests.sh diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index f9f0caf..01e1fdd 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -27,26 +27,18 @@ jobs: strategy: matrix: - php: [ '7.4', '8.2' ] + php: [ '7.4', '8.3' ] wordpress: [ '6.4', '6.8' ] allowed_failure: [false] - coverage: [false] include: # Check upcoming WP. - - php: '8.2' - wordpress: 'trunk' + - php: '8.3' + wordpress: 'master' allowed_failure: true - coverage: false # Check upcoming PHP. - - php: '8.3' - wordpress: 'latest' + - php: '8.5' + wordpress: '6.8' allowed_failure: true - coverage: false - # Code coverage on latest PHP and WP. - - php: '8.2' - wordpress: 'latest' - allowed_failure: false - coverage: true fail-fast: false continue-on-error: ${{ matrix.allowed_failure }} @@ -54,11 +46,15 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Install wordpress environment + run: npm install -g @wordpress/env + - name: Setup PHP ${{ matrix.php }} uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} + coverage: none + tools: composer - name: Setup problem matchers for PHP run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" @@ -69,29 +65,10 @@ jobs: - name: Install Composer dependencies uses: ramsey/composer-install@v3 - - name: Start MySQL Service - run: sudo systemctl start mysql.service - - - name: Setting mysql_native_password for PHP <= 7.3 - if: ${{ matrix.php <= 7.3 }} - run: mysql -u root -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';" - - - name: Prepare environment for integration tests - run: composer prepare-ci --no-interaction + - name: Setup wp-env + run: wp-env start + env: + WP_ENV_CORE: WordPress/WordPress#${{ matrix.wordpress }} - name: Run integration tests - if: ${{ matrix.coverage == false }} - run: composer test --no-interaction - - - name: Run integration tests with code coverage - if: ${{ matrix.coverage == true }} - run: composer coverage-ci --no-interaction - - - name: Send coverage report to Codecov - if: ${{ success() && matrix.coverage == true }} - uses: codecov/codecov-action@v5 - with: - files: ./clover.xml - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true + run: composer test-integration --no-interaction diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh deleted file mode 100755 index ee05775..0000000 --- a/bin/install-wp-tests.sh +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env bash - -if [ $# -lt 3 ]; then - echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" - exit 1 -fi - -DB_NAME=$1 -DB_USER=$2 -DB_PASS=$3 -DB_HOST=${4-localhost} -WP_VERSION=${5-latest} -SKIP_DB_CREATE=${6-false} - -TMPDIR=${TMPDIR-/tmp} -TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") -WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} -WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} - -download() { - if [ `which curl` ]; then - curl -s "$1" > "$2"; - elif [ `which wget` ]; then - wget -nv -O "$2" "$1" - fi -} - -if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then - WP_BRANCH=${WP_VERSION%\-*} - WP_TESTS_TAG="branches/$WP_BRANCH" - -elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then - WP_TESTS_TAG="branches/$WP_VERSION" -elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then - if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then - # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x - WP_TESTS_TAG="tags/${WP_VERSION%??}" - else - WP_TESTS_TAG="tags/$WP_VERSION" - fi -elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then - WP_TESTS_TAG="trunk" -else - # http serves a single offer, whereas https serves multiple. we only want one - download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json - grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json - LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') - if [[ -z "$LATEST_VERSION" ]]; then - echo "Latest WordPress version could not be found" - exit 1 - fi - WP_TESTS_TAG="tags/$LATEST_VERSION" -fi -set -ex - -install_wp() { - - if [ -d $WP_CORE_DIR ]; then - return; - fi - - mkdir -p $WP_CORE_DIR - - if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then - mkdir -p $TMPDIR/wordpress-trunk - rm -rf $TMPDIR/wordpress-trunk/* - svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress - mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR - else - if [ $WP_VERSION == 'latest' ]; then - local ARCHIVE_NAME='latest' - elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then - # https serves multiple offers, whereas http serves single. - download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json - if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then - # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x - LATEST_VERSION=${WP_VERSION%??} - else - # otherwise, scan the releases and get the most up to date minor version of the major release - local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` - LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) - fi - if [[ -z "$LATEST_VERSION" ]]; then - local ARCHIVE_NAME="wordpress-$WP_VERSION" - else - local ARCHIVE_NAME="wordpress-$LATEST_VERSION" - fi - else - local ARCHIVE_NAME="wordpress-$WP_VERSION" - fi - download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz - tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR - fi - - download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php -} - -install_test_suite() { - # portable in-place argument for both GNU sed and Mac OSX sed - if [[ $(uname -s) == 'Darwin' ]]; then - local ioption='-i.bak' - else - local ioption='-i' - fi - - # set up testing suite if it doesn't yet exist - if [ ! -d $WP_TESTS_DIR ]; then - # set up testing suite - mkdir -p $WP_TESTS_DIR - rm -rf $WP_TESTS_DIR/{includes,data} - svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes - svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data - fi - - if [ ! -f wp-tests-config.php ]; then - download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php - # remove all forward slashes in the end - WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") - sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php - fi - -} - -recreate_db() { - shopt -s nocasematch - if [[ $1 =~ ^(y|yes)$ ]] - then - mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA - create_db - echo "Recreated the database ($DB_NAME)." - else - echo "Leaving the existing database ($DB_NAME) in place." - fi - shopt -u nocasematch -} - -create_db() { - mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA -} - -install_db() { - - if [ ${SKIP_DB_CREATE} = "true" ]; then - return 0 - fi - - # parse DB_HOST for port or socket references - local PARTS=(${DB_HOST//\:/ }) - local DB_HOSTNAME=${PARTS[0]}; - local DB_SOCK_OR_PORT=${PARTS[1]}; - local EXTRA="" - - if ! [ -z $DB_HOSTNAME ] ; then - if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then - EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" - elif ! [ -z $DB_SOCK_OR_PORT ] ; then - EXTRA=" --socket=$DB_SOCK_OR_PORT" - elif ! [ -z $DB_HOSTNAME ] ; then - EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" - fi - fi - - # create database - if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] - then - echo "Reinstalling will delete the existing test database ($DB_NAME)" - read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB - recreate_db $DELETE_EXISTING_DB - else - create_db - fi -} - -install_wp -install_test_suite -install_db diff --git a/composer.json b/composer.json index b9d7897..c8f0939 100644 --- a/composer.json +++ b/composer.json @@ -35,9 +35,7 @@ "coverage": [ "@php ./vendor/bin/phpunit --coverage-html ./.phpunit.cache/coverage-html" ], - "coverage-ci": [ - "@php ./vendor/bin/phpunit" - ], + "coverage-ci": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager ./vendor/bin/phpunit", "cs": [ "@php ./vendor/bin/phpcs -q" ], @@ -51,27 +49,18 @@ "lint-ci": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git --checkstyle" ], - "prepare-ci": [ - "bash bin/install-wp-tests.sh wordpress_test root root localhost" - ], - "test": [ - "@php ./vendor/bin/phpunit --no-coverage --order-by=random" - ], - "test-ms": [ - "@putenv WP_MULTISITE=1", - "@composer test" - ] + "test-integration": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager ./vendor/bin/phpunit --no-coverage --order-by=random", + "test-integration-ms": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager /bin/bash -c 'WP_MULTISITE=1 ./vendor/bin/phpunit --no-coverage --order-by=random'" }, "scripts-descriptions": { "coverage": "Run tests with code coverage reporting", - "coverage-ci": "Run tests with code coverage reporting and send results to stdout", + "coverage-ci": "Run integration tests with code coverage reporting in wp-env", "cs": "Run PHP Code Sniffer", "cs-fix": "Run PHP Code Sniffer and fix violations", "i18n": "Generate a POT file for translation", "lint": "Run PHP linting", "lint-ci": "Run PHP linting and send results to stdout", - "prepare-ci": "Prepare the environment for integration tests", - "test": "Run all tests for the Ad Code Manager plugin", - "test-ms": "Run integration tests for the Ad Code Manager plugin in multisite mode" + "test-integration": "Run integration tests in wp-env", + "test-integration-ms": "Run integration tests in multisite mode in wp-env" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 98803cd..63d8505 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -27,7 +27,7 @@ - + From 63465b600701363509f1b8efda35a6f10bba9516 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 01:02:49 +0000 Subject: [PATCH 11/36] Bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/cs-lint.yml | 2 +- .github/workflows/deploy.yml | 2 +- .github/workflows/integrations.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cs-lint.yml b/.github/workflows/cs-lint.yml index 43b01ca..848583a 100644 --- a/.github/workflows/cs-lint.yml +++ b/.github/workflows/cs-lint.yml @@ -49,7 +49,7 @@ jobs: uses: korelstar/xmllint-problem-matcher@v1 - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5c0f81e..d93e1b3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install SVN (Subversion) run: | diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 0cffe7b..3ceccf3 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install wordpress environment run: npm install -g @wordpress/env From 15f34c9bd19e55d3ffa827e88ee85406a13a5948 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sat, 13 Dec 2025 00:57:23 -0500 Subject: [PATCH 12/36] ci: standardise GitHub Actions and dependency management Aligns CI configuration with Automattic plugin standards to improve security posture and reduce maintenance overhead across the plugin portfolio. These changes bring the repository in line with established patterns used in other a8c plugins, making it easier for the team to maintain consistent security practices and dependency update schedules. The Dependabot configuration moves from daily to weekly updates with intelligent grouping of related dependencies, reducing PR noise whilst maintaining currency. GitHub Actions now use SHA-pinned references rather than mutable tags, preventing supply chain attacks where action maintainers could push malicious code to existing version tags. Template injection vulnerabilities in workflow commands are eliminated by using environment variables rather than direct context expansion. The addition of explicit permission blocks and credential persistence controls implements least privilege principles, ensuring workflows can only access the resources they genuinely require. The yoast/wp-test-utils constraint is tightened to ^1.2 to ensure compatibility with the updated testing approach, whilst the integration workflow filename is simplified to match standard naming conventions across the plugin collection. --- .github/dependabot.yml | 45 +++++++++++++++---- .github/workflows/cs-lint.yml | 17 ++++--- .github/workflows/deploy.yml | 9 +++- .../{integrations.yml => integration.yml} | 19 +++++--- composer.json | 2 +- 5 files changed, 69 insertions(+), 23 deletions(-) rename .github/workflows/{integrations.yml => integration.yml} (73%) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 727cd89..4ca0efe 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,19 +1,46 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# Configuration for Dependabot version updates +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file version: 2 updates: - - # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" + day: "monday" + groups: + actions: + patterns: ["*"] + labels: + - "dependencies" + reviewers: + - "Automattic/vip-plugins" + commit-message: + prefix: "Actions" + include: "scope" + open-pull-requests-limit: 5 - # Maintain dependencies for Composer - package-ecosystem: "composer" directory: "/" schedule: - interval: "daily" + interval: "weekly" + day: "tuesday" + groups: + dev-dependencies: + patterns: + - "automattic/*" + - "dealerdirect/*" + - "php-parallel-lint/*" + - "phpcompatibility/*" + - "phpunit/*" + - "squizlabs/*" + - "yoast/*" + labels: + - "dependencies" + reviewers: + - "Automattic/vip-plugins" + commit-message: + prefix: "Composer" + include: "scope" + open-pull-requests-limit: 5 + versioning-strategy: increase-if-necessary diff --git a/.github/workflows/cs-lint.yml b/.github/workflows/cs-lint.yml index 848583a..6a7f1e7 100644 --- a/.github/workflows/cs-lint.yml +++ b/.github/workflows/cs-lint.yml @@ -11,6 +11,9 @@ on: # Allow manually triggering the workflow. workflow_dispatch: +# Disable all permissions by default. Enable specific permissions per job. +permissions: {} + jobs: checkcs: name: Lint checks for PHP ${{ matrix.php }} @@ -32,7 +35,7 @@ jobs: steps: - name: Setup PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # 2.36.0 with: php-version: ${{ matrix.php }} coverage: none @@ -41,20 +44,22 @@ jobs: # Show PHP lint violations inline in the file diff. # @link https://github.com/marketplace/actions/xmllint-problem-matcher - name: Register PHP lint violations to appear as file diff comments - uses: korelstar/phplint-problem-matcher@v1 + uses: korelstar/phplint-problem-matcher@cb2b753750ec7bf13a7cde0a476df8c5605bdfb1 # v1.2.0 # Show XML violations inline in the file diff. # @link https://github.com/marketplace/actions/xmllint-problem-matcher - name: Register XML violations to appear as file diff comments - uses: korelstar/xmllint-problem-matcher@v1 + uses: korelstar/xmllint-problem-matcher@1bd292d642ddf3d369d02aaa8b262834d61198c0 # v1.2.0 - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: ramsey/composer-install@v3 + uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # 3.1.1 # Lint PHP. - name: Lint PHP against parse errors @@ -64,7 +69,7 @@ jobs: # @link https://github.com/marketplace/actions/xml-lint - name: Lint phpunit.xml.dist if: ${{ matrix.php >= 8.0 }} - uses: ChristophWurst/xmllint-action@v1 + uses: ChristophWurst/xmllint-action@7c54ff113fc0f6d4588a15cb4dfe31b6ecca5212 # v1.2.1 with: xml-file: ./phpunit.xml.dist xml-schema-file: ./vendor/phpunit/phpunit/phpunit.xsd diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d93e1b3..134fced 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,13 +6,18 @@ on: # Allow manually triggering the workflow. workflow_dispatch: +# Disable all permissions by default. Enable specific permissions per job. +permissions: {} + jobs: tag: name: New tag runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Install SVN (Subversion) run: | @@ -20,7 +25,7 @@ jobs: sudo apt-get install subversion - name: WordPress Plugin Deploy - uses: 10up/action-wordpress-plugin-deploy@stable + uses: 10up/action-wordpress-plugin-deploy@54bd289b8525fd23a5c365ec369185f2966529c2 # 2.3.0 env: SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} SVN_USERNAME: ${{ secrets.SVN_USERNAME }} diff --git a/.github/workflows/integrations.yml b/.github/workflows/integration.yml similarity index 73% rename from .github/workflows/integrations.yml rename to .github/workflows/integration.yml index 3ceccf3..6575c5c 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integration.yml @@ -11,6 +11,9 @@ on: # Allow manually triggering the workflow. workflow_dispatch: +# Disable all permissions by default. Enable specific permissions per job. +permissions: {} + # Cancels all previous workflow runs for the same branch that have not yet completed. concurrency: # The concurrency group contains the workflow name and the branch name. @@ -44,26 +47,32 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false - name: Install wordpress environment run: npm install -g @wordpress/env - name: Setup PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # 2.36.0 with: php-version: ${{ matrix.php }} coverage: none tools: composer - name: Setup problem matchers for PHP - run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" + run: echo "::add-matcher::${TOOL_CACHE}/php.json" + env: + TOOL_CACHE: ${{ runner.tool_cache }} - name: Setup Problem Matchers for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + run: echo "::add-matcher::${TOOL_CACHE}/phpunit.json" + env: + TOOL_CACHE: ${{ runner.tool_cache }} - name: Install Composer dependencies - uses: ramsey/composer-install@v3 + uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # 3.1.1 - name: Setup wp-env run: wp-env start diff --git a/composer.json b/composer.json index c8f0939..be724f7 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "automattic/vipwpcs": "^3", "php-parallel-lint/php-parallel-lint": "^1.0", "phpcompatibility/phpcompatibility-wp": "^2.1", - "yoast/wp-test-utils": "^1" + "yoast/wp-test-utils": "^1.2" }, "config": { "allow-plugins": { From 14acc622348f911fa74250bababdd04126c9d449 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sat, 13 Dec 2025 01:59:21 -0500 Subject: [PATCH 13/36] chore: standardise .gitignore and add .distignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardises .gitignore with consistent patterns and adds .distignore for WordPress.org release packaging. Development files like tests, vendor, and node_modules are now properly excluded from distributions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .distignore | 22 ++++++++++++++++++++++ .gitignore | 19 ++++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 .distignore diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..f049057 --- /dev/null +++ b/.distignore @@ -0,0 +1,22 @@ +# Directories +/.git/ +/.github/ +/bin/ +/node_modules/ +/tests/ +/vendor/ + +# Files +.distignore +.editorconfig +.gitattributes +.gitignore +.phpcs.xml.dist +.wp-env.json +.wp-env.override.json +CHANGELOG.md +composer.json +composer.lock +package.json +package-lock.json +phpunit.xml.dist diff --git a/.gitignore b/.gitignore index 6356f23..51620d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,16 @@ -/.phpunit.cache -/node_modules -/vendor +# Dependencies +/node_modules/ +/vendor/ -/clover.xml +# Composer /composer.lock -/package-lock.json + +# Tests +/.phpunit.cache/ +/clover.xml + +# Local config overrides +/.phpcs.xml +/phpcs.xml +/phpunit.xml +/.wp-env.override.json From e7fb1dfd0ced0bc7d7d2a7882c69f9f89e783098 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sat, 13 Dec 2025 08:18:01 +0000 Subject: [PATCH 14/36] test: add unit test suite with Brain Monkey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds unit tests for Ad_Code_Manager class methods that can be tested without WordPress, including validate_script_url() for URL whitelist validation and filter_output_tokens() for token replacement logic. Changes: - Add tests/Unit/ directory with TestCase base class - Add AdCodeManagerTest.php (10 tests) covering URL validation and token filtering - Add tests/bootstrap.php to support both unit and integration tests - Update phpunit.xml.dist to include Unit testsuite - Add test:unit composer script - Add phpunit ^9.6 to require-dev - Rename test-integration to test:integration for consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- composer.json | 11 +- phpunit.xml.dist | 5 +- tests/Unit/AdCodeManagerTest.php | 182 +++++++++++++++++++++++++++++++ tests/Unit/TestCase.php | 19 ++++ tests/bootstrap.php | 43 ++++++++ 5 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/AdCodeManagerTest.php create mode 100644 tests/Unit/TestCase.php create mode 100644 tests/bootstrap.php diff --git a/composer.json b/composer.json index be724f7..4e6541f 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "automattic/vipwpcs": "^3", "php-parallel-lint/php-parallel-lint": "^1.0", "phpcompatibility/phpcompatibility-wp": "^2.1", + "phpunit/phpunit": "^9.6", "yoast/wp-test-utils": "^1.2" }, "config": { @@ -49,8 +50,9 @@ "lint-ci": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git --checkstyle" ], - "test-integration": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager ./vendor/bin/phpunit --no-coverage --order-by=random", - "test-integration-ms": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager /bin/bash -c 'WP_MULTISITE=1 ./vendor/bin/phpunit --no-coverage --order-by=random'" + "test:unit": "@php ./vendor/bin/phpunit --testsuite Unit", + "test:integration": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager ./vendor/bin/phpunit --testsuite integration --no-coverage --order-by=random", + "test:integration-ms": "wp-env run tests-cli --env-cwd=wp-content/plugins/ad-code-manager /bin/bash -c 'WP_MULTISITE=1 ./vendor/bin/phpunit --testsuite integration --no-coverage --order-by=random'" }, "scripts-descriptions": { "coverage": "Run tests with code coverage reporting", @@ -60,7 +62,8 @@ "i18n": "Generate a POT file for translation", "lint": "Run PHP linting", "lint-ci": "Run PHP linting and send results to stdout", - "test-integration": "Run integration tests in wp-env", - "test-integration-ms": "Run integration tests in multisite mode in wp-env" + "test:unit": "Run unit tests (no WordPress required)", + "test:integration": "Run integration tests in wp-env", + "test:integration-ms": "Run integration tests in multisite mode in wp-env" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 63d8505..1d5a597 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ + + tests/Unit + tests/Integration diff --git a/tests/Unit/AdCodeManagerTest.php b/tests/Unit/AdCodeManagerTest.php new file mode 100644 index 0000000..72827fe --- /dev/null +++ b/tests/Unit/AdCodeManagerTest.php @@ -0,0 +1,182 @@ + null ] ); + + $this->ad_code_manager = new Ad_Code_Manager(); + + // Create a mock provider with whitelisted URLs. + $this->ad_code_manager->current_provider = new stdClass(); + $this->ad_code_manager->current_provider->whitelisted_script_urls = [ + 'example.com', + 'ads.google.com', + 'secure.pagead2.googlesyndication.com', + ]; + } + + /** + * Test validate_script_url with empty URL returns true. + */ + public function testValidateScriptUrlEmptyReturnsTrue(): void { + $this->assertTrue( $this->ad_code_manager->validate_script_url( '' ) ); + } + + /** + * Test validate_script_url with exact domain match. + */ + public function testValidateScriptUrlExactDomainMatch(): void { + Functions\when( 'wp_parse_url' )->alias( 'parse_url' ); + + $this->assertTrue( + $this->ad_code_manager->validate_script_url( 'https://example.com/script.js' ) + ); + } + + /** + * Test validate_script_url with subdomain match. + */ + public function testValidateScriptUrlSubdomainMatch(): void { + Functions\when( 'wp_parse_url' )->alias( 'parse_url' ); + + $this->assertTrue( + $this->ad_code_manager->validate_script_url( 'https://cdn.example.com/ads/script.js' ) + ); + } + + /** + * Test validate_script_url with non-whitelisted domain. + */ + public function testValidateScriptUrlNonWhitelistedDomain(): void { + Functions\when( 'wp_parse_url' )->alias( 'parse_url' ); + + $this->assertFalse( + $this->ad_code_manager->validate_script_url( 'https://malicious-site.com/script.js' ) + ); + } + + /** + * Test validate_script_url prevents domain spoofing. + * + * Ensures that 'evilexample.com' doesn't match 'example.com'. + */ + public function testValidateScriptUrlPreventsDomainSpoofing(): void { + Functions\when( 'wp_parse_url' )->alias( 'parse_url' ); + + $this->assertFalse( + $this->ad_code_manager->validate_script_url( 'https://evilexample.com/script.js' ) + ); + } + + /** + * Test validate_script_url with Google Ads URL. + */ + public function testValidateScriptUrlGoogleAds(): void { + Functions\when( 'wp_parse_url' )->alias( 'parse_url' ); + + $this->assertTrue( + $this->ad_code_manager->validate_script_url( 'https://ads.google.com/ad-manager/tag.js' ) + ); + } + + /** + * Test validate_script_url with deep subdomain. + */ + public function testValidateScriptUrlDeepSubdomain(): void { + Functions\when( 'wp_parse_url' )->alias( 'parse_url' ); + + $this->assertTrue( + $this->ad_code_manager->validate_script_url( 'https://a.b.c.example.com/script.js' ) + ); + } + + /** + * Test filter_output_tokens adds URL vars as tokens. + */ + public function testFilterOutputTokensAddsUrlVars(): void { + $code_to_display = [ + 'url_vars' => [ + 'site_id' => '12345', + 'zone' => 'header', + 'width' => '728', + 'height' => '90', + ], + ]; + + $output_tokens = $this->ad_code_manager->filter_output_tokens( [], 'test_tag', $code_to_display ); + + $this->assertArrayHasKey( '%site_id%', $output_tokens ); + $this->assertArrayHasKey( '%zone%', $output_tokens ); + $this->assertArrayHasKey( '%width%', $output_tokens ); + $this->assertArrayHasKey( '%height%', $output_tokens ); + $this->assertSame( '12345', $output_tokens['%site_id%'] ); + $this->assertSame( 'header', $output_tokens['%zone%'] ); + } + + /** + * Test filter_output_tokens returns original tokens when no URL vars. + */ + public function testFilterOutputTokensReturnsOriginalWhenNoUrlVars(): void { + $code_to_display = []; + $original_tokens = [ '%existing%' => 'value' ]; + + $output_tokens = $this->ad_code_manager->filter_output_tokens( + $original_tokens, + 'test_tag', + $code_to_display + ); + + $this->assertSame( $original_tokens, $output_tokens ); + } + + /** + * Test filter_output_tokens preserves existing tokens. + */ + public function testFilterOutputTokensPreservesExistingTokens(): void { + $code_to_display = [ + 'url_vars' => [ + 'new_var' => 'new_value', + ], + ]; + $original_tokens = [ '%existing%' => 'value' ]; + + $output_tokens = $this->ad_code_manager->filter_output_tokens( + $original_tokens, + 'test_tag', + $code_to_display + ); + + $this->assertArrayHasKey( '%existing%', $output_tokens ); + $this->assertArrayHasKey( '%new_var%', $output_tokens ); + } +} diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php new file mode 100644 index 0000000..3ee4d52 --- /dev/null +++ b/tests/Unit/TestCase.php @@ -0,0 +1,19 @@ + Date: Sat, 13 Dec 2025 20:38:06 +0000 Subject: [PATCH 15/36] ci: add unit test workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds GitHub Actions workflow for running unit tests across multiple PHP versions (7.4, 8.1, 8.2, 8.3). Unit tests run without WordPress using Brain Monkey, enabling faster feedback on pure PHP logic. Workflow is hardened with: - Minimal permissions (contents: read) - Pinned action versions with SHA - persist-credentials: false 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/integration.yml | 2 +- .github/workflows/unit.yml | 65 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/unit.yml diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 6575c5c..3e4c846 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -80,4 +80,4 @@ jobs: WP_ENV_CORE: WordPress/WordPress#${{ matrix.wordpress }} - name: Run integration tests - run: composer test-integration --no-interaction + run: composer test:integration --no-interaction diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml new file mode 100644 index 0000000..455ddd5 --- /dev/null +++ b/.github/workflows/unit.yml @@ -0,0 +1,65 @@ +name: Unit Tests + +on: + push: + branches: + - develop + - trunk + paths: + - '**.php' + - 'composer.json' + - 'phpunit.xml.dist' + - '.github/workflows/unit.yml' + pull_request: + paths: + - '**.php' + - 'composer.json' + - 'phpunit.xml.dist' + - '.github/workflows/unit.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit-tests: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + timeout-minutes: 10 + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.1' + - '8.2' + - '8.3' + + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # 2.36.0 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # 3.1.1 + + - name: Setup problem matchers for PHP + run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" + + - name: Setup problem matchers for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run unit tests + run: composer test:unit From 8785eb69b88650f57e4b15873375a498ad7b1e58 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 14 Dec 2025 13:04:21 +0000 Subject: [PATCH 16/36] chore: migrate dependabot reviewers to CODEOWNERS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move reviewer configuration from dependabot.yml to CODEOWNERS file per GitHub's recommended approach. This allows the same reviewers to be automatically assigned for all PRs, not just Dependabot ones. See: https://github.com/dependabot/codeowner-migration-action 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/CODEOWNERS | 3 +++ .github/dependabot.yml | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3b8daef --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# The following teams will get auto-tagged for a review. +# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +* @Automattic/vip-plugins diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4ca0efe..d03f389 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,8 +13,6 @@ updates: patterns: ["*"] labels: - "dependencies" - reviewers: - - "Automattic/vip-plugins" commit-message: prefix: "Actions" include: "scope" @@ -37,8 +35,6 @@ updates: - "yoast/*" labels: - "dependencies" - reviewers: - - "Automattic/vip-plugins" commit-message: prefix: "Composer" include: "scope" From d6a941d7fbf460bcb0852b71d3541f0583359933 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 14 Dec 2025 16:59:52 +0000 Subject: [PATCH 17/36] fix: prevent empty widget wrapper output when no ad codes found Resolves issue #72 where the ACM_Ad_Zones widget would output empty wrapper HTML (before_widget/after_widget) even when no valid ad codes were found for the specified tag. This created unnecessary empty markup in the page output. The widget now uses output buffering to capture the ad content first, checks if any content was generated, and only outputs the wrapper HTML if there's actual ad content to display. A new filter 'acm_display_empty_widget' has been added to allow themes to override this behaviour if they need the wrapper HTML to display even when empty (defaults to false). Changes: - Modified ACM_Ad_Zones::widget() to buffer ad output and conditionally render wrapper - Added 'acm_display_empty_widget' filter with ad zone, args, and instance parameters - Excluded short prefix warning from PHPCS for established 'acm' prefix - Added 7 integration tests covering empty output behaviour and filter functionality - Added unit tests verifying get_acm_tag() returns empty string when no ad codes found Co-Authored-By: Claude Opus 4.5 --- .phpcs.xml.dist | 2 + src/class-acm-widget.php | 24 ++- tests/Integration/WidgetTest.php | 297 +++++++++++++++++++++++++++++++ tests/Unit/AdCodeManagerTest.php | 100 ++++++++--- 4 files changed, 402 insertions(+), 21 deletions(-) create mode 100644 tests/Integration/WidgetTest.php diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 02b110c..fabf828 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -53,6 +53,8 @@ + + diff --git a/src/class-acm-widget.php b/src/class-acm-widget.php index b3e5d20..ec5724e 100644 --- a/src/class-acm-widget.php +++ b/src/class-acm-widget.php @@ -60,13 +60,35 @@ function update( $new_instance, $old_instance ) { // Display the widget function widget( $args, $instance ) { + // Capture the ad content to check if we have anything to display. + // This fixes issue #72: don't output empty widget wrapper when no ad code found. + ob_start(); + do_action( 'acm_tag', $instance['ad_zone'] ); + $ad_content = ob_get_clean(); + + /** + * Filters whether to display the widget when no ad content is found. + * + * @since 0.8.0 + * + * @param bool $display Whether to display the widget. Default false. + * @param string $ad_zone The ad zone ID. + * @param array $args Widget display arguments. + * @param array $instance Widget instance settings. + */ + if ( empty( $ad_content ) && ! apply_filters( 'acm_display_empty_widget', false, $instance['ad_zone'], $args, $instance ) ) { + return; + } + echo $args['before_widget']; $title = apply_filters( 'widget_title', $instance['title'] ); if ( ! empty( $title ) ) { echo $args['before_title'] . esc_html( $title ) . $args['after_title']; } - do_action( 'acm_tag', $instance['ad_zone'] ); + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Ad content is escaped during token replacement in get_acm_tag(). + echo $ad_content; echo $args['after_widget']; } } diff --git a/tests/Integration/WidgetTest.php b/tests/Integration/WidgetTest.php new file mode 100644 index 0000000..3c3ac05 --- /dev/null +++ b/tests/Integration/WidgetTest.php @@ -0,0 +1,297 @@ +widget = new ACM_Ad_Zones(); + } + + /** + * Test widget outputs nothing when no ad code is found. + * + * This is the main fix for issue #72 - the widget should not output + * empty wrapper HTML when there's no ad content to display. + * + * @covers ACM_Ad_Zones::widget + */ + public function test_widget_outputs_nothing_when_no_ad_code_found(): void { + $args = array( + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ); + $instance = array( + 'title' => 'Test Ad', + 'ad_zone' => 'nonexistent_zone', + ); + + ob_start(); + $this->widget->widget( $args, $instance ); + $output = ob_get_clean(); + + $this->assertEmpty( $output, 'Widget should output nothing when no ad code is found.' ); + } + + /** + * Test widget outputs nothing for empty ad zone. + * + * @covers ACM_Ad_Zones::widget + */ + public function test_widget_outputs_nothing_for_empty_ad_zone(): void { + $args = array( + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ); + $instance = array( + 'title' => '', + 'ad_zone' => '', + ); + + ob_start(); + $this->widget->widget( $args, $instance ); + $output = ob_get_clean(); + + $this->assertEmpty( $output, 'Widget should output nothing for empty ad zone.' ); + } + + /** + * Test widget outputs wrapper when filter returns true for empty content. + * + * The acm_display_empty_widget filter allows themes to force display + * of the widget wrapper even when no ad content is found. + * + * @covers ACM_Ad_Zones::widget + */ + public function test_widget_outputs_wrapper_when_filter_returns_true(): void { + add_filter( 'acm_display_empty_widget', '__return_true' ); + + $args = array( + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ); + $instance = array( + 'title' => '', + 'ad_zone' => 'nonexistent_zone', + ); + + ob_start(); + $this->widget->widget( $args, $instance ); + $output = ob_get_clean(); + + remove_filter( 'acm_display_empty_widget', '__return_true' ); + + $this->assertStringContainsString( '
', $output, 'Widget should output wrapper when filter returns true.' ); + $this->assertStringContainsString( '
', $output, 'Widget should output closing wrapper when filter returns true.' ); + } + + /** + * Test widget outputs title when filter allows empty content. + * + * @covers ACM_Ad_Zones::widget + */ + public function test_widget_outputs_title_when_filter_allows_empty_content(): void { + add_filter( 'acm_display_empty_widget', '__return_true' ); + + $args = array( + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ); + $instance = array( + 'title' => 'Advertisement', + 'ad_zone' => 'nonexistent_zone', + ); + + ob_start(); + $this->widget->widget( $args, $instance ); + $output = ob_get_clean(); + + remove_filter( 'acm_display_empty_widget', '__return_true' ); + + $this->assertStringContainsString( '

Advertisement

', $output, 'Widget should output title when filter allows.' ); + } + + /** + * Test acm_display_empty_widget filter receives correct arguments. + * + * @covers ACM_Ad_Zones::widget + */ + public function test_filter_receives_correct_arguments(): void { + $received_args = array(); + + $filter_callback = function ( $display, $ad_zone, $args, $instance ) use ( &$received_args ) { + $received_args = array( + 'display' => $display, + 'ad_zone' => $ad_zone, + 'args' => $args, + 'instance' => $instance, + ); + return false; + }; + + add_filter( 'acm_display_empty_widget', $filter_callback, 10, 4 ); + + $args = array( + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ); + $instance = array( + 'title' => 'Test', + 'ad_zone' => 'test_zone', + ); + + ob_start(); + $this->widget->widget( $args, $instance ); + ob_get_clean(); + + remove_filter( 'acm_display_empty_widget', $filter_callback, 10 ); + + $this->assertFalse( $received_args['display'], 'First argument should be false.' ); + $this->assertSame( 'test_zone', $received_args['ad_zone'], 'Second argument should be the ad zone.' ); + $this->assertSame( $args, $received_args['args'], 'Third argument should be widget args.' ); + $this->assertSame( $instance, $received_args['instance'], 'Fourth argument should be widget instance.' ); + } + + /** + * Test widget with valid ad code outputs content. + * + * @covers ACM_Ad_Zones::widget + */ + public function test_widget_outputs_content_with_valid_ad_code(): void { + // Create a valid ad code. + $ad_code_data = array(); + foreach ( $this->acm->current_provider->ad_code_args as $arg ) { + $ad_code_data[ $arg['key'] ] = 'test_value_' . $arg['key']; + } + $ad_code_data['priority'] = 10; + $ad_code_data['operator'] = 'AND'; + + // Need to set up a tag that's registered. + // First, let's check what tags are available. + $ad_tag_ids = $this->acm->ad_tag_ids; + if ( empty( $ad_tag_ids ) ) { + $this->markTestSkipped( 'No ad tag IDs available for testing.' ); + } + + // Get the first tag with enable_ui_mapping. + $test_tag = null; + foreach ( $ad_tag_ids as $tag ) { + if ( isset( $tag['enable_ui_mapping'] ) && $tag['enable_ui_mapping'] ) { + $test_tag = $tag['tag']; + break; + } + } + + if ( ! $test_tag ) { + $this->markTestSkipped( 'No tags with enable_ui_mapping available for testing.' ); + } + + // Set the tag in ad code data. + $ad_code_data['tag'] = $test_tag; + + // Create the ad code. + $ad_code_id = $this->acm->create_ad_code( $ad_code_data ); + $this->assertIsInt( $ad_code_id, 'Ad code should be created successfully.' ); + + // Register ad codes to make them available. + $this->acm->register_ad_codes( $this->acm->get_ad_codes() ); + + // Enable display of ad codes without conditionals. + add_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + + $args = array( + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ); + $instance = array( + 'title' => 'Ad Widget', + 'ad_zone' => $test_tag, + ); + + ob_start(); + $this->widget->widget( $args, $instance ); + $output = ob_get_clean(); + + remove_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + + // Clean up. + $this->acm->delete_ad_code( $ad_code_id ); + + $this->assertStringContainsString( '
', $output, 'Widget should output wrapper with valid ad code.' ); + $this->assertStringContainsString( '
', $output, 'Widget should output closing wrapper.' ); + } + + /** + * Test that acm_tag action is still fired (backwards compatibility). + * + * @covers ACM_Ad_Zones::widget + */ + public function test_acm_tag_action_is_fired(): void { + $action_fired = false; + $action_tag = null; + + $action_callback = function ( $tag_id ) use ( &$action_fired, &$action_tag ) { + $action_fired = true; + $action_tag = $tag_id; + }; + + add_action( 'acm_tag', $action_callback, 5 ); // Priority 5 to run before the default handler. + + $args = array( + 'before_widget' => '
', + 'after_widget' => '
', + 'before_title' => '

', + 'after_title' => '

', + ); + $instance = array( + 'title' => '', + 'ad_zone' => 'test_action_zone', + ); + + ob_start(); + $this->widget->widget( $args, $instance ); + ob_get_clean(); + + remove_action( 'acm_tag', $action_callback, 5 ); + + $this->assertTrue( $action_fired, 'acm_tag action should be fired for backwards compatibility.' ); + $this->assertSame( 'test_action_zone', $action_tag, 'acm_tag action should receive the correct tag ID.' ); + } +} diff --git a/tests/Unit/AdCodeManagerTest.php b/tests/Unit/AdCodeManagerTest.php index 72827fe..c059873 100644 --- a/tests/Unit/AdCodeManagerTest.php +++ b/tests/Unit/AdCodeManagerTest.php @@ -32,17 +32,17 @@ protected function setUp(): void { parent::setUp(); // Stub WordPress functions. - Functions\stubs( [ '__' => null ] ); + Functions\stubs( array( '__' => null ) ); $this->ad_code_manager = new Ad_Code_Manager(); // Create a mock provider with whitelisted URLs. - $this->ad_code_manager->current_provider = new stdClass(); - $this->ad_code_manager->current_provider->whitelisted_script_urls = [ + $this->ad_code_manager->current_provider = new stdClass(); + $this->ad_code_manager->current_provider->whitelisted_script_urls = array( 'example.com', 'ads.google.com', 'secure.pagead2.googlesyndication.com', - ]; + ); } /** @@ -124,16 +124,16 @@ public function testValidateScriptUrlDeepSubdomain(): void { * Test filter_output_tokens adds URL vars as tokens. */ public function testFilterOutputTokensAddsUrlVars(): void { - $code_to_display = [ - 'url_vars' => [ - 'site_id' => '12345', - 'zone' => 'header', - 'width' => '728', - 'height' => '90', - ], - ]; + $code_to_display = array( + 'url_vars' => array( + 'site_id' => '12345', + 'zone' => 'header', + 'width' => '728', + 'height' => '90', + ), + ); - $output_tokens = $this->ad_code_manager->filter_output_tokens( [], 'test_tag', $code_to_display ); + $output_tokens = $this->ad_code_manager->filter_output_tokens( array(), 'test_tag', $code_to_display ); $this->assertArrayHasKey( '%site_id%', $output_tokens ); $this->assertArrayHasKey( '%zone%', $output_tokens ); @@ -147,8 +147,8 @@ public function testFilterOutputTokensAddsUrlVars(): void { * Test filter_output_tokens returns original tokens when no URL vars. */ public function testFilterOutputTokensReturnsOriginalWhenNoUrlVars(): void { - $code_to_display = []; - $original_tokens = [ '%existing%' => 'value' ]; + $code_to_display = array(); + $original_tokens = array( '%existing%' => 'value' ); $output_tokens = $this->ad_code_manager->filter_output_tokens( $original_tokens, @@ -163,12 +163,12 @@ public function testFilterOutputTokensReturnsOriginalWhenNoUrlVars(): void { * Test filter_output_tokens preserves existing tokens. */ public function testFilterOutputTokensPreservesExistingTokens(): void { - $code_to_display = [ - 'url_vars' => [ + $code_to_display = array( + 'url_vars' => array( 'new_var' => 'new_value', - ], - ]; - $original_tokens = [ '%existing%' => 'value' ]; + ), + ); + $original_tokens = array( '%existing%' => 'value' ); $output_tokens = $this->ad_code_manager->filter_output_tokens( $original_tokens, @@ -179,4 +179,64 @@ public function testFilterOutputTokensPreservesExistingTokens(): void { $this->assertArrayHasKey( '%existing%', $output_tokens ); $this->assertArrayHasKey( '%new_var%', $output_tokens ); } + + /** + * Test get_acm_tag returns empty string when ad rendering is disabled. + * + * @covers Ad_Code_Manager::get_acm_tag + */ + public function testGetAcmTagReturnsEmptyWhenRenderingDisabled(): void { + Functions\when( 'is_preview' )->justReturn( false ); + Functions\when( 'apply_filters' )->alias( + function ( $filter_name, $value ) { + if ( 'acm_disable_ad_rendering' === $filter_name ) { + return true; // Disable rendering. + } + return $value; + } + ); + + $result = $this->ad_code_manager->get_acm_tag( 'test_tag' ); + + $this->assertSame( '', $result ); + } + + /** + * Test get_acm_tag returns empty string when no ad codes registered for tag. + * + * This verifies the fix for issue #72 - no broken HTML output when no ad codes found. + * + * @covers Ad_Code_Manager::get_acm_tag + */ + public function testGetAcmTagReturnsEmptyWhenNoAdCodesRegistered(): void { + Functions\when( 'apply_filters' )->alias( + function ( $filter_name, $value ) { + if ( 'acm_disable_ad_rendering' === $filter_name ) { + return false; // Rendering enabled. + } + return $value; + } + ); + Functions\when( 'is_preview' )->justReturn( false ); + Functions\when( 'wp_cache_get' )->justReturn( false ); + Functions\when( 'wp_cache_add' )->justReturn( true ); + + // Ensure no ad codes are registered for this tag. + $this->assertArrayNotHasKey( 'nonexistent_tag', $this->ad_code_manager->ad_codes ); + + $result = $this->ad_code_manager->get_acm_tag( 'nonexistent_tag' ); + + $this->assertSame( '', $result ); + } + + /** + * Test get_matching_ad_code returns null when tag has no registered ad codes. + * + * @covers Ad_Code_Manager::get_matching_ad_code + */ + public function testGetMatchingAdCodeReturnsNullWhenNoAdCodes(): void { + $result = $this->ad_code_manager->get_matching_ad_code( 'nonexistent_tag' ); + + $this->assertNull( $result ); + } } From 2ccd5990289931c0ae9b839cddd776744b461853 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Tue, 16 Dec 2025 23:57:53 +0000 Subject: [PATCH 18/36] ci: standardise test matrix and update readme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates CI workflow to test against WP 6.4 and master with PHP 7.4 and latest. Removes allowed_failure flag so CI fails if tests fail on any supported configuration. Updates README to reflect tested versions (WP 6.9). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/integration.yml | 18 ++++++------------ README.md | 2 +- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 3e4c846..d27f69e 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -30,20 +30,14 @@ jobs: strategy: matrix: - php: [ '7.4', '8.3' ] - wordpress: [ '6.4', '6.8' ] - allowed_failure: [false] include: - # Check upcoming WP. - - php: '8.3' - wordpress: 'master' - allowed_failure: true - # Check upcoming PHP. - - php: '8.5' - wordpress: '6.8' - allowed_failure: true + # Check lowest supported WP version, with the lowest supported PHP. + - wordpress: '6.4' + php: '7.4' + # Check latest WP with the latest PHP. + - wordpress: 'master' + php: 'latest' fail-fast: false - continue-on-error: ${{ matrix.allowed_failure }} steps: - name: Checkout code diff --git a/README.md b/README.md index f991a8e..54b5d06 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Stable tag: 0.7.1 Requires at least: 6.4 -Tested up to: 6.8 +Tested up to: 6.9 Requires PHP: 7.4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html From cce518005c1076aff5871033c9967f61f373a88d Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sat, 20 Dec 2025 19:52:38 +0000 Subject: [PATCH 19/36] feat: add wrapper div with CSS classes for improved ad styling This addresses a long-standing request from 2013 for better styling control of ad output. Previously, themes needed to target ads by numeric IDs that could change, making consistent styling difficult. The implementation wraps all ad output in a div with descriptive CSS classes: a generic 'acm-wrapper' class for broad styling rules, and a tag-specific 'acm-tag-{tag_id}' class for targeted control. This enables straightforward centering and other layout adjustments without depending on fragile numeric IDs. A new 'acm_wrapper_classes' filter allows developers to customise the classes or disable the wrapper entirely by returning an empty array, ensuring backwards compatibility for sites that need to opt out. Fixes #71 --- src/class-ad-code-manager.php | 24 +++++ tests/Integration/AdCodeManagerTest.php | 111 ++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/src/class-ad-code-manager.php b/src/class-ad-code-manager.php index 557a7f2..324e956 100644 --- a/src/class-ad-code-manager.php +++ b/src/class-ad-code-manager.php @@ -797,6 +797,30 @@ function get_acm_tag( $tag_id ): string { */ $output_html = apply_filters( 'acm_output_html_after_tokens_processed', $output_html, $tag_id ); + /** + * Configuration filter: acm_wrapper_classes + * Filter the CSS classes applied to the ad wrapper div. + * + * @since 0.8.0 + * + * @param array $classes Array of CSS class names. Default: 'acm-wrapper', 'acm-tag-{tag_id}'. + * @param string $tag_id The ad tag ID being rendered. + */ + $wrapper_classes = apply_filters( + 'acm_wrapper_classes', + array( 'acm-wrapper', 'acm-tag-' . sanitize_html_class( $tag_id ) ), + $tag_id + ); + + // Allow disabling the wrapper by returning an empty array. + if ( ! empty( $wrapper_classes ) && is_array( $wrapper_classes ) ) { + $output_html = sprintf( + '
%s
', + esc_attr( implode( ' ', array_map( 'sanitize_html_class', $wrapper_classes ) ) ), + $output_html + ); + } + return $output_html; } diff --git a/tests/Integration/AdCodeManagerTest.php b/tests/Integration/AdCodeManagerTest.php index 6271986..42ec5f9 100644 --- a/tests/Integration/AdCodeManagerTest.php +++ b/tests/Integration/AdCodeManagerTest.php @@ -66,4 +66,115 @@ private function mock_ad_code() { private function create_ad_code_and_return() { return $this->acm->create_ad_code( $this->mock_ad_code() ); } + + /** + * Test that ad output includes wrapper div with default classes. + * + * @covers Ad_Code_Manager::get_acm_tag + */ + public function test_get_acm_tag_includes_wrapper_with_default_classes(): void { + // Create an ad code and register it. + $ad_code_id = $this->create_ad_code_and_return(); + $this->acm->flush_cache(); + $this->acm->register_ad_codes( $this->acm->get_ad_codes() ); + + // Get the first available tag. + $test_tag = null; + foreach ( $this->acm->ad_tag_ids as $tag ) { + $test_tag = $tag['tag']; + break; + } + + if ( ! $test_tag ) { + $this->markTestSkipped( 'No ad tags available for testing.' ); + } + + // Enable display of ad codes without conditionals. + add_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + + $output = $this->acm->get_acm_tag( $test_tag ); + + remove_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + $this->acm->delete_ad_code( $ad_code_id ); + + $this->assertStringContainsString( 'class="acm-wrapper', $output, 'Output should contain acm-wrapper class.' ); + $this->assertStringContainsString( 'acm-tag-' . sanitize_html_class( $test_tag ), $output, 'Output should contain tag-specific class.' ); + } + + /** + * Test that acm_wrapper_classes filter can modify wrapper classes. + * + * @covers Ad_Code_Manager::get_acm_tag + */ + public function test_acm_wrapper_classes_filter_modifies_classes(): void { + // Create an ad code and register it. + $ad_code_id = $this->create_ad_code_and_return(); + $this->acm->flush_cache(); + $this->acm->register_ad_codes( $this->acm->get_ad_codes() ); + + // Get the first available tag. + $test_tag = null; + foreach ( $this->acm->ad_tag_ids as $tag ) { + $test_tag = $tag['tag']; + break; + } + + if ( ! $test_tag ) { + $this->markTestSkipped( 'No ad tags available for testing.' ); + } + + // Filter to add custom classes. + $filter_callback = function ( $classes, $tag_id ) { + $classes[] = 'custom-ad-class'; + $classes[] = 'another-class'; + return $classes; + }; + + add_filter( 'acm_wrapper_classes', $filter_callback, 10, 2 ); + add_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + + $output = $this->acm->get_acm_tag( $test_tag ); + + remove_filter( 'acm_wrapper_classes', $filter_callback, 10 ); + remove_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + $this->acm->delete_ad_code( $ad_code_id ); + + $this->assertStringContainsString( 'custom-ad-class', $output, 'Output should contain custom class.' ); + $this->assertStringContainsString( 'another-class', $output, 'Output should contain another custom class.' ); + } + + /** + * Test that acm_wrapper_classes filter can disable wrapper. + * + * @covers Ad_Code_Manager::get_acm_tag + */ + public function test_acm_wrapper_classes_filter_can_disable_wrapper(): void { + // Create an ad code and register it. + $ad_code_id = $this->create_ad_code_and_return(); + $this->acm->flush_cache(); + $this->acm->register_ad_codes( $this->acm->get_ad_codes() ); + + // Get the first available tag. + $test_tag = null; + foreach ( $this->acm->ad_tag_ids as $tag ) { + $test_tag = $tag['tag']; + break; + } + + if ( ! $test_tag ) { + $this->markTestSkipped( 'No ad tags available for testing.' ); + } + + // Filter to return empty array (disables wrapper). + add_filter( 'acm_wrapper_classes', '__return_empty_array' ); + add_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + + $output = $this->acm->get_acm_tag( $test_tag ); + + remove_filter( 'acm_wrapper_classes', '__return_empty_array' ); + remove_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); + $this->acm->delete_ad_code( $ad_code_id ); + + $this->assertStringNotContainsString( 'acm-wrapper', $output, 'Output should not contain wrapper when filter returns empty array.' ); + } } From 53a731fe675c34de50e2a9e68c8de4c8e62118b5 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 12:32:15 +0000 Subject: [PATCH 20/36] fix: ensure row actions display in first data column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, if a provider didn't define a specific column method for the first data column, the fallback column_default() was used without row actions. This left users unable to edit or delete ad codes for such providers. The fix adds automatic row action detection: when rendering via column_default(), we now check if the current column is the first data column (after 'cb' and 'id') and append the edit/delete links accordingly. Fixes #51 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/class-acm-wp-list-table.php | 41 ++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/class-acm-wp-list-table.php b/src/class-acm-wp-list-table.php index 5e88aa7..57ae3bf 100644 --- a/src/class-acm-wp-list-table.php +++ b/src/class-acm-wp-list-table.php @@ -169,15 +169,50 @@ function column_default( $item, $column_name ) { case 'operator': return ( ! empty( $item['operator'] ) ) ? $item['operator'] : $ad_code_manager->logical_operator; default: - // @todo need to make the first column (whatever it is filtered) to show row actions - // Handle custom columns, if any + // Handle custom columns, if any. if ( isset( $item['url_vars'][ $column_name ] ) ) { - return esc_html( $item['url_vars'][ $column_name ] ); + $output = esc_html( $item['url_vars'][ $column_name ] ); + + // Add row actions to the first data column (after cb and id). + if ( $this->is_first_data_column( $column_name ) ) { + $output .= $this->row_actions_output( $item ); + } + + return $output; } break; } } + /** + * Check if the given column is the first data column. + * + * The first data column is the first column after 'cb' (checkbox) and 'id' (hidden). + * This column should display the row actions (edit/delete links). + * + * @since 0.8.0 + * + * @param string $column_name The column name to check. + * @return bool True if this is the first data column, false otherwise. + */ + protected function is_first_data_column( $column_name ) { + $columns = $this->get_columns(); + + // Skip 'cb' and 'id' columns to find the first data column. + $skip_columns = array( 'cb', 'id' ); + + foreach ( $columns as $key => $label ) { + if ( in_array( $key, $skip_columns, true ) ) { + continue; + } + + // The first column we encounter after skipping is the first data column. + return $key === $column_name; + } + + return false; + } + /** * Column with a checkbox * Used for bulk actions From 6b49a8b4cc17d314c4dec10beadc4c9db25d8071 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 12:42:28 +0000 Subject: [PATCH 21/36] docs: add contextual help for DFP and AdSense provider fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users unfamiliar with Google Ad Manager (formerly DFP) or AdSense often struggle to understand what values are required for fields like Tag ID, DFP ID, Tag Name, and Publisher ID. This is a common support request. The new "Provider Fields" help tab explains each field with examples and guidance on where to find the values in the respective ad platforms. This complements the existing help tabs for Overview, Configuration, and Conditionals. Fixes #123 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/UI/class-contextual-help.php | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/UI/class-contextual-help.php b/src/UI/class-contextual-help.php index f8f1c30..d6cac64 100644 --- a/src/UI/class-contextual-help.php +++ b/src/UI/class-contextual-help.php @@ -110,6 +110,47 @@ public function render( WP_Screen $screen ): void { +

+
+
+
+ +
+
+ +
+
+ +
+ Admin > Global Settings > Network Code.', 'ad-code-manager' ) ); ?> +
+ +
+ +
+
+ +
+ +
+
+

+

+
+
+
+ +
+ +
+ Account > Account Information > Publisher ID.', 'ad-code-manager' ) ); ?> +
+
+ add_help_tab( array( 'id' => 'acm-overview', @@ -127,9 +168,16 @@ public function render( WP_Screen $screen ): void { get_current_screen()->add_help_tab( array( 'id' => 'acm-conditionals', - 'title' => 'Conditionals', + 'title' => __( 'Conditionals', 'ad-code-manager' ), 'content' => $conditionals, ) ); + get_current_screen()->add_help_tab( + array( + 'id' => 'acm-provider-fields', + 'title' => __( 'Provider Fields', 'ad-code-manager' ), + 'content' => $provider_fields, + ) + ); } } From 2ea1751845a8a90b248ad933fb4ef198eb6da68a Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 12:34:31 +0000 Subject: [PATCH 22/36] fix: validate unique tag IDs for DFP Async provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DFP Async uses the tag_id field as a div ID in the rendered HTML. When two ad codes share the same tag_id, Google's ad serving becomes confused because the same element ID appears multiple times on the page. This adds validation that prevents saving ad codes with duplicate tag_id values. The implementation uses a new 'acm_validate_ad_code' filter that providers can hook into for custom validation, keeping the solution extensible for other providers that may need similar checks. Fixes #69 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- package-lock.json | 943 ++++++++++++++++++ ...class-doubleclick-for-publishers-async.php | 79 +- src/class-ad-code-manager.php | 26 + views/ad-code-manager.tpl.php | 13 +- 4 files changed, 1055 insertions(+), 6 deletions(-) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ad8a3cb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,943 @@ +{ + "name": "ad-code-manager", + "version": "0.7.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ad-code-manager", + "version": "0.7.0", + "license": "GPL-2.0-or-later", + "devDependencies": { + "version-bump-prompt": "^6.1.0" + } + }, + "node_modules/@jsdevtools/ez-spawn": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", + "integrity": "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "cross-spawn": "^7.0.3", + "string-argv": "^0.3.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jsdevtools/version-bump-prompt": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@jsdevtools/version-bump-prompt/-/version-bump-prompt-6.1.0.tgz", + "integrity": "sha512-NJFLJRiD3LLFBgSxAb6B255xhWCGgdtzmh6UjHK2b7SRGX2DDKJH5O4BJ0GTStBu4NnaNgMbkr1TLW3pLOBkOQ==", + "dev": true, + "dependencies": { + "@jsdevtools/ez-spawn": "^3.0.4", + "command-line-args": "^5.1.1", + "detect-indent": "^6.0.0", + "detect-newline": "^3.1.0", + "globby": "^11.0.1", + "inquirer": "^7.3.3", + "log-symbols": "^4.0.0", + "semver": "^7.3.2" + }, + "bin": { + "bump": "bin/bump.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/version-bump-prompt": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/version-bump-prompt/-/version-bump-prompt-6.1.0.tgz", + "integrity": "sha512-GYC83GP8QOunWueKf2mbtZkdmisXhnBZPhIHWUmN/Yi4XXAQlIi9avM/IGWdI7KkJLfMENzGN1Xee+Zl3VJ5jg==", + "dev": true, + "dependencies": { + "@jsdevtools/version-bump-prompt": "6.1.0" + }, + "bin": { + "bump": "bump.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/src/Providers/class-doubleclick-for-publishers-async.php b/src/Providers/class-doubleclick-for-publishers-async.php index 24e873a..41f51f0 100644 --- a/src/Providers/class-doubleclick-for-publishers-async.php +++ b/src/Providers/class-doubleclick-for-publishers-async.php @@ -100,6 +100,7 @@ public function __construct() { add_filter( 'acm_ad_code_args', array( $this, 'filter_ad_code_args' ) ); add_filter( 'acm_output_html', array( $this, 'filter_output_html' ), 10, 2 ); + add_filter( 'acm_validate_ad_code', array( $this, 'validate_unique_tag_id' ), 10, 4 ); add_filter( 'acm_display_ad_codes_without_conditionals', '__return_true' ); @@ -167,11 +168,7 @@ public function filter_output_html( $output_html, $tag_id ) { $tt = $tag['url_vars']; $matching_ad_code = $ad_code_manager->get_matching_ad_code( $tag['tag'] ); if ( ! empty( $matching_ad_code ) ) { - // @todo There might be a case when there are two tags registered with the same dimensions - // and the same tag id ( which is just a div id ). This confuses DFP Async, so we need to make sure - // that tags are unique - - // Parse ad tags to output flexible unit dimensions + // Parse ad tags to output flexible unit dimensions. $unit_sizes = $this->parse_ad_tag_sizes( $tt ); ?> @@ -235,6 +232,78 @@ public function parse_ad_tag_sizes( $url_vars ) { return $unit_sizes_output; } + /** + * Validate that the tag_id is unique for DFP Async. + * + * DFP Async uses the tag_id as a div ID, so each tag_id must be unique + * to prevent conflicts when multiple ads are rendered on the same page. + * + * @since 0.8.0 + * + * @param true|WP_Error $valid Current validation state. + * @param array $ad_code_vals The ad code values being saved. + * @param int $id The ad code ID (0 for new ad codes). + * @param string $method The method being performed ('add' or 'edit'). + * @return true|WP_Error True if valid, WP_Error if tag_id is not unique. + */ + public function validate_unique_tag_id( $valid, $ad_code_vals, $id, $method ) { + // If already invalid, don't override the error. + if ( is_wp_error( $valid ) ) { + return $valid; + } + + // Check if tag_id is provided. + if ( empty( $ad_code_vals['tag_id'] ) ) { + return $valid; + } + + global $ad_code_manager; + + $tag_id = $ad_code_vals['tag_id']; + $existing_id = $this->find_ad_code_by_tag_id( $tag_id ); + + // If no existing ad code with this tag_id, it's valid. + if ( ! $existing_id ) { + return true; + } + + // If editing and the existing ad code is the same one we're editing, it's valid. + if ( 'edit' === $method && $existing_id === $id ) { + return true; + } + + return new WP_Error( + 'duplicate-tag-id', + sprintf( + /* translators: %s: the duplicate tag ID */ + __( 'The Tag ID "%s" is already in use. Each Tag ID must be unique to prevent conflicts with DFP Async.', 'ad-code-manager' ), + esc_html( $tag_id ) + ) + ); + } + + /** + * Find an ad code by its tag_id. + * + * @since 0.8.0 + * + * @param string $tag_id The tag_id to search for. + * @return int|false The ad code post ID if found, false otherwise. + */ + protected function find_ad_code_by_tag_id( $tag_id ) { + global $ad_code_manager; + + $ad_codes = $ad_code_manager->get_ad_codes(); + + foreach ( $ad_codes as $ad_code ) { + if ( isset( $ad_code['url_vars']['tag_id'] ) && $ad_code['url_vars']['tag_id'] === $tag_id ) { + return $ad_code['post_id']; + } + } + + return false; + } + } class Doubleclick_For_Publishers_Async_ACM_WP_List_Table extends ACM_WP_List_Table { diff --git a/src/class-ad-code-manager.php b/src/class-ad-code-manager.php index 557a7f2..8d61316 100644 --- a/src/class-ad-code-manager.php +++ b/src/class-ad-code-manager.php @@ -275,6 +275,32 @@ function handle_admin_action() { foreach ( $this->current_provider->ad_code_args as $arg ) { $ad_code_vals[ $arg['key'] ] = sanitize_text_field( $_REQUEST['acm-column'][ $arg['key'] ] ?? '' ); } + + /** + * Filter to validate ad code data before saving. + * + * Providers can hook into this filter to perform custom validation. + * Return a WP_Error to prevent saving and display an error message. + * + * @since 0.8.0 + * + * @param true|WP_Error $valid True if valid, WP_Error if validation fails. + * @param array $ad_code_vals The ad code values being saved. + * @param int $id The ad code ID (0 for new ad codes). + * @param string $method The method being performed ('add' or 'edit'). + */ + $validation_result = apply_filters( 'acm_validate_ad_code', true, $ad_code_vals, $id, $method ); + + if ( is_wp_error( $validation_result ) ) { + if ( isset( $_REQUEST['doing_ajax'] ) && sanitize_text_field( $_REQUEST['doing_ajax'] ) ) { + die( '
' . esc_html( $validation_result->get_error_message() ) . '
' ); + } + // Store the error message in a transient for display after redirect. + set_transient( 'acm_validation_error_' . get_current_user_id(), $validation_result->get_error_message(), 30 ); + $message = 'validation-error'; + break; + } + if ( 'add' === $method ) { $id = $this->create_ad_code( $ad_code_vals ); } else { diff --git a/views/ad-code-manager.tpl.php b/views/ad-code-manager.tpl.php index 82fd151..e869bb2 100644 --- a/views/ad-code-manager.tpl.php +++ b/views/ad-code-manager.tpl.php @@ -7,6 +7,9 @@

' . esc_html( $message_text ) . '

'; + echo '

' . esc_html( $message_text ) . '

'; } } ?> From bf56c9244dc52d52662a21fb7626f846160c2aae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 13:20:23 +0000 Subject: [PATCH 23/36] chore(deps-dev): bump braces from 3.0.2 to 3.0.3 Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3. - [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md) - [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3) --- updated-dependencies: - dependency-name: braces dependency-version: 3.0.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad8a3cb..6599017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ad-code-manager", - "version": "0.7.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ad-code-manager", - "version": "0.7.0", + "version": "0.7.1", "license": "GPL-2.0-or-later", "devDependencies": { "version-bump-prompt": "^6.1.0" @@ -142,12 +142,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -349,10 +350,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -493,6 +495,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -860,6 +863,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, From 41528d954f12874b9033b8fe712fdd898e0b95d7 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 13:29:03 +0000 Subject: [PATCH 24/36] docs: add PHPDoc documentation for all hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive PHPDoc documentation for all 24 filters and 2 actions in the Ad Code Manager plugin, improving discoverability and developer experience. Documented filters include: - Configuration: acm_register_provider_slug, acm_provider_slug, acm_whitelisted_script_urls, acm_whitelisted_conditionals, acm_logical_operator, acm_manage_ads_cap, acm_ad_tag_ids, acm_ad_code_args - Rendering: acm_output_html, acm_output_tokens, acm_output_html_after_tokens_processed, acm_disable_ad_rendering, acm_wrapper_classes (already documented in #188) - Matching: acm_display_ad_codes_without_conditionals, acm_conditional_args, acm_reset_postdata_before_match, acm_matching_ad_code_cache_expiration - Admin: acm_list_table_columns, acm_list_table_per_page, acm_validate_ad_code, acm_allowed_get_posts_args, acm_ad_code_count - Other: acm_default_url, acm_should_do_robotstxt, acm_robotstxt_disallow Documented actions: - acm_tag: Displays an ad tag on the frontend - acm_options_form: Fires in the options form Fixes #157 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/class-acm-provider.php | 33 ++++- src/class-acm-wp-list-table.php | 21 ++- src/class-ad-code-manager.php | 237 +++++++++++++++++++++++++------- views/ad-code-manager.tpl.php | 12 +- 4 files changed, 248 insertions(+), 55 deletions(-) diff --git a/src/class-acm-provider.php b/src/class-acm-provider.php index 0cc5ebd..b57e46a 100644 --- a/src/class-acm-provider.php +++ b/src/class-acm-provider.php @@ -33,7 +33,15 @@ public function __construct() { ); } /** - * Configuration filter: acm_ad_code_args + * Filters the ad code arguments/fields for a provider. + * + * This filter is also applied in the main Ad_Code_Manager class after + * provider instantiation. It allows modification of the fields displayed + * in the admin UI for this provider's ad codes. + * + * @since 0.1 + * + * @param array $ad_code_args Array of field configurations. */ $this->ad_code_args = apply_filters( 'acm_ad_code_args', $this->ad_code_args ); @@ -44,6 +52,17 @@ public function __construct() { } if ( ! empty( $this->crawler_user_agent ) ) { + /** + * Filters whether to add robots.txt rules for ad crawlers. + * + * When a provider has a crawler_user_agent defined, this filter + * controls whether rules are added to robots.txt for that crawler. + * + * @since 0.1 + * + * @param bool $should_do Whether to add robots.txt rules. Default true. + * @param ACM_Provider $provider The provider instance. + */ $should_do_robotstxt = apply_filters( 'acm_should_do_robotstxt', true, $this ); if ( true === $should_do_robotstxt ) { @@ -63,6 +82,18 @@ public function action_do_robotstxt() { $disallowed[] = ''; } + /** + * Filters the disallowed paths for ad crawlers in robots.txt. + * + * Allows modification of which paths should be disallowed for the + * ad network's crawler in the robots.txt file. + * + * @since 0.1 + * + * @param array $disallowed Array of paths to disallow. Default array('') + * or array('/') if blog is not public. + * @param ACM_Provider $provider The provider instance. + */ $disallowed = apply_filters( 'acm_robotstxt_disallow', $disallowed, $this ); // If we have no disallows to add, don't add anything (including User-agent). diff --git a/src/class-acm-wp-list-table.php b/src/class-acm-wp-list-table.php index 57ae3bf..0d91ef9 100644 --- a/src/class-acm-wp-list-table.php +++ b/src/class-acm-wp-list-table.php @@ -25,6 +25,17 @@ function __construct( $params = array() ) { * @return array $columns, the array of columns to use with the table */ function get_columns() { + /** + * Filters the columns displayed in the ad codes list table. + * + * Allows providers to customise which columns appear in the admin list table. + * Note: 'cb', 'id', 'priority', 'operator', and 'conditionals' are required + * and will be added automatically if missing. + * + * @since 0.1.3 + * + * @param array $columns Associative array of column IDs and labels. + */ $columns = apply_filters( 'acm_list_table_columns', array( @@ -34,7 +45,7 @@ function get_columns() { 'priority' => __( 'Priority', 'ad-code-manager' ), 'operator' => __( 'Logical Operator', 'ad-code-manager' ), 'conditionals' => __( 'Conditionals', 'ad-code-manager' ), - ) + ) ); // Fail-safe for misconfiguration $required_before = array( @@ -83,7 +94,13 @@ function prepare_items() { // Number of elements in your table? $totalitems = count( $this->items ); // return the total number of affected rows - // How many to display per page? + /** + * Filters the number of ad codes displayed per page in the list table. + * + * @since 0.1.3 + * + * @param int $per_page Number of ad codes per page. Default 25. + */ $perpage = apply_filters( 'acm_list_table_per_page', 25 ); // Which page is this? diff --git a/src/class-ad-code-manager.php b/src/class-ad-code-manager.php index 412f937..9803096 100644 --- a/src/class-ad-code-manager.php +++ b/src/class-ad-code-manager.php @@ -86,21 +86,30 @@ function action_load_providers() { } /** - * Configuration filter: acm_register_provider_slug + * Filters the registered ad providers. * * We've already gathered a list of default Providers by scanning the ACM plugin - * directory for classes that we can use. To add a provider already included via - * a different directory, the following filter is provided. + * directory for classes that we can use. To add a provider included via + * a different directory, use this filter. + * + * @since 0.1.3 + * + * @param object $providers Object containing provider configurations, keyed by slug. + * Each provider has 'provider' (class name) and 'table' (list table class) keys. */ $this->providers = apply_filters( 'acm_register_provider_slug', $this->providers ); /** - * Configuration filter: acm_provider_slug + * Filters the current ad provider slug. + * + * By default, the provider selected in the admin UI is used. + * Use this filter to programmatically switch to a different ad provider. + * + * @since 0.1.3 * - * By default we use doubleclick-for-publishers provider - * To switch to a different ad provider use this filter + * @param string|false $provider The provider slug (e.g., 'doubleclick_for_publishers', + * 'google_adsense'). False if not set. */ - $this->current_provider_slug = apply_filters( 'acm_provider_slug', $this->get_option( 'provider' ) ); // Instantiate one that we need @@ -114,8 +123,14 @@ function action_load_providers() { return; } /** - * Configuration filter: acm_whitelisted_script_urls - * A security filter to whitelist which ad code script URLs can be added in the admin + * Filters the whitelisted script URLs for ad codes. + * + * A security filter to control which ad code script URLs can be added in the admin. + * URLs not matching domains in this list will be rejected. + * + * @since 0.1 + * + * @param array $whitelisted_urls Array of whitelisted domain names (e.g., 'googleads.g.doubleclick.net'). */ $this->current_provider->whitelisted_script_urls = apply_filters( 'acm_whitelisted_script_urls', $this->current_provider->whitelisted_script_urls ); } @@ -138,29 +153,74 @@ function action_init() { 'has_tag', ); /** - * Configuration filter: acm_whitelisted_conditionals - * Extend the list of usable conditional functions with your own awesome ones. + * Filters the whitelisted conditional functions. + * + * Extend the list of usable conditional functions with your own. + * These functions determine when an ad code should be displayed. + * + * @since 0.1 + * + * @param array $conditionals Array of whitelisted function names + * (e.g., 'is_home', 'is_single', 'is_category'). */ $this->whitelisted_conditionals = apply_filters( 'acm_whitelisted_conditionals', $this->whitelisted_conditionals ); - // Allow users to filter default logical operator + + /** + * Filters the default logical operator for conditionals. + * + * Determines how multiple conditionals are evaluated together. + * 'OR' means any conditional can match; 'AND' means all must match. + * + * @since 0.1 + * + * @param string $operator The logical operator. Default 'OR'. Accepts 'OR' or 'AND'. + */ $this->logical_operator = apply_filters( 'acm_logical_operator', 'OR' ); - // Allow the ad management cap to be filtered if need be + /** + * Filters the capability required to manage ads. + * + * Controls which users can access the Ad Code Manager admin interface. + * + * @since 0.1 + * + * @param string $capability The capability required. Default 'manage_options'. + */ $this->manage_ads_cap = apply_filters( 'acm_manage_ads_cap', $this->manage_ads_cap ); // Load default ad tags for provider $this->ad_tag_ids = $this->current_provider->ad_tag_ids; + /** - * Configuration filter: acm_ad_tag_ids - * Extend set of default tag ids. Ad tag ids are used as a parameter - * for your template tag (e.g. do_action( 'acm_tag', 'my_top_leaderboard' )) + * Filters the registered ad tag IDs. + * + * Extend the set of default tag IDs. Ad tag IDs are used as parameters + * for template tags, e.g., `do_action( 'acm_tag', 'my_top_leaderboard' )`. + * + * @since 0.1 + * + * @param array $ad_tag_ids Array of ad tag configurations. Each tag has: + * - 'tag': (string) The tag identifier. + * - 'url_vars': (array) Variables for URL token replacement. + * - 'enable_ui_mapping': (bool) Optional. Whether tag can be mapped in admin UI. */ $this->ad_tag_ids = apply_filters( 'acm_ad_tag_ids', $this->ad_tag_ids ); /** - * Configuration filter: acm_ad_code_args - * Allow the ad code arguments to be filtered - * Useful if we need to dynamically change these arguments based on the above + * Filters the ad code arguments/fields. + * + * Allows customisation of the fields displayed in the admin UI for ad codes. + * Useful for adding custom fields or modifying existing ones. + * + * @since 0.1 + * + * @param array $ad_code_args Array of field configurations. Each field has: + * - 'key': (string) Field identifier. + * - 'label': (string) Display label. + * - 'editable': (bool) Whether field is editable. + * - 'required': (bool) Whether field is required. + * - 'type': (string) Optional. Field type (e.g., 'select'). + * - 'options': (array) Optional. Options for select fields. */ $this->current_provider->ad_code_args = apply_filters( 'acm_ad_code_args', $this->current_provider->ad_code_args ); @@ -168,6 +228,18 @@ function action_init() { // Ad tags are only run on the frontend if ( ! is_admin() ) { + /** + * Action: acm_tag + * + * Displays an ad tag on the frontend. Use this action in your theme + * templates to output ads. + * + * Example: do_action( 'acm_tag', 'my_leaderboard' ); + * + * @since 0.1 + * + * @param string $tag_id The unique identifier for the ad tag to display. + */ add_action( 'acm_tag', array( $this, 'action_acm_tag' ) ); add_filter( 'acm_output_tokens', array( $this, 'filter_output_tokens' ), 5, 3 ); } @@ -379,14 +451,26 @@ function handle_admin_action() { * Get the ad codes stored in our custom post type */ function get_ad_codes( $query_args = array() ) { + /** + * Filters the allowed query parameters for retrieving ad codes. + * + * Controls which query parameters can be passed to modify the ad codes query. + * + * @since 0.1 + * + * @param array $allowed_params Array of allowed query parameter names. Default array( 'offset' ). + */ $allowed_query_params = apply_filters( 'acm_allowed_get_posts_args', array( 'offset' ) ); - /** - * Configuration filter: acm_ad_code_count + * Filters the maximum number of ad codes to retrieve. + * + * By default, queries are limited to 50 ad codes. + * Use this filter to increase or decrease the limit. * - * By default we limit query to 50 ad codes - * Use this filter to change limit + * @since 0.1 + * + * @param int $count Maximum number of ad codes to retrieve. Default 50. */ $args = array( 'post_type' => $this->post_type, @@ -748,12 +832,6 @@ function register_ad_codes( $ad_codes = array() ) { continue; } - /** - * Configuration filter: acm_default_url - * If you don't specify a URL for your ad code when registering it in - * the WordPress admin or at a code level, you can simply apply it with - * a custom filter defined. - */ $ad_code['priority'] = $ad_code['priority'] === '' ? 10 : (int) $ad_code['priority']; // make sure priority is int, if it's unset, we set it to 10 // Make sure our operator is 'OR' or 'AND' @@ -761,6 +839,16 @@ function register_ad_codes( $ad_codes = array() ) { $operator = $this->logical_operator; } + /** + * Filters the default URL for an ad code. + * + * If you don't specify a URL for your ad code when registering it in + * the WordPress admin or at a code level, you can apply it with this filter. + * + * @since 0.1 + * + * @param string $url The ad code URL. May be empty. + */ $this->register_ad_code( $default_tag['tag'], apply_filters( 'acm_default_url', $ad_code['url'] ), $ad_code['conditionals'], array_merge( $default_tag['url_vars'], $ad_code['url_vars'] ), $ad_code['priority'], $ad_code['operator'] ); } } @@ -776,10 +864,13 @@ function register_ad_codes( $ad_codes = array() ) { */ function get_acm_tag( $tag_id ): string { /** - * See http://adcodemanager.wordpress.com/2013/04/10/hi-all-on-a-dotcom-site-that-uses/ + * Filters whether to disable ad rendering. + * + * By default, ads are disabled on post previews to prevent ad tracking issues. * - * Configuration filter: acm_disable_ad_rendering - * Should be boolean, defaulting to disabling ads on previews + * @since 0.1 + * + * @param bool $disable Whether to disable ad rendering. Default is the result of is_preview(). */ if ( apply_filters( 'acm_disable_ad_rendering', is_preview() ) ) { return ''; @@ -798,9 +889,15 @@ function get_acm_tag( $tag_id ): string { } /** - * Configuration filter: acm_output_html - * Support multiple ad formats ( e.g. Javascript ad tags, or simple HTML tags ) + * Filters the output HTML template for an ad tag. + * + * Support multiple ad formats (e.g., JavaScript ad tags, or simple HTML tags) * by adjusting the HTML rendered for a given ad tag. + * + * @since 0.1 + * + * @param string $output_html The HTML template with tokens (e.g., '%url%', '%width%'). + * @param string $tag_id The ad tag ID being rendered. */ $output_html = apply_filters( 'acm_output_html', $this->current_provider->output_html, $tag_id ); @@ -808,9 +905,17 @@ function get_acm_tag( $tag_id ): string { $output_html = str_replace( '%url%', $code_to_display['url'], $output_html ); /** - * Configuration filter: acm_output_tokens - * Register output tokens depending on the needs of your setup. Tokens are the - * keys to be replaced in your script URL. + * Filters the output tokens for an ad tag. + * + * Tokens are placeholders in the output HTML that get replaced with actual values. + * Register custom tokens depending on your setup. + * + * @since 0.1 + * + * @param array $output_tokens Associative array of tokens and their replacement values. + * Keys should include % (e.g., '%width%' => '300'). + * @param string $tag_id The ad tag ID being rendered. + * @param array $code_to_display The matching ad code configuration. */ $output_tokens = apply_filters( 'acm_output_tokens', $this->current_provider->output_tokens, $tag_id, $code_to_display ); foreach ( (array) $output_tokens as $token => $val ) { @@ -818,8 +923,15 @@ function get_acm_tag( $tag_id ): string { } /** - * Configuration filter: acm_output_html_after_tokens_processed - * In some rare cases you might want to filter html after the tokens are processed + * Filters the output HTML after token replacement. + * + * Use this filter for final modifications to the ad HTML after all tokens + * have been processed. Useful for adding wrappers or final adjustments. + * + * @since 0.2 + * + * @param string $output_html The processed HTML with all tokens replaced. + * @param string $tag_id The ad tag ID being rendered. */ $output_html = apply_filters( 'acm_output_html_after_tokens_processed', $output_html, $tag_id ); @@ -882,13 +994,16 @@ public function get_matching_ad_code( $tag_id ) { $cache_key = "acm:{$tag_id}:" . md5( serialize( $wp_query->query_vars ) ); /** - * Filters the amount of time to cache the matching ad code. + * Filters the cache expiration time for matching ad codes. + * + * Controls how long a matched ad code is cached to improve performance. + * Return false to disable object caching (request-level caching only). * - * Returning false to this filter will cause the ad code to be cached - * only for the duration of the request, not within the object cache. + * @since 0.4 * - * @param int $cache_expiration The amount of time, in seconds, to cache the matching ad code. Default 10 minutes. - * @param string $tag_id The tag ID. + * @param int|false $cache_expiration Cache time in seconds. Default 600 (10 minutes). + * Return false to disable object caching. + * @param string $tag_id The tag ID being matched. */ $cache_expiration = apply_filters( 'acm_matching_ad_code_cache_expiration', 600, $tag_id ); @@ -903,12 +1018,15 @@ public function get_matching_ad_code( $tag_id ) { } /** - * Prevent $post polution if ad code is getting rendered inside a loop: + * Filters whether to reset post data before matching ad codes. + * + * Most conditionals check against the global $post. When matching ad codes + * inside a loop, this can result in incorrect matches. Enable this filter + * to call wp_reset_postdata() before evaluation. * - * Most of conditionals are getting checked against global $post, - * Getting matched ad code inside the loop might result in wrong ad code matched. + * @since 0.4 * - * Filter is for back compat since not thoroughly tested + * @param bool $reset Whether to reset post data. Default false for backwards compatibility. */ if ( apply_filters( 'acm_reset_postdata_before_match', false ) ) { wp_reset_postdata(); @@ -919,6 +1037,17 @@ public function get_matching_ad_code( $tag_id ) { $display_codes = array(); foreach ( (array) $this->ad_codes[ $tag_id ] as $ad_code ) { + /** + * Filters whether to display ad codes without conditionals. + * + * By default, ad codes without conditionals are not displayed. + * Enable this filter to show ad codes on all pages when they have + * no conditional restrictions. + * + * @since 0.1 + * + * @param bool $display Whether to display ad codes without conditionals. Default false. + */ // If the ad code doesn't have any conditionals // we should add it to the display list if ( empty( $ad_code['conditionals'] ) && apply_filters( 'acm_display_ad_codes_without_conditionals', false ) ) { @@ -966,9 +1095,15 @@ public function get_matching_ad_code( $tag_id ) { // Run our conditional and use any arguments that were passed if ( ! empty( $cond_args ) ) { /** - * Configuration filter: acm_conditional_args - * For certain conditionals (has_tag, has_category), you might need to - * pass additional arguments. + * Filters the arguments passed to conditional functions. + * + * For certain conditionals like has_tag or has_category, you might need + * to modify the arguments passed to the function. + * + * @since 0.1 + * + * @param array $cond_args Array of arguments to pass to the conditional function. + * @param string $cond_func The conditional function name being called. */ $result = call_user_func_array( $cond_func, apply_filters( 'acm_conditional_args', $cond_args, $cond_func ) ); } else { diff --git a/views/ad-code-manager.tpl.php b/views/ad-code-manager.tpl.php index e869bb2..8c7d9b4 100644 --- a/views/ad-code-manager.tpl.php +++ b/views/ad-code-manager.tpl.php @@ -84,7 +84,17 @@ - + From e6a1ed480b8814a522731f361a83a4769a23e099 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 14:30:49 +0000 Subject: [PATCH 25/36] feat: add autocomplete for conditional arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Select2-based autocomplete for conditional arguments (categories, tags, pages, posts) to make it easier for users to select values when configuring ad code conditions. Key implementation details: - Uses WordPress's bundled SelectWoo/Select2 library - AJAX search with 3-character minimum and 100 result limit - Supports both taxonomy terms and post type searches - Allows custom values via Select2's tags option - Extensible via `acm_autocomplete_conditionals` and `acm_autocomplete_results` filters Fixes #42 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- acm-autocomplete.js | 215 +++++++++++++++ ad-code-manager.php | 5 + phpunit.xml.dist | 2 +- src/UI/class-conditional-autocomplete.php | 259 ++++++++++++++++++ .../UI/ConditionalAutocompleteTest.php | 233 ++++++++++++++++ 5 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 acm-autocomplete.js create mode 100644 src/UI/class-conditional-autocomplete.php create mode 100644 tests/Integration/UI/ConditionalAutocompleteTest.php diff --git a/acm-autocomplete.js b/acm-autocomplete.js new file mode 100644 index 0000000..94e2559 --- /dev/null +++ b/acm-autocomplete.js @@ -0,0 +1,215 @@ +/** + * Conditional Autocomplete for Ad Code Manager + * + * Provides Select2-powered autocomplete for conditional arguments + * like categories, tags, pages, etc. + * + * @since 0.9.0 + */ +( function( $, acmAutocomplete ) { + 'use strict'; + + if ( typeof acmAutocomplete === 'undefined' ) { + return; + } + + var ConditionalAutocomplete = { + + /** + * Initialize the autocomplete functionality. + */ + init: function() { + this.bindEvents(); + this.initExistingFields(); + }, + + /** + * Bind event handlers. + */ + bindEvents: function() { + var self = this; + + // Use event delegation for dynamically added conditional selects. + $( document ).on( 'change', 'select[name="acm-conditionals[]"]', function() { + self.handleConditionalChange( $( this ) ); + }); + + // Re-initialize when new conditional rows are added. + $( document ).on( 'click', '.add-more-conditionals', function() { + // Small delay to allow DOM to update. + setTimeout( function() { + self.initExistingFields(); + }, 100 ); + }); + }, + + /** + * Initialize any existing conditional fields on page load. + */ + initExistingFields: function() { + var self = this; + + $( 'select[name="acm-conditionals[]"]' ).each( function() { + var $select = $( this ); + var conditional = $select.val(); + + if ( conditional && self.hasAutocomplete( conditional ) ) { + self.initAutocomplete( $select ); + } + }); + }, + + /** + * Handle when a conditional select changes. + * + * @param {jQuery} $select The conditional select element. + */ + handleConditionalChange: function( $select ) { + var conditional = $select.val(); + var $argumentsContainer = $select.closest( '.conditional-single-field' ).find( '.conditional-arguments' ); + var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' ); + + // Destroy existing Select2 if present. + if ( $input.hasClass( 'select2-hidden-accessible' ) ) { + $input.select2( 'destroy' ); + } + + // Reset the input. + $input.val( '' ).attr( 'type', 'text' ).show(); + + // If this conditional supports autocomplete, initialize it. + if ( conditional && this.hasAutocomplete( conditional ) ) { + this.initAutocomplete( $select ); + } + }, + + /** + * Check if a conditional has autocomplete configuration. + * + * @param {string} conditional The conditional function name. + * @return {boolean} True if autocomplete is available. + */ + hasAutocomplete: function( conditional ) { + return acmAutocomplete.conditionals.hasOwnProperty( conditional ); + }, + + /** + * Get the autocomplete configuration for a conditional. + * + * @param {string} conditional The conditional function name. + * @return {Object|null} The configuration or null. + */ + getConfig: function( conditional ) { + return acmAutocomplete.conditionals[ conditional ] || null; + }, + + /** + * Initialize Select2 autocomplete on an arguments input. + * + * @param {jQuery} $conditionalSelect The conditional select element. + */ + initAutocomplete: function( $conditionalSelect ) { + var self = this; + var conditional = $conditionalSelect.val(); + var config = this.getConfig( conditional ); + + if ( ! config ) { + return; + } + + var $argumentsContainer = $conditionalSelect.closest( '.conditional-single-field' ).find( '.conditional-arguments' ); + var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' ); + var currentValue = $input.val(); + + // Initialize Select2 with AJAX. + $input.select2({ + ajax: { + url: acmAutocomplete.ajaxUrl, + dataType: 'json', + delay: 250, + data: function( params ) { + return { + action: 'acm_search_terms', + nonce: acmAutocomplete.nonce, + search: params.term, + conditional: conditional, + type: config.type, + taxonomy: config.taxonomy || '', + post_type: config.post_type || '' + }; + }, + processResults: function( response ) { + if ( response.success && response.data.results ) { + return { + results: response.data.results + }; + } + return { results: [] }; + }, + cache: true + }, + minimumInputLength: acmAutocomplete.minChars, + placeholder: self.getPlaceholder( conditional ), + allowClear: true, + tags: true, // Allow custom values (user can type their own). + createTag: function( params ) { + var term = $.trim( params.term ); + if ( term === '' ) { + return null; + } + return { + id: term, + text: term, + newTag: true + }; + }, + language: { + inputTooShort: function() { + return acmAutocomplete.i18n.inputTooShort; + }, + searching: function() { + return acmAutocomplete.i18n.searching; + }, + noResults: function() { + return acmAutocomplete.i18n.noResults; + }, + errorLoading: function() { + return acmAutocomplete.i18n.errorLoading; + } + }, + width: '100%' + }); + + // If there's an existing value, set it. + if ( currentValue ) { + var option = new Option( currentValue, currentValue, true, true ); + $input.append( option ).trigger( 'change' ); + } + }, + + /** + * Get placeholder text for a conditional. + * + * @param {string} conditional The conditional function name. + * @return {string} The placeholder text. + */ + getPlaceholder: function( conditional ) { + var placeholders = { + 'is_category': acmAutocomplete.i18n.searchCategories || 'Search categories...', + 'has_category': acmAutocomplete.i18n.searchCategories || 'Search categories...', + 'is_tag': acmAutocomplete.i18n.searchTags || 'Search tags...', + 'has_tag': acmAutocomplete.i18n.searchTags || 'Search tags...', + 'is_page': acmAutocomplete.i18n.searchPages || 'Search pages...', + 'is_single': acmAutocomplete.i18n.searchPosts || 'Search posts...' + }; + + return placeholders[ conditional ] || 'Search...'; + } + }; + + // Initialize when document is ready. + $( document ).ready( function() { + ConditionalAutocomplete.init(); + }); + +} )( jQuery, window.acmAutocomplete ); diff --git a/ad-code-manager.php b/ad-code-manager.php index 1a5e93a..7d2c051 100644 --- a/ad-code-manager.php +++ b/ad-code-manager.php @@ -27,6 +27,7 @@ namespace Automattic\AdCodeManager; use Ad_Code_Manager; +use Automattic\AdCodeManager\UI\Conditional_Autocomplete; use Automattic\AdCodeManager\UI\Contextual_Help; use Automattic\AdCodeManager\UI\Plugin_Actions; @@ -37,6 +38,7 @@ require_once __DIR__ . '/src/class-acm-wp-list-table.php'; require_once __DIR__ . '/src/class-acm-widget.php'; require_once __DIR__ . '/src/class-ad-code-manager.php'; +require_once __DIR__ . '/src/UI/class-conditional-autocomplete.php'; require_once __DIR__ . '/src/UI/class-contextual-help.php'; require_once __DIR__ . '/src/UI/class-plugin-actions.php'; @@ -51,6 +53,9 @@ function () { add_action( 'admin_init', function () { + $conditional_autocomplete = new Conditional_Autocomplete(); + $conditional_autocomplete->run(); + $contextual_help = new Contextual_Help(); $contextual_help->run(); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1d5a597..c7776ae 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,7 +19,7 @@ tests/Unit - tests/Integration + tests/Integration diff --git a/src/UI/class-conditional-autocomplete.php b/src/UI/class-conditional-autocomplete.php new file mode 100644 index 0000000..791eaec --- /dev/null +++ b/src/UI/class-conditional-autocomplete.php @@ -0,0 +1,259 @@ + admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'acm-search-terms' ), + 'minChars' => self::MIN_CHARS, + 'conditionals' => $this->get_autocomplete_conditionals(), + 'i18n' => array( + 'searching' => __( 'Searching...', 'ad-code-manager' ), + 'noResults' => __( 'No results found', 'ad-code-manager' ), + 'inputTooShort' => sprintf( + /* translators: %d: minimum number of characters */ + __( 'Please enter %d or more characters', 'ad-code-manager' ), + self::MIN_CHARS + ), + 'errorLoading' => __( 'Error loading results', 'ad-code-manager' ), + ), + ) + ); + } + + /** + * Get the conditionals that support autocomplete. + * + * Returns a mapping of conditional function names to their search configuration. + * + * @return array + */ + private function get_autocomplete_conditionals(): array { + $conditionals = array( + 'is_category' => array( + 'type' => 'taxonomy', + 'taxonomy' => 'category', + ), + 'has_category' => array( + 'type' => 'taxonomy', + 'taxonomy' => 'category', + ), + 'is_tag' => array( + 'type' => 'taxonomy', + 'taxonomy' => 'post_tag', + ), + 'has_tag' => array( + 'type' => 'taxonomy', + 'taxonomy' => 'post_tag', + ), + 'is_page' => array( + 'type' => 'post_type', + 'post_type' => 'page', + ), + 'is_single' => array( + 'type' => 'post_type', + 'post_type' => 'post', + ), + ); + + /** + * Filters the conditionals that support autocomplete. + * + * Allows themes and plugins to add or modify which conditionals + * get autocomplete functionality and how they search. + * + * @since 0.9.0 + * + * @param array $conditionals Associative array of conditional configurations. + * Each key is a conditional function name. + * Each value is an array with: + * - 'type': 'taxonomy' or 'post_type' + * - 'taxonomy': (for taxonomy type) The taxonomy to search + * - 'post_type': (for post_type type) The post type to search + */ + return apply_filters( 'acm_autocomplete_conditionals', $conditionals ); + } + + /** + * AJAX handler for searching terms. + * + * @return void + */ + public function ajax_search_terms(): void { + check_ajax_referer( 'acm-search-terms', 'nonce' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'ad-code-manager' ) ) ); + } + + $search = isset( $_GET['search'] ) ? sanitize_text_field( wp_unslash( $_GET['search'] ) ) : ''; + $conditional = isset( $_GET['conditional'] ) ? sanitize_key( $_GET['conditional'] ) : ''; + $type = isset( $_GET['type'] ) ? sanitize_key( $_GET['type'] ) : ''; + $taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( $_GET['taxonomy'] ) : ''; + $post_type = isset( $_GET['post_type'] ) ? sanitize_key( $_GET['post_type'] ) : ''; + + if ( strlen( $search ) < self::MIN_CHARS ) { + wp_send_json_error( array( 'message' => __( 'Search term too short.', 'ad-code-manager' ) ) ); + } + + $results = array(); + + if ( 'taxonomy' === $type && ! empty( $taxonomy ) ) { + $results = $this->search_taxonomy_terms( $search, $taxonomy ); + } elseif ( 'post_type' === $type && ! empty( $post_type ) ) { + $results = $this->search_posts( $search, $post_type ); + } + + /** + * Filters the autocomplete search results. + * + * @since 0.9.0 + * + * @param array $results The search results. + * @param string $search The search term. + * @param string $conditional The conditional function name. + * @param string $type The search type ('taxonomy' or 'post_type'). + */ + $results = apply_filters( 'acm_autocomplete_results', $results, $search, $conditional, $type ); + + wp_send_json_success( array( 'results' => $results ) ); + } + + /** + * Search for taxonomy terms. + * + * @param string $search The search term. + * @param string $taxonomy The taxonomy to search. + * @return array + */ + private function search_taxonomy_terms( string $search, string $taxonomy ): array { + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'search' => $search, + 'number' => self::MAX_RESULTS, + 'hide_empty' => false, + 'orderby' => 'name', + 'order' => 'ASC', + ) + ); + + if ( is_wp_error( $terms ) || empty( $terms ) ) { + return array(); + } + + $results = array(); + foreach ( $terms as $term ) { + $results[] = array( + 'id' => $term->slug, + 'text' => sprintf( '%s (%s)', $term->name, $term->slug ), + ); + } + + return $results; + } + + /** + * Search for posts. + * + * @param string $search The search term. + * @param string $post_type The post type to search. + * @return array + */ + private function search_posts( string $search, string $post_type ): array { + $posts = get_posts( + array( + 'post_type' => $post_type, + 's' => $search, + 'posts_per_page' => self::MAX_RESULTS, + 'post_status' => 'publish', + 'orderby' => 'title', + 'order' => 'ASC', + ) + ); + + if ( empty( $posts ) ) { + return array(); + } + + $results = array(); + foreach ( $posts as $post ) { + // For is_page/is_single, we can use ID, slug, or title. + $results[] = array( + 'id' => (string) $post->ID, + 'text' => sprintf( '%s (ID: %d)', $post->post_title, $post->ID ), + ); + } + + return $results; + } +} diff --git a/tests/Integration/UI/ConditionalAutocompleteTest.php b/tests/Integration/UI/ConditionalAutocompleteTest.php new file mode 100644 index 0000000..f56201d --- /dev/null +++ b/tests/Integration/UI/ConditionalAutocompleteTest.php @@ -0,0 +1,233 @@ +autocomplete = new Conditional_Autocomplete(); + } + + /** + * Test that the run method registers the necessary hooks. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete::run + */ + public function test_run_registers_hooks(): void { + $this->autocomplete->run(); + + self::assertNotFalse( + has_action( 'admin_enqueue_scripts', array( $this->autocomplete, 'enqueue_scripts' ) ) + ); + self::assertNotFalse( + has_action( 'wp_ajax_acm_search_terms', array( $this->autocomplete, 'ajax_search_terms' ) ) + ); + } + + /** + * Test that scripts are not enqueued on non-ACM pages. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete::enqueue_scripts + */ + public function test_scripts_not_enqueued_on_other_pages(): void { + $this->autocomplete->enqueue_scripts( 'edit.php' ); + + self::assertFalse( wp_script_is( 'acm-conditional-autocomplete', 'enqueued' ) ); + } + + /** + * Test that the acm_autocomplete_conditionals filter is applied. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete + */ + public function test_autocomplete_conditionals_filter(): void { + $filter_called = false; + $custom_conditionals = array( + 'custom_conditional' => array( + 'type' => 'taxonomy', + 'taxonomy' => 'custom_tax', + ), + ); + + add_filter( + 'acm_autocomplete_conditionals', + function ( $conditionals ) use ( &$filter_called, $custom_conditionals ) { + $filter_called = true; + return array_merge( $conditionals, $custom_conditionals ); + } + ); + + // Trigger script enqueue to get the conditionals. + set_current_screen( 'settings_page_ad-code-manager' ); + $this->autocomplete->enqueue_scripts( 'settings_page_ad-code-manager' ); + + self::assertTrue( $filter_called ); + } + + /** + * Test default autocomplete conditionals include expected entries. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete + */ + public function test_default_autocomplete_conditionals(): void { + // Use reflection to access the private method. + $reflection = new ReflectionClass( $this->autocomplete ); + $method = $reflection->getMethod( 'get_autocomplete_conditionals' ); + $method->setAccessible( true ); + + $conditionals = $method->invoke( $this->autocomplete ); + + // Verify expected conditionals are present. + self::assertArrayHasKey( 'is_category', $conditionals ); + self::assertArrayHasKey( 'has_category', $conditionals ); + self::assertArrayHasKey( 'is_tag', $conditionals ); + self::assertArrayHasKey( 'has_tag', $conditionals ); + self::assertArrayHasKey( 'is_page', $conditionals ); + self::assertArrayHasKey( 'is_single', $conditionals ); + + // Verify structure of a taxonomy conditional. + self::assertEquals( 'taxonomy', $conditionals['is_category']['type'] ); + self::assertEquals( 'category', $conditionals['is_category']['taxonomy'] ); + + // Verify structure of a post_type conditional. + self::assertEquals( 'post_type', $conditionals['is_page']['type'] ); + self::assertEquals( 'page', $conditionals['is_page']['post_type'] ); + } + + /** + * Test taxonomy term search. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete + */ + public function test_search_taxonomy_terms(): void { + // Create test categories. + self::factory()->category->create( array( 'name' => 'Technology News' ) ); + self::factory()->category->create( array( 'name' => 'Tech Reviews' ) ); + self::factory()->category->create( array( 'name' => 'Sports' ) ); + + // Use reflection to access the private method. + $reflection = new ReflectionClass( $this->autocomplete ); + $method = $reflection->getMethod( 'search_taxonomy_terms' ); + $method->setAccessible( true ); + + $results = $method->invoke( $this->autocomplete, 'Tech', 'category' ); + + self::assertCount( 2, $results ); + self::assertArrayHasKey( 'id', $results[0] ); + self::assertArrayHasKey( 'text', $results[0] ); + } + + /** + * Test post search. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete + */ + public function test_search_posts(): void { + // Create test pages. + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'About Us', + 'post_status' => 'publish', + ) + ); + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'About Our Team', + 'post_status' => 'publish', + ) + ); + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Contact', + 'post_status' => 'publish', + ) + ); + + // Use reflection to access the private method. + $reflection = new ReflectionClass( $this->autocomplete ); + $method = $reflection->getMethod( 'search_posts' ); + $method->setAccessible( true ); + + $results = $method->invoke( $this->autocomplete, 'About', 'page' ); + + self::assertCount( 2, $results ); + self::assertArrayHasKey( 'id', $results[0] ); + self::assertArrayHasKey( 'text', $results[0] ); + } + + /** + * Test empty search returns empty array. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete + */ + public function test_search_no_results(): void { + // Use reflection to access the private method. + $reflection = new ReflectionClass( $this->autocomplete ); + $method = $reflection->getMethod( 'search_taxonomy_terms' ); + $method->setAccessible( true ); + + $results = $method->invoke( $this->autocomplete, 'NonexistentTerm', 'category' ); + + self::assertIsArray( $results ); + self::assertEmpty( $results ); + } + + /** + * Test search for tags returns correct structure. + * + * @covers \Automattic\AdCodeManager\UI\Conditional_Autocomplete + */ + public function test_search_tags(): void { + // Create test tags. + self::factory()->tag->create( array( 'name' => 'WordPress Tips' ) ); + self::factory()->tag->create( array( 'name' => 'WordPress Plugins' ) ); + + // Use reflection to access the private method. + $reflection = new ReflectionClass( $this->autocomplete ); + $method = $reflection->getMethod( 'search_taxonomy_terms' ); + $method->setAccessible( true ); + + $results = $method->invoke( $this->autocomplete, 'WordPress', 'post_tag' ); + + self::assertCount( 2, $results ); + // Results should use slug as ID. + self::assertStringContainsString( 'wordpress', $results[0]['id'] ); + } + + /** + * Clean up after each test. + */ + public function tear_down(): void { + unset( $_GET['search'], $_GET['conditional'], $_GET['type'], $_GET['taxonomy'], $_GET['post_type'], $_GET['nonce'] ); + parent::tear_down(); + } +} From 78398df2f28f84c13b7c00f7fafd92bbc1ebf1b5 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 14:36:44 +0000 Subject: [PATCH 26/36] fix: convert input to hidden type for Select2 AJAX compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Select2 with AJAX data requires hidden inputs, not text inputs. Also fix test pollution by dequeuing scripts between tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- acm-autocomplete.js | 8 +++++++- tests/Integration/UI/ConditionalAutocompleteTest.php | 12 ++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/acm-autocomplete.js b/acm-autocomplete.js index 94e2559..5420cda 100644 --- a/acm-autocomplete.js +++ b/acm-autocomplete.js @@ -72,9 +72,11 @@ // Destroy existing Select2 if present. if ( $input.hasClass( 'select2-hidden-accessible' ) ) { $input.select2( 'destroy' ); + // Remove the Select2 container that gets left behind. + $argumentsContainer.find( '.select2-container' ).remove(); } - // Reset the input. + // Reset the input to a visible text input. $input.val( '' ).attr( 'type', 'text' ).show(); // If this conditional supports autocomplete, initialize it. @@ -121,6 +123,10 @@ var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' ); var currentValue = $input.val(); + // Select2 with AJAX requires a hidden input, not text input. + // Convert the text input to hidden type for Select2. + $input.attr( 'type', 'hidden' ); + // Initialize Select2 with AJAX. $input.select2({ ajax: { diff --git a/tests/Integration/UI/ConditionalAutocompleteTest.php b/tests/Integration/UI/ConditionalAutocompleteTest.php index f56201d..7bfcbb6 100644 --- a/tests/Integration/UI/ConditionalAutocompleteTest.php +++ b/tests/Integration/UI/ConditionalAutocompleteTest.php @@ -31,6 +31,12 @@ final class ConditionalAutocompleteTest extends TestCase { */ public function set_up(): void { parent::set_up(); + + // Ensure scripts are dequeued at the start of each test. + wp_dequeue_script( 'acm-conditional-autocomplete' ); + wp_dequeue_script( 'selectWoo' ); + wp_dequeue_style( 'select2' ); + $this->autocomplete = new Conditional_Autocomplete(); } @@ -228,6 +234,12 @@ public function test_search_tags(): void { */ public function tear_down(): void { unset( $_GET['search'], $_GET['conditional'], $_GET['type'], $_GET['taxonomy'], $_GET['post_type'], $_GET['nonce'] ); + + // Dequeue scripts to prevent test pollution. + wp_dequeue_script( 'acm-conditional-autocomplete' ); + wp_dequeue_script( 'selectWoo' ); + wp_dequeue_style( 'select2' ); + parent::tear_down(); } } From e879cbf1957c409dbbd0d35374ef54fe31ccfeac Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 14:42:14 +0000 Subject: [PATCH 27/36] fix: load Select2 from CDN when not already available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Select2/selectWoo is not bundled with WordPress core - it comes from WooCommerce. When neither selectWoo nor select2 is registered, we now load Select2 v4.0.13 from jsDelivr CDN. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/UI/class-conditional-autocomplete.php | 33 ++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/UI/class-conditional-autocomplete.php b/src/UI/class-conditional-autocomplete.php index 791eaec..400a63a 100644 --- a/src/UI/class-conditional-autocomplete.php +++ b/src/UI/class-conditional-autocomplete.php @@ -43,6 +43,13 @@ public function run(): void { add_action( 'wp_ajax_acm_search_terms', array( $this, 'ajax_search_terms' ) ); } + /** + * Select2 version to use from CDN. + * + * @var string + */ + private const SELECT2_VERSION = '4.0.13'; + /** * Enqueue scripts and styles for the autocomplete functionality. * @@ -54,15 +61,35 @@ public function enqueue_scripts( string $hook_suffix ): void { return; } - // Enqueue Select2 from WordPress (available since WP 4.0). - wp_enqueue_script( 'selectWoo' ); + // Register Select2 from CDN if not already available. + // Check for common Select2 handles (selectWoo from WooCommerce, select2 from other plugins). + if ( ! wp_script_is( 'selectWoo', 'registered' ) && ! wp_script_is( 'select2', 'registered' ) ) { + wp_register_script( + 'select2', + 'https://cdn.jsdelivr.net/npm/select2@' . self::SELECT2_VERSION . '/dist/js/select2.min.js', + array( 'jquery' ), + self::SELECT2_VERSION, + true + ); + wp_register_style( + 'select2', + 'https://cdn.jsdelivr.net/npm/select2@' . self::SELECT2_VERSION . '/dist/css/select2.min.css', + array(), + self::SELECT2_VERSION + ); + } + + // Determine which Select2 handle to use (prefer selectWoo if available). + $select2_handle = wp_script_is( 'selectWoo', 'registered' ) ? 'selectWoo' : 'select2'; + + wp_enqueue_script( $select2_handle ); wp_enqueue_style( 'select2' ); // Enqueue our autocomplete handler. wp_enqueue_script( 'acm-conditional-autocomplete', plugins_url( 'acm-autocomplete.js', dirname( __DIR__, 2 ) . '/ad-code-manager.php' ), - array( 'jquery', 'selectWoo' ), + array( 'jquery', $select2_handle ), filemtime( dirname( __DIR__, 2 ) . '/acm-autocomplete.js' ), true ); From 93f398ae9fda874d0045676f39b055cb96bf7067 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 14:56:53 +0000 Subject: [PATCH 28/36] fix: use dynamic select element for Select2 AJAX compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Select2 with AJAX requires a for AJAX). + var $select = $( '' ) + .attr( 'name', 'acm-arguments[]' ) + .addClass( 'acm-autocomplete-select' ); + + // If there's an existing value, add it as an option. + if ( currentValue ) { + $select.append( new Option( currentValue, currentValue, true, true ) ); + } + + // Insert the select after the hidden input. + $input.after( $select ); + + // Remove the name from the original input to avoid duplicate submission. + $input.removeAttr( 'name' ).attr( 'data-original-name', 'acm-arguments[]' ); // Initialize Select2 with AJAX. - $input.select2({ + $select.select2({ ajax: { url: acmAutocomplete.ajaxUrl, dataType: 'json', @@ -185,12 +213,6 @@ }, width: '100%' }); - - // If there's an existing value, set it. - if ( currentValue ) { - var option = new Option( currentValue, currentValue, true, true ); - $input.append( option ).trigger( 'change' ); - } }, /** From ad363f36c7d4c6279f3bbd10e176a9fbac390f7e Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 15:00:11 +0000 Subject: [PATCH 29/36] fix: hide arguments field for conditionals that take no parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conditionals like is_home, is_front_page, is_404, etc. don't accept arguments, so showing an input field is confusing. Now the arguments field is hidden when these conditionals are selected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- acm-autocomplete.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/acm-autocomplete.js b/acm-autocomplete.js index c503868..76b46ef 100644 --- a/acm-autocomplete.js +++ b/acm-autocomplete.js @@ -92,6 +92,15 @@ // Reset the input value. $input.val( '' ); + // Hide arguments container for conditionals that take no parameters. + if ( conditional && this.hasNoParameters( conditional ) ) { + $argumentsContainer.hide(); + return; + } + + // Show arguments container for conditionals that take parameters. + $argumentsContainer.show(); + // If this conditional supports autocomplete, initialize it. if ( conditional && this.hasAutocomplete( conditional ) ) { this.initAutocomplete( $select ); @@ -232,6 +241,35 @@ }; return placeholders[ conditional ] || 'Search...'; + }, + + /** + * Check if a conditional takes no parameters. + * + * @param {string} conditional The conditional function name. + * @return {boolean} True if the conditional takes no parameters. + */ + hasNoParameters: function( conditional ) { + var noParamConditionals = [ + 'is_home', + 'is_front_page', + 'is_archive', + 'is_search', + 'is_404', + 'is_date', + 'is_year', + 'is_month', + 'is_day', + 'is_time', + 'is_feed', + 'is_comment_feed', + 'is_trackback', + 'is_preview', + 'is_paged', + 'is_admin' + ]; + + return noParamConditionals.indexOf( conditional ) !== -1; } }; From 227c6957213ae94ea0045746933006d40c2edbea Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 15:15:31 +0000 Subject: [PATCH 30/36] fix: improve admin UI layout and usability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change "Add more" to "Add another condition" - Remove boxed styling from Configuration section - Fix Select2 field heights to match native inputs - Fix Edit labels to use proper labels from provider config - Link labels to their corresponding form fields - Hide "Add another condition" button until first condition selected - Show Logical Operator only when 2+ conditions are set - Move Logical Operator field near Conditionals in Edit view 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- acm-autocomplete.js | 50 ++++++++++++++++++++++-- acm.css | 68 +++++++++++++++++++++++++++++++++ src/class-acm-wp-list-table.php | 46 ++++++++++++---------- views/ad-code-manager.tpl.php | 61 +++++++++++++++-------------- 4 files changed, 170 insertions(+), 55 deletions(-) diff --git a/acm-autocomplete.js b/acm-autocomplete.js index 76b46ef..c38c147 100644 --- a/acm-autocomplete.js +++ b/acm-autocomplete.js @@ -21,6 +21,7 @@ init: function() { this.bindEvents(); this.initExistingFields(); + this.updateAddButtonVisibility(); }, /** @@ -32,6 +33,7 @@ // Use event delegation for dynamically added conditional selects. $( document ).on( 'change', 'select[name="acm-conditionals[]"]', function() { self.handleConditionalChange( $( this ) ); + self.updateAddButtonVisibility(); }); // Re-initialize when new conditional rows are added. @@ -39,6 +41,14 @@ // Small delay to allow DOM to update. setTimeout( function() { self.initExistingFields(); + self.updateAddButtonVisibility(); + }, 100 ); + }); + + // Handle remove conditional click. + $( document ).on( 'click', '.acm-remove-conditional', function() { + setTimeout( function() { + self.updateAddButtonVisibility(); }, 100 ); }); }, @@ -52,8 +62,17 @@ $( 'select[name="acm-conditionals[]"]' ).each( function() { var $select = $( this ); var conditional = $select.val(); + var $argumentsContainer = $select.closest( '.conditional-single-field' ).find( '.conditional-arguments' ); - if ( conditional && self.hasAutocomplete( conditional ) ) { + // Hide arguments for empty selection or no-parameter conditionals. + if ( ! conditional || self.hasNoParameters( conditional ) ) { + $argumentsContainer.hide(); + return; + } + + // Show arguments and init autocomplete if applicable. + $argumentsContainer.show(); + if ( self.hasAutocomplete( conditional ) ) { self.initAutocomplete( $select ); } }); @@ -92,8 +111,8 @@ // Reset the input value. $input.val( '' ); - // Hide arguments container for conditionals that take no parameters. - if ( conditional && this.hasNoParameters( conditional ) ) { + // Hide arguments container when no conditional selected or for conditionals that take no parameters. + if ( ! conditional || this.hasNoParameters( conditional ) ) { $argumentsContainer.hide(); return; } @@ -270,6 +289,31 @@ ]; return noParamConditionals.indexOf( conditional ) !== -1; + }, + + /** + * Update visibility of the "Add another condition" button. + * + * Shows the button only when at least one condition is selected. + */ + updateAddButtonVisibility: function() { + var $form = $( '#add-adcode' ); + var $addButton = $form.find( '.form-add-more' ); + var hasSelectedCondition = false; + + // Check if any conditional select has a value. + $form.find( 'select[name="acm-conditionals[]"]' ).each( function() { + if ( $( this ).val() ) { + hasSelectedCondition = true; + return false; // Break the loop. + } + }); + + if ( hasSelectedCondition ) { + $addButton.addClass( 'visible' ); + } else { + $addButton.removeClass( 'visible' ); + } } }; diff --git a/acm.css b/acm.css index 3f1303d..3b55ff7 100644 --- a/acm.css +++ b/acm.css @@ -160,6 +160,22 @@ tr:hover .row-actions { .acm-global-options { clear: both; + margin-bottom: 20px; +} + +.acm-global-options .acm-config-form p { + display: flex; + align-items: center; + gap: 10px; + margin: 0; +} + +.acm-global-options .acm-config-form label { + font-weight: 600; +} + +.acm-global-options .acm-config-form .button { + margin-left: 10px; } .acm-global-options input[type="text"] { @@ -171,3 +187,55 @@ tr:hover .row-actions { float: left; width: 125px; } + +/* Ensure conditional row has consistent height even when arguments hidden */ +#add-adcode .conditional-single-field { + min-height: 36px; + margin-bottom: 8px; +} + +/* Select2 styling for conditional autocomplete */ +#add-adcode .conditional-arguments .select2-container { + width: 100% !important; +} + +#add-adcode .conditional-arguments .select2-container--default .select2-selection--single { + height: 30px; + border-color: #8c8f94; + border-radius: 4px; +} + +#add-adcode .conditional-arguments .select2-container--default .select2-selection--single .select2-selection__rendered { + line-height: 28px; + padding-left: 8px; +} + +#add-adcode .conditional-arguments .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 28px; +} + +/* Inline edit Select2 styling */ +.inline-edit-col .conditional-arguments .select2-container { + width: 70% !important; + margin-bottom: 5px; +} + +/* Hide Add button initially until first condition is selected */ +#add-adcode .form-add-more { + display: none; +} + +#add-adcode .form-add-more.visible { + display: block; +} + +/* Operator field styling in inline edit */ +.inline-edit-col .acm-operator-field { + clear: both; + margin-top: 10px; + padding-top: 10px; +} + +.inline-edit-col .acm-operator-field .acm-section-label { + margin-bottom: 5px; +} diff --git a/src/class-acm-wp-list-table.php b/src/class-acm-wp-list-table.php index 0d91ef9..4f0b4cf 100644 --- a/src/class-acm-wp-list-table.php +++ b/src/class-acm-wp-list-table.php @@ -273,26 +273,43 @@ function column_id( $item ) { $output .= 'Remove'; } } - $output .= ''; + $output .= ''; + // Build the field for the logical operator (near conditionals) + $condition_count = ! empty( $item['conditionals'] ) ? count( $item['conditionals'] ) : 0; + $operator_style = $condition_count < 2 ? ' style="display:none;"' : ''; + $output .= '
'; + $output .= ''; + $output .= ''; + $output .= '
'; $output .= ''; // Build the fields for the normal columns $output .= '
'; $output .= ''; foreach ( (array) $item['url_vars'] as $slug => $value ) { $output .= '
'; - $column_id = 'acm-column[' . $slug . ']'; - $output .= ''; - // Support for select dropdowns + $column_id = 'acm-column-' . $slug; + // Get the proper label from provider's ad_code_args $ad_code_args = wp_filter_object_list( $ad_code_manager->current_provider->ad_code_args, array( 'key' => $slug ) ); $ad_code_arg = array_shift( $ad_code_args ); + $field_label = isset( $ad_code_arg['label'] ) ? $ad_code_arg['label'] : ucwords( str_replace( '_', ' ', $slug ) ); + $output .= ''; + // Support for select dropdowns if ( isset( $ad_code_arg['type'] ) && 'select' == $ad_code_arg['type'] ) { - $output .= ''; foreach ( $ad_code_arg['options'] as $key => $label ) { $output .= ''; } $output .= ''; } else { - $output .= ''; + $output .= ''; } $output .= '
'; } @@ -300,20 +317,7 @@ function column_id( $item ) { // Build the field for the priority $output .= '
'; $output .= ''; - $output .= ''; - $output .= '
'; - // Build the field for the logical operator - $output .= '
'; - $output .= ''; - $output .= ''; + $output .= ''; $output .= '
'; $output .= '
'; @@ -387,9 +391,9 @@ function inline_edit() {
-
+

diff --git a/views/ad-code-manager.tpl.php b/views/ad-code-manager.tpl.php index 8c7d9b4..4b73889 100644 --- a/views/ad-code-manager.tpl.php +++ b/views/ad-code-manager.tpl.php @@ -41,39 +41,15 @@ } ?>

- - -
-
- -
-
-
-wp_list_table->prepare_items(); -$this->wp_list_table->display(); -?> -
- -
-
- -
-
- - -

-
-
-
- + +

+ -

- + +

-
+
+ +
+
+ +
+
+

+
+wp_list_table->prepare_items(); +$this->wp_list_table->display(); +?> +
+ +
+
+ +
+
+ + +

@@ -164,7 +163,7 @@
- +

From 62e46d48e3746110e7abc11dea4652a9a8aa35c2 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 17:11:09 +0000 Subject: [PATCH 31/36] fix: scope autocomplete to Add form only, add Select2 fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove inline edit autocomplete initialization (will be addressed with dedicated edit page in future PR) - Scope all autocomplete functionality to #add-adcode form only - Add isSelect2Available() check to gracefully handle CDN failures - Prevent JS errors when Select2 fails to load 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- acm-autocomplete.js | 79 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/acm-autocomplete.js b/acm-autocomplete.js index c38c147..5f8a853 100644 --- a/acm-autocomplete.js +++ b/acm-autocomplete.js @@ -15,6 +15,15 @@ var ConditionalAutocomplete = { + /** + * Check if Select2 is available. + * + * @return {boolean} True if Select2 is loaded. + */ + isSelect2Available: function() { + return typeof $.fn.select2 === 'function'; + }, + /** * Initialize the autocomplete functionality. */ @@ -30,8 +39,8 @@ bindEvents: function() { var self = this; - // Use event delegation for dynamically added conditional selects. - $( document ).on( 'change', 'select[name="acm-conditionals[]"]', function() { + // Use event delegation for dynamically added conditional selects (Add form only). + $( document ).on( 'change', '#add-adcode select[name="acm-conditionals[]"]', function() { self.handleConditionalChange( $( this ) ); self.updateAddButtonVisibility(); }); @@ -40,7 +49,7 @@ $( document ).on( 'click', '.add-more-conditionals', function() { // Small delay to allow DOM to update. setTimeout( function() { - self.initExistingFields(); + self.cleanupNewRows(); self.updateAddButtonVisibility(); }, 100 ); }); @@ -55,11 +64,13 @@ /** * Initialize any existing conditional fields on page load. + * Only targets the Add form, not inline edit. */ initExistingFields: function() { var self = this; - $( 'select[name="acm-conditionals[]"]' ).each( function() { + // Only target the Add form to avoid conflicts with inline edit. + $( '#add-adcode select[name="acm-conditionals[]"]' ).each( function() { var $select = $( this ); var conditional = $select.val(); var $argumentsContainer = $select.closest( '.conditional-single-field' ).find( '.conditional-arguments' ); @@ -72,12 +83,59 @@ // Show arguments and init autocomplete if applicable. $argumentsContainer.show(); - if ( self.hasAutocomplete( conditional ) ) { + + // Only init autocomplete if not already initialized. + var $existingSelect2 = $argumentsContainer.find( 'select.acm-autocomplete-select' ); + if ( self.hasAutocomplete( conditional ) && ! $existingSelect2.length ) { self.initAutocomplete( $select ); } }); }, + /** + * Clean up newly added conditional rows. + * + * When rows are cloned from the master template, they may contain + * leftover Select2 markup that needs to be cleaned up. + */ + cleanupNewRows: function() { + var self = this; + + $( 'select[name="acm-conditionals[]"]' ).each( function() { + var $conditionalSelect = $( this ); + var conditional = $conditionalSelect.val(); + var $argumentsContainer = $conditionalSelect.closest( '.conditional-single-field' ).find( '.conditional-arguments' ); + + // If no conditional is selected, clean up any Select2 remnants. + if ( ! conditional ) { + // Remove any cloned Select2 elements. + $argumentsContainer.find( 'select.acm-autocomplete-select' ).remove(); + $argumentsContainer.find( '.select2-container' ).remove(); + + // Restore the original input if it was hidden. + var $hiddenInput = $argumentsContainer.find( 'input[data-original-name="acm-arguments[]"]' ); + if ( $hiddenInput.length ) { + $hiddenInput + .attr( 'name', 'acm-arguments[]' ) + .removeAttr( 'data-original-name' ) + .val( '' ) + .show(); + } + + // Ensure input exists and is visible with correct attributes. + var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' ); + if ( ! $input.length ) { + $argumentsContainer.prepend( '' ); + } else { + $input.val( '' ).attr( 'type', 'text' ).show(); + } + + // Hide the arguments container since no conditional is selected. + $argumentsContainer.hide(); + } + }); + }, + /** * Handle when a conditional select changes. * @@ -160,6 +218,11 @@ return; } + // Skip if Select2 is not available (CDN failed to load). + if ( ! this.isSelect2Available() ) { + return; + } + var $argumentsContainer = $conditionalSelect.closest( '.conditional-single-field' ).find( '.conditional-arguments' ); var $input = $argumentsContainer.find( 'input[name="acm-arguments[]"]' ); var currentValue = $input.val(); @@ -183,8 +246,14 @@ // Remove the name from the original input to avoid duplicate submission. $input.removeAttr( 'name' ).attr( 'data-original-name', 'acm-arguments[]' ); + // Determine the dropdown parent - use body for inline edit to avoid z-index issues. + var $dropdownParent = $argumentsContainer.closest( '.acm-editor-row' ).length + ? $( 'body' ) + : $argumentsContainer; + // Initialize Select2 with AJAX. $select.select2({ + dropdownParent: $dropdownParent, ajax: { url: acmAutocomplete.ajaxUrl, dataType: 'json', From 5cc139a55229f97811cf8c3921130d032c7e6d39 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 22:49:06 +0000 Subject: [PATCH 32/36] feat: add dedicated edit page to replace inline editing Replace the inline edit functionality with a full-page edit interface that provides clearer layout and better autocomplete support for conditional arguments. The dedicated edit page allows users to see all ad code properties simultaneously without table layout constraints. The new edit page includes Select2 autocomplete for conditional arguments, dynamically shows or hides the logical operator field based on the number of conditions, and provides a more intuitive editing experience. This change removes the legacy inline edit JavaScript (acm.js), associated CSS styles, and the column_id() and inline_edit() methods from the list table class. Resolves #195 --- acm-autocomplete.js | 139 +++++++++-- acm.css | 197 ++++++---------- acm.js | 396 -------------------------------- src/class-acm-wp-list-table.php | 128 +---------- src/class-ad-code-manager.php | 30 ++- views/ad-code-manager.tpl.php | 5 +- views/edit-ad-code.tpl.php | 212 +++++++++++++++++ 7 files changed, 445 insertions(+), 662 deletions(-) delete mode 100644 acm.js create mode 100644 views/edit-ad-code.tpl.php diff --git a/acm-autocomplete.js b/acm-autocomplete.js index 5f8a853..3acc58b 100644 --- a/acm-autocomplete.js +++ b/acm-autocomplete.js @@ -39,38 +39,52 @@ bindEvents: function() { var self = this; - // Use event delegation for dynamically added conditional selects (Add form only). - $( document ).on( 'change', '#add-adcode select[name="acm-conditionals[]"]', function() { + // Use event delegation for dynamically added conditional selects (Add and Edit forms). + $( document ).on( 'change', '#add-adcode select[name="acm-conditionals[]"], #edit-adcode select[name="acm-conditionals[]"]', function() { self.handleConditionalChange( $( this ) ); self.updateAddButtonVisibility(); + self.updateOperatorVisibility(); }); - // Re-initialize when new conditional rows are added. - $( document ).on( 'click', '.add-more-conditionals', function() { - // Small delay to allow DOM to update. + // Handle adding new conditional rows. + $( document ).on( 'click', '.add-more-conditionals', function( e ) { + e.preventDefault(); + var $button = $( this ); + var $form = $button.closest( 'form' ); + var $container = $form.find( '.form-new-row' ); + + self.addConditionalRow( $container ); + + // Small delay to allow DOM to update, then clean up. setTimeout( function() { self.cleanupNewRows(); self.updateAddButtonVisibility(); + self.updateOperatorVisibility(); }, 100 ); }); // Handle remove conditional click. - $( document ).on( 'click', '.acm-remove-conditional', function() { + $( document ).on( 'click', '.acm-remove-conditional', function( e ) { + e.preventDefault(); + var $row = $( this ).closest( '.conditional-single-field' ); + $row.remove(); + setTimeout( function() { self.updateAddButtonVisibility(); + self.updateOperatorVisibility(); }, 100 ); }); }, /** * Initialize any existing conditional fields on page load. - * Only targets the Add form, not inline edit. + * Targets both Add and Edit forms. */ initExistingFields: function() { var self = this; - // Only target the Add form to avoid conflicts with inline edit. - $( '#add-adcode select[name="acm-conditionals[]"]' ).each( function() { + // Target both Add and Edit forms. + $( '#add-adcode select[name="acm-conditionals[]"], #edit-adcode select[name="acm-conditionals[]"]' ).each( function() { var $select = $( this ); var conditional = $select.val(); var $argumentsContainer = $select.closest( '.conditional-single-field' ).find( '.conditional-arguments' ); @@ -92,6 +106,62 @@ }); }, + /** + * Add a new conditional row to the form. + * + * @param {jQuery} $container The container to add the row to. + */ + addConditionalRow: function( $container ) { + var $master = $container.find( '#conditional-single-field-master' ); + + if ( ! $master.length ) { + $master = $container.find( '.conditional-single-field' ).first(); + } + + if ( ! $master.length ) { + return; + } + + // Clone the master row. + var $newRow = $master.clone(); + + // Remove the master ID so only one exists. + $newRow.removeAttr( 'id' ); + + // Reset select to default. + $newRow.find( 'select[name="acm-conditionals[]"]' ).val( '' ); + + // Reset and show the input. + var $input = $newRow.find( 'input[name="acm-arguments[]"]' ); + $input.val( '' ).show(); + + // Remove any Select2 elements that may have been cloned. + $newRow.find( 'select.acm-autocomplete-select' ).remove(); + $newRow.find( '.select2-container' ).remove(); + + // Restore hidden input if present. + var $hiddenInput = $newRow.find( 'input[data-original-name="acm-arguments[]"]' ); + if ( $hiddenInput.length ) { + $hiddenInput + .attr( 'name', 'acm-arguments[]' ) + .removeAttr( 'data-original-name' ) + .val( '' ) + .show(); + } + + // Add remove link if not present. + var $arguments = $newRow.find( '.conditional-arguments' ); + if ( ! $arguments.find( '.acm-remove-conditional' ).length ) { + $arguments.append( ' Remove' ); + } + + // Hide the arguments container initially. + $arguments.hide(); + + // Append to container. + $container.append( $newRow ); + }, + /** * Clean up newly added conditional rows. * @@ -366,22 +436,53 @@ * Shows the button only when at least one condition is selected. */ updateAddButtonVisibility: function() { - var $form = $( '#add-adcode' ); - var $addButton = $form.find( '.form-add-more' ); - var hasSelectedCondition = false; + var $forms = $( '#add-adcode, #edit-adcode' ); + + $forms.each( function() { + var $form = $( this ); + var $addButton = $form.find( '.form-add-more' ); + var hasSelectedCondition = false; + + // Check if any conditional select has a value. + $form.find( 'select[name="acm-conditionals[]"]' ).each( function() { + if ( $( this ).val() ) { + hasSelectedCondition = true; + return false; // Break the loop. + } + }); + + if ( hasSelectedCondition ) { + $addButton.addClass( 'visible' ); + } else { + $addButton.removeClass( 'visible' ); + } + }); + }, + + /** + * Update visibility of the Logical Operator row. + * + * Shows the operator row only when 2+ conditions are selected. + */ + updateOperatorVisibility: function() { + var $operatorRow = $( '#operator-row' ); + + if ( ! $operatorRow.length ) { + return; + } - // Check if any conditional select has a value. - $form.find( 'select[name="acm-conditionals[]"]' ).each( function() { + // Count selected conditionals. + var selectedCount = 0; + $( '#edit-adcode select[name="acm-conditionals[]"]' ).each( function() { if ( $( this ).val() ) { - hasSelectedCondition = true; - return false; // Break the loop. + selectedCount++; } }); - if ( hasSelectedCondition ) { - $addButton.addClass( 'visible' ); + if ( selectedCount >= 2 ) { + $operatorRow.show(); } else { - $addButton.removeClass( 'visible' ); + $operatorRow.hide(); } } }; diff --git a/acm.css b/acm.css index 3b55ff7..86ba15c 100644 --- a/acm.css +++ b/acm.css @@ -37,127 +37,11 @@ tr:hover .row-actions { margin-bottom: 5px; } -.inline-edit-col .acm-section-label { - margin-bottom: 5px; - text-transform: none; -} - -.inline-edit-col .acm-float-left { - float: left; -} - -.inline-edit-col .acm-column-fields, -.inline-edit-col .acm-priority-field { - width: 250px; -} - -.inline-edit-col .acm-column-fields label { - float: left; - width: 75px; -} - -.inline-edit-col .acm-column-fields input { - margin-bottom: 5px; -} - -.inline-edit-col .acm-conditional-fields { - float: left; - min-width: 270px; -} - -.inline-edit-col .acm-conditional-fields .conditional-arguments { - margin-left: 140px; -} - -.inline-edit-col .acm-conditional-fields .conditional-arguments input { - width: 70%; - margin-bottom: 5px; -} - -.inline-edit-col .acm-conditional-fields .form-add-more { - margin-top: 10px; - clear: both; -} - #add-adcode .form-add-more { clear:left; width: 90%; } -.acm-edit-field { - position: relative; - max-width: 200px; - float: left; - margin: 15px 0 0 15px; -} - -.acm-edit-field label { - position: relative; - float: left; - font-weight: 700; - color: #333; -} - -.acm-edit-cond, -.acm-cancel-button, -.acm-edit-field input, -.acm-conditional-label { - position: relative; - float: left; - clear: left; -} - -.acm-edit-cond { - margin: 5px 0 0 15px; -} - -.acm-edit-cond input { - margin-left: 15px; -} - -.acm-conditional-label { - width: 100%; - font-weight: 700; - color: #333; - margin: 10px 0 0 15px; -} - -.acm-cancel-button { - margin: 10px 0 30px 15px; -} - -.acm-edit-button { - float: left; - margin: 10px 0 30px 15px; -} - -.acm-x-cond { - font-size: 14px; - padding-left: 5px; - color: #888; - cursor: pointer; - font-family: sans-serif -} - -.acm-x-cond:hover { - color: #333; - text-decoration: underline; -} - -#acm-add-inline-cond { - display: block; - float: left; - clear: left; - padding-top: 5px; - width: 295px; - text-align: right; - cursor:pointer; -} - -#acm-add-inline-cond:hover { - text-decoration: underline; -} - .acm-global-options { clear: both; margin-bottom: 20px; @@ -214,12 +98,6 @@ tr:hover .row-actions { height: 28px; } -/* Inline edit Select2 styling */ -.inline-edit-col .conditional-arguments .select2-container { - width: 70% !important; - margin-bottom: 5px; -} - /* Hide Add button initially until first condition is selected */ #add-adcode .form-add-more { display: none; @@ -229,13 +107,76 @@ tr:hover .row-actions { display: block; } -/* Operator field styling in inline edit */ -.inline-edit-col .acm-operator-field { - clear: both; +/* Edit page styles */ +#edit-adcode .form-table th { + width: 200px; + padding: 20px 10px 20px 0; +} + +#edit-adcode .regular-text { + width: 25em; +} + +#edit-adcode .conditional-single-field { + margin-bottom: 10px; + display: flex; + align-items: flex-start; + gap: 10px; +} + +#edit-adcode .conditional-function { + flex: 0 0 200px; +} + +#edit-adcode .conditional-function select { + width: 100%; +} + +#edit-adcode .conditional-arguments { + flex: 1; + display: flex; + align-items: center; + gap: 10px; +} + +#edit-adcode .conditional-arguments input[type="text"] { + width: 200px; +} + +#edit-adcode .form-add-more { + display: none; margin-top: 10px; - padding-top: 10px; } -.inline-edit-col .acm-operator-field .acm-section-label { - margin-bottom: 5px; +#edit-adcode .form-add-more.visible { + display: block; +} + +#edit-adcode .acm-remove-conditional { + color: #b32d2e; + text-decoration: none; +} + +#edit-adcode .acm-remove-conditional:hover { + color: #a00; +} + +/* Select2 styling for edit page */ +#edit-adcode .conditional-arguments .select2-container { + width: 200px !important; +} + +#edit-adcode .conditional-arguments .select2-container--default .select2-selection--single { + height: 30px; + border-color: #8c8f94; + border-radius: 4px; +} + +#edit-adcode .conditional-arguments .select2-container--default .select2-selection--single .select2-selection__rendered { + line-height: 28px; + padding-left: 8px; +} + +#edit-adcode .conditional-arguments .select2-container--default .select2-selection--single .select2-selection__arrow { + height: 28px; } diff --git a/acm.js b/acm.js deleted file mode 100644 index 58c1777..0000000 --- a/acm.js +++ /dev/null @@ -1,396 +0,0 @@ -( function( window, $, undefined ) { - var document = window.document; - - var AdCodeManager = function() { - /** - * A reference to the AdCodeManager object so we avoid confusion with `this` later on - * - * @type {*} - */ - var SELF = this; - - /** - * Container for cached UI elements - * - * @type {Object} - */ - var UI = {}; - - /** - * Used for storing the currently edited ACM ID - * @type {Boolean} - */ - var EDIT_ID = false; - - /** - * Initializes the AdCodeManager when the object is instantiated. This script must always run from the footer - * after the DOM elements are rendered - * - * @private - */ - var _init = function() { - _cacheElements(); - _bindEvents(); - }; - - /** - * Caches useful DOM elements so we can easily reference them later without the extra lookup - * - * @private - */ - var _cacheElements = function() { - UI.addMoreButton = document.getElementById( 'conditional-tpl' ).querySelector( '.add-more-conditionals' ); - UI.theList = document.getElementById( 'the-list' ); - UI.theNew = document.getElementById( 'add-adcode' ); - }; - - /** - * Handles binding our events for the page to work - * - * @private - */ - var _bindEvents = function() { - _addEvent( 'click', UI.addMoreButton, _addConditional ); - _addEvent( 'click', UI.theList, _delegateListClicks ); - _addEvent( 'click', UI.theNew, _delegateNewAdClicks ); - _addEvent( 'keydown', UI.theList, _delegateListKeyEvents ); - }; - - /** - * Registers a DOM event for the specified element - * - * @param event The event we're hooking to - * @param element The element we want to monitor for the event - * @param callback The callback to be fired when the event is triggered - * @private - */ - var _addEvent = function( event, element, callback ) { - if( window.addEventListener ) { - element.addEventListener( event, callback, false ); - } - else { - element.attachEvent( 'on' + event, callback ); - } - }; - - /** - * Handles adding a new conditional row to the UI for the user - * - * @param e The event object - * @private - */ - var _addConditional = function( e ) { - e = e || window.event; - var target = e.srcElement || e.target; - var parent = target.parentNode.parentNode.querySelector( '.form-new-row' ); - _addInlineConditionalRow( parent ); - - _killEvent( e ); - }; - - /** - * Kills the passed event and prevents it from bubbling up the DOM - * - * @param e The event we're killing - * @private - */ - var _killEvent = function( e ) { - e.returnValue = false; - e.cancelBubble = true; - if( e.stopPropagation ) { - e.stopPropagation(); - } - if( e.preventDefault ) { - e.preventDefault(); - } - }; - - /** - * Handles checking delegated events for the add ad code area - * - * @param e The event object - * @private - */ - var _delegateNewAdClicks = function( e ) { - e = e || window.event; - var target = e.srcElement || e.target; - - // check for remove conditional call - if( _hasClass( target, 'acm-remove-conditional' ) === true ) { - _removeInlineConditionalRow( target ); - _killEvent( e ); - } - }; - - /** - * Handles checking delegated key events for the inline editor and table rows - * @param e - * @private - */ - var _delegateListKeyEvents = function( e ) { - e = e || window.event; - var key = e.which || e.keyCode; - - // 13 is Enter, which avoids the default form on the page from saving - if ( key === 13 ) { - _saveInlineEditorChanges(); - _killEvent( e ); - } - }; - - /** - * Handles checking delegated events for the inline editor and table rows - * - * @param e The event object - * @private - */ - var _delegateListClicks = function( e ) { - e = e || window.event; - var target = e.srcElement || e.target; - - // check for ajax edit click - if( _hasClass( target, 'acm-ajax-edit' ) === true ) { - // close other editors - if( EDIT_ID !== false ) { - if( confirm( 'Are you sure you want to do this? Any unsaved data will be lost.' ) === false ) { - _killEvent( e ); - return; - } - _toggleInlineEdit( false ); - } - - EDIT_ID = parseInt( target.id.replace( 'acm-edit-', '' ), 10 ); - _toggleInlineEdit( true ); - _killEvent( e ); - } - - // check for cancel button - else if( _hasClass( target, 'cancel' ) === true && EDIT_ID !== false ) { - _toggleInlineEdit( false ); - EDIT_ID = false; - } - - // check for remove conditional call - else if( _hasClass( target, 'acm-remove-conditional' ) === true ) { - _removeInlineConditionalRow( target ); - _killEvent( e ); - } - - // check for save button - else if( _hasClass( target, 'save' ) === true ) { - _toggleLoader( true ); - _saveInlineEditorChanges(); - _killEvent( e ); - } - - // check for add more conditionals - else if( _hasClass( target, 'add-more-conditionals' ) === true ) { - _addInlineConditionalRow( UI.theList.querySelector( '#ad-code-' + EDIT_ID + ' .acm-editor-row .acm-conditional-fields .form-new-row' ) ); - _killEvent( e ); - } - }; - - /** - * Saves any inline editor changes that occurred - * - * @private - */ - var _saveInlineEditorChanges = function() { - $.post( window.ajaxurl, _getFormData(), function( result ) { - if( result ) { - if( result.indexOf( ' -1 ) { - $( document.getElementById( 'ad-code-' + EDIT_ID ) ).before( result).remove(); - EDIT_ID = false; - } - else { - _showError( result ); - } - } - else { - _showError( inlineEditL10n.error ); - } - } ); - }; - - /** - * Shows the error for this ad code if it exists - * - * @param html - * @private - */ - var _showError = function( html ) { - var errorContainer = document.getElementById( 'ad-code-' + EDIT_ID ).querySelector( '.acm-editor-row .inline-edit-save .error' ); - errorContainer.innerHTML = html; - errorContainer.style.display = 'block'; - }; - - /** - * Custom serialization function based off of $.serializeArray() - slimmed down to exactly what we need - * - * @return {Array} - * @private - */ - var _getFormData = function() { - var data = []; - var fields = document.getElementById( 'ad-code-' + EDIT_ID ).querySelector( '.acm-editor-row fieldset' ); - var elements = fields.querySelectorAll( 'input, select, textarea' ), element, name; - - for( var i = 0, len = elements.length; i < len; i++ ) { - element = elements[ i ]; - name = element.name.replace( /^\s+|\s+$/i, '' ); - if( name === '' ) { - continue; - } - - data.push( { name : name, value : element.value } ); - } - - return data; - }; - - /** - * Removes the conditional row from the perspective of the button clicked - * - * @param target The `remove` button that was clicked. - * @private - */ - var _removeInlineConditionalRow = function( target ) { - var row = target.parentNode.parentNode; - var parent = row.parentNode; - parent.removeChild( row ); - }; - - /** - * Add a new inline editor conditional row for the current ad-code - * - * @private - */ - var _addInlineConditionalRow = function( parent ) { - // create a new element - var newConditional = document.createElement( 'div' ); - newConditional.className = 'conditional-single-field'; - newConditional.innerHTML = document.getElementById( 'conditional-single-field-master' ).innerHTML; - newConditional.querySelector( '.conditional-arguments' ).innerHTML += 'Remove'; - - parent.appendChild( newConditional ); - }; - - /** - * Toggles the loader for the form. This should only be used when the save button is clicked and we have a current - * EDIT_ID available - * - * @param showing Indicates whether the loader should be showing or not - * @private - */ - var _toggleLoader = function( showing ) { - var loader = document.querySelector( '#ad-code-' + EDIT_ID + ' .acm-editor-row .inline-edit-save .waiting' ); - loader.style.display = ( showing === true ) ? 'block' : 'none'; - }; - - /** - * Lightweight utility function that handles checking an element to see if it contains a class - * - * @param element The element we're checking against - * @param className The class name we're looking for - * @return {Boolean} - * @private - */ - var _hasClass = function( element, className ) { - return ( ' ' + element.className + ' ' ).indexOf( ' ' + className + ' ' ) > -1; - }; - - /** - * Handles toggling the inline editor. We assume EDIT_ID is being handled correctly for this to work. - * - * @param visible Indicates whether we are hiding/showing the inline editor. - * @private - */ - var _toggleInlineEdit = function( visible ) { - var row = document.getElementById( 'ad-code-' + EDIT_ID ); - - if( visible === true ) { - _toggleTableChildrenDisplay( row, false ); - _createNewInlineRow( EDIT_ID, row ); - } - else { - _toggleTableChildrenDisplay( row, true ); - _removeEditInlineRow( row ); - } - }; - - /** - * Toggles all the table children of the parent. - * - * @param parent The parent table row we're toggling td's for - * @param display Indicates whether or not the row children should be shown or not - * @private - */ - var _toggleTableChildrenDisplay = function( parent, display ) { - display = ( display === true ) ? 'table-cell' : 'none'; - var children = parent.children; - for( var i = 0, len = children.length; i < len; i++ ) { - children[ i ].style.display = display; - } - }; - - /** - * Handles creating the new inline editor row with all of the necessary UI controls. Notice that we do not rebind - * events because they are already handled by the delegation technique in `_delegateListClicks` - * - * @param id The ID of the ad-code you are editing - * @param parentToBe The DOM element that the newRow will be inserted into - * @private - */ - var _createNewInlineRow = function( id, parentToBe ) { - var newRow = document.createElement( 'td' ); - newRow.setAttribute( 'colspan', ( parentToBe.children.length - 1 ) ); - newRow.className = 'acm-editor-row'; - newRow.innerHTML = document.getElementById( 'inline-edit' ).innerHTML; - - // fill in the rows with existing HTML here - var data = _getDataFromRow( id ); - newRow.querySelector( 'input[name="id"]' ).value = id; - newRow.querySelector( '.acm-conditional-fields' ).innerHTML = data.conditionalFields; - newRow.querySelector( '.acm-column-fields' ).innerHTML = data.columnFields; - newRow.querySelector( '.acm-priority-field' ).innerHTML = data.priority; - newRow.querySelector( '.acm-operator-field' ).innerHTML = data.operator; - - parentToBe.appendChild( newRow ); - }; - - /** - * Removes the editor inline row if it exists - * - * @param parent The parent DOM element where we are removing the oldRow from - * @private - */ - var _removeEditInlineRow = function( parent ) { - var oldRow = parent.querySelector( '.acm-editor-row' ); - parent.removeChild( oldRow ); - parent.querySelector( '.column-id' ).style.display = 'none'; - }; - - /** - * Builds an object literal containing HTML for the new Row based off of existing DOM elements and their HTML - * - * @param id The ID of the ad-code we're retrieving information from. - * @return {Object} - * @private - */ - var _getDataFromRow = function( id ) { - var dataParent = document.getElementById( 'inline_' + id ); - return { - conditionalFields : dataParent.querySelector( '.acm-conditional-fields' ).innerHTML, - columnFields : dataParent.querySelector( '.acm-column-fields' ).innerHTML, - priority : dataParent.querySelector( '.acm-priority-field' ).innerHTML, - operator : dataParent.querySelector( '.acm-operator-field' ).innerHTML - }; - }; - - // fire our initialization method - _init(); - }; - - window.AdCodeManager = new AdCodeManager(); - -} )( window, jQuery ); diff --git a/src/class-acm-wp-list-table.php b/src/class-acm-wp-list-table.php index 4f0b4cf..cb9d1ac 100644 --- a/src/class-acm-wp-list-table.php +++ b/src/class-acm-wp-list-table.php @@ -245,84 +245,6 @@ function column_cb( $item ) { return $output; } - /** - * Display hidden information we need for inline editing - */ - function column_id( $item ) { - global $ad_code_manager; - $output = ''; - return $output; - } /** * @@ -357,8 +279,18 @@ function column_conditionals( $item ) { */ function row_actions_output( $item ) { $output = ''; - // $row_actions['preview-ad-code'] = '' . __( 'Preview Ad Code', 'ad-code-manager' ) . ''; - $row_actions['edit'] = '' . __( 'Edit Ad Code', 'ad-code-manager' ) . ''; + + // Build edit URL for dedicated edit page. + $edit_url = add_query_arg( + array( + 'page' => 'ad-code-manager', + 'action' => 'edit', + 'id' => $item['post_id'], + ), + admin_url( 'options-general.php' ) + ); + + $row_actions['edit'] = '' . __( 'Edit', 'ad-code-manager' ) . ''; $args = array( 'action' => 'acm_admin_action', @@ -373,41 +305,5 @@ function row_actions_output( $item ) { return $output; } - /** - * Hidden form used for inline editing functionality - * - * @since 0.2 - */ - function inline_edit() { - ?> - - -
- render_edit_page(); + return; + } + require_once dirname( AD_CODE_MANAGER_FILE ) . '/views/ad-code-manager.tpl.php'; } + /** + * Render the dedicated edit page for an ad code. + * + * @since 0.10.0 + */ + function render_edit_page() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a view, nonce verified on form submission. + $ad_code_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; + $ad_code = $this->get_ad_code( $ad_code_id ); + + // Handle invalid ID gracefully. + if ( ! $ad_code ) { + wp_die( + esc_html__( 'Invalid ad code ID.', 'ad-code-manager' ), + esc_html__( 'Error', 'ad-code-manager' ), + array( 'back_link' => true ) + ); + } + + require_once dirname( AD_CODE_MANAGER_FILE ) . '/views/edit-ad-code.tpl.php'; + } + /** * Register a custom widget to display ad zones */ @@ -749,7 +778,6 @@ function register_scripts_and_styles() { } wp_enqueue_style( 'acm-style', plugins_url( '/', AD_CODE_MANAGER_FILE ) . '/acm.css', array(), AD_CODE_MANAGER_VERSION ); - wp_enqueue_script( 'acm', plugins_url( '/', AD_CODE_MANAGER_FILE ) . '/acm.js', array( 'jquery' ), AD_CODE_MANAGER_VERSION, true ); } /** diff --git a/views/ad-code-manager.tpl.php b/views/ad-code-manager.tpl.php index 4b73889..ef9022b 100644 --- a/views/ad-code-manager.tpl.php +++ b/views/ad-code-manager.tpl.php @@ -14,6 +14,9 @@ case 'ad-code-added': $message_text = __( 'Ad code created.', 'ad-code-manager' ); break; + case 'ad-code-updated': + $message_text = __( 'Ad code updated.', 'ad-code-manager' ); + break; case 'ad-code-deleted': $message_text = __( 'Ad code deleted.', 'ad-code-manager' ); break; @@ -173,7 +176,5 @@
-wp_list_table->inline_edit(); ?> -
diff --git a/views/edit-ad-code.tpl.php b/views/edit-ad-code.tpl.php new file mode 100644 index 0000000..0b4767d --- /dev/null +++ b/views/edit-ad-code.tpl.php @@ -0,0 +1,212 @@ + +
+

+ + + + +

+ +

' . esc_html( $message_text ) . '

'; + } + } + ?> + +
+ + + + + + + + current_provider->ad_code_args as $arg ) : + if ( ! $arg['editable'] ) { + continue; + } + $column_id = 'acm-column[' . $arg['key'] . ']'; + $current_value = isset( $ad_code['url_vars'][ $arg['key'] ] ) + ? $ad_code['url_vars'][ $arg['key'] ] + : ''; + ?> + + + + + + + + + + + + + + + + + + + + = 2; ?> + > + + + + + + + +
+
From f28ce3ac5fd84bdbc553cfa4756506df8484e468 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Wed, 31 Dec 2025 15:16:22 +0000 Subject: [PATCH 33/36] chore: add .npmrc and update package-lock --- .distignore | 1 + .npmrc | 1 + package-lock.json | 179 ++++++++++++++++++++++++++++++---------------- 3 files changed, 119 insertions(+), 62 deletions(-) create mode 100644 .npmrc diff --git a/.distignore b/.distignore index f049057..1eecb4c 100644 --- a/.distignore +++ b/.distignore @@ -11,6 +11,7 @@ .editorconfig .gitattributes .gitignore +.npmrc .phpcs.xml.dist .wp-env.json .wp-env.override.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d373ccb --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +omit=optional diff --git a/package-lock.json b/package-lock.json index 6599017..788c90a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ad-code-manager", - "version": "0.7.1", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ad-code-manager", - "version": "0.7.1", + "version": "0.8.0", "license": "GPL-2.0-or-later", "devDependencies": { "version-bump-prompt": "^6.1.0" @@ -17,6 +17,7 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", "integrity": "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==", "dev": true, + "license": "MIT", "dependencies": { "call-me-maybe": "^1.0.1", "cross-spawn": "^7.0.3", @@ -32,6 +33,7 @@ "resolved": "https://registry.npmjs.org/@jsdevtools/version-bump-prompt/-/version-bump-prompt-6.1.0.tgz", "integrity": "sha512-NJFLJRiD3LLFBgSxAb6B255xhWCGgdtzmh6UjHK2b7SRGX2DDKJH5O4BJ0GTStBu4NnaNgMbkr1TLW3pLOBkOQ==", "dev": true, + "license": "MIT", "dependencies": { "@jsdevtools/ez-spawn": "^3.0.4", "command-line-args": "^5.1.1", @@ -54,6 +56,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -67,6 +70,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -76,6 +80,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -89,6 +94,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -104,6 +110,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -113,6 +120,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -128,6 +136,7 @@ "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -137,6 +146,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -158,13 +168,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -180,13 +192,15 @@ "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" }, @@ -199,6 +213,7 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true, + "license": "ISC", "engines": { "node": ">= 10" } @@ -208,6 +223,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -219,13 +235,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/command-line-args": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "dev": true, + "license": "MIT", "dependencies": { "array-back": "^3.1.0", "find-replace": "^3.0.0", @@ -237,10 +255,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -255,6 +274,7 @@ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -264,6 +284,7 @@ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -273,6 +294,7 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -284,13 +306,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -300,6 +324,7 @@ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, + "license": "MIT", "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -310,26 +335,28 @@ } }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -339,6 +366,7 @@ "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -367,6 +395,7 @@ "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "dev": true, + "license": "MIT", "dependencies": { "array-back": "^3.0.1" }, @@ -379,6 +408,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -391,6 +421,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -411,6 +442,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -420,6 +452,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -428,10 +461,11 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -441,6 +475,7 @@ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.0", @@ -465,6 +500,7 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -474,6 +510,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -483,6 +520,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -505,6 +543,7 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -516,25 +555,29 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -546,34 +589,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -585,6 +618,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -593,13 +627,15 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -615,6 +651,7 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -624,6 +661,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -633,6 +671,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -642,6 +681,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -667,13 +707,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -683,10 +725,11 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -697,6 +740,7 @@ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -720,6 +764,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -729,6 +774,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" }, @@ -740,16 +786,15 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -762,6 +807,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -774,6 +820,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -782,13 +829,15 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -798,6 +847,7 @@ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } @@ -807,6 +857,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -821,6 +872,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -833,6 +885,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -844,13 +897,15 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, + "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" }, @@ -875,13 +930,15 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -891,6 +948,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -903,6 +961,7 @@ "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -912,6 +971,7 @@ "resolved": "https://registry.npmjs.org/version-bump-prompt/-/version-bump-prompt-6.1.0.tgz", "integrity": "sha512-GYC83GP8QOunWueKf2mbtZkdmisXhnBZPhIHWUmN/Yi4XXAQlIi9avM/IGWdI7KkJLfMENzGN1Xee+Zl3VJ5jg==", "dev": true, + "license": "MIT", "dependencies": { "@jsdevtools/version-bump-prompt": "6.1.0" }, @@ -927,6 +987,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -936,12 +997,6 @@ "engines": { "node": ">= 8" } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } } From 2029798038aa9016f7e57224d55de356513ff098 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 23:30:42 +0000 Subject: [PATCH 34/36] Version 0.8.0 changelog --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 342a108..97552b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.0] - 2025-12-21 + +This version requires WordPress 6.4 and PHP 7.4 as a minimum. + +### Added +- Dedicated edit page for ad codes, replacing inline edit functionality by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/196 +- Autocomplete for conditional arguments using Select2 (categories, tags, pages, posts) by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/194 +- Wrapper div with CSS classes for improved ad styling and targeting by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/188 +- wp-env configuration for local development by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/178 + +### Fixed +- Validate unique tag IDs for DFP Async provider by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/190 +- Prevent empty widget wrapper output when no ad codes found by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/186 +- Ensure row actions display in first data column by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/189 + +### Changed +- Update minimum WordPress version to 6.4 by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/181 + +### Documentation +- Add PHPDoc documentation for all hooks by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/193 +- Add contextual help for DFP and AdSense provider fields by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/191 + +### Maintenance +- Standardise GitHub Actions, add unit test workflow with Brain Monkey by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/184 +- Migrate integration tests from SVN to wp-env by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/182 +- Migrate dependabot reviewers to CODEOWNERS by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/185 +- Standardise test matrix and update readme by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/187 +- Add .npmrc and update package-lock by @GaryJones in https://github.com/Automattic/ad-code-manager/pull/198 +- Bump actions/checkout from 5 to 6 by @dependabot in https://github.com/Automattic/ad-code-manager/pull/183 +- Bump codecov/codecov-action from 3 to 5 by @dependabot in https://github.com/Automattic/ad-code-manager/pull/174 +- Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/Automattic/ad-code-manager/pull/173 + ## [0.7.1] - 2023-09-09 ### Changed @@ -166,6 +198,7 @@ Bug fix release. Initial release. +[0.8.0]: https://github.com/Automattic/ad-code-manager/compare/0.7.1...0.8.0 [0.7.1]: https://github.com/Automattic/ad-code-manager/compare/0.7.0...0.7.1 [0.7.0]: https://github.com/Automattic/ad-code-manager/compare/0.6.0...0.7.0 [0.6.0]: https://github.com/Automattic/ad-code-manager/compare/0.5...0.6.0 From f947364108a3b4051b6bb8768f22237904d2869f Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 23:38:59 +0000 Subject: [PATCH 35/36] Version 0.8.0 i18n --- languages/ad-code-manager.pot | 493 ++++++++++++++++++++++++++-------- src/class-acm-widget.php | 5 +- 2 files changed, 388 insertions(+), 110 deletions(-) diff --git a/languages/ad-code-manager.pot b/languages/ad-code-manager.pot index cce69bb..9ccf27a 100644 --- a/languages/ad-code-manager.pot +++ b/languages/ad-code-manager.pot @@ -1,213 +1,488 @@ -# Copyright (C) 2013 Ad Code Manager -# This file is distributed under the same license as the Ad Code Manager package. +# Copyright (C) 2025 Automattic and contributors +# This file is distributed under the GPL-2.0-or-later. msgid "" msgstr "" -"Project-Id-Version: Ad Code Manager 0.4\n" -"Report-Msgid-Bugs-To: http://wordpress.org/tag/ad-code-manager\n" -"POT-Creation-Date: 2013-04-16 05:02:49+00:00\n" +"Project-Id-Version: Ad Code Manager 0.8.0\n" +"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/ad-code-manager\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2013-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" +"POT-Creation-Date: 2025-12-21T23:37:35+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.12.0\n" +"X-Domain: ad-code-manager\n" + +#. Plugin Name of the plugin +#: ad-code-manager.php +#: src/class-ad-code-manager.php:694 +#: src/class-ad-code-manager.php:695 +msgid "Ad Code Manager" +msgstr "" -#: ad-code-manager.php:154 -msgid "Ad Codes" +#. Plugin URI of the plugin +#: ad-code-manager.php +msgid "https://wordpress.org/plugins/ad-code-manager/" msgstr "" -#: ad-code-manager.php:155 -msgid "Ad Code" +#. Description of the plugin +#: ad-code-manager.php +msgid "Easy ad code management." msgstr "" -#: ad-code-manager.php:276 -msgid "Doing something fishy, eh?" +#. Author of the plugin +#: ad-code-manager.php +msgid "Automattic and contributors" msgstr "" -#: ad-code-manager.php:279 -msgid "You do not have the necessary permissions to perform this action" +#. Author URI of the plugin +#: ad-code-manager.php +msgid "https://github.com/Automattic/ad-code-manager/graphs/contributors" msgstr "" -#: common/lib/acm-provider.php:27 common/lib/acm-wp-list-table.php:31 +#: src/class-acm-provider.php:29 +#: src/class-acm-wp-list-table.php:44 msgid "Name" msgstr "" -#: common/lib/acm-widget.php:14 +#: src/class-acm-widget.php:14 msgid "Display an Ad Code Manager ad zone within a widget area" msgstr "" -#: common/lib/acm-widget.php:16 +#: src/class-acm-widget.php:16 msgid "Ad Code Manager Ad Zone" msgstr "" -#: common/lib/acm-widget.php:46 +#. translators: %s is the URL to the Ad Code Manager settings page. +#: src/class-acm-widget.php:48 +#, php-format msgid "No ad codes have been added yet. Please create one." msgstr "" -#: common/lib/acm-wp-list-table.php:30 common/lib/acm-wp-list-table.php:39 -#: providers/doubleclick-for-publishers-async.php:189 -#: providers/doubleclick-for-publishers.php:101 -#: providers/google-adsense.php:168 +#: src/class-acm-wp-list-table.php:43 +#: src/class-acm-wp-list-table.php:52 +#: src/Providers/class-doubleclick-for-publishers-async.php:326 +#: src/Providers/class-doubleclick-for-publishers.php:104 +#: src/Providers/class-google-adsense-async.php:172 +#: src/Providers/class-google-adsense.php:171 msgid "ID" msgstr "" -#: common/lib/acm-wp-list-table.php:32 common/lib/acm-wp-list-table.php:43 -#: common/lib/acm-wp-list-table.php:243 -#: providers/doubleclick-for-publishers-async.php:194 -#: providers/doubleclick-for-publishers.php:104 -#: providers/google-adsense.php:172 +#: src/class-acm-wp-list-table.php:45 +#: src/class-acm-wp-list-table.php:56 +#: src/Providers/class-doubleclick-for-publishers-async.php:331 +#: src/Providers/class-doubleclick-for-publishers.php:107 +#: src/Providers/class-google-adsense-async.php:176 +#: src/Providers/class-google-adsense.php:175 +#: views/edit-ad-code.tpl.php:108 msgid "Priority" msgstr "" -#: common/lib/acm-wp-list-table.php:33 common/lib/acm-wp-list-table.php:44 -#: common/lib/acm-wp-list-table.php:248 -#: providers/doubleclick-for-publishers-async.php:195 -#: providers/doubleclick-for-publishers.php:105 -#: providers/google-adsense.php:173 +#: src/class-acm-wp-list-table.php:46 +#: src/class-acm-wp-list-table.php:57 +#: src/Providers/class-doubleclick-for-publishers-async.php:332 +#: src/Providers/class-doubleclick-for-publishers.php:108 +#: src/Providers/class-google-adsense-async.php:177 +#: src/Providers/class-google-adsense.php:176 +#: views/edit-ad-code.tpl.php:191 msgid "Logical Operator" msgstr "" -#: common/lib/acm-wp-list-table.php:34 common/lib/acm-wp-list-table.php:45 -#: common/lib/acm-wp-list-table.php:198 -#: common/views/ad-code-manager.tpl.php:125 -#: providers/doubleclick-for-publishers-async.php:196 -#: providers/doubleclick-for-publishers.php:106 -#: providers/google-adsense.php:174 +#: src/class-acm-wp-list-table.php:47 +#: src/class-acm-wp-list-table.php:58 +#: src/Providers/class-doubleclick-for-publishers-async.php:333 +#: src/Providers/class-doubleclick-for-publishers.php:109 +#: src/Providers/class-google-adsense-async.php:178 +#: src/Providers/class-google-adsense.php:177 +#: src/UI/class-contextual-help.php:171 +#: views/ad-code-manager.tpl.php:151 +#: views/edit-ad-code.tpl.php:124 msgid "Conditionals" msgstr "" -#: common/lib/acm-wp-list-table.php:60 common/lib/acm-wp-list-table.php:307 +#: src/class-acm-wp-list-table.php:73 +#: src/class-acm-wp-list-table.php:302 msgid "Delete" msgstr "" -#: common/lib/acm-wp-list-table.php:128 +#: src/class-acm-wp-list-table.php:152 msgid "No ad codes have been configured." msgstr "" -#: common/lib/acm-wp-list-table.php:205 -#: common/views/ad-code-manager.tpl.php:129 -msgid "Select conditional" +#: src/class-acm-wp-list-table.php:265 +msgid "None" msgstr "" -#: common/lib/acm-wp-list-table.php:217 -msgid "Add more" +#: src/class-acm-wp-list-table.php:293 +msgid "Edit" +msgstr "" + +#: src/class-ad-code-manager.php:258 +msgctxt "Post Type General Name" +msgid "Ad Codes" msgstr "" -#: common/lib/acm-wp-list-table.php:221 -msgid "URL Variables" +#: src/class-ad-code-manager.php:259 +msgctxt "Post Type Singular Name" +msgid "Ad Code" msgstr "" -#: common/lib/acm-wp-list-table.php:251 -msgid "OR" +#: src/class-ad-code-manager.php:326 +msgid "Doing something fishy, eh?" msgstr "" -#: common/lib/acm-wp-list-table.php:252 -msgid "AND" +#: src/class-ad-code-manager.php:330 +msgid "You do not have the necessary permissions to perform this action" msgstr "" -#: common/lib/acm-wp-list-table.php:280 -msgid "None" +#: src/class-ad-code-manager.php:753 +msgid "Invalid ad code ID." msgstr "" -#: common/lib/acm-wp-list-table.php:298 -msgid "Edit Ad Code" +#: src/class-ad-code-manager.php:754 +msgid "Error" +msgstr "" + +#: src/Providers/class-doubleclick-for-publishers-async.php:73 +#: src/Providers/class-doubleclick-for-publishers-async.php:327 +#: src/Providers/class-google-adsense-async.php:96 +#: src/Providers/class-google-adsense-async.php:173 +#: src/Providers/class-google-adsense.php:95 +#: src/Providers/class-google-adsense.php:172 +#: src/UI/class-contextual-help.php:117 +msgid "Tag" +msgstr "" + +#: src/Providers/class-doubleclick-for-publishers-async.php:83 +#: src/Providers/class-doubleclick-for-publishers-async.php:328 +#: src/Providers/class-google-adsense-async.php:106 +#: src/Providers/class-google-adsense-async.php:174 +#: src/Providers/class-google-adsense.php:105 +#: src/Providers/class-google-adsense.php:173 +#: src/UI/class-contextual-help.php:120 +msgid "Tag ID" +msgstr "" + +#: src/Providers/class-doubleclick-for-publishers-async.php:89 +#: src/Providers/class-doubleclick-for-publishers-async.php:329 +#: src/UI/class-contextual-help.php:123 +msgid "DFP ID" +msgstr "" + +#: src/Providers/class-doubleclick-for-publishers-async.php:95 +#: src/Providers/class-doubleclick-for-publishers-async.php:330 +#: src/UI/class-contextual-help.php:132 +msgid "Tag Name" +msgstr "" + +#. translators: %s: the duplicate tag ID +#: src/Providers/class-doubleclick-for-publishers-async.php:279 +#, php-format +msgid "The Tag ID \"%s\" is already in use. Each Tag ID must be unique to prevent conflicts with DFP Async." +msgstr "" + +#: src/Providers/class-doubleclick-for-publishers.php:69 +#: src/Providers/class-doubleclick-for-publishers.php:105 +msgid "Site Name" +msgstr "" + +#: src/Providers/class-doubleclick-for-publishers.php:75 +msgid "zone1" +msgstr "" + +#: src/Providers/class-doubleclick-for-publishers.php:106 +msgid "Zone1" +msgstr "" + +#: src/Providers/class-google-adsense-async.php:112 +#: src/Providers/class-google-adsense-async.php:175 +#: src/Providers/class-google-adsense.php:111 +#: src/Providers/class-google-adsense.php:174 +#: src/UI/class-contextual-help.php:142 +msgid "Publisher ID" +msgstr "" + +#: src/UI/class-conditional-autocomplete.php:107 +msgid "Searching..." +msgstr "" + +#: src/UI/class-conditional-autocomplete.php:108 +msgid "No results found" +msgstr "" + +#. translators: %d: minimum number of characters +#: src/UI/class-conditional-autocomplete.php:111 +#, php-format +msgid "Please enter %d or more characters" +msgstr "" + +#: src/UI/class-conditional-autocomplete.php:114 +msgid "Error loading results" +msgstr "" + +#: src/UI/class-conditional-autocomplete.php:182 +msgid "Permission denied." +msgstr "" + +#: src/UI/class-conditional-autocomplete.php:192 +msgid "Search term too short." +msgstr "" + +#: src/UI/class-contextual-help.php:46 +msgid "Ad Code Manager gives non-developers an interface in the WordPress admin for configuring your complex set of ad codes. Generally an \"Ad Code\" is a set of parameters you need to pass to an ad server, so it can serve the proper ad." +msgstr "" + +#: src/UI/class-contextual-help.php:47 +msgid "Some code-level configuration may be necessary to display the ads. See the GitHub repository for developer information." +msgstr "" + +#: src/UI/class-contextual-help.php:53 +msgid "Choose your ad network, and you will see a set of required fields to fill in, such as IDs. You can also set conditionals for each ad tag, which restricts the contexts for displaying the adverts. Priorities work pretty much the same way they work in WordPress. Lower numbers correspond with higher priority." +msgstr "" + +#: src/UI/class-contextual-help.php:54 +msgid "Once you've finished creating the ad codes, you can display them in your theme using:" +msgstr "" + +#: src/UI/class-contextual-help.php:56 +msgid "a template tag in your theme: <?php do_action( 'acm_tag', $tag_id ); ?>" +msgstr "" + +#: src/UI/class-contextual-help.php:57 +msgid "a shortcode: [acm-tag id=\"tag_id\"]" +msgstr "" + +#: src/UI/class-contextual-help.php:58 +msgid "or using a widget." +msgstr "" + +#: src/UI/class-contextual-help.php:65 +msgid "In the fields below, you can choose which conditionals you want. Some can take a value (i.e. define a specific category) in the second field." +msgstr "" + +#: src/UI/class-contextual-help.php:66 +msgid "Here's an overview of the conditionals; they work the same as the functions of the same name in WordPress." +msgstr "" + +#: src/UI/class-contextual-help.php:69 +msgid "When the main blog page is being displayed. This is the page which shows the time-based blog content of your site, so if you've set a static Page for the Front Page (see below), then this will only be true on the Page which you set as the \"Posts page\" in Settings > Reading." +msgstr "" + +#: src/UI/class-contextual-help.php:72 +msgid "When the front of the site is displayed, whether it is posts or a Page. Returns true when the main blog page is being displayed and the Settings > Reading > Front page displays is set to \"Your latest posts\", or when Settings > Reading > Front page displays is set to \"A static page\" and the \"Front Page\" value is the current page displayed." msgstr "" -#: common/lib/acm-wp-list-table.php:337 -msgid "Cancel" +#: src/UI/class-contextual-help.php:75 +msgid "When any Category archive page is being displayed." msgstr "" -#: common/lib/acm-wp-list-table.php:339 -msgid "Update" +#: src/UI/class-contextual-help.php:78 +msgid "When the archive page for Category 9 is being displayed." msgstr "" -#: common/views/ad-code-manager.tpl.php:11 +#: src/UI/class-contextual-help.php:80 +msgid "Stinky Cheeses" +msgstr "" + +#: src/UI/class-contextual-help.php:81 +msgid "When the archive page for the Category with Name \"Stinky Cheeses\" is being displayed." +msgstr "" + +#: src/UI/class-contextual-help.php:83 +msgid "blue-cheese" +msgstr "" + +#: src/UI/class-contextual-help.php:84 +msgid "When the archive page for the Category with Category Slug \"blue-cheese\" is being displayed." +msgstr "" + +#: src/UI/class-contextual-help.php:86 +msgid "array( 9, 'blue-cheese', 'Stinky Cheeses' )" +msgstr "" + +#: src/UI/class-contextual-help.php:87 +msgid "Returns true when the category of posts being displayed is either term_ID 9, or slug \"blue-cheese\", or name \"Stinky Cheeses\"." +msgstr "" + +#: src/UI/class-contextual-help.php:90 +msgid "Returns true if the current post is in the specified category id." +msgstr "" + +#: src/UI/class-contextual-help.php:93 +msgid "When any Tag archive page is being displayed." +msgstr "" + +#: src/UI/class-contextual-help.php:96 +msgid "When the archive page for tag with the slug of \"mild\" is being displayed." +msgstr "" + +#: src/UI/class-contextual-help.php:99 +msgid "Returns true when the tag archive being displayed has a slug of either \"sharp\", \"mild\", or \"extreme\"." +msgstr "" + +#: src/UI/class-contextual-help.php:102 +msgid "When the current post has a tag. Must be used inside The Loop." +msgstr "" + +#: src/UI/class-contextual-help.php:105 +msgid "When the current post has the tag \"mild\"." +msgstr "" + +#: src/UI/class-contextual-help.php:108 +msgid "When the current post has any of the tags in the array." +msgstr "" + +#: src/UI/class-contextual-help.php:115 +msgid "When using the DFP Async (DoubleClick for Publishers / Google Ad Manager) provider, you need to configure the following fields:" +msgstr "" + +#: src/UI/class-contextual-help.php:118 +msgid "The ad size/placement type (e.g., 728x90, 300x250). This determines which ad tag definition is used for this ad code." +msgstr "" + +#: src/UI/class-contextual-help.php:121 +msgid "A unique identifier for this specific ad unit placement. This is used as the HTML div ID where the ad will be rendered. Each Tag ID must be unique across all ad codes to prevent conflicts. Example: \"homepage-leaderboard\" or \"sidebar-mpu-1\"." +msgstr "" + +#: src/UI/class-contextual-help.php:125 +msgid "Your Google Ad Manager (formerly DFP) Network Code. This is a numeric identifier for your Ad Manager account." +msgstr "" + +#: src/UI/class-contextual-help.php:127 +msgid "Find it in Google Ad Manager: Admin > Global Settings > Network Code." +msgstr "" + +#: src/UI/class-contextual-help.php:129 +msgid "Example: 12345678" +msgstr "" + +#: src/UI/class-contextual-help.php:134 +msgid "The ad unit name/path as configured in Google Ad Manager. This is the hierarchical path to your ad unit." +msgstr "" + +#: src/UI/class-contextual-help.php:136 +msgid "Example: \"MySite/Homepage/Leaderboard\" or just \"Leaderboard\" for top-level units." +msgstr "" + +#: src/UI/class-contextual-help.php:139 +msgid "Google AdSense Provider" +msgstr "" + +#: src/UI/class-contextual-help.php:140 +msgid "When using the Google AdSense provider:" +msgstr "" + +#: src/UI/class-contextual-help.php:144 +msgid "Your AdSense Publisher ID. This starts with \"ca-pub-\" followed by a 16-digit number." +msgstr "" + +#: src/UI/class-contextual-help.php:146 +msgid "Example: ca-pub-1234567890123456" +msgstr "" + +#: src/UI/class-contextual-help.php:148 +msgid "Find it in your AdSense account: Account > Account Information > Publisher ID." +msgstr "" + +#: src/UI/class-contextual-help.php:157 +msgid "Overview" +msgstr "" + +#: src/UI/class-contextual-help.php:164 +#: views/ad-code-manager.tpl.php:52 +msgid "Configuration" +msgstr "" + +#: src/UI/class-contextual-help.php:178 +msgid "Provider Fields" +msgstr "" + +#: src/UI/class-plugin-actions.php:43 +msgid "Settings" +msgstr "" + +#: views/ad-code-manager.tpl.php:15 msgid "Ad code created." msgstr "" -#: common/views/ad-code-manager.tpl.php:14 +#: views/ad-code-manager.tpl.php:18 +#: views/edit-ad-code.tpl.php:33 +msgid "Ad code updated." +msgstr "" + +#: views/ad-code-manager.tpl.php:21 msgid "Ad code deleted." msgstr "" -#: common/views/ad-code-manager.tpl.php:17 +#: views/ad-code-manager.tpl.php:24 msgid "Ad codes deleted." msgstr "" -#: common/views/ad-code-manager.tpl.php:20 +#: views/ad-code-manager.tpl.php:27 msgid "Options saved." msgstr "" -#: common/views/ad-code-manager.tpl.php:57 -msgid "Configuration" +#: views/ad-code-manager.tpl.php:46 +msgid "Refer to help section for more information." msgstr "" -#: common/views/ad-code-manager.tpl.php:61 -msgid "Select a provider:" +#: views/ad-code-manager.tpl.php:55 +msgid "Provider:" msgstr "" -#: common/views/ad-code-manager.tpl.php:78 +#: views/ad-code-manager.tpl.php:79 msgid "Save Options" msgstr "" -#: common/views/ad-code-manager.tpl.php:83 -#: common/views/ad-code-manager.tpl.php:147 -msgid "Add New Ad Code" -msgstr "" - -#: providers/doubleclick-for-publishers-async.php:40 -#: providers/doubleclick-for-publishers-async.php:190 -#: providers/google-adsense.php:94 providers/google-adsense.php:169 -msgid "Tag" +#: views/ad-code-manager.tpl.php:91 +msgid "Existing Ad Codes" msgstr "" -#: providers/doubleclick-for-publishers-async.php:50 -#: providers/doubleclick-for-publishers-async.php:191 -#: providers/google-adsense.php:104 providers/google-adsense.php:170 -msgid "Tag ID" +#: views/ad-code-manager.tpl.php:108 +#: views/ad-code-manager.tpl.php:173 +msgid "Add New Ad Code" msgstr "" -#: providers/doubleclick-for-publishers-async.php:56 -#: providers/doubleclick-for-publishers-async.php:192 -msgid "DFP ID" +#: views/ad-code-manager.tpl.php:155 +#: views/edit-ad-code.tpl.php:134 +#: views/edit-ad-code.tpl.php:163 +msgid "Select conditional" msgstr "" -#: providers/doubleclick-for-publishers-async.php:62 -#: providers/doubleclick-for-publishers-async.php:193 -msgid "Tag Name" +#: views/ad-code-manager.tpl.php:169 +#: views/edit-ad-code.tpl.php:180 +msgid "Add another condition" msgstr "" -#: providers/doubleclick-for-publishers.php:67 -#: providers/doubleclick-for-publishers.php:102 -msgid "Site Name" +#: views/edit-ad-code.tpl.php:18 +msgid "Edit Ad Code" msgstr "" -#: providers/doubleclick-for-publishers.php:73 -msgid "zone1" +#: views/edit-ad-code.tpl.php:20 +msgid "Back to Ad Codes" msgstr "" -#: providers/doubleclick-for-publishers.php:103 -msgid "Zone1" +#: views/edit-ad-code.tpl.php:117 +msgid "Lower numbers have higher priority." msgstr "" -#: providers/google-adsense.php:110 providers/google-adsense.php:171 -msgid "Publisher ID" +#: views/edit-ad-code.tpl.php:151 +msgid "Remove" msgstr "" -#. Plugin Name of the plugin/theme -msgid "Ad Code Manager" +#: views/edit-ad-code.tpl.php:196 +msgid "OR (any condition)" msgstr "" -#. #-#-#-#-# plugin.pot (Ad Code Manager 0.4) #-#-#-#-# -#. Plugin URI of the plugin/theme -#. #-#-#-#-# plugin.pot (Ad Code Manager 0.4) #-#-#-#-# -#. Author URI of the plugin/theme -msgid "http://automattic.com" +#: views/edit-ad-code.tpl.php:199 +msgid "AND (all conditions)" msgstr "" -#. Description of the plugin/theme -msgid "Easy ad code management" +#: views/edit-ad-code.tpl.php:203 +msgid "How multiple conditionals are evaluated." msgstr "" -#. Author of the plugin/theme -msgid "Rinat Khaziev, Jeremy Felt, Daniel Bachhuber, Automattic, doejo" +#: views/edit-ad-code.tpl.php:210 +msgid "Update Ad Code" msgstr "" diff --git a/src/class-acm-widget.php b/src/class-acm-widget.php index ec5724e..c31e993 100644 --- a/src/class-acm-widget.php +++ b/src/class-acm-widget.php @@ -43,7 +43,10 @@ function form( $instance ) { plugin_slug, admin_url( 'options-general.php' ) ); ?> - Please create one.", 'ad-code-manager' ), esc_url( $create_url ) ); ?> + Please create one.", 'ad-code-manager' ), esc_url( $create_url ) ); + ?>

From fcef2b0215311183e3d826b5ced3e54764dd262b Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 21 Dec 2025 23:40:08 +0000 Subject: [PATCH 36/36] Version 0.8.0 --- README.md | 4 ++-- ad-code-manager.php | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 54b5d06..5ca169c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # Ad Code Manager -Stable tag: 0.7.1 +Stable tag: 0.8.0 Requires at least: 6.4 Tested up to: 6.9 Requires PHP: 7.4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Tags: advertising, ad codes, ads, adsense, dfp, doubleclick for publishers -Contributors: rinatkhaziev, jeremyfelt, danielbachhuber, carldanley, zztimur, automattic, doejo +Contributors: rinatkhaziev, jeremyfelt, danielbachhuber, carldanley, zztimur, automattic, doejo, garyj Manage your ad codes through the WordPress admin safely and easily. diff --git a/ad-code-manager.php b/ad-code-manager.php index 7d2c051..2dc00a3 100644 --- a/ad-code-manager.php +++ b/ad-code-manager.php @@ -11,7 +11,7 @@ * Plugin Name: Ad Code Manager * Plugin URI: https://wordpress.org/plugins/ad-code-manager/ * Description: Easy ad code management. - * Version: 0.7.1 + * Version: 0.8.0 * Author: Automattic and contributors * Author URI: https://github.com/Automattic/ad-code-manager/graphs/contributors * Text Domain: ad-code-manager @@ -31,7 +31,7 @@ use Automattic\AdCodeManager\UI\Contextual_Help; use Automattic\AdCodeManager\UI\Plugin_Actions; -const AD_CODE_MANAGER_VERSION = '0.7.1'; +const AD_CODE_MANAGER_VERSION = '0.8.0'; const AD_CODE_MANAGER_FILE = __FILE__; require_once __DIR__ . '/src/class-acm-provider.php'; diff --git a/package.json b/package.json index 2bad730..6b591c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ad-code-manager", - "version": "0.7.1", + "version": "0.8.0", "description": "Easy ad code management.", "license": "GPL-2.0-or-later", "author": "Automattic",