diff --git a/.distignore b/.distignore new file mode 100644 index 0000000..6fb1763 --- /dev/null +++ b/.distignore @@ -0,0 +1,14 @@ +/docker +/tests +/node_modules +/vendor +/package.json +/package-lock.json +/phpunit.xml.dist +/playwright.config.ts +/.phpunit.result.cache +/test-results +/playwright-report +/build +/.git +/.github diff --git a/.github/workflows/deploy-wordpress-org.yml b/.github/workflows/deploy-wordpress-org.yml new file mode 100644 index 0000000..a71c020 --- /dev/null +++ b/.github/workflows/deploy-wordpress-org.yml @@ -0,0 +1,146 @@ +name: Release pipeline + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: wordpress-org-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve release mode + run: | + set -eu + release_version="$(awk -F 'Version:[[:space:]]*' '/^[[:space:]]*\*[[:space:]]Version:/ { print $2; exit }' progress-agentic-rag.php | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" + if ! printf '%s\n' "${release_version}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "Plugin Version must use numeric x.y.z format: ${release_version}" + exit 1 + fi + if [ "${GITHUB_EVENT_NAME}" = "push" ] && [ "${GITHUB_REF}" = "refs/heads/main" ]; then + echo "Running WordPress.org SVN deployment for ${release_version}." + else + echo "Running WordPress.org deployment dry run for ${release_version}; SVN deploy will be skipped." + fi + echo "RELEASE_VERSION=${release_version}" >> "${GITHUB_ENV}" + echo "VERSION=${release_version}" >> "${GITHUB_ENV}" + + - name: Show Docker versions + run: | + docker --version + docker compose version + + - name: PHP lint + run: docker compose -f docker/docker-compose.yml run --rm php-lint + + - name: PHPUnit + run: docker compose -f docker/docker-compose.yml run --rm phpunit + + - name: Prepare WordPress for Playwright + run: | + set -eux + docker network create progress-agentic-rag-ci + docker volume create progress-agentic-rag-ci-wp + docker run -d \ + --name progress-agentic-rag-ci-db \ + --network progress-agentic-rag-ci \ + -e MYSQL_ROOT_PASSWORD=root \ + -e MYSQL_DATABASE=wordpress \ + -e MYSQL_USER=wordpress \ + -e MYSQL_PASSWORD=wordpress \ + mysql:8.0 + until docker exec progress-agentic-rag-ci-db mysqladmin ping -h 127.0.0.1 -uroot -proot --silent; do sleep 2; done + docker run -d \ + --name progress-agentic-rag-ci-wordpress \ + --network progress-agentic-rag-ci \ + -p 8080:80 \ + -e WORDPRESS_DB_HOST=progress-agentic-rag-ci-db \ + -e WORDPRESS_DB_USER=wordpress \ + -e WORDPRESS_DB_PASSWORD=wordpress \ + -e WORDPRESS_DB_NAME=wordpress \ + -v progress-agentic-rag-ci-wp:/var/www/html \ + -v "$PWD":/var/www/html/wp-content/plugins/progress-agentic-rag:ro \ + wordpress:latest + until curl -fsS http://localhost:8080/wp-admin/install.php >/dev/null; do sleep 2; done + docker run --rm \ + --network progress-agentic-rag-ci \ + --user 0 \ + -e WORDPRESS_DB_HOST=progress-agentic-rag-ci-db \ + -e WORDPRESS_DB_USER=wordpress \ + -e WORDPRESS_DB_PASSWORD=wordpress \ + -e WORDPRESS_DB_NAME=wordpress \ + -v progress-agentic-rag-ci-wp:/var/www/html \ + -v "$PWD":/var/www/html/wp-content/plugins/progress-agentic-rag:ro \ + wordpress:cli \ + sh -c 'set -e + wp core install --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress --title="Progress Agentic RAG CI" --admin_user=admin --admin_password=admin123 --admin_email=admin@example.com --skip-email + mkdir -p /var/www/html/wp-content/mu-plugins + cp /var/www/html/wp-content/plugins/progress-agentic-rag/tests/playwright/fixtures/mu-plugins/progress-agentic-rag-e2e.php /var/www/html/wp-content/mu-plugins/progress-agentic-rag-e2e.php + wp plugin activate progress-agentic-rag/progress-agentic-rag.php --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update progress_agentic_rag_e2e_key progress-agentic-rag-e2e --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_zone europe-1 --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_kbid ci-kb --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_token progress-agentic-rag-secret --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_api_is_reachable yes --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress' + + - name: Playwright + run: | + docker run --rm \ + --network progress-agentic-rag-ci \ + -e WP_BASE_URL=http://progress-agentic-rag-ci-wordpress \ + -e PROGRESS_AGENTIC_RAG_E2E_KEY=progress-agentic-rag-e2e \ + -e PROGRESS_AGENTIC_RAG_TEST_TOKEN=progress-agentic-rag-secret \ + -v "$PWD":/plugin \ + -w /plugin \ + mcr.microsoft.com/playwright:v1.61.0-noble \ + sh -c 'npm ci && sh docker/scripts/run-playwright.sh' + + - name: Prepare release artifact + run: | + rm -rf build/progress-agentic-rag + mkdir -p build/progress-agentic-rag + rsync -a ./ build/progress-agentic-rag/ \ + --exclude '.git' \ + --exclude '.github' \ + --exclude '.distignore' \ + --exclude '.gitignore' \ + --exclude 'build' \ + --exclude 'docker' \ + --exclude 'tests' \ + --exclude 'node_modules' \ + --exclude 'vendor' \ + --exclude 'package.json' \ + --exclude 'package-lock.json' \ + --exclude 'phpunit.xml.dist' \ + --exclude 'playwright.config.ts' + + - name: Release artifact check + run: RELEASE_VERSION="${RELEASE_VERSION}" RELEASE_DIR=build/progress-agentic-rag docker compose -f docker/docker-compose.yml run --rm release-check + + - name: Deployment dry run complete + if: ${{ github.event_name != 'push' || github.ref != 'refs/heads/main' }} + run: echo "Dry run complete; WordPress.org SVN deploy was skipped." + + - name: Deploy to WordPress.org SVN + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + uses: 10up/action-wordpress-plugin-deploy@2.3.0 + env: + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SLUG: progress-agentic-rag + BUILD_DIR: build/progress-agentic-rag diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6388273 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,109 @@ +name: Tests + +on: + pull_request: + workflow_dispatch: +permissions: + contents: read + +jobs: + test: + name: Lint and Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Show Docker versions + run: | + docker --version + docker compose version + + - name: PHP lint + run: docker compose -f docker/docker-compose.yml run --rm php-lint + + - name: PHPUnit + run: docker compose -f docker/docker-compose.yml run --rm phpunit + + - name: Prepare WordPress for Playwright + run: | + set -eux + docker network create progress-agentic-rag-ci + docker volume create progress-agentic-rag-ci-wp + docker run -d \ + --name progress-agentic-rag-ci-db \ + --network progress-agentic-rag-ci \ + -e MYSQL_ROOT_PASSWORD=root \ + -e MYSQL_DATABASE=wordpress \ + -e MYSQL_USER=wordpress \ + -e MYSQL_PASSWORD=wordpress \ + mysql:8.0 + until docker exec progress-agentic-rag-ci-db mysqladmin ping -h 127.0.0.1 -uroot -proot --silent; do sleep 2; done + docker run -d \ + --name progress-agentic-rag-ci-wordpress \ + --network progress-agentic-rag-ci \ + -p 8080:80 \ + -e WORDPRESS_DB_HOST=progress-agentic-rag-ci-db \ + -e WORDPRESS_DB_USER=wordpress \ + -e WORDPRESS_DB_PASSWORD=wordpress \ + -e WORDPRESS_DB_NAME=wordpress \ + -v progress-agentic-rag-ci-wp:/var/www/html \ + -v "$PWD":/var/www/html/wp-content/plugins/progress-agentic-rag:ro \ + wordpress:latest + until curl -fsS http://localhost:8080/wp-admin/install.php >/dev/null; do sleep 2; done + docker run --rm \ + --network progress-agentic-rag-ci \ + --user 0 \ + -e WORDPRESS_DB_HOST=progress-agentic-rag-ci-db \ + -e WORDPRESS_DB_USER=wordpress \ + -e WORDPRESS_DB_PASSWORD=wordpress \ + -e WORDPRESS_DB_NAME=wordpress \ + -v progress-agentic-rag-ci-wp:/var/www/html \ + -v "$PWD":/var/www/html/wp-content/plugins/progress-agentic-rag:ro \ + wordpress:cli \ + sh -c 'set -e + wp core install --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress --title="Progress Agentic RAG CI" --admin_user=admin --admin_password=admin123 --admin_email=admin@example.com --skip-email + mkdir -p /var/www/html/wp-content/mu-plugins + cp /var/www/html/wp-content/plugins/progress-agentic-rag/tests/playwright/fixtures/mu-plugins/progress-agentic-rag-e2e.php /var/www/html/wp-content/mu-plugins/progress-agentic-rag-e2e.php + wp plugin activate progress-agentic-rag/progress-agentic-rag.php --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update progress_agentic_rag_e2e_key progress-agentic-rag-e2e --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_zone europe-1 --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_kbid ci-kb --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_token progress-agentic-rag-secret --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress + wp option update nuclia_api_is_reachable yes --allow-root --path=/var/www/html --url=http://progress-agentic-rag-ci-wordpress' + + - name: Playwright + run: | + docker run --rm \ + --network progress-agentic-rag-ci \ + -e WP_BASE_URL=http://progress-agentic-rag-ci-wordpress \ + -e PROGRESS_AGENTIC_RAG_E2E_KEY=progress-agentic-rag-e2e \ + -e PROGRESS_AGENTIC_RAG_TEST_TOKEN=progress-agentic-rag-secret \ + -v "$PWD":/plugin \ + -w /plugin \ + mcr.microsoft.com/playwright:v1.61.0-noble \ + sh -c 'npm ci && sh docker/scripts/run-playwright.sh' + + - name: Prepare release check fixture + run: | + rm -rf build/progress-agentic-rag + mkdir -p build/progress-agentic-rag + rsync -a ./ build/progress-agentic-rag/ \ + --exclude '.git' \ + --exclude '.github' \ + --exclude '.distignore' \ + --exclude '.gitignore' \ + --exclude 'build' \ + --exclude 'docker' \ + --exclude 'tests' \ + --exclude 'node_modules' \ + --exclude 'vendor' \ + --exclude 'package.json' \ + --exclude 'package-lock.json' \ + --exclude 'phpunit.xml.dist' \ + --exclude 'playwright.config.ts' + + - name: Release artifact check + run: RELEASE_DIR=build/progress-agentic-rag docker compose -f docker/docker-compose.yml run --rm release-check diff --git a/.gitignore b/.gitignore index 58b805f..b4c14d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,171 @@ +# ------------------------------------------------------------ +# Dependencies +# ------------------------------------------------------------ + +/vendor/ +/node_modules/ + +composer.phar + + +# ------------------------------------------------------------ +# PHPUnit +# ------------------------------------------------------------ + +.phpunit.result.cache +.phpunit.cache/ +.phpunit/ + +coverage/ +coverage-php/ +clover.xml +junit.xml + +/test-results/phpunit/ +/tests/_output/ + + +# ------------------------------------------------------------ +# WordPress PHPUnit test installs +# Only relevant if your plugin installs WP test files locally +# ------------------------------------------------------------ + +/wordpress/ +/wordpress-tests-lib/ +/tmp/ + + +# ------------------------------------------------------------ +# Playwright +# ------------------------------------------------------------ + +/playwright-report/ +/test-results/ +/blob-report/ +/playwright/.cache/ + +# Playwright artifacts +*.webm +*.mp4 +trace.zip +*-trace.zip +*-screenshot.png + + +# ------------------------------------------------------------ +# Node / package manager logs and cache +# ------------------------------------------------------------ + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +.npm/ +.pnpm-store/ + +.yarn/cache/ +.yarn/unplugged/ +.yarn/build-state.yml +.yarn/install-state.gz + + +# ------------------------------------------------------------ +# Build outputs +# Remove these if your plugin commits built assets +# ------------------------------------------------------------ + +/dist/ +/build/ +/assets/build/ +/public/build/ + + +# ------------------------------------------------------------ +# Environment and local config +# ------------------------------------------------------------ + +.env +.env.* +!.env.example +!.env.dist + +*.local +*.local.php +*.local.json + +.wp-env.override.json + + +# ------------------------------------------------------------ +# Logs +# ------------------------------------------------------------ + +*.log +debug.log +error_log + + +# ------------------------------------------------------------ +# IDEs and editors +# ------------------------------------------------------------ + +.idea/ + +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json.example + +*.swp +*.swo +*~ + + +# ------------------------------------------------------------ +# OS files +# ------------------------------------------------------------ + .DS_Store -node_modules/ \ No newline at end of file +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes + +Thumbs.db +ehthumbs.db +Desktop.ini + + +# ------------------------------------------------------------ +# Temporary files +# ------------------------------------------------------------ + +.cache/ +temp/ +tmp/ +*.tmp +*.bak +*.orig +*.rej + + +# ------------------------------------------------------------ +# Plugin package archives +# ------------------------------------------------------------ + +*.zip +*.tar +*.tar.gz +*.tgz +*.rar + + +# ------------------------------------------------------------ +# Database dumps +# ------------------------------------------------------------ + +*.sql +*.sql.gz +*.sqlite +*.sqlite3 diff --git a/LICENSE b/LICENSE index d159169..947d07a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,96 +1,98 @@ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. +Copyright (C) 1989, 1991 Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. Preamble - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public +The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This +software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to your programs, too. - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. - To protect your rights, we need to make restrictions that forbid +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. - For example, if you distribute copies of such a program, whether +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their rights. - We protect your rights with two steps: (1) copyright the software, and +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. - Also, for each author's protection and ours, we want to make certain +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we +software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free +Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any +program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. - The precise terms and conditions for copying, distribution and +The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains + a notice placed by the copyright holder saying it may be distributed + under the terms of this General Public License. The "Program", below, + refers to any such program or work, and a "work based on the Program" + means either the Program or any derivative work under copyright law: + that is to say, a work containing the Program or a portion of it, + either verbatim or with modifications and/or translated into another + language. (Hereinafter, translation is included without limitation in + the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of +covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. +1. You may copy and distribute verbatim copies of the Program's + source code as you receive it, in any medium, provided that you + conspicuously and appropriately publish on each copy an appropriate + copyright notice and disclaimer of warranty; keep intact all the + notices that refer to this License and to the absence of any warranty; + and give any other recipients of the Program a copy of this License + along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: +2. You may modify your copy or copies of the Program or any portion + of it, thus forming a work based on the Program, and copy and + distribute such modifications or work under the terms of Section 1 + above, provided that you also meet all of these conditions: + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. @@ -111,11 +113,11 @@ above, provided that you also meet all of these conditions: does not normally print such an announcement, your work based on the Program is not required to print an announcement.) -These requirements apply to the modified work as a whole. If +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you +sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the @@ -131,9 +133,10 @@ with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: +3. You may copy and distribute the Program (or a work based on it, + under Section 2) in object code or executable form under the terms of + Sections 1 and 2 above provided that you also do one of the following: + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections @@ -153,10 +156,10 @@ Sections 1 and 2 above provided that you also do one of the following: an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source +making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a +control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the @@ -169,43 +172,43 @@ access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. +4. You may not copy, modify, sublicense, or distribute the Program + except as expressly provided under this License. Any attempt + otherwise to copy, modify, sublicense or distribute the Program is + void, and will automatically terminate your rights under this License. + However, parties who have received copies, or rights, from you under + this License will not have their licenses terminated so long as such + parties remain in full compliance. + +5. You are not required to accept this License, since you have not + signed it. However, nothing else grants you permission to modify or + distribute the Program or its derivative works. These actions are + prohibited by law if you do not accept this License. Therefore, by + modifying or distributing the Program (or any work based on the + Program), you indicate your acceptance of this License to do so, and + all its terms and conditions for copying, distributing or modifying + the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the + Program), the recipient automatically receives a license from the + original licensor to copy, distribute or modify the Program subject to + these terms and conditions. You may not impose any further + restrictions on the recipients' exercise of the rights granted herein. + You are not responsible for enforcing compliance by third parties to + this License. + +7. If, as a consequence of a court judgment or allegation of patent + infringement or for any other reason (not limited to patent issues), + conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot + distribute so as to satisfy simultaneously your obligations under this + License and any other pertinent obligations, then as a consequence you + may not distribute the Program at all. For example, if a patent + license would not permit royalty-free redistribution of the Program by + all those who receive copies directly or indirectly through you, then + the only way you could satisfy both it and this License would be to + refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to @@ -216,7 +219,7 @@ It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is -implemented by public license practices. Many people have made +implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing @@ -226,66 +229,66 @@ impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. +8. If the distribution and/or use of the Program is restricted in + certain countries either by patents or by copyrighted interfaces, the + original copyright holder who places the Program under this License + may add an explicit geographical distribution limitation excluding + those countries, so that distribution is permitted only in or among + countries not thus excluded. In such case, this License incorporates + the limitation as if written in the body of this License. - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. +9. The Free Software Foundation may publish revised and/or new versions + of the General Public License from time to time. Such new versions will + be similar in spirit to the present version, but may differ in detail to + address new problems or concerns. -Each version is given a distinguishing version number. If the Program +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of +Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest +10. If you wish to incorporate parts of the Program into other free + programs whose distribution conditions are different, write to the author + to ask for permission. For software which is copyrighted by the Free + Software Foundation, write to the Free Software Foundation; we sometimes + make exceptions for this. Our decision will be guided by the two goals + of preserving the free status of all derivatives of our free software and + of promoting the sharing and reuse of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN + OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES + PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED + OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS + TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE + PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, + REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR + REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, + INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING + OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED + TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY + YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - To do so, attach the following notices to the program. It is safest +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. @@ -318,22 +321,22 @@ when it starts in an interactive mode: under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may +parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: +necessary. Here is a sample; alter the names: - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. +Yoyodyne, Inc., hereby disclaims all copyright interest in the program +`Gnomovision' (which makes passes at compilers) written by James Hacker. - , 1 April 1989 - Ty Coon, President of Vice +, 1 April 1989 +Ty Coon, President of Vice This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may +proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General +library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. diff --git a/README.md b/README.md index d032661..af41f31 100644 --- a/README.md +++ b/README.md @@ -76,4 +76,4 @@ GNU General Public License v2.0 / MIT License ## Contributors - Kalyx -- Radek Friedl (Progress Software) +- Radek Friedl (Progress Software) \ No newline at end of file diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..e6c5f9e --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,923 @@ +.progress-agentic-rag { + --progress-agentic-rag-color-dark-stronger: #000; + --progress-agentic-rag-color-light-stronger: #fff; + --progress-agentic-rag-color-neutral-regular: hsl(0, 0%, 44%); + --progress-agentic-rag-color-neutral-light: hsl(0, 0%, 77%); + --progress-agentic-rag-color-neutral-lighter: hsl(0, 0%, 90%); + --progress-agentic-rag-color-neutral-lightest: hsl(240, 7%, 97%); + --progress-agentic-rag-color-primary-stronger: hsl(252, 100%, 24%); + --progress-agentic-rag-color-primary-strong: hsl(223.74, 100%, 39.8%); + --progress-agentic-rag-color-primary-regular: hsl(223.2, 100%, 50.98%); + --progress-agentic-rag-color-primary-lighter: hsl(212.57, 100%, 93.14%); + --progress-agentic-rag-color-primary-lightest: hsl(215.29, 100%, 96.67%); + --progress-agentic-rag-color-secondary-regular: hsl(49, 100%, 60%); + --progress-agentic-rag-color-secondary-lightest: hsl(49, 100%, 95%); + --progress-agentic-rag-color-success-regular: hsl(154, 100%, 37%); + --progress-agentic-rag-color-danger-regular: hsl(7, 90%, 59%); + --progress-agentic-rag-color-danger-lightest: hsl(7, 90%, 95%); + --progress-agentic-rag-font-family-sans-serif: Roboto, Arial, sans-serif; + --progress-agentic-rag-font-family-monospace: "Source Code Pro", Consolas, Monaco, monospace; + --progress-agentic-rag-font-size-xs: 0.75rem; + --progress-agentic-rag-font-size-s: 0.875rem; + --progress-agentic-rag-font-size-m: 1rem; + --progress-agentic-rag-font-size-title-m: 1rem; + --progress-agentic-rag-font-size-display-s: 1.5rem; + --progress-agentic-rag-font-weight-semi-bold: 600; + --progress-agentic-rag-line-height-m: 1.25rem; + --progress-agentic-rag-line-height-title-m: 1.625rem; + --progress-agentic-rag-line-height-body: 1.4; + --progress-agentic-rag-letter-spacing-default: 0; + --progress-agentic-rag-space-025: 0.125rem; + --progress-agentic-rag-space-05: 0.25rem; + --progress-agentic-rag-space-075: 0.375rem; + --progress-agentic-rag-space-1: 0.5rem; + --progress-agentic-rag-space-15: 0.75rem; + --progress-agentic-rag-space-2: 1rem; + --progress-agentic-rag-space-25: 1.25rem; + --progress-agentic-rag-space-3: 1.5rem; + --progress-agentic-rag-space-4: 2rem; + --progress-agentic-rag-space-5: 2.5rem; + --progress-agentic-rag-border-radius-default: 0.25rem; + --progress-agentic-rag-button-border-radius: var(--progress-agentic-rag-border-radius-default); + --progress-agentic-rag-button-height-medium: var(--progress-agentic-rag-space-5); + --progress-agentic-rag-button-padding-sides-large: var(--progress-agentic-rag-space-2); + --progress-agentic-rag-button-primary-solid-hover-bg: var(--progress-agentic-rag-color-primary-strong); + --progress-agentic-rag-button-primary-solid-active-bg: var(--progress-agentic-rag-color-primary-stronger); + --progress-agentic-rag-card-border-radius: var(--progress-agentic-rag-space-1); + --progress-agentic-rag-chip-border-radius: var(--progress-agentic-rag-border-radius-default); + --progress-agentic-rag-chip-font-size: var(--progress-agentic-rag-font-size-xs); + --progress-agentic-rag-chip-padding: var(--progress-agentic-rag-space-05) var(--progress-agentic-rag-space-1); + --progress-agentic-rag-field-bg: var(--progress-agentic-rag-color-light-stronger); + --progress-agentic-rag-field-border-color: var(--progress-agentic-rag-color-neutral-light); + --progress-agentic-rag-field-border-hover-color: var(--progress-agentic-rag-color-neutral-regular); + --progress-agentic-rag-field-control-height: calc(var(--progress-agentic-rag-space-5) - var(--progress-agentic-rag-space-025)); + --progress-agentic-rag-shadow-focus-color: var(--progress-agentic-rag-color-primary-regular); + --progress-agentic-rag-tab-link-text: var(--progress-agentic-rag-color-dark-stronger); + --progress-agentic-rag-tab-link-bg: var(--progress-agentic-rag-color-light-stronger); + --progress-agentic-rag-tab-link-active-bg: var(--progress-agentic-rag-color-neutral-lightest); + --progress-agentic-rag-tab-font-size: var(--progress-agentic-rag-font-size-m); + --progress-agentic-rag-tab-font-weight-active: var(--progress-agentic-rag-font-weight-semi-bold); + --progress-agentic-rag-tab-gap: var(--progress-agentic-rag-space-025); + --progress-agentic-rag-tab-padding-medium: var(--progress-agentic-rag-space-15) var(--progress-agentic-rag-space-3); + --progress-agentic-rag-panel-border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + --progress-agentic-rag-panel-bg: var(--progress-agentic-rag-color-light-stronger); + color: var(--progress-agentic-rag-color-dark-stronger); + font-family: var(--progress-agentic-rag-font-family-sans-serif); + font-size: var(--progress-agentic-rag-font-size-m); + letter-spacing: var(--progress-agentic-rag-letter-spacing-default); + line-height: var(--progress-agentic-rag-line-height-body); +} + +.progress-agentic-rag__shell { + box-sizing: border-box; + max-width: 72rem; + padding-top: var(--progress-agentic-rag-space-2); +} + +.progress-agentic-rag__masthead { + align-items: flex-end; + border-bottom: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + display: flex; + gap: var(--progress-agentic-rag-space-3); + justify-content: space-between; + margin-bottom: var(--progress-agentic-rag-space-2); + padding-bottom: var(--progress-agentic-rag-space-2); +} + +.progress-agentic-rag__eyebrow, +.progress-agentic-rag__section-label { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-font-size-xs); + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); + line-height: var(--progress-agentic-rag-line-height-m); + text-transform: uppercase; +} + +.progress-agentic-rag h1 { + color: var(--progress-agentic-rag-color-dark-stronger); + font-size: var(--progress-agentic-rag-font-size-display-s); + line-height: var(--progress-agentic-rag-line-height-title-m); + margin: 0; +} + +.progress-agentic-rag__summary { + display: flex; + flex-wrap: wrap; + gap: var(--progress-agentic-rag-space-1); + justify-content: flex-end; +} + +.progress-agentic-rag__connection-state { + display: flex; + flex-wrap: wrap; + gap: var(--progress-agentic-rag-space-1); + justify-content: flex-end; +} + +.progress-agentic-rag__summary-item, +.progress-agentic-rag__token-state { + background: var(--progress-agentic-rag-color-neutral-lightest); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-chip-border-radius); + box-sizing: border-box; + color: var(--progress-agentic-rag-color-dark-stronger); + display: inline-flex; + gap: var(--progress-agentic-rag-space-075); + min-height: var(--progress-agentic-rag-space-4); + padding: var(--progress-agentic-rag-chip-padding); +} + +.progress-agentic-rag__summary-item span { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-chip-font-size); +} + +.progress-agentic-rag__summary-item strong, +.progress-agentic-rag__token-state { + font-size: var(--progress-agentic-rag-chip-font-size); + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); +} + +.progress-agentic-rag__tabs { + background: var(--progress-agentic-rag-color-neutral-lighter); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + display: inline-flex; + gap: var(--progress-agentic-rag-tab-gap); + margin: 0 0 var(--progress-agentic-rag-space-2); + padding: var(--progress-agentic-rag-space-025); +} + +.progress-agentic-rag__tab { + background: var(--progress-agentic-rag-tab-link-bg); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + color: var(--progress-agentic-rag-tab-link-text); + font-size: var(--progress-agentic-rag-tab-font-size); + line-height: var(--progress-agentic-rag-line-height-m); + min-height: var(--progress-agentic-rag-space-5); + padding: var(--progress-agentic-rag-tab-padding-medium); + text-decoration: none; +} + +.progress-agentic-rag__tab:hover, +.progress-agentic-rag__tab:focus { + color: var(--progress-agentic-rag-color-primary-strong); +} + +.progress-agentic-rag__tab:focus-visible, +.progress-agentic-rag__button:focus-visible, +.progress-agentic-rag__field input:focus-visible { + box-shadow: 0 0 0 2px var(--progress-agentic-rag-shadow-focus-color); + outline: none; +} + +.progress-agentic-rag__tab--active { + background: var(--progress-agentic-rag-tab-link-active-bg); + color: var(--progress-agentic-rag-color-primary-strong); + font-weight: var(--progress-agentic-rag-tab-font-weight-active); +} + +.progress-agentic-rag__panel { + background: var(--progress-agentic-rag-panel-bg); + border: var(--progress-agentic-rag-panel-border); + border-radius: var(--progress-agentic-rag-card-border-radius); + box-sizing: border-box; + max-width: 72rem; + padding: var(--progress-agentic-rag-space-3); +} + +.progress-agentic-rag__panel-header { + align-items: flex-start; + display: flex; + gap: var(--progress-agentic-rag-space-2); + justify-content: space-between; + margin-bottom: var(--progress-agentic-rag-space-3); +} + +.progress-agentic-rag__panel h2 { + font-size: var(--progress-agentic-rag-font-size-title-m); + line-height: var(--progress-agentic-rag-line-height-title-m); + margin: 0; +} + + .progress-agentic-rag__status-grid { + display: grid; + gap: var(--progress-agentic-rag-space-1); + grid-template-columns: repeat(5, minmax(7rem, 1fr)); + min-width: 40rem; + } + +.progress-agentic-rag__status { + border-left: 3px solid var(--progress-agentic-rag-color-primary-regular); + box-sizing: border-box; + min-height: var(--progress-agentic-rag-space-5); + padding: var(--progress-agentic-rag-space-05) var(--progress-agentic-rag-space-15); +} + +.progress-agentic-rag__status span { + color: var(--progress-agentic-rag-color-neutral-regular); + display: block; + font-size: var(--progress-agentic-rag-font-size-xs); + line-height: var(--progress-agentic-rag-line-height-m); +} + +.progress-agentic-rag__status strong { + color: var(--progress-agentic-rag-color-dark-stronger); + display: block; + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); + line-height: var(--progress-agentic-rag-line-height-m); +} + +.progress-agentic-rag__scheduler-note { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + margin: calc(-1 * var(--progress-agentic-rag-space-1)) 0 var(--progress-agentic-rag-space-3); +} + +.progress-agentic-rag__indexation-layout { + display: grid; + gap: var(--progress-agentic-rag-space-2); + grid-template-columns: minmax(0, 1fr); +} + +.progress-agentic-rag__indexation-layout--has-background { + grid-template-columns: minmax(16rem, 22rem) minmax(0, 1fr); +} + +.progress-agentic-rag__indexation-form { + min-width: 0; +} + +.progress-agentic-rag__background-sync { + align-self: start; + background: var(--progress-agentic-rag-color-neutral-lightest); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + display: grid; + gap: var(--progress-agentic-rag-space-1); + padding: var(--progress-agentic-rag-space-15); +} + +.progress-agentic-rag__background-sync[hidden] { + display: none; +} + +.progress-agentic-rag__background-sync-header { + align-items: baseline; + display: flex; + flex-wrap: wrap; + gap: var(--progress-agentic-rag-space-1); + justify-content: space-between; +} + +.progress-agentic-rag__background-sync h3 { + font-size: var(--progress-agentic-rag-font-size-title-m); + line-height: var(--progress-agentic-rag-line-height-title-m); + margin: 0; +} + +.progress-agentic-rag__background-sync-header strong { + color: var(--progress-agentic-rag-color-primary-regular); + font-size: var(--progress-agentic-rag-font-size-display-s); + line-height: var(--progress-agentic-rag-line-height-title-m); +} + +.progress-agentic-rag__background-sync p, +.progress-agentic-rag__background-sync-meta { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + margin: 0; +} + +.progress-agentic-rag__background-sync-meta { + display: grid; + gap: var(--progress-agentic-rag-space-05); +} + +.progress-agentic-rag__background-sync-meta strong, +.progress-agentic-rag__background-sync-current span { + color: var(--progress-agentic-rag-color-dark-stronger); +} + +.progress-agentic-rag__field-grid { + display: grid; + gap: var(--progress-agentic-rag-space-2); + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.progress-agentic-rag__field { + display: grid; + gap: var(--progress-agentic-rag-space-075); +} + +.progress-agentic-rag__field span { + color: var(--progress-agentic-rag-color-dark-stronger); + font-size: var(--progress-agentic-rag-font-size-s); + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); +} + +.progress-agentic-rag__field input { + background: var(--progress-agentic-rag-field-bg); + border: 1px solid var(--progress-agentic-rag-field-border-color); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + color: var(--progress-agentic-rag-color-dark-stronger); + min-height: var(--progress-agentic-rag-field-control-height); + width: 100%; +} + +.progress-agentic-rag__field input:hover { + border-color: var(--progress-agentic-rag-field-border-hover-color); +} + +.progress-agentic-rag__mapping { + border-top: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + display: grid; + gap: var(--progress-agentic-rag-space-2); + grid-template-columns: minmax(12rem, 18rem) 1fr; + margin-top: var(--progress-agentic-rag-space-3); + padding-top: var(--progress-agentic-rag-space-3); +} + +.progress-agentic-rag__mapping-heading h3 { + font-size: var(--progress-agentic-rag-font-size-title-m); + line-height: var(--progress-agentic-rag-line-height-title-m); + margin: 0; +} + +.progress-agentic-rag__mapping-heading p { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + margin: var(--progress-agentic-rag-space-1) 0 0; +} + +.progress-agentic-rag__mapping-control { + align-items: center; + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-card-border-radius); + box-sizing: border-box; + display: grid; + gap: var(--progress-agentic-rag-space-1); + grid-template-columns: auto minmax(14rem, 1fr) auto; + min-height: var(--progress-agentic-rag-space-5); + padding: var(--progress-agentic-rag-space-2); +} + +.progress-agentic-rag__mapping-control label { + font-size: var(--progress-agentic-rag-font-size-s); + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); +} + +.progress-agentic-rag__mapping-control select { + background-color: var(--progress-agentic-rag-field-bg); + border: 1px solid var(--progress-agentic-rag-field-border-color); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + color: var(--progress-agentic-rag-color-dark-stronger); + min-height: var(--progress-agentic-rag-field-control-height); + max-width: none; + width: 100%; +} + +.progress-agentic-rag__mapping-control select:hover { + border-color: var(--progress-agentic-rag-field-border-hover-color); +} + +.progress-agentic-rag__mapping-blocks { + display: grid; + gap: var(--progress-agentic-rag-space-2); + margin-top: var(--progress-agentic-rag-space-2); +} + +.progress-agentic-rag__mapping-block { + background: var(--progress-agentic-rag-color-light-stronger); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + display: grid; + gap: var(--progress-agentic-rag-space-2); + padding: var(--progress-agentic-rag-space-2); +} + +.progress-agentic-rag__mapping-block-header { + align-items: start; + display: flex; + gap: var(--progress-agentic-rag-space-2); + justify-content: space-between; +} + +.progress-agentic-rag__mapping-block-header h4 { + font-size: var(--progress-agentic-rag-font-size-title-m); + line-height: var(--progress-agentic-rag-line-height-title-m); + margin: 0; +} + +.progress-agentic-rag__mapping-select { + display: grid; + gap: var(--progress-agentic-rag-space-075); +} + +.progress-agentic-rag__mapping-select span { + font-size: var(--progress-agentic-rag-font-size-s); + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); +} + +.progress-agentic-rag__mapping-select select { + background-color: var(--progress-agentic-rag-field-bg); + border: 1px solid var(--progress-agentic-rag-field-border-color); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + color: var(--progress-agentic-rag-color-dark-stronger); + min-height: var(--progress-agentic-rag-field-control-height); + max-width: none; + width: 100%; +} + +.progress-agentic-rag__mapping-table { + border-collapse: collapse; + width: 100%; +} + +.progress-agentic-rag__mapping-table th, +.progress-agentic-rag__mapping-table td { + border-top: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + padding: var(--progress-agentic-rag-space-1); + text-align: left; + vertical-align: top; +} + +.progress-agentic-rag__mapping-table thead th { + background: var(--progress-agentic-rag-color-neutral-lightest); +} + +.progress-agentic-rag__label-checkboxes, +.progress-agentic-rag__fallback-labels { + display: flex; + flex-wrap: wrap; + gap: var(--progress-agentic-rag-space-1); +} + +.progress-agentic-rag__label-checkbox { + align-items: center; + display: inline-flex; + gap: var(--progress-agentic-rag-space-05); + margin: 0; +} + +.progress-agentic-rag__label-checkbox input { + accent-color: var(--progress-agentic-rag-color-primary-regular); + margin: 0; +} + +.progress-agentic-rag__fallback { + background: var(--progress-agentic-rag-color-neutral-lightest); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + display: grid; + gap: var(--progress-agentic-rag-space-1); + padding: var(--progress-agentic-rag-space-15); +} + +.progress-agentic-rag__fallback p, +.progress-agentic-rag__mapping-muted { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + margin: 0; +} + +.progress-agentic-rag__checkbox-grid { + display: grid; + gap: var(--progress-agentic-rag-space-1); + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); +} + +.progress-agentic-rag__checkbox { + align-items: start; + background: var(--progress-agentic-rag-color-light-stronger); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + display: grid; + gap: var(--progress-agentic-rag-space-05) var(--progress-agentic-rag-space-1); + grid-template-columns: auto 1fr; + margin: 0; + min-height: var(--progress-agentic-rag-space-5); + padding: var(--progress-agentic-rag-space-15); +} + +.progress-agentic-rag__checkbox:has(input:checked) { + background: var(--progress-agentic-rag-color-primary-lightest); + border-color: var(--progress-agentic-rag-color-primary-regular); +} + +.progress-agentic-rag__checkbox input { + accent-color: var(--progress-agentic-rag-color-primary-regular); + grid-row: span 2; + margin: var(--progress-agentic-rag-space-025) 0 0; +} + +.progress-agentic-rag__checkbox span { + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); + line-height: var(--progress-agentic-rag-line-height-m); + min-width: 0; +} + +.progress-agentic-rag__checkbox code { + background: transparent; + color: var(--progress-agentic-rag-color-neutral-regular); + font-family: var(--progress-agentic-rag-font-family-monospace); + font-size: var(--progress-agentic-rag-font-size-xs); + grid-column: 2; + overflow-wrap: anywhere; + padding: 0; +} + +.progress-agentic-rag__button { + align-items: center; + border: 0; + border-radius: var(--progress-agentic-rag-button-border-radius); + box-sizing: border-box; + cursor: pointer; + display: inline-flex; + font-family: var(--progress-agentic-rag-font-family-sans-serif); + font-size: var(--progress-agentic-rag-font-size-s); + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); + justify-content: center; + line-height: var(--progress-agentic-rag-line-height-m); + min-height: var(--progress-agentic-rag-button-height-medium); + padding: 0 var(--progress-agentic-rag-button-padding-sides-large); +} + +.progress-agentic-rag__button--primary { + background: var(--progress-agentic-rag-color-primary-regular); + color: var(--progress-agentic-rag-color-light-stronger); +} + +.progress-agentic-rag__button--primary:hover { + background: var(--progress-agentic-rag-button-primary-solid-hover-bg); + color: var(--progress-agentic-rag-color-light-stronger); +} + +.progress-agentic-rag__button--primary:active { + background: var(--progress-agentic-rag-button-primary-solid-active-bg); +} + +.progress-agentic-rag__button--secondary { + background: var(--progress-agentic-rag-color-light-stronger); + border: 1px solid var(--progress-agentic-rag-color-primary-regular); + color: var(--progress-agentic-rag-color-primary-regular); +} + + .progress-agentic-rag__button--secondary:hover { + background: var(--progress-agentic-rag-color-primary-lightest); + color: var(--progress-agentic-rag-color-primary-strong); + } + + .progress-agentic-rag__button--danger { + background: var(--progress-agentic-rag-color-light-stronger); + border: 1px solid var(--progress-agentic-rag-color-danger-regular); + color: var(--progress-agentic-rag-color-danger-regular); + } + + .progress-agentic-rag__button--danger:hover { + background: var(--progress-agentic-rag-color-danger-lightest); + color: var(--progress-agentic-rag-color-danger-regular); + } + +.progress-agentic-rag__button:disabled { + background: var(--progress-agentic-rag-color-neutral-lighter); + border-color: var(--progress-agentic-rag-color-neutral-lighter); + color: var(--progress-agentic-rag-color-neutral-regular); + cursor: not-allowed; +} + + .progress-agentic-rag__actions { + border-top: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + display: flex; + flex-wrap: wrap; + gap: var(--progress-agentic-rag-space-1); + margin-top: var(--progress-agentic-rag-space-3); + padding-top: var(--progress-agentic-rag-space-2); + } + + .progress-agentic-rag__sync-overview, + .progress-agentic-rag__label-reprocess { + border-top: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + margin-top: var(--progress-agentic-rag-space-3); + padding-top: var(--progress-agentic-rag-space-3); + } + + .progress-agentic-rag__sync-overview-header, + .progress-agentic-rag__label-reprocess { + align-items: flex-start; + display: flex; + gap: var(--progress-agentic-rag-space-2); + justify-content: space-between; + } + + .progress-agentic-rag__sync-overview h3, + .progress-agentic-rag__label-reprocess h3 { + font-size: var(--progress-agentic-rag-font-size-title-m); + line-height: var(--progress-agentic-rag-line-height-title-m); + margin: 0; + } + + .progress-agentic-rag__label-reprocess p { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + margin: var(--progress-agentic-rag-space-1) 0 0; + } + + .progress-agentic-rag__sync-total { + background: var(--progress-agentic-rag-color-neutral-lightest); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + min-width: 9rem; + padding: var(--progress-agentic-rag-space-1) var(--progress-agentic-rag-space-15); + } + + .progress-agentic-rag__sync-total span { + color: var(--progress-agentic-rag-color-neutral-regular); + display: block; + font-size: var(--progress-agentic-rag-font-size-xs); + line-height: var(--progress-agentic-rag-line-height-m); + } + + .progress-agentic-rag__sync-total strong { + display: block; + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); + line-height: var(--progress-agentic-rag-line-height-m); + } + + .progress-agentic-rag__sync-table { + border-collapse: collapse; + margin-top: var(--progress-agentic-rag-space-2); + width: 100%; + } + + .progress-agentic-rag__sync-table th, + .progress-agentic-rag__sync-table td { + border-bottom: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + padding: var(--progress-agentic-rag-space-1); + text-align: left; + } + + .progress-agentic-rag__sync-table thead th { + background: var(--progress-agentic-rag-color-neutral-lightest); + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); + } + + .progress-agentic-rag__sync-table code { + background: transparent; + color: var(--progress-agentic-rag-color-neutral-regular); + display: block; + font-family: var(--progress-agentic-rag-font-family-monospace); + font-size: var(--progress-agentic-rag-font-size-xs); + padding: 0; + } + + .progress-agentic-rag__label-actions { + display: flex; + flex: 0 0 auto; + gap: var(--progress-agentic-rag-space-1); + } + +.progress-agentic-rag__alert { + background: var(--progress-agentic-rag-color-neutral-lightest); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-left: var(--progress-agentic-rag-space-05) solid var(--progress-agentic-rag-color-primary-regular); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + color: var(--progress-agentic-rag-color-dark-stronger); + margin-top: var(--progress-agentic-rag-space-2); + max-width: 72rem; + padding: var(--progress-agentic-rag-space-15) var(--progress-agentic-rag-space-2); +} + +.progress-agentic-rag__alert--success { + background: var(--progress-agentic-rag-color-primary-lightest); + border-left-color: var(--progress-agentic-rag-color-success-regular); +} + +.progress-agentic-rag__alert--error { + background: var(--progress-agentic-rag-color-danger-lightest); + border-left-color: var(--progress-agentic-rag-color-danger-regular); +} + +.progress-agentic-rag__alert p { + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + margin: 0; +} + +.progress-agentic-rag-sync-open { + overflow: hidden; +} + +.progress-agentic-rag__modal[hidden] { + display: none; +} + +.progress-agentic-rag__modal { + align-items: center; + bottom: 0; + display: flex; + justify-content: center; + left: 0; + padding: var(--progress-agentic-rag-space-3); + position: fixed; + right: 0; + top: 0; + z-index: 10000; +} + +.progress-agentic-rag__modal-backdrop { + background: rgba(0, 0, 0, 0.42); + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; +} + +.progress-agentic-rag__modal-panel { + background: var(--progress-agentic-rag-color-light-stronger); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-card-border-radius); + box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.22); + box-sizing: border-box; + max-width: 38rem; + padding: var(--progress-agentic-rag-space-3); + position: relative; + width: min(38rem, 100%); +} + +.progress-agentic-rag__modal-header { + align-items: flex-start; + display: flex; + gap: var(--progress-agentic-rag-space-2); + justify-content: space-between; +} + +.progress-agentic-rag__modal-header h2 { + font-size: var(--progress-agentic-rag-font-size-display-s); + line-height: var(--progress-agentic-rag-line-height-title-m); + margin: 0; +} + +.progress-agentic-rag__modal-close { + align-items: center; + background: var(--progress-agentic-rag-color-neutral-lightest); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + color: var(--progress-agentic-rag-color-dark-stronger); + cursor: pointer; + display: inline-flex; + font-size: var(--progress-agentic-rag-font-size-title-m); + height: var(--progress-agentic-rag-space-4); + justify-content: center; + line-height: 1; + width: var(--progress-agentic-rag-space-4); +} + +.progress-agentic-rag__progress { + margin-top: var(--progress-agentic-rag-space-3); +} + +.progress-agentic-rag__progress-meta { + align-items: baseline; + display: flex; + gap: var(--progress-agentic-rag-space-1); + justify-content: space-between; +} + +.progress-agentic-rag__progress-meta strong { + font-size: var(--progress-agentic-rag-font-size-display-s); + line-height: var(--progress-agentic-rag-line-height-title-m); +} + +.progress-agentic-rag__progress-meta span { + color: var(--progress-agentic-rag-color-neutral-regular); + font-size: var(--progress-agentic-rag-font-size-s); + line-height: var(--progress-agentic-rag-line-height-m); + text-align: right; +} + +.progress-agentic-rag__progress-track { + background: var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + height: var(--progress-agentic-rag-space-1); + margin-top: var(--progress-agentic-rag-space-1); + overflow: hidden; +} + +.progress-agentic-rag__progress-bar { + background: var(--progress-agentic-rag-color-primary-regular); + height: 100%; + transition: width 180ms ease; + width: 0; +} + +.progress-agentic-rag__sync-stats { + display: grid; + gap: var(--progress-agentic-rag-space-1); + grid-template-columns: repeat(3, minmax(0, 1fr)); + margin-top: var(--progress-agentic-rag-space-3); +} + +.progress-agentic-rag__sync-stats div, +.progress-agentic-rag__sync-current { + background: var(--progress-agentic-rag-color-neutral-lightest); + border: 1px solid var(--progress-agentic-rag-color-neutral-lighter); + border-radius: var(--progress-agentic-rag-border-radius-default); + box-sizing: border-box; + padding: var(--progress-agentic-rag-space-15); +} + +.progress-agentic-rag__sync-stats span, +.progress-agentic-rag__sync-current span { + color: var(--progress-agentic-rag-color-neutral-regular); + display: block; + font-size: var(--progress-agentic-rag-font-size-xs); + line-height: var(--progress-agentic-rag-line-height-m); +} + +.progress-agentic-rag__sync-stats strong, +.progress-agentic-rag__sync-current strong { + display: block; + font-weight: var(--progress-agentic-rag-font-weight-semi-bold); + line-height: var(--progress-agentic-rag-line-height-m); + margin-top: var(--progress-agentic-rag-space-05); + overflow-wrap: anywhere; +} + +.progress-agentic-rag__sync-current { + margin-top: var(--progress-agentic-rag-space-1); +} + +@media (max-width: 782px) { + .progress-agentic-rag__masthead, + .progress-agentic-rag__panel-header { + align-items: stretch; + display: grid; + } + + .progress-agentic-rag__summary { + justify-content: flex-start; + } + + .progress-agentic-rag__connection-state { + justify-content: flex-start; + } + + .progress-agentic-rag__tabs { + display: grid; + } + + .progress-agentic-rag__status-grid { + grid-template-columns: 1fr; + min-width: 0; + } + + .progress-agentic-rag__field-grid { + grid-template-columns: 1fr; + } + + .progress-agentic-rag__mapping, + .progress-agentic-rag__mapping-control, + .progress-agentic-rag__indexation-layout--has-background, + .progress-agentic-rag__sync-overview-header, + .progress-agentic-rag__label-reprocess { + display: grid; + grid-template-columns: 1fr; + } + + .progress-agentic-rag__modal { + padding: var(--progress-agentic-rag-space-2); + } + + .progress-agentic-rag__modal-panel { + padding: var(--progress-agentic-rag-space-2); + } + + .progress-agentic-rag__progress-meta, + .progress-agentic-rag__sync-stats { + grid-template-columns: 1fr; + display: grid; + } + + .progress-agentic-rag__progress-meta span { + text-align: left; + } +} diff --git a/assets/css/frontend.css b/assets/css/frontend.css new file mode 100644 index 0000000..c6599cb --- /dev/null +++ b/assets/css/frontend.css @@ -0,0 +1,13 @@ +.progress-agentic-rag-search-widget { + box-sizing: border-box; + margin: 0 auto 2rem; + max-width: 72rem; + padding: 1.5rem; + width: 100%; +} + +.progress-agentic-rag-search-widget *, +.progress-agentic-rag-search-widget *::before, +.progress-agentic-rag-search-widget *::after { + box-sizing: border-box; +} diff --git a/assets/img/menu-icon.svg b/assets/img/menu-icon.svg new file mode 100644 index 0000000..5fcab5f --- /dev/null +++ b/assets/img/menu-icon.svg @@ -0,0 +1,3 @@ + diff --git a/assets/js/admin.js b/assets/js/admin.js new file mode 100644 index 0000000..0dd63ce --- /dev/null +++ b/assets/js/admin.js @@ -0,0 +1,717 @@ +( function () { + const config = window.progressAgenticRagAdmin || {}; + const startButton = document.querySelector( '[data-progress-agentic-rag-manual-sync]' ); + const deleteSyncedButton = document.querySelector( '[data-progress-agentic-rag-delete-synced]' ); + const labelReprocessButton = document.querySelector( '[data-progress-agentic-rag-label-reprocess]' ); + const labelReprocessCancelButton = document.querySelector( '[data-progress-agentic-rag-label-reprocess-cancel]' ); + const syncedCount = document.querySelector( '[data-progress-agentic-rag-synced-count]' ); + const backgroundSync = document.querySelector( '[data-progress-agentic-rag-background-sync]' ); + const backgroundLayout = backgroundSync ? backgroundSync.closest( '.progress-agentic-rag__indexation-layout' ) : null; + const taxonomySelect = document.querySelector( '[data-progress-agentic-rag-taxonomy-select]' ); + const mappingContainer = document.querySelector( '[data-progress-agentic-rag-mapping-container]' ); + const addMappingButton = document.querySelector( '[data-progress-agentic-rag-add-mapping]' ); + const modal = document.querySelector( '[data-progress-agentic-rag-sync-modal]' ); + const defaultStartButtonText = config.strings && config.strings.sync ? config.strings.sync : startButton ? startButton.textContent : ''; + const defaultDeleteButtonText = deleteSyncedButton ? deleteSyncedButton.textContent : ''; + const startButtonInitiallyDisabled = startButton ? startButton.disabled : false; + const deleteButtonInitiallyDisabled = deleteSyncedButton ? deleteSyncedButton.disabled : false; + + if ( ! config.ajaxUrl || ! config.nonce ) { + return; + } + + const selectors = { + bar: '[data-progress-agentic-rag-sync-bar]', + close: '[data-progress-agentic-rag-sync-close]', + closeButton: '[data-progress-agentic-rag-modal-close]', + completed: '[data-progress-agentic-rag-sync-completed]', + completedLabel: '[data-progress-agentic-rag-sync-completed-label]', + current: '[data-progress-agentic-rag-sync-current]', + currentLabel: '[data-progress-agentic-rag-sync-current-label]', + failed: '[data-progress-agentic-rag-sync-failed]', + label: '[data-progress-agentic-rag-modal-label]', + message: '[data-progress-agentic-rag-sync-message]', + percent: '[data-progress-agentic-rag-sync-percent]', + title: '[data-progress-agentic-rag-modal-title]', + total: '[data-progress-agentic-rag-sync-total]', + totalLabel: '[data-progress-agentic-rag-sync-total-label]', + }; + let pollTimer = null; + let deletePollTimer = null; + let backgroundPollTimer = null; + let syncStatus = config.initialSyncStatus || {}; + let deleteStatus = config.initialDeleteStatus || {}; + let backgroundSyncStatus = config.initialBackgroundSyncStatus || {}; + const mappingConfig = config.mapping || { taxonomies: {}, labelsets: [] }; + const selectorValue = ( value ) => window.CSS && window.CSS.escape ? window.CSS.escape( value ) : String( value ).replace( /"/g, '\\"' ); + + const field = ( selector ) => modal.querySelector( selector ); + + const setText = ( selector, value ) => { + const element = field( selector ); + if ( element ) { + element.textContent = value; + } + }; + + const selectedPostTypes = () => + Array.from( document.querySelectorAll( 'input[name="indexable_post_types[]"]:checked' ) ).map( ( input ) => input.value ); + + const currentSyncedCount = () => { + const countElement = syncedCount || document.querySelector( '[data-progress-agentic-rag-label-synced-count]' ); + if ( ! countElement ) { + return 0; + } + + return Number.parseInt( countElement.textContent, 10 ) || 0; + }; + + const syncIsRunning = () => syncStatus && syncStatus.status === 'running'; + + const deleteIsRunning = () => deleteStatus && deleteStatus.status === 'running'; + + const backgroundSyncIsActive = () => backgroundSyncStatus && backgroundSyncStatus.is_active; + + const statusNumber = ( status, key ) => Number.parseInt( status && status[ key ], 10 ) || 0; + + const processedCount = ( status ) => { + if ( status && status.processed !== undefined ) { + return statusNumber( status, 'processed' ); + } + + return statusNumber( status, 'completed' ) + statusNumber( status, 'failed' ); + }; + + const runningButtonText = ( status ) => { + const total = statusNumber( status, 'total' ); + const percent = Math.max( 0, Math.min( 100, statusNumber( status, 'percent' ) ) ); + const runningText = config.strings.running || defaultStartButtonText; + + if ( total > 0 ) { + return runningText + ' (' + Math.min( total, processedCount( status ) ) + '/' + total + ', ' + percent + '%)'; + } + + return runningText; + }; + + const deleteButtonText = ( status ) => { + const total = statusNumber( status, 'total' ); + const percent = Math.max( 0, Math.min( 100, statusNumber( status, 'percent' ) ) ); + const runningText = config.strings.deleteRunning || defaultDeleteButtonText; + + if ( total > 0 ) { + return runningText + ' (' + Math.min( total, processedCount( status ) ) + '/' + total + ', ' + percent + '%)'; + } + + return runningText; + }; + + const setModalMode = ( mode ) => { + const isDelete = mode === 'delete'; + const closeButton = field( selectors.closeButton ); + + setText( selectors.label, isDelete ? config.strings.deleteModalLabel : config.strings.syncModalLabel ); + setText( selectors.title, isDelete ? config.strings.deleteModalTitle : config.strings.syncModalTitle ); + setText( selectors.totalLabel, isDelete ? config.strings.deleteTotalLabel : config.strings.syncTotalLabel ); + setText( selectors.completedLabel, isDelete ? config.strings.deleteDoneLabel : config.strings.syncDoneLabel ); + setText( selectors.currentLabel, isDelete ? config.strings.deleteCurrentLabel : config.strings.syncCurrentLabel ); + + if ( closeButton ) { + closeButton.setAttribute( 'aria-label', isDelete ? config.strings.closeDeleteProgress : config.strings.closeSyncProgress ); + } + }; + + const request = async ( action, values = {} ) => { + const body = new FormData(); + body.append( 'action', action ); + body.append( '_ajax_nonce', config.nonce ); + + Object.entries( values ).forEach( ( [ key, value ] ) => { + if ( Array.isArray( value ) ) { + value.forEach( ( item ) => body.append( key + '[]', item ) ); + return; + } + + body.append( key, value ); + } ); + + const response = await fetch( config.ajaxUrl, { + method: 'POST', + body, + credentials: 'same-origin', + } ); + const payload = await response.json(); + + if ( ! response.ok || ! payload.success ) { + throw new Error( payload.data && payload.data.message ? payload.data.message : config.strings.failed ); + } + + return payload.data; + }; + + const openModal = () => { + modal.hidden = false; + document.body.classList.add( 'progress-agentic-rag-sync-open' ); + const closeButton = modal.querySelector( '.progress-agentic-rag__modal-close' ); + if ( closeButton ) { + closeButton.focus(); + } + }; + + const closeModal = () => { + modal.hidden = true; + document.body.classList.remove( 'progress-agentic-rag-sync-open' ); + if ( pollTimer && ! syncIsRunning() ) { + window.clearTimeout( pollTimer ); + pollTimer = null; + } + if ( deletePollTimer && ! deleteIsRunning() ) { + window.clearTimeout( deletePollTimer ); + deletePollTimer = null; + } + startButton.disabled = deleteIsRunning() ? true : startButtonInitiallyDisabled; + }; + + const renderStatus = ( status ) => { + syncStatus = status || {}; + if ( pollTimer ) { + window.clearTimeout( pollTimer ); + pollTimer = null; + } + + const percent = statusNumber( status, 'percent' ); + const bar = field( selectors.bar ); + + if ( bar ) { + bar.style.width = Math.max( 0, Math.min( 100, percent ) ) + '%'; + } + + setText( selectors.percent, percent + '%' ); + setText( selectors.message, status.message || config.strings.starting ); + setText( selectors.total, String( status.total || 0 ) ); + setText( selectors.completed, String( status.completed || 0 ) ); + setText( selectors.failed, String( status.failed || 0 ) ); + setText( selectors.current, status.current || '' ); + + if ( syncIsRunning() ) { + startButton.textContent = runningButtonText( status ); + startButton.disabled = false; + pollTimer = window.setTimeout( pollStatus, 2000 ); + return; + } + + if ( status.status !== 'starting' ) { + startButton.textContent = defaultStartButtonText; + startButton.disabled = startButtonInitiallyDisabled; + } + }; + + const renderError = ( message ) => { + renderStatus( { + status: 'failed', + total: 0, + completed: 0, + failed: 0, + current: '', + message, + percent: 0, + } ); + }; + + const pollStatus = async () => { + try { + renderStatus( await request( 'progress_agentic_rag_manual_sync_status' ) ); + } catch ( error ) { + startButton.disabled = false; + renderError( error.message ); + } + }; + + const renderDeleteStatus = ( status, reloadOnComplete = false ) => { + if ( ! deleteSyncedButton ) { + return; + } + + deleteStatus = status || {}; + if ( deletePollTimer ) { + window.clearTimeout( deletePollTimer ); + deletePollTimer = null; + } + + const percent = statusNumber( status, 'percent' ); + const bar = field( selectors.bar ); + + if ( bar ) { + bar.style.width = Math.max( 0, Math.min( 100, percent ) ) + '%'; + } + + setText( selectors.percent, percent + '%' ); + setText( selectors.message, status.message || config.strings.deleting ); + setText( selectors.total, String( status.total || 0 ) ); + setText( selectors.completed, String( status.deleted || status.completed || 0 ) ); + setText( selectors.failed, String( status.failed || 0 ) ); + setText( selectors.current, status.current || '' ); + + if ( deleteIsRunning() ) { + deleteSyncedButton.textContent = deleteButtonText( status ); + deleteSyncedButton.disabled = false; + startButton.disabled = true; + deletePollTimer = window.setTimeout( pollDeleteStatus, 2000 ); + return; + } + + if ( reloadOnComplete && status && status.status === 'complete' ) { + if ( status.message ) { + window.alert( status.message ); + } + window.location.reload(); + return; + } + + if ( status.status !== 'starting' ) { + deleteSyncedButton.textContent = defaultDeleteButtonText; + deleteSyncedButton.disabled = deleteButtonInitiallyDisabled; + if ( ! syncIsRunning() ) { + startButton.disabled = startButtonInitiallyDisabled; + } + } + }; + + const setBackgroundText = ( selector, value ) => { + if ( ! backgroundSync ) { + return; + } + + const element = backgroundSync.querySelector( selector ); + if ( element ) { + element.textContent = value; + } + }; + + const renderBackgroundSyncStatus = ( status ) => { + if ( ! backgroundSync ) { + return; + } + + status = status || {}; + backgroundSyncStatus = status || {}; + backgroundSync.hidden = ! backgroundSyncIsActive(); + if ( backgroundLayout ) { + backgroundLayout.classList.toggle( 'progress-agentic-rag__indexation-layout--has-background', backgroundSyncIsActive() ); + } + + if ( backgroundPollTimer ) { + window.clearTimeout( backgroundPollTimer ); + backgroundPollTimer = null; + } + + const percent = Math.max( 0, Math.min( 100, statusNumber( status, 'percent' ) ) ); + const track = backgroundSync.querySelector( '[data-progress-agentic-rag-background-track]' ); + const bar = backgroundSync.querySelector( '[data-progress-agentic-rag-background-bar]' ); + + if ( track ) { + track.setAttribute( 'aria-valuenow', String( percent ) ); + } + + if ( bar ) { + bar.style.width = percent + '%'; + } + + setBackgroundText( '[data-progress-agentic-rag-background-percent]', percent + '%' ); + setBackgroundText( '[data-progress-agentic-rag-background-message]', status.message || '' ); + setBackgroundText( '[data-progress-agentic-rag-background-processed]', String( status.processed || 0 ) ); + setBackgroundText( '[data-progress-agentic-rag-background-total]', String( status.total || 0 ) ); + setBackgroundText( '[data-progress-agentic-rag-background-failed]', String( status.failed || 0 ) ); + setBackgroundText( '[data-progress-agentic-rag-background-pending]', String( status.pending || 0 ) ); + setBackgroundText( '[data-progress-agentic-rag-background-running]', String( status.running || 0 ) ); + setBackgroundText( '[data-progress-agentic-rag-background-current]', status.current || '' ); + + backgroundPollTimer = window.setTimeout( pollBackgroundSyncStatus, backgroundSyncIsActive() ? 2000 : 8000 ); + }; + + const pollBackgroundSyncStatus = async () => { + try { + renderBackgroundSyncStatus( await request( 'progress_agentic_rag_background_sync_status' ) ); + } catch ( error ) { + backgroundPollTimer = window.setTimeout( pollBackgroundSyncStatus, 10000 ); + } + }; + + const pollDeleteStatus = async () => { + try { + renderDeleteStatus( await request( 'progress_agentic_rag_delete_synced_resources_status' ), true ); + } catch ( error ) { + if ( deleteSyncedButton ) { + deleteSyncedButton.disabled = false; + } + window.alert( error.message || config.strings.deleteFailed ); + } + }; + + const setMappingMessage = ( container, message ) => { + container.replaceChildren(); + const notice = document.createElement( 'em' ); + notice.textContent = message; + container.appendChild( notice ); + }; + + const labelsetOptions = () => { + const fragment = document.createDocumentFragment(); + const emptyOption = document.createElement( 'option' ); + emptyOption.value = ''; + emptyOption.textContent = config.strings.mappingSelectLabelset; + fragment.appendChild( emptyOption ); + + ( mappingConfig.labelsets || [] ).forEach( ( labelset ) => { + const option = document.createElement( 'option' ); + option.value = labelset; + option.textContent = labelset; + fragment.appendChild( option ); + } ); + + return fragment; + }; + + const labelCheckboxName = ( taxonomy, termId, fallback ) => + fallback ? + 'nuclia_taxonomy_label_map[' + taxonomy + '][fallback][labels][]' : + 'nuclia_taxonomy_label_map[' + taxonomy + '][terms][' + termId + '][]'; + + const renderLabelCheckboxes = ( container, taxonomy, labels, termId = '', fallback = false ) => { + container.replaceChildren(); + + if ( ! labels.length ) { + setMappingMessage( container, config.strings.mappingNoLabels ); + return; + } + + labels.forEach( ( label ) => { + const wrapper = document.createElement( 'label' ); + const input = document.createElement( 'input' ); + const text = document.createElement( 'span' ); + + wrapper.className = 'progress-agentic-rag__label-checkbox'; + input.type = 'checkbox'; + input.value = label; + input.name = labelCheckboxName( taxonomy, termId, fallback ); + text.textContent = label; + + wrapper.append( input, text ); + container.appendChild( wrapper ); + } ); + }; + + const loadMappingLabels = async ( labelset ) => { + if ( ! labelset ) { + return []; + } + + const data = await request( 'progress_agentic_rag_get_labelset_labels', { labelset } ); + return Array.isArray( data.labels ) ? data.labels : []; + }; + + const buildMappingBlock = ( taxonomy ) => { + const taxonomyConfig = mappingConfig.taxonomies[ taxonomy ]; + const block = document.createElement( 'div' ); + const header = document.createElement( 'div' ); + const titleWrap = document.createElement( 'div' ); + const title = document.createElement( 'h4' ); + const code = document.createElement( 'code' ); + const remove = document.createElement( 'button' ); + const labelsetLabel = document.createElement( 'label' ); + const labelsetText = document.createElement( 'span' ); + const labelsetSelect = document.createElement( 'select' ); + const table = document.createElement( 'table' ); + const thead = document.createElement( 'thead' ); + const tbody = document.createElement( 'tbody' ); + const fallback = document.createElement( 'div' ); + const fallbackTitle = document.createElement( 'p' ); + const fallbackStrong = document.createElement( 'strong' ); + const fallbackLabel = document.createElement( 'label' ); + const fallbackText = document.createElement( 'span' ); + const fallbackSelect = document.createElement( 'select' ); + const fallbackLabels = document.createElement( 'div' ); + + block.className = 'progress-agentic-rag__mapping-block'; + block.dataset.progressAgenticRagMappingBlock = ''; + block.dataset.taxonomy = taxonomy; + + header.className = 'progress-agentic-rag__mapping-block-header'; + title.textContent = taxonomyConfig.label; + code.textContent = taxonomy; + remove.type = 'button'; + remove.className = 'progress-agentic-rag__button progress-agentic-rag__button--danger'; + remove.dataset.progressAgenticRagRemoveMapping = ''; + remove.textContent = config.strings.mappingRemove; + titleWrap.append( title, code ); + header.append( titleWrap, remove ); + + labelsetLabel.className = 'progress-agentic-rag__mapping-select'; + labelsetText.textContent = config.strings.mappingLabelset; + labelsetSelect.name = 'nuclia_taxonomy_label_map[' + taxonomy + '][labelset]'; + labelsetSelect.dataset.progressAgenticRagLabelsetSelect = ''; + labelsetSelect.dataset.taxonomy = taxonomy; + labelsetSelect.appendChild( labelsetOptions() ); + labelsetLabel.append( labelsetText, labelsetSelect ); + + table.className = 'progress-agentic-rag__mapping-table'; + { + const headerRow = document.createElement( 'tr' ); + const termHeading = document.createElement( 'th' ); + const labelsHeading = document.createElement( 'th' ); + + termHeading.scope = 'col'; + termHeading.textContent = config.strings.mappingTerm; + labelsHeading.scope = 'col'; + labelsHeading.textContent = config.strings.mappingLabels; + headerRow.append( termHeading, labelsHeading ); + thead.appendChild( headerRow ); + } + + ( taxonomyConfig.terms || [] ).forEach( ( term ) => { + const row = document.createElement( 'tr' ); + const termCell = document.createElement( 'th' ); + const labelsCell = document.createElement( 'td' ); + const labels = document.createElement( 'div' ); + + termCell.scope = 'row'; + termCell.textContent = term.name; + labels.className = 'progress-agentic-rag__label-checkboxes'; + labels.dataset.progressAgenticRagLabelCheckboxes = ''; + labels.dataset.taxonomy = taxonomy; + labels.dataset.termId = String( term.id ); + setMappingMessage( labels, config.strings.mappingSelectLabels ); + labelsCell.appendChild( labels ); + row.append( termCell, labelsCell ); + tbody.appendChild( row ); + } ); + + table.append( thead, tbody ); + + fallback.className = 'progress-agentic-rag__fallback'; + fallbackStrong.textContent = config.strings.mappingFallback; + fallbackTitle.appendChild( fallbackStrong ); + fallbackLabel.className = 'progress-agentic-rag__mapping-select'; + fallbackText.textContent = config.strings.mappingLabelset; + fallbackSelect.name = 'nuclia_taxonomy_label_map[' + taxonomy + '][fallback][labelset]'; + fallbackSelect.dataset.progressAgenticRagFallbackLabelsetSelect = ''; + fallbackSelect.dataset.taxonomy = taxonomy; + fallbackSelect.appendChild( labelsetOptions() ); + fallbackLabel.append( fallbackText, fallbackSelect ); + fallbackLabels.className = 'progress-agentic-rag__fallback-labels'; + fallbackLabels.dataset.progressAgenticRagFallbackLabels = ''; + fallbackLabels.dataset.taxonomy = taxonomy; + setMappingMessage( fallbackLabels, config.strings.mappingSelectLabels ); + fallback.append( fallbackTitle, fallbackLabel, fallbackLabels ); + + block.append( header, labelsetLabel ); + if ( ( taxonomyConfig.terms || [] ).length ) { + block.appendChild( table ); + } else { + const message = document.createElement( 'p' ); + message.className = 'progress-agentic-rag__mapping-muted'; + message.textContent = config.strings.mappingNoTerms; + block.appendChild( message ); + } + block.appendChild( fallback ); + + return block; + }; + + const restoreTaxonomyOption = ( taxonomy ) => { + if ( ! taxonomySelect || ! mappingConfig.taxonomies[ taxonomy ] ) { + return; + } + + const option = document.createElement( 'option' ); + option.value = taxonomy; + option.textContent = mappingConfig.taxonomies[ taxonomy ].label; + taxonomySelect.appendChild( option ); + }; + + if ( addMappingButton && taxonomySelect && mappingContainer ) { + addMappingButton.addEventListener( 'click', () => { + const taxonomy = taxonomySelect.value; + + if ( ! taxonomy || ! mappingConfig.taxonomies[ taxonomy ] || mappingContainer.querySelector( '[data-taxonomy="' + selectorValue( taxonomy ) + '"]' ) ) { + return; + } + + mappingContainer.appendChild( buildMappingBlock( taxonomy ) ); + const option = taxonomySelect.querySelector( 'option[value="' + selectorValue( taxonomy ) + '"]' ); + if ( option ) { + option.remove(); + } + taxonomySelect.value = ''; + } ); + + mappingContainer.addEventListener( 'click', ( event ) => { + const button = event.target.closest( '[data-progress-agentic-rag-remove-mapping]' ); + if ( ! button ) { + return; + } + + const block = button.closest( '[data-progress-agentic-rag-mapping-block]' ); + if ( ! block ) { + return; + } + + restoreTaxonomyOption( block.dataset.taxonomy ); + block.remove(); + } ); + + mappingContainer.addEventListener( 'change', async ( event ) => { + const select = event.target.closest( '[data-progress-agentic-rag-labelset-select], [data-progress-agentic-rag-fallback-labelset-select]' ); + if ( ! select ) { + return; + } + + const taxonomy = select.dataset.taxonomy; + const isFallback = select.matches( '[data-progress-agentic-rag-fallback-labelset-select]' ); + const containers = isFallback ? + [ mappingContainer.querySelector( '[data-progress-agentic-rag-fallback-labels][data-taxonomy="' + selectorValue( taxonomy ) + '"]' ) ] : + Array.from( mappingContainer.querySelectorAll( '[data-progress-agentic-rag-label-checkboxes][data-taxonomy="' + selectorValue( taxonomy ) + '"]' ) ); + + containers.filter( Boolean ).forEach( ( container ) => setMappingMessage( container, select.value ? config.strings.mappingLoadingLabels : config.strings.mappingSelectLabels ) ); + if ( ! select.value ) { + return; + } + + try { + const labels = await loadMappingLabels( select.value ); + containers.filter( Boolean ).forEach( ( container ) => { + renderLabelCheckboxes( container, taxonomy, labels, container.dataset.termId || '', isFallback ); + } ); + } catch ( error ) { + containers.filter( Boolean ).forEach( ( container ) => setMappingMessage( container, config.strings.mappingLabelsFailed ) ); + } + } ); + } + + if ( startButton && modal ) { + startButton.addEventListener( 'click', async () => { + setModalMode( 'sync' ); + openModal(); + + if ( syncIsRunning() ) { + try { + renderStatus( await request( 'progress_agentic_rag_manual_sync_status' ) ); + } catch ( error ) { + renderError( error.message ); + } + return; + } + + const postTypes = selectedPostTypes(); + startButton.disabled = true; + + try { + renderStatus( { + status: 'starting', + total: 0, + completed: 0, + failed: 0, + current: '', + message: config.strings.starting, + percent: 0, + } ); + renderStatus( await request( 'progress_agentic_rag_manual_sync_start', { post_types: postTypes } ) ); + } catch ( error ) { + startButton.disabled = false; + renderError( error.message ); + } + } ); + + if ( syncIsRunning() ) { + renderStatus( syncStatus ); + } + + if ( deleteSyncedButton && deleteIsRunning() ) { + renderDeleteStatus( deleteStatus ); + } + + renderBackgroundSyncStatus( backgroundSyncStatus ); + + if ( deleteSyncedButton ) { + deleteSyncedButton.addEventListener( 'click', async () => { + if ( deleteIsRunning() ) { + try { + setModalMode( 'delete' ); + openModal(); + renderDeleteStatus( await request( 'progress_agentic_rag_delete_synced_resources_status' ), true ); + } catch ( error ) { + window.alert( error.message || config.strings.deleteFailed ); + } + return; + } + + const count = currentSyncedCount(); + const message = ( config.strings.confirmDelete || '' ).replace( '%d', String( count ) ); + if ( ! window.confirm( message ) ) { + return; + } + + deleteSyncedButton.disabled = true; + deleteSyncedButton.textContent = config.strings.deleting || defaultDeleteButtonText; + setModalMode( 'delete' ); + openModal(); + + try { + renderDeleteStatus( { + status: 'starting', + total: 0, + deleted: 0, + failed: 0, + current: '', + message: config.strings.deleting, + percent: 0, + } ); + renderDeleteStatus( await request( 'progress_agentic_rag_delete_synced_resources' ), true ); + } catch ( error ) { + deleteSyncedButton.disabled = false; + deleteSyncedButton.textContent = defaultDeleteButtonText; + window.alert( error.message || config.strings.deleteFailed ); + } + } ); + } + + modal.querySelectorAll( selectors.close ).forEach( ( closeButton ) => { + closeButton.addEventListener( 'click', closeModal ); + } ); + } + + if ( labelReprocessButton ) { + labelReprocessButton.addEventListener( 'click', async () => { + const count = currentSyncedCount(); + const message = ( config.strings.confirmReprocess || '' ).replace( '%d', String( count ) ); + if ( ! window.confirm( message ) ) { + return; + } + + labelReprocessButton.disabled = true; + + try { + window.alert( ( await request( 'progress_agentic_rag_label_reprocess_start' ) ).message || '' ); + window.location.reload(); + } catch ( error ) { + labelReprocessButton.disabled = false; + window.alert( error.message || config.strings.reprocessFailed ); + } + } ); + } + + if ( labelReprocessCancelButton ) { + labelReprocessCancelButton.addEventListener( 'click', async () => { + labelReprocessCancelButton.disabled = true; + + try { + await request( 'progress_agentic_rag_label_reprocess_cancel' ); + window.location.reload(); + } catch ( error ) { + labelReprocessCancelButton.disabled = false; + window.alert( error.message || config.strings.reprocessFailed ); + } + } ); + } + +}() ); diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..52eccb5 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,78 @@ +# Progress Agentic RAG for WordPress + +Optimize your WordPress search with Progress Agentic RAG's AI-powered search API. + +> **Disclaimer:** This plugin is provided as-is and is not officially supported software. It is intended for demonstration and educational purposes only. Use at your own risk. No warranty, support, or maintenance is guaranteed. + +## Description + +Improve search on your site with AI-powered capabilities. + +### Features + +- Push your content to Progress Agentic RAG for indexing +- Configure your search widget directly in the Progress Agentic RAG dashboard +- Widget requests are proxied through your WordPress site using the configured credentials + +This plugin requires an Agentic RAG account. You can sign up for a free trial at [Progress Agentic RAG](https://rag.progress.cloud). + +Only published posts (not private) are indexed. If a post's status changes, it will be automatically unindexed. + +### Configuring the Search Widget + +After configuring your credentials in the plugin settings: + +1. Click **Open Progress Agentic RAG Dashboard** on the settings page +2. Navigate to the Widgets section in your dashboard +3. Configure and customize your search widget +4. Copy the generated embed code to your WordPress site + +### About Progress Agentic RAG + +Progress Agentic RAG is an easy-to-use, low-code API enabling developers to build AI-powered search engines for any data and any data source in minutes—without worrying about scalability, data indexing, or the complexity of implementing search systems. + +### Links + +- [Progress Agentic RAG](https://rag.progress.cloud) +- [Development](https://github.com/nuclia/wordpress-plugin) + +## Installation + +From your WordPress dashboard: + +1. Go to **Plugins > Add New > Upload Plugin** +2. Click **Choose File** and select the plugin zip file +3. Click **Install Now** +4. **Activate** Progress Agentic RAG from your Plugins page +5. Click on the new menu item **Progress Agentic RAG** and enter your Zone, Knowledge Box ID, Account ID, and API key +6. Select the post types you want to index and use the buttons to start indexing +7. Click **Open Progress Agentic RAG Dashboard** and navigate to the Widgets section to configure your search widget + +### Minimum Requirements + +- WordPress 6.8+ +- PHP 8.1+ (PHP 8.3 recommended) +- MySQL 5.0+ (MySQL 5.6+ recommended) +- cURL PHP extension +- mbstring PHP extension +- OpenSSL 1.0.1+ +- wp_cron enabled + +## Screenshots + +1. Plugin settings page with credentials and indexing options +2. Widgets dashboard link for search widget configuration + +## Changelog + +### 1.0.0 + +- Initial release + +## License + +GNU General Public License v2.0 / MIT License + +## Contributors + +- Progress Software Corporation diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..f121ef9 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,40 @@ +name: progress-agentic-rag-plugin + +services: + php-lint: + image: php:8.1-cli + working_dir: /plugin + volumes: + - ..:/plugin + command: ["sh", "docker/scripts/lint-php.sh"] + + phpunit: + image: php:8.1-cli + working_dir: /plugin + environment: + PHPUNIT_COVERAGE: ${PHPUNIT_COVERAGE:-0} + volumes: + - ..:/plugin + command: ["sh", "docker/scripts/run-phpunit.sh"] + + playwright: + image: mcr.microsoft.com/playwright:v1.61.0-noble + working_dir: /plugin + environment: + WP_BASE_URL: ${WP_BASE_URL:-http://wordpress} + PROGRESS_AGENTIC_RAG_E2E_KEY: ${PROGRESS_AGENTIC_RAG_E2E_KEY:-progress-agentic-rag-e2e} + PROGRESS_AGENTIC_RAG_TEST_TOKEN: ${PROGRESS_AGENTIC_RAG_TEST_TOKEN:-progress-agentic-rag-secret} + PROGRESS_AGENTIC_RAG_DISABLE_SCHEDULER: ${PROGRESS_AGENTIC_RAG_DISABLE_SCHEDULER:-0} + volumes: + - ..:/plugin + command: ["sh", "docker/scripts/run-playwright.sh"] + + release-check: + image: php:8.1-cli + working_dir: /plugin + environment: + RELEASE_DIR: ${RELEASE_DIR:-build/progress-agentic-rag} + RELEASE_VERSION: ${RELEASE_VERSION:-} + volumes: + - ..:/plugin + command: ["sh", "docker/scripts/check-release.sh"] diff --git a/docker/scripts/check-release-metadata.sh b/docker/scripts/check-release-metadata.sh new file mode 100755 index 0000000..725e81a --- /dev/null +++ b/docker/scripts/check-release-metadata.sh @@ -0,0 +1,82 @@ +#!/bin/sh +set -eu + +release_dir="${RELEASE_DIR:-build/progress-agentic-rag}" +expected_version="${RELEASE_VERSION:-}" +main_file="$release_dir/progress-agentic-rag.php" +readme_file="$release_dir/readme.txt" + +fail() { + echo "$1" + exit 1 +} + +trim() { + sed 's/^[[:space:]]*//; s/[[:space:]]*$//' +} + +validate_release_version() { + if ! printf '%s\n' "$1" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + fail "Release version must use numeric x.y.z format without a v prefix: $1" + fi +} + +if [ ! -d "$release_dir" ]; then + fail "Release directory not found: $release_dir" +fi + +if [ ! -f "$main_file" ]; then + if [ -f "$release_dir/progress-agentic-rag/progress-agentic-rag.php" ]; then + fail "Release artifact is nested under progress-agentic-rag; WordPress.org trunk must contain plugin files directly." + fi + + fail "Main plugin file not found in release root: $main_file" +fi + +if [ ! -f "$readme_file" ]; then + fail "readme.txt not found in release root: $readme_file" +fi + +plugin_header_version="$(awk -F 'Version:[[:space:]]*' '/^[[:space:]]*\*[[:space:]]Version:/ { print $2; exit }' "$main_file" | tr -d '\r' | trim)" +constant_version="$(awk -F "'" '/PROGRESS_AGENTIC_RAG_VERSION/ { print $4; exit }' "$main_file" | tr -d '\r' | trim)" +stable_tag="$(awk -F 'Stable tag:[[:space:]]*' '/^Stable tag:/ { print $2; exit }' "$readme_file" | tr -d '\r' | trim)" + +if [ -z "$plugin_header_version" ]; then + fail "Plugin header Version is missing." +fi + +if [ -z "$constant_version" ]; then + fail "PROGRESS_AGENTIC_RAG_VERSION constant is missing." +fi + +if [ -z "$stable_tag" ]; then + fail "readme.txt Stable tag is missing." +fi + +if [ -z "$expected_version" ]; then + expected_version="$plugin_header_version" +fi + +validate_release_version "$expected_version" + +if [ "$plugin_header_version" != "$expected_version" ]; then + fail "Plugin header Version ($plugin_header_version) does not match release version ($expected_version)." +fi + +if [ "$constant_version" != "$expected_version" ]; then + fail "PROGRESS_AGENTIC_RAG_VERSION ($constant_version) does not match release version ($expected_version)." +fi + +if [ "$stable_tag" = "trunk" ]; then + fail "readme.txt Stable tag must be a numeric release version, not trunk." +fi + +if [ "$stable_tag" != "$expected_version" ]; then + fail "readme.txt Stable tag ($stable_tag) does not match release version ($expected_version)." +fi + +if ! grep -Fq "= $expected_version =" "$readme_file"; then + fail "readme.txt Changelog is missing a heading for $expected_version." +fi + +echo "Release metadata matches version $expected_version." diff --git a/docker/scripts/check-release.sh b/docker/scripts/check-release.sh new file mode 100644 index 0000000..73cae42 --- /dev/null +++ b/docker/scripts/check-release.sh @@ -0,0 +1,21 @@ +#!/bin/sh +set -eu + +release_dir="${RELEASE_DIR:-build/progress-agentic-rag}" +script_dir="$(CDPATH= cd "$(dirname "$0")" && pwd)" + +if [ ! -d "$release_dir" ]; then + echo "Release directory not found: $release_dir" + exit 1 +fi + +sh "$script_dir/check-release-metadata.sh" + +for path in .git .github .distignore .gitignore docker tests node_modules vendor package.json package-lock.json phpunit.xml.dist playwright.config.ts; do + if [ -e "$release_dir/$path" ]; then + echo "Release artifact includes dev-only path: $path" + exit 1 + fi +done + +echo "Release artifact excludes dev-only paths." diff --git a/docker/scripts/lint-php.sh b/docker/scripts/lint-php.sh new file mode 100644 index 0000000..4f8d612 --- /dev/null +++ b/docker/scripts/lint-php.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +find . \ + -path './vendor' -prune -o \ + -path './node_modules' -prune -o \ + -name '*.php' -print0 | xargs -0 -n1 php -l diff --git a/docker/scripts/run-local-plugin-check.sh b/docker/scripts/run-local-plugin-check.sh new file mode 100755 index 0000000..d597af7 --- /dev/null +++ b/docker/scripts/run-local-plugin-check.sh @@ -0,0 +1,27 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) +plugin_root=$(CDPATH= cd -- "$script_dir/../.." && pwd -P) +repo_root=$(CDPATH= cd -- "$plugin_root/../../.." && pwd -P) +build_dir="$plugin_root/build" +fixture_dir="$build_dir/progress-agentic-rag" + +cleanup() { + rm -rf "$fixture_dir" + rmdir "$build_dir" 2>/dev/null || true +} +trap cleanup EXIT HUP INT TERM + +cleanup +mkdir -p "$fixture_dir" +rsync -a \ + --exclude-from="$plugin_root/.distignore" \ + --exclude='/.distignore' \ + --exclude='/.gitignore' \ + "$plugin_root"/ "$fixture_dir"/ + +cd "$repo_root" +WORDPRESS_PORT="${WORDPRESS_PORT:-8080}" WP_URL="${WP_URL:-http://wordpress}" docker compose --profile plugin-check run --rm \ + -v "$fixture_dir:/var/www/html/wp-content/plugins/progress-agentic-rag:ro" \ + plugin-check diff --git a/docker/scripts/run-phpunit.sh b/docker/scripts/run-phpunit.sh new file mode 100644 index 0000000..2c41823 --- /dev/null +++ b/docker/scripts/run-phpunit.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu + +phpunit_phar="${PHPUNIT_PHAR:-/tmp/phpunit-10.5.63.phar}" +phpunit_url="${PHPUNIT_PHAR_URL:-https://phar.phpunit.de/phpunit-10.5.63.phar}" + +if [ ! -f "$phpunit_phar" ]; then + php -r '$url = $argv[1]; $target = $argv[2]; if (! copy($url, $target)) { exit(1); }' "$phpunit_url" "$phpunit_phar" +fi + +if [ "${PHPUNIT_COVERAGE:-0}" = "1" ]; then + if ! php -m | grep -qi '^pcov$'; then + printf "\n" | pecl install pcov-1.0.11 >/tmp/progress-agentic-rag-pcov-install.log + docker-php-ext-enable pcov >/tmp/progress-agentic-rag-pcov-enable.log + fi + + mkdir -p build/coverage + php -d pcov.enabled=1 -d pcov.directory=/plugin/src "$phpunit_phar" -c phpunit.xml.dist --testdox --coverage-text --coverage-clover build/coverage/clover.xml + exit +fi + +php "$phpunit_phar" -c phpunit.xml.dist --testdox diff --git a/docker/scripts/run-playwright.sh b/docker/scripts/run-playwright.sh new file mode 100644 index 0000000..1f3ea69 --- /dev/null +++ b/docker/scripts/run-playwright.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -eu + +if [ ! -d node_modules ]; then + npm ci +fi + +npx playwright test -c playwright.config.ts diff --git a/docker/scripts/run-plugin-check.sh b/docker/scripts/run-plugin-check.sh new file mode 100644 index 0000000..8a1aea9 --- /dev/null +++ b/docker/scripts/run-plugin-check.sh @@ -0,0 +1,156 @@ +#!/bin/sh +set -eu + +wp_path="${WP_PATH:-/var/www/html}" +wp_url="${WP_URL:-http://wordpress}" +plugin_check_version="${PLUGIN_CHECK_VERSION:-2.0.0}" +plugin_check_target="${PLUGIN_CHECK_TARGET:-progress-agentic-rag/progress-agentic-rag.php}" +plugin_check_slug="${PLUGIN_CHECK_SLUG:-progress-agentic-rag}" +plugin_check_categories="${PLUGIN_CHECK_CATEGORIES:-plugin_repo}" +plugin_check_mode="${PLUGIN_CHECK_MODE:-new}" +plugin_check_output="$(mktemp)" + +cleanup() { + rm -f "$plugin_check_output" +} +trap cleanup EXIT + +wp_cli() { + wp --allow-root --path="$wp_path" --url="$wp_url" "$@" +} + +install_plugin_check() { + attempt=1 + max_attempts=5 + delay=5 + + while true; do + if wp_cli plugin install plugin-check --version="$plugin_check_version" --activate --force; then + return 0 + fi + + if [ "$attempt" -ge "$max_attempts" ]; then + echo "Unable to install Plugin Check $plugin_check_version after $attempt attempts." >&2 + return 1 + fi + + attempt=$(( attempt + 1 )) + echo "Plugin Check install failed; retrying in $delay seconds (attempt $attempt of $max_attempts)." >&2 + sleep "$delay" + delay=$(( delay * 2 )) + done +} + +if ! wp_cli core is-installed >/dev/null 2>&1; then + echo "WordPress is not installed at $wp_path." + exit 1 +fi + +if ! wp_cli plugin is-installed plugin-check >/dev/null 2>&1; then + install_plugin_check +else + wp_cli plugin activate plugin-check >/dev/null +fi + +plugin_check_cli="$wp_path/wp-content/plugins/plugin-check/cli.php" +if [ ! -f "$plugin_check_cli" ]; then + echo "Plugin Check CLI loader not found: $plugin_check_cli" + exit 1 +fi + +set +e +wp_cli plugin check "$plugin_check_target" \ + --require="$plugin_check_cli" \ + --categories="$plugin_check_categories" \ + --format=strict-json \ + --fields=file,line,column,type,code,message,docs \ + --mode="$plugin_check_mode" \ + --slug="$plugin_check_slug" \ + --exclude-directories=includes/libraries/action-scheduler \ + > "$plugin_check_output" +plugin_check_status=$? +set -e + +if [ "$plugin_check_status" -ne 0 ]; then + cat "$plugin_check_output" + exit "$plugin_check_status" +fi + +php -r ' +$output = trim( file_get_contents( $argv[1] ) ); + +if ( "" === $output ) { + fwrite( STDERR, "Plugin Check produced no output.\n" ); + exit( 2 ); +} + +if ( 0 === strpos( $output, "Success:" ) ) { + echo $output . PHP_EOL; + exit( 0 ); +} + +$start = strpos( $output, "[" ); +$end = strrpos( $output, "]" ); + +if ( false !== $start && false !== $end && $end >= $start ) { + $output = substr( $output, $start, $end - $start + 1 ); +} + +$results = json_decode( $output, true ); + +if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $results ) ) { + fwrite( STDERR, "Unable to parse Plugin Check strict JSON output.\n" ); + echo $output . PHP_EOL; + exit( 2 ); +} + +$count = count( $results ); + +if ( $count > 0 ) { + fwrite( STDERR, sprintf( "Plugin Check found %d Plugin repo issue(s).\n", $count ) ); + + $types = array(); + $codes = array(); + + foreach ( $results as $result ) { + $type = isset( $result["type"] ) ? $result["type"] : "unknown"; + $code = isset( $result["code"] ) ? $result["code"] : "unknown"; + + $types[ $type ] = isset( $types[ $type ] ) ? $types[ $type ] + 1 : 1; + $codes[ $code ] = isset( $codes[ $code ] ) ? $codes[ $code ] + 1 : 1; + } + + arsort( $types ); + arsort( $codes ); + + fwrite( STDERR, "Issue types:\n" ); + foreach ( $types as $type => $type_count ) { + fwrite( STDERR, sprintf( " %s: %d\n", $type, $type_count ) ); + } + + fwrite( STDERR, "Top issue codes:\n" ); + $shown = 0; + foreach ( $codes as $code => $code_count ) { + fwrite( STDERR, sprintf( " %d %s\n", $code_count, $code ) ); + ++$shown; + if ( 10 <= $shown ) { + break; + } + } + + fwrite( STDERR, "First findings:\n" ); + foreach ( array_slice( $results, 0, 25 ) as $result ) { + $file = isset( $result["file"] ) ? $result["file"] : "(unknown file)"; + $line = isset( $result["line"] ) ? $result["line"] : 0; + $type = isset( $result["type"] ) ? $result["type"] : "unknown"; + $code = isset( $result["code"] ) ? $result["code"] : "unknown"; + $message = isset( $result["message"] ) ? html_entity_decode( $result["message"], ENT_QUOTES ) : ""; + + fwrite( STDERR, sprintf( " %s:%s %s %s - %s\n", $file, $line, $type, $code, $message ) ); + } + + exit( 1 ); +} +' "$plugin_check_output" + +echo "Plugin Check found no Plugin repo issues." diff --git a/includes/admin/class-nuclia-admin-page-settings.php b/includes/admin/class-nuclia-admin-page-settings.php deleted file mode 100644 index 0e603cc..0000000 --- a/includes/admin/class-nuclia-admin-page-settings.php +++ /dev/null @@ -1,1472 +0,0 @@ -plugin = $plugin; - $this->settings = $plugin->get_settings(); - - add_action( 'admin_menu', [ $this, 'add_page' ] ); - add_action( 'admin_init', [ $this, 'add_settings' ] ); - add_action( 'admin_notices', [ $this, 'display_errors' ] ); - - // Display a link to this page from the plugins page. - add_filter( 'plugin_action_links_' . PROGRESS_NUCLIA_PLUGIN_BASENAME, [ $this, 'add_action_links' ] ); - - // AJAX handlers for background processing - add_action( 'wp_ajax_nuclia_schedule_indexing', [ $this, 'ajax_schedule_indexing' ] ); - add_action( 'wp_ajax_nuclia_cancel_indexing', [ $this, 'ajax_cancel_indexing' ] ); - add_action( 'wp_ajax_nuclia_get_indexing_status', [ $this, 'ajax_get_indexing_status' ] ); - add_action( 'wp_ajax_nuclia_get_labelset_labels', [ $this, 'ajax_get_labelset_labels' ] ); - add_action( 'wp_ajax_nuclia_clear_synced_files', [ $this, 'ajax_clear_synced_files' ] ); - - // AJAX handlers for label reprocessing - add_action( 'wp_ajax_nuclia_reprocess_labels', [ $this, 'ajax_reprocess_labels' ] ); - add_action( 'wp_ajax_nuclia_cancel_reprocess_labels', [ $this, 'ajax_cancel_reprocess_labels' ] ); - add_action( 'wp_ajax_nuclia_get_reprocess_status', [ $this, 'ajax_get_reprocess_status' ] ); - } - - /** - * Add action links. - * - * @since 1.0.0 - * - * @param array $links Array of action links. - * - * @return array - */ - public function add_action_links( array $links ): array { - return array_merge( - $links, - [ - '' . esc_html__( 'Settings', 'progress-agentic-rag' ) . '', - ] - ); - } - - /** - * Add admin menu page. - * - * @since 1.0.0 - * - * @return void The resulting page's hook_suffix or false on failure. - */ - public function add_page(): void { - - add_menu_page( - 'Progress Agentic RAG', - esc_html__( 'Progress Agentic RAG', 'progress-agentic-rag' ), - $this->capability, - $this->slug, - [ $this, 'display_page' ], - 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2aWV3Qm94PSI3LjE0OCAxMy40NTYgOTEuMDM1IDk0LjAzNyIgd2lkdGg9IjkxLjAzNSIgaGVpZ2h0PSI5NC4wMzciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnM+CiAgICA8c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZDkxYjt9LmNscy0ye2ZpbGw6IzI1MDBmZjt9LmNscy0ze2ZpbGw6I2ZmMDA2YTt9PC9zdHlsZT4KICA8L2RlZnM+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNOTEuNjYsMzUuNzgsNTMuNDcsMTQuNDNhLjE5LjE5LDAsMCwwLS4xOCwwTDE0Ljk0LDM1LjQ5YS4xOS4xOSwwLDAsMCwwLC4zM0w1MC40LDU1LjUyYS4xOS4xOSwwLDAsMCwuMTgsMCw1LjQ3LDUuNDcsMCwwLDEsNS43MS4xMy4xNy4xNywwLDAsMCwuMTgsMEw5MS42NiwzNi4xMUEuMTkuMTksMCwwLDAsOTEuNjYsMzUuNzhaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNTguNzcsNjAuMDhhLjcxLjcxLDAsMCwxLDAsLjE0QTUuNDcsNS40NywwLDAsMSw1Niw2NWEuMTYuMTYsMCwwLDAtLjA5LjE1djQxYS4xOS4xOSwwLDAsMCwuMjguMTZMOTQuNDEsODUuMTFhLjIuMiwwLDAsMCwuMDktLjE3VjQwLjU1YS4xOC4xOCwwLDAsMC0uMjctLjE3WiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTUxLjA1LDY1LjI5djQxYS4xOC4xOCwwLDAsMS0uMjcuMTZMMTIuMjEsODVhLjIxLjIxLDAsMCwxLS4xLS4xN1Y0MC4yN2EuMTkuMTksMCwwLDEsLjI4LS4xNkw0Ny45LDU5LjgzYzAsLjEzLDAsLjI2LDAsLjM5QTUuNDYsNS40NiwwLDAsMCw1MSw2NS4xMy4xOC4xOCwwLDAsMSw1MS4wNSw2NS4yOVoiLz4KPC9zdmc+' - ); - - } - - /** - * Add settings. - * - * @since 1.0.0 - */ - public function add_settings(): void { - add_settings_section( - 'nuclia_section_settings', - null, - [ $this, 'print_settings_section' ], - $this->slug - ); - - add_settings_field( - 'nuclia_zone', - esc_html__( 'Zone', 'progress-agentic-rag' ), - [ $this, 'zone_callback' ], - $this->slug, - 'nuclia_section_settings' - ); - - add_settings_field( - 'nuclia_kbid', - esc_html__( 'Knowledge Box ID', 'progress-agentic-rag' ), - [ $this, 'kbid_callback' ], - $this->slug, - 'nuclia_section_settings' - ); - - add_settings_field( - 'nuclia_account_id', - esc_html__( 'Account ID', 'progress-agentic-rag' ), - [ $this, 'account_id_callback' ], - $this->slug, - 'nuclia_section_settings' - ); - - add_settings_field( - 'nuclia_token', - esc_html__( 'Token', 'progress-agentic-rag' ), - [ $this, 'token_callback' ], - $this->slug, - 'nuclia_section_settings' - ); - - - add_settings_field( - 'nuclia_indexable_post_types', - esc_html__( 'Post types to index', 'progress-agentic-rag' ), - [ $this, 'indexable_post_types_callback' ], - $this->slug, - 'nuclia_section_settings' - ); - - add_settings_field( - 'nuclia_taxonomy_label_map', - esc_html__( 'Taxonomy label mapping', 'progress-agentic-rag' ), - [ $this, 'taxonomy_label_map_callback' ], - $this->slug, - 'nuclia_section_settings' - ); - - register_setting( - 'nuclia_settings', - 'nuclia_zone', - [ - 'type' => 'text', - 'sanitize_callback' => [ $this, 'sanitize_zone' ] - ] - ); - - register_setting( - 'nuclia_settings', - 'nuclia_kbid', - [ - 'type' => 'text', - 'sanitize_callback' => [ $this, 'sanitize_kbid' ] - ] - ); - - register_setting( - 'nuclia_settings', - 'nuclia_account_id', - [ - 'type' => 'text', - 'sanitize_callback' => [ $this, 'sanitize_account_id' ] - ] - ); - - register_setting( - 'nuclia_settings', - 'nuclia_token', - [ - 'type' => 'text', - 'sanitize_callback' => [ $this, 'sanitize_token' ] - ] - ); - - register_setting( - 'nuclia_settings', - 'nuclia_indexable_post_types', - [ - 'type' => 'array', - 'sanitize_callback' => [ $this, 'sanitize_indexable_post_types' ] - ] - ); - - register_setting( - 'nuclia_settings', - 'nuclia_taxonomy_label_map', - [ - 'type' => 'array', - 'sanitize_callback' => [ $this, 'sanitize_taxonomy_label_map' ] - ] - ); - } - - /** - * Zone callback. - * - * @since 1.0.0 - */ - public function zone_callback(): void { - $settings = $this->plugin->get_settings(); - $setting = $settings->get_zone(); - ?> - -

- -

-plugin->get_settings(); - $setting = $settings->get_token(); - ?> - -

- -

-plugin->get_settings(); - $setting = $settings->get_kbid(); - ?> - -

- -

-plugin->get_settings(); - $setting = $settings->get_account_id(); - ?> - -

- -

-plugin->get_settings(); - $background_processor = $this->plugin->get_background_processor(); - - // current value - $indexable_post_types = $this->plugin->get_indexable_post_types(); - - // registered searchable post types - $args = apply_filters( 'nuclia_searchable_post_types', - [ - 'public' => true, - 'exclude_from_search' => false - ] - ); - - $searchable_post_types = get_post_types( - $args, - 'names' - ); - - // Get overall background processing status - $bg_status = $background_processor->get_status(); - - foreach ( $searchable_post_types as $post_type ) : - $indexed = $this->count_indexed_posts( $post_type ); - $indexables = $this->count_indexable_posts( $post_type ); - $pending_for_type = $background_processor->get_pending_count_for_post_type( $post_type ); - ?> -

- - - - get_api_is_reachable() ) : ?> -   - - 0 ) : ?> - - - - - - 0 ) : ?> - - - - 0 ) : ?> - - - -

-get_api_is_reachable() ) : - $total_pending = $bg_status['pending']; - $total_running = $bg_status['running']; - $total_failed = $bg_status['failed']; - ?> -
-

- - | - - 0 ) : ?> - | - -

- 0 || $total_running > 0 ) : ?> -

- -

- -
- -
-

- -

-
- -
-

-

- -
-plugin->get_settings(); - $mapping = $settings->get_taxonomy_label_map(); - $labelsets = $this->plugin->get_api()->get_labelsets(); - - $taxonomies = get_taxonomies( - [ - 'public' => true, - ], - 'objects' - ); - - if ( empty( $taxonomies ) ) { - echo '

' . esc_html__( 'No public taxonomies available for mapping.', 'progress-agentic-rag' ) . '

'; - return; - } - - echo '

' . esc_html__( 'Map WordPress taxonomy terms to Nuclia labels. Labelset suggestions come from your Nuclia Knowledge Box.', 'progress-agentic-rag' ) . '

'; - - $mapped_taxonomies = array_keys( $mapping ); - - echo '
'; - echo ' '; - echo ' '; - echo ''; - echo '
'; - - echo '
'; - - foreach ( $mapping as $taxonomy_key => $config ) { - if ( ! isset( $taxonomies[ $taxonomy_key ] ) ) { - continue; - } - - $taxonomy = $taxonomies[ $taxonomy_key ]; - $taxonomy_labelset = $config['labelset'] ?? ''; - $term_map = $config['terms'] ?? []; - $fallback_config = is_array( $config['fallback'] ?? null ) ? $config['fallback'] : []; - $fallback_labelset = $fallback_config['labelset'] ?? ''; - $fallback_labels = is_array( $fallback_config['labels'] ?? null ) ? $fallback_config['labels'] : []; - - echo '
'; - echo '
'; - echo '
'; - echo '

' . esc_html( $taxonomy->labels->name ) . '

'; - echo '

' . esc_html( $taxonomy->name ) . '

'; - echo '
'; - echo ''; - echo '
'; - - echo ' '; - echo ''; - - if ( empty( $labelsets ) ) { - echo '

' . esc_html__( 'No labelsets available. Check your Nuclia credentials.', 'progress-agentic-rag' ) . '

'; - } - - $terms = get_terms( - [ - 'taxonomy' => $taxonomy_key, - 'hide_empty' => false, - ] - ); - - if ( empty( $terms ) || is_wp_error( $terms ) ) { - echo '

' . esc_html__( 'No terms available for this taxonomy.', 'progress-agentic-rag' ) . '

'; - } else { - $labels = $this->plugin->get_api()->get_labelset_labels( (string) $taxonomy_labelset ); - echo ''; - echo ''; - echo ''; - - foreach ( $terms as $term ) { - $term_labels = $term_map[ $term->term_id ] ?? []; - if ( ! is_array( $term_labels ) ) { - $term_labels = $term_labels !== '' ? [ (string) $term_labels ] : []; - } - echo ''; - echo ''; - echo ''; - echo ''; - } - - echo '
' . esc_html__( 'Term', 'progress-agentic-rag' ) . '' . esc_html__( 'Nuclia labels', 'progress-agentic-rag' ) . '
' . esc_html( $term->name ) . ''; - echo '
'; - foreach ( $labels as $label ) { - $checked = in_array( $label, $term_labels, true ) ? 'checked="checked"' : ''; - echo ''; - } - if ( empty( $labels ) ) { - echo '' . esc_html__( 'No labels available.', 'progress-agentic-rag' ) . ''; - } - echo '
'; - echo '
'; - if ( $taxonomy_labelset !== '' && empty( $labels ) ) { - echo '

' . esc_html__( 'No labels found for the selected labelset. Please verify the labelset exists in Nuclia and reload the page.', 'progress-agentic-rag' ) . '

'; - } - } - - echo '
'; - echo '

' . esc_html__( 'Fallback labels (when no terms assigned)', 'progress-agentic-rag' ) . '

'; - echo ' '; - echo ''; - - $fallback_available_labels = $fallback_labelset !== '' ? $this->plugin->get_api()->get_labelset_labels( (string) $fallback_labelset ) : []; - echo '
'; - if ( $fallback_labelset === '' ) { - echo '' . esc_html__( 'Select a labelset to load labels.', 'progress-agentic-rag' ) . ''; - } elseif ( empty( $fallback_available_labels ) ) { - echo '' . esc_html__( 'No labels available.', 'progress-agentic-rag' ) . ''; - } else { - foreach ( $fallback_available_labels as $label ) { - $checked = in_array( $label, $fallback_labels, true ) ? 'checked="checked"' : ''; - echo ''; - } - } - echo '
'; - echo '
'; - echo '
'; - } - - echo '
'; - - // Add label reprocessing section - if ( $settings->get_api_is_reachable() ) { - $indexed_count = $this->count_all_indexed_posts(); - $reprocess_status = $this->plugin->get_label_reprocessor()->get_status(); - - echo '
'; - echo '

' . esc_html__( 'Label Reprocessing', 'progress-agentic-rag' ) . '

'; - echo '

' . esc_html__( 'When you change the taxonomy-to-label mappings above, existing synced resources do not automatically get their labels updated. Use this to reprocess all synced resources with the new label mappings (no file re-upload required).', 'progress-agentic-rag' ) . '

'; - - echo '

'; - echo '' . esc_html__( 'Synced resources:', 'progress-agentic-rag' ) . ' '; - echo '' . esc_html( $indexed_count ) . ''; - echo '

'; - - // Status display - echo '
'; - if ( $reprocess_status['is_active'] ) { - echo '

'; - echo ' '; - echo '' . sprintf( esc_html__( '%d pending', 'progress-agentic-rag' ), $reprocess_status['pending'] ) . ''; - echo ' | ' . sprintf( esc_html__( '%d running', 'progress-agentic-rag' ), $reprocess_status['running'] ) . ''; - if ( $reprocess_status['failed'] > 0 ) { - echo ' | ' . sprintf( esc_html__( '%d failed', 'progress-agentic-rag' ), $reprocess_status['failed'] ) . ''; - } - echo '

'; - echo '

'; - echo ''; - echo '

'; - } else { - echo '

'; - if ( $indexed_count > 0 ) { - echo ''; - } else { - echo '' . esc_html__( 'No synced resources to reprocess.', 'progress-agentic-rag' ) . ''; - } - echo '

'; - } - echo '
'; - - echo '
'; - } - } - - /** - * Count all indexed posts across all post types. - * - * @since 1.4.0 - * - * @return int - */ - private function count_all_indexed_posts(): int { - return count( $this->plugin->get_api()->get_all_indexed_posts() ); - } - - /** - * Get post type name - * - * @since 1.0.0 - * - * @param string $post_type The post type slug. - * - * @return string - */ - public function get_post_type_name( string $post_type ): string { - $post_type_object = get_post_type_object( $post_type ); - if ( $post_type_object !== NULL ) { - return $post_type_object->labels->name; - }; - return ''; - } - /** - * Returns the number of indexed posts - * - * @since 1.1.0 - * - * @param string $post_type The post type slug. - * - * @return string - */ - public function count_indexed_posts( string $post_type ): string { - if ( ! post_type_exists( $post_type ) ) { - return '0'; - } - - global $wpdb; - - $post_status = ( $post_type !== 'attachment' ) ? 'publish' : 'inherit'; - - $req = $wpdb->prepare( - "SELECT COUNT( DISTINCT p.ID ) - FROM {$wpdb->posts} AS p - INNER JOIN {$wpdb->prefix}agentic_rag_for_wp AS pm - ON ( p.ID = pm.post_id ) - WHERE p.post_type = %s - AND p.post_status = %s", - $post_type, - $post_status - ); - - $count = $wpdb->get_var( $req ); - - if ( ! empty( $wpdb->last_error ) ) { - return 'X'; - } - - return $count; - } - - /** - * Returns the number of indexable posts - * - * @since 1.1.0 - * - * @param string $post_type The post type slug. - * - * @return int - */ - public function count_indexable_posts( string $post_type ): int { - nuclia_log( 'count_indexable_posts: '.$post_type ); - if ( ! post_type_exists( $post_type ) ) { - return 0; - } - - global $wpdb; - $post_status = ( $post_type !== 'attachment' ) ? 'publish' : 'inherit'; - - $limit = 100; - $offset = 0; - $indexable_posts = []; - - do { - $results = $wpdb->get_results( - $wpdb->prepare( - "SELECT p.ID FROM {$wpdb->posts} AS p - LEFT JOIN {$wpdb->prefix}agentic_rag_for_wp AS pm ON ( p.ID = pm.post_id ) - WHERE pm.post_id IS NULL AND p.post_type = %s AND p.post_status = %s - LIMIT %d OFFSET %d", - $post_type, - $post_status, - $limit, - $offset - ) - ); - - if ( ! empty( $wpdb->last_error ) ) { - return 0; - } - - $indexable_posts = array_merge( $indexable_posts, wp_list_pluck( $results, 'ID' ) ); - $offset += $limit; - } while ( count( $results ) === $limit ); - - update_option( 'nuclia_indexable_' . $post_type, $indexable_posts ); - - return count( $indexable_posts ); - } - - - /** - * Sanitize Knowledge box UID. - * - * @since 1.0.0 - * - * @param string $value The value to sanitize. - * - * @return string - */ - public function sanitize_kbid( string $value ): string { - - $value = sanitize_text_field( $value ); - - $settings = $this->plugin->get_settings(); - - if ( empty( $value ) ) { - add_settings_error( - 'nuclia_settings', - 'empty', - esc_html__( 'Knowledge box UID should not be empty.', 'progress-agentic-rag' ) - ); - $settings->set_api_is_reachable( false ); - return $value; - } - - - $valid_credentials = true; - try { - self::is_valid_credentials( $settings->get_zone(), $value, $settings->get_token() ); - } catch ( Exception $exception ) { - $valid_credentials = false; - add_settings_error( - 'nuclia_settings', - 'login_exception', - $exception->getMessage() - ); - } - - if ( ! $valid_credentials ) { - add_settings_error( - 'nuclia_settings', - 'no_connection', - esc_html__( - 'We were unable to authenticate you against the Progress Agentic RAG servers with the provided information. Please ensure that you used a valid Zone and Knowledge Box ID.', - 'progress-agentic-rag' - ) - ); - $settings->set_api_is_reachable( false ); - }; - - return $value; - } - - /** - * Sanitize zone. - * - * @since 1.0.0 - * - * @param string $value The value to sanitize. - * - * @return string - */ - public function sanitize_zone( string $value ): string { - - $value = sanitize_text_field( $value ); - - if ( empty( $value ) ) { - add_settings_error( - 'nuclia_settings', - 'empty', - esc_html__( 'Zone should not be empty.', 'progress-agentic-rag' ) - ); - $settings = $this->plugin->get_settings(); - $settings->set_api_is_reachable( false ); - } - - return $value; - } - - /** - * Sanitize account ID. - * - * @since 1.2.0 - * - * @param string $value The value to sanitize. - * - * @return string - */ - public function sanitize_account_id( string $value ): string { - $value = sanitize_text_field( $value ); - - if ( empty( $value ) ) { - add_settings_error( - 'nuclia_settings', - 'empty_account_id', - esc_html__( 'Account ID should not be empty.', 'progress-agentic-rag' ) - ); - $settings = $this->plugin->get_settings(); - $settings->set_api_is_reachable( false ); - } - - return $value; - } - - /** - * Sanitize Service Access token. - * - * @since 1.0.0 - * - * @param string $value The value to sanitize. - * - * @return string - */ - public function sanitize_token( string $value ): string { - - $value = sanitize_text_field( $value ); - - $settings = $this->plugin->get_settings(); - - if ( empty( $value ) ) { - add_settings_error( - 'nuclia_settings', - 'empty', - esc_html__( 'Service Access token should not be empty.', 'progress-agentic-rag' ) - ); - $settings->set_api_is_reachable( false ); - return $value; - } - - - if ( ! $this->is_valid_token( $settings->get_zone(), $settings->get_kbid(), $value ) ) { - add_settings_error( - 'nuclia_settings', - 'wrong_token', - esc_html__( - 'It looks like your token is wrong.', - 'progress-agentic-rag' - ) - ); - $settings->set_api_is_reachable( false ); - } else { - add_settings_error( - 'nuclia_settings', - 'connection_success', - esc_html__( 'We succesfully managed to connect to the Progress Agentic RAG servers with the provided information. Background indexing has been scheduled.', 'progress-agentic-rag' ), - 'updated' - ); - $settings->set_api_is_reachable( true ); - - // Trigger background indexing for all enabled post types - do_action( 'nuclia_schedule_full_reindex' ); - } - - return $value; - } - - /** - * Sanitize indexable post_types - * - * @since 1.0.0 - * - * @param array|mixed $value The data to sanitize. - * - * @return array - */ - public function sanitize_indexable_post_types( mixed $value ): array { - $settings = $this->plugin->get_settings(); - - if ( is_array( $value ) ) { - - foreach( $value as $post_type => $checked ) { - // remove disabled post types - if ( !$checked ) { - unset( $value[$post_type] ); - } - } - - } else { - $value = []; - } - - // no post type selected, display a notice - if ( empty( $value ) ) { - add_settings_error( - 'nuclia_settings', - 'nothing_to_index', - esc_html__( - 'No post type selected. No indexing will take place.', - 'progress-agentic-rag' - ) - ); - $settings->set_api_is_reachable( false ); - } - - return $value; - } - - /** - * Sanitize taxonomy label map. - * - * @since 1.2.0 - * - * @param array|mixed $value - * - * @return array - */ - public function sanitize_taxonomy_label_map( mixed $value ): array { - if ( ! is_array( $value ) ) { - return []; - } - - $sanitized = []; - - foreach ( $value as $taxonomy => $config ) { - $taxonomy = sanitize_key( (string) $taxonomy ); - if ( ! taxonomy_exists( $taxonomy ) || ! is_array( $config ) ) { - continue; - } - - $labelset = isset( $config['labelset'] ) ? sanitize_text_field( (string) $config['labelset'] ) : ''; - $terms = is_array( $config['terms'] ?? null ) ? $config['terms'] : []; - $clean_terms = []; - $fallback = is_array( $config['fallback'] ?? null ) ? $config['fallback'] : []; - $fallback_labelset = isset( $fallback['labelset'] ) ? sanitize_text_field( (string) $fallback['labelset'] ) : ''; - $fallback_labels = is_array( $fallback['labels'] ?? null ) ? $fallback['labels'] : []; - $clean_fallback_labels = []; - - foreach ( $terms as $term_id => $labels ) { - $term_id = (int) $term_id; - if ( $term_id <= 0 || ! term_exists( $term_id, $taxonomy ) ) { - continue; - } - - if ( ! is_array( $labels ) ) { - $labels = $labels !== '' ? [ (string) $labels ] : []; - } - - $clean_labels = []; - foreach ( $labels as $label ) { - $label = sanitize_text_field( (string) $label ); - if ( $label === '' ) { - continue; - } - $clean_labels[] = $label; - } - - $clean_labels = array_values( array_unique( $clean_labels ) ); - if ( empty( $clean_labels ) ) { - continue; - } - - $clean_terms[ $term_id ] = $clean_labels; - } - - foreach ( $fallback_labels as $label ) { - $label = sanitize_text_field( (string) $label ); - if ( $label === '' ) { - continue; - } - $clean_fallback_labels[] = $label; - } - - $clean_fallback_labels = array_values( array_unique( $clean_fallback_labels ) ); - $has_term_mapping = ( $labelset !== '' && ! empty( $clean_terms ) ); - $has_fallback = ( $fallback_labelset !== '' && ! empty( $clean_fallback_labels ) ); - - if ( $has_term_mapping || $has_fallback ) { - $sanitized[ $taxonomy ] = []; - if ( $has_term_mapping ) { - $sanitized[ $taxonomy ]['labelset'] = $labelset; - $sanitized[ $taxonomy ]['terms'] = $clean_terms; - } - if ( $has_fallback ) { - $sanitized[ $taxonomy ]['fallback'] = [ - 'labelset' => $fallback_labelset, - 'labels' => $clean_fallback_labels, - ]; - } - } - } - - return $sanitized; - } - - /** - * Assert that the credentials are valid. - * - * @since 1.0.0 - * - * @param string $zone The Nuclia Zone. - * @param string $kbid The Nuclia KBID. - * - * @return bool - * @throws Exception - */ - public static function is_valid_credentials( string $zone, string $kbid, string $token ): bool { - $endpoint = sprintf( 'https://%1s.rag.progress.cloud/api/v1/kb/%2s',$zone,$kbid); - $headers = [ - 'X-NUCLIA-SERVICEACCOUNT' => "Bearer {$token}" - ]; - $args = [ - 'headers' => $headers - ]; - $response = wp_remote_get( $endpoint, $args ); - - if ( is_wp_error( $response ) ) { - //bad zone - throw new Exception( - __('Cannot connect to Progress Agentic RAG API, please check your Nuclia zone : '.$response->get_error_message(), 'progress-agentic-rag') - ); - } - - $response_code = wp_remote_retrieve_response_code( $response ); - if( $response_code === 200 ) { - return true; - } elseif( $response_code === 422 ) { - throw new Exception( - __('Cannot connect to Progress Agentic RAG API, please check your Knowledge Box ID.', 'progress-agentic-rag') - ); - } else { - throw new Exception( - __('Cannot connect to Progress Agentic RAG API, no response from the server.', 'progress-agentic-rag') - ); - }; - } - - /** - * Check if the token is valid. - * - * @since 1.0.0 - * - * @param string $zone The Nuclia Zone. - * @param string $kbid The Nuclia KBID. - * @param string $token The Nuclia Search API Key. - * - * @return bool - */ - public static function is_valid_token( string $zone, string $kbid, string $token ): bool { - - $endpoint = sprintf( 'https://%1s.rag.progress.cloud/api/v1/kb/%2s',$zone,$kbid); - $args = [ - 'headers' => [ - 'X-NUCLIA-SERVICEACCOUNT' => "Bearer {$token}" - ] - ]; - $response = wp_remote_get( $endpoint, $args ); - - if ( is_wp_error( $response ) ) { - return false; - }; - $response_code = wp_remote_retrieve_response_code( $response ); - return $response_code === 200; - } - - - /** - * Display the page. - * - * @since 1.0.0 - */ - public function display_page(): void { - require_once dirname( __FILE__ ) . '/partials/page-settings.php'; - } - - /** - * Display errors. - * - * @since 1.0.0 - */ - public function display_errors(): void { - settings_errors( 'nuclia_settings' ); - } - - /** - * Print the settings section header. - * - * @since 1.0.0 - */ - public function print_settings_section(): void { - echo ''; - - echo '
'; - echo '
'; - echo '

' . esc_html__( 'Progress Agentic RAG setup guide', 'progress-agentic-rag' ) . '

'; - echo '

' . wp_kses_post( - sprintf( - __( - 'Find your zone, token, knowledge base ID, and account ID in your Progress Agentic RAG cloud account. Create an account at %1$s and sign in at %2$s.', - 'progress-agentic-rag' - ), - 'rag.progress.cloud/user/signup', - 'rag.progress.cloud/user/login' - ) - ) . '

'; - echo '
'; - echo '
'; - echo '
'; - echo '
'; - echo '

' . esc_html__( '1) Connect your account', 'progress-agentic-rag' ) . '

'; - echo '

' . esc_html__( 'After you save your zone, knowledge box ID, account ID, and API key, the plugin validates them against Progress Agentic RAG servers to ensure everything is correct.', 'progress-agentic-rag' ) . '

'; - echo '
'; - // Configure Widgets section - echo '
'; - echo '

' . esc_html__( '2) Configure your search widget', 'progress-agentic-rag' ) . '

'; - echo '

' . esc_html__( 'Visit the Progress Agentic RAG dashboard to configure and customize your search widget. Go to the Widgets section to generate embed code for your site.', 'progress-agentic-rag' ) . '

'; - echo ''; - echo esc_html__( 'Open Progress Agentic RAG Dashboard', 'progress-agentic-rag' ); - echo ''; - echo '
'; - echo '
'; - echo '
'; - echo '
'; - - // Embed code instructions section - $settings = $this->plugin->get_settings(); - $zone = $settings->get_zone(); - $proxy_url = $zone ? nuclia_proxy_url( $zone ) : ''; - - if ( $proxy_url && $settings->get_api_is_reachable() ) { - echo '
'; - echo '
'; - echo '

' . esc_html__( 'Using the Search Widget', 'progress-agentic-rag' ) . '

'; - echo '

' . esc_html__( 'Copy the embed code from your dashboard, then modify it to use your WordPress proxy. This keeps your API token secure on the server.', 'progress-agentic-rag' ) . '

'; - echo '
'; - echo '
'; - - echo '
'; - echo '

' . esc_html__( 'Step 1: Copy from Dashboard', 'progress-agentic-rag' ) . '

'; - echo '

' . esc_html__( 'In the Progress Agentic RAG dashboard, go to Widgets and copy the embed code. It will look like this:', 'progress-agentic-rag' ) . '

'; - echo '
<script src="https://cdn.rag.progress.cloud/nuclia-widget.umd.js"></script> -<nuclia-search-bar - knowledgebox="your-kbid" - zone="' . esc_attr( $zone ) . '" - apikey="YOUR_API_TOKEN" - features="answers,rephrase,filter,suggestions" - ... -></nuclia-search-bar> -<nuclia-search-results></nuclia-search-results>
'; - echo '
'; - - echo '
'; - echo '

' . esc_html__( 'Step 2: Replace the API Key', 'progress-agentic-rag' ) . '

'; - echo '

' . esc_html__( 'Replace the apikey attribute with backend and proxy attributes. Your proxy URL is:', 'progress-agentic-rag' ) . '

'; - echo '
' . esc_html( $proxy_url ) . '
'; - echo '

' . esc_html__( 'Change this:', 'progress-agentic-rag' ) . '

'; - echo '
apikey="YOUR_API_TOKEN"
'; - echo '

' . esc_html__( 'To this:', 'progress-agentic-rag' ) . '

'; - - $replacement_code = 'backend="' . $proxy_url . '" proxy="true"'; - echo '
'; - echo ''; - echo '' . esc_html( $replacement_code ) . '
'; - echo '
'; - - echo '
'; - echo '

' . esc_html__( 'Step 3: Final Code', 'progress-agentic-rag' ) . '

'; - echo '

' . esc_html__( 'Your modified embed code:', 'progress-agentic-rag' ) . '

'; - - $final_code = ' - -'; - - echo '
'; - echo ''; - echo '' . esc_html( $final_code ) . '
'; - - echo '

'; - echo ' '; - echo esc_html__( 'Your API token stays on the server. All requests are proxied through your WordPress site, which adds authentication server-side.', 'progress-agentic-rag' ); - echo '

'; - echo '
'; - - echo '
'; - echo '
'; - } - - echo '

'. esc_html__("Your Progress Agentic RAG credentials", 'progress-agentic-rag').'

'; - } - - /** - * AJAX handler to schedule indexing for a post type. - * - * @since 1.1.0 - */ - public function ajax_schedule_indexing(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_reindex_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $post_type = sanitize_text_field( $_POST['post_type'] ?? '' ); - - if ( empty( $post_type ) ) { - // Schedule all post types - do_action( 'nuclia_schedule_full_reindex' ); - wp_send_json_success( [ - 'message' => __( 'Background indexing scheduled for all post types.', 'progress-agentic-rag' ), - 'status' => $this->plugin->get_background_processor()->get_status(), - ] ); - } else { - // Schedule specific post type - $this->plugin->get_background_processor()->schedule_post_type( $post_type ); - wp_send_json_success( [ - 'message' => sprintf( __( 'Background indexing scheduled for %s.', 'progress-agentic-rag' ), $post_type ), - 'status' => $this->plugin->get_background_processor()->get_status(), - 'post_type' => $post_type, - 'pending' => $this->plugin->get_background_processor()->get_pending_count_for_post_type( $post_type ), - ] ); - } - } - - /** - * AJAX handler to cancel all pending indexing. - * - * @since 1.1.0 - */ - public function ajax_cancel_indexing(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_reindex_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $post_type = sanitize_text_field( $_POST['post_type'] ?? '' ); - - if ( empty( $post_type ) ) { - // Cancel all - $this->plugin->get_background_processor()->cancel_all(); - wp_send_json_success( [ - 'message' => __( 'All pending indexing jobs cancelled.', 'progress-agentic-rag' ), - 'status' => $this->plugin->get_background_processor()->get_status(), - ] ); - } else { - // Cancel specific post type - $this->plugin->get_background_processor()->cancel_post_type( $post_type ); - wp_send_json_success( [ - 'message' => sprintf( __( 'Pending indexing jobs cancelled for %s.', 'progress-agentic-rag' ), $post_type ), - 'status' => $this->plugin->get_background_processor()->get_status(), - 'post_type' => $post_type, - ] ); - } - } - - /** - * AJAX handler to get current indexing status. - * - * @since 1.1.0 - */ - public function ajax_get_indexing_status(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_reindex_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $status = $this->plugin->get_background_processor()->get_status(); - - // Get per-post-type pending counts - $indexable_post_types = $this->plugin->get_indexable_post_types(); - $per_type_pending = []; - - foreach ( array_keys( $indexable_post_types ) as $post_type ) { - $per_type_pending[ $post_type ] = $this->plugin->get_background_processor()->get_pending_count_for_post_type( $post_type ); - } - - wp_send_json_success( [ - 'status' => $status, - 'per_type_pending' => $per_type_pending, - ] ); - } - - /** - * AJAX handler to get labelset labels. - * - * @since 1.2.0 - */ - public function ajax_get_labelset_labels(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_labels_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $labelset = sanitize_text_field( $_POST['labelset'] ?? '' ); - if ( $labelset === '' ) { - wp_send_json_success( [ 'labels' => [] ] ); - } - - $labels = $this->plugin->get_api()->get_labelset_labels( $labelset ); - wp_send_json_success( [ 'labels' => $labels ] ); - } - - /** - * AJAX handler to clear all synced files cache. - * - * @since 1.3.0 - */ - public function ajax_clear_synced_files(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_clear_synced_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $result = $this->plugin->get_api()->clear_all_indexed(); - - if ( $result === false ) { - wp_send_json_error( 'Failed to clear synced files cache.', 500 ); - } - - wp_send_json_success( [ - 'message' => __( 'Synced files cache cleared successfully. All posts will need to be re-synced.', 'progress-agentic-rag' ), - ] ); - } - - /** - * AJAX handler to start label reprocessing. - * - * @since 1.4.0 - */ - public function ajax_reprocess_labels(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_labels_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $scheduled_count = $this->plugin->get_label_reprocessor()->schedule_full_reprocess(); - - wp_send_json_success( [ - 'message' => sprintf( - /* translators: %d is the number of posts scheduled */ - _n( 'Scheduled label update for %d post.', 'Scheduled label updates for %d posts.', $scheduled_count, 'progress-agentic-rag' ), - $scheduled_count - ), - 'scheduled' => $scheduled_count, - 'reprocessStatus' => $this->plugin->get_label_reprocessor()->get_status(), - ] ); - } - - /** - * AJAX handler to cancel label reprocessing. - * - * @since 1.4.0 - */ - public function ajax_cancel_reprocess_labels(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_labels_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $this->plugin->get_label_reprocessor()->cancel_all(); - - wp_send_json_success( [ - 'message' => __( 'Label reprocessing cancelled.', 'progress-agentic-rag' ), - 'reprocessStatus' => $this->plugin->get_label_reprocessor()->get_status(), - ] ); - } - - /** - * AJAX handler to get label reprocessing status. - * - * @since 1.4.0 - */ - public function ajax_get_reprocess_status(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_labels_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - wp_send_json_success( [ - 'reprocessStatus' => $this->plugin->get_label_reprocessor()->get_status(), - ] ); - } - -} diff --git a/includes/admin/index.php b/includes/admin/index.php deleted file mode 100644 index e71af0e..0000000 --- a/includes/admin/index.php +++ /dev/null @@ -1 +0,0 @@ - 0) { - statusEl.innerHTML = '' + - '' + count + ' pending'; - } else { - statusEl.innerHTML = ''; - } - } - - // Enable/disable schedule button - var scheduleBtn = document.querySelector('.nuclia-schedule-button[data-post-type="' + postType + '"]'); - if (scheduleBtn) { - scheduleBtn.disabled = count > 0; - if (count === 0) { - scheduleBtn.textContent = 'Schedule indexing'; - } - } - }); - } - } - - /** - * Handle labelset select change - */ - function handleLabelsetChange(e) { - var select = e.target; - var taxonomy = select.dataset.taxonomy || ''; - var labelset = select.value || ''; - - if (!taxonomy) { - return; - } - - updateLabelSelects(taxonomy, labelset); - } - - /** - * Handle fallback labelset select change - */ - function handleFallbackLabelsetChange(e) { - var select = e.target; - var taxonomy = select.dataset.taxonomy || ''; - var labelset = select.value || ''; - if (!taxonomy) { - return; - } - updateFallbackLabelSelects(taxonomy, labelset); - } - - /** - * Handle add mapping button click - */ - function handleAddMappingClick() { - var taxonomySelect = document.getElementById('nuclia_add_taxonomy_select'); - var container = document.getElementById('nuclia-mapping-container'); - if (!taxonomySelect || !container) { - return; - } - - var taxonomyKey = taxonomySelect.value; - if (!taxonomyKey) { - return; - } - - if (container.querySelector('.nuclia-mapping-block[data-taxonomy="' + taxonomyKey + '"]')) { - return; - } - - var block = buildMappingBlock(taxonomyKey); - if (!block) { - return; - } - - container.appendChild(block); - - var selectedOption = taxonomySelect.querySelector('option[value="' + taxonomyKey + '"]'); - if (selectedOption) { - selectedOption.remove(); - taxonomySelect.value = ''; - } - } - - /** - * Handle remove mapping button click - */ - function handleRemoveMappingClick(e) { - var block = e.target.closest('.nuclia-mapping-block'); - if (!block) { - return; - } - - var taxonomyKey = block.dataset.taxonomy || ''; - block.remove(); - - if (!taxonomyKey) { - return; - } - - var taxonomySelect = document.getElementById('nuclia_add_taxonomy_select'); - var data = getMappingData(); - if (taxonomySelect && data.taxonomies && data.taxonomies[taxonomyKey]) { - var option = document.createElement('option'); - option.value = taxonomyKey; - option.textContent = data.taxonomies[taxonomyKey].label || taxonomyKey; - taxonomySelect.appendChild(option); - } - } - - /** - * Build a new mapping block for a taxonomy - */ - function buildMappingBlock(taxonomyKey) { - var data = getMappingData(); - if (!data.taxonomies || !data.taxonomies[taxonomyKey]) { - return null; - } - - var taxonomy = data.taxonomies[taxonomyKey]; - var labelsets = data.labelsets || []; - - var block = document.createElement('div'); - block.className = 'nuclia-mapping-block pl-nuclia-section-card'; - block.dataset.taxonomy = taxonomyKey; - - var header = document.createElement('div'); - header.className = 'pl-nuclia-flex-between'; - - var headerText = document.createElement('div'); - var title = document.createElement('h4'); - title.className = 'pl-nuclia-fallback-title'; - title.textContent = taxonomy.label || taxonomyKey; - var slug = document.createElement('p'); - slug.className = 'pl-nuclia-muted'; - slug.textContent = taxonomyKey; - headerText.appendChild(title); - headerText.appendChild(slug); - - var removeButton = document.createElement('button'); - removeButton.type = 'button'; - removeButton.className = 'button link-delete nuclia-remove-mapping'; - removeButton.textContent = 'Remove'; - - header.appendChild(headerText); - header.appendChild(removeButton); - block.appendChild(header); - - var labelsetLabel = document.createElement('label'); - labelsetLabel.setAttribute('for', 'nuclia_labelset_' + taxonomyKey); - labelsetLabel.textContent = 'Labelset:'; - block.appendChild(labelsetLabel); - block.appendChild(document.createTextNode(' ')); - - var labelsetSelect = document.createElement('select'); - labelsetSelect.className = 'regular-text nuclia-labelset-select'; - labelsetSelect.dataset.taxonomy = taxonomyKey; - labelsetSelect.id = 'nuclia_labelset_' + taxonomyKey; - labelsetSelect.name = 'nuclia_taxonomy_label_map[' + taxonomyKey + '][labelset]'; - - var defaultOption = document.createElement('option'); - defaultOption.value = ''; - defaultOption.textContent = 'Select a labelset'; - labelsetSelect.appendChild(defaultOption); - - labelsets.forEach(function(labelset) { - var option = document.createElement('option'); - option.value = labelset; - option.textContent = labelset; - labelsetSelect.appendChild(option); - }); - - block.appendChild(labelsetSelect); - - if (!labelsets.length) { - var labelsetNotice = document.createElement('p'); - labelsetNotice.className = 'pl-nuclia-muted'; - labelsetNotice.textContent = 'No labelsets available. Check your Nuclia credentials.'; - block.appendChild(labelsetNotice); - } - - var terms = taxonomy.terms || []; - if (!terms.length) { - var noTermsNotice = document.createElement('p'); - noTermsNotice.className = 'pl-nuclia-muted'; - noTermsNotice.textContent = 'No terms available for this taxonomy.'; - block.appendChild(noTermsNotice); - } else { - var table = document.createElement('table'); - table.className = 'widefat striped pl-nuclia-label-table'; - - var thead = document.createElement('thead'); - var headRow = document.createElement('tr'); - var termTh = document.createElement('th'); - termTh.textContent = 'Term'; - var labelTh = document.createElement('th'); - labelTh.textContent = 'Nuclia labels'; - headRow.appendChild(termTh); - headRow.appendChild(labelTh); - thead.appendChild(headRow); - table.appendChild(thead); - - var tbody = document.createElement('tbody'); - terms.forEach(function(term) { - var row = document.createElement('tr'); - var termCell = document.createElement('td'); - termCell.textContent = term.name; - var labelCell = document.createElement('td'); - var labelContainer = document.createElement('div'); - labelContainer.className = 'nuclia-label-checkboxes'; - labelContainer.dataset.taxonomy = taxonomyKey; - labelContainer.dataset.termId = term.id; - labelContainer.innerHTML = 'Select a labelset to load labels.'; - labelCell.appendChild(labelContainer); - row.appendChild(termCell); - row.appendChild(labelCell); - tbody.appendChild(row); - }); - - table.appendChild(tbody); - block.appendChild(table); - } - - var fallbackSection = document.createElement('div'); - fallbackSection.className = 'nuclia-fallback-section'; - - var fallbackTitle = document.createElement('p'); - fallbackTitle.className = 'pl-nuclia-fallback-title'; - fallbackTitle.innerHTML = 'Fallback labels (when no terms assigned)'; - fallbackSection.appendChild(fallbackTitle); - - var fallbackLabel = document.createElement('label'); - fallbackLabel.setAttribute('for', 'nuclia_fallback_labelset_' + taxonomyKey); - fallbackLabel.textContent = 'Labelset:'; - fallbackSection.appendChild(fallbackLabel); - fallbackSection.appendChild(document.createTextNode(' ')); - - var fallbackSelect = document.createElement('select'); - fallbackSelect.className = 'regular-text nuclia-fallback-labelset-select'; - fallbackSelect.dataset.taxonomy = taxonomyKey; - fallbackSelect.id = 'nuclia_fallback_labelset_' + taxonomyKey; - fallbackSelect.name = 'nuclia_taxonomy_label_map[' + taxonomyKey + '][fallback][labelset]'; - - var fallbackDefault = document.createElement('option'); - fallbackDefault.value = ''; - fallbackDefault.textContent = 'Select a labelset'; - fallbackSelect.appendChild(fallbackDefault); - - labelsets.forEach(function(labelset) { - var option = document.createElement('option'); - option.value = labelset; - option.textContent = labelset; - fallbackSelect.appendChild(option); - }); - - fallbackSection.appendChild(fallbackSelect); - - var fallbackLabels = document.createElement('div'); - fallbackLabels.className = 'nuclia-fallback-labels pl-nuclia-muted'; - fallbackLabels.dataset.taxonomy = taxonomyKey; - fallbackLabels.innerHTML = 'Select a labelset to load labels.'; - fallbackSection.appendChild(fallbackLabels); - - block.appendChild(fallbackSection); - - return block; - } - - /** - * Get mapping data from localization - */ - function getMappingData() { - if (typeof nucliaMappingData !== 'undefined') { - return nucliaMappingData; - } - return {taxonomies: {}, labelsets: []}; - } - - /** - * Update label selects for a taxonomy - */ - function updateLabelSelects(taxonomy, labelset) { - var labelContainers = document.querySelectorAll('.nuclia-label-checkboxes[data-taxonomy="' + taxonomy + '"]'); - - labelContainers.forEach(function(container) { - container.innerHTML = 'Loading labels...'; - }); - - if (!labelset) { - labelContainers.forEach(function(container) { - container.innerHTML = 'Select a labelset to load labels.'; - }); - return; - } - - var formData = new FormData(); - formData.append('action', 'nuclia_get_labelset_labels'); - formData.append('labelset', labelset); - formData.append('nonce', getLabelsNonce()); - - fetch(ajaxurl, { - method: 'POST', - credentials: 'same-origin', - body: formData - }) - .then(function(response) { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(function(response) { - var labels = (response && response.success && response.data && response.data.labels) ? response.data.labels : []; - labelContainers.forEach(function(container) { - var current = []; - var currentInputs = container.querySelectorAll('input[type="checkbox"]:checked'); - currentInputs.forEach(function(input) { - current.push(input.value); - }); - - container.innerHTML = ''; - if (!labels.length) { - container.innerHTML = 'No labels available.'; - return; - } - - labels.forEach(function(label) { - var labelWrap = document.createElement('label'); - labelWrap.className = 'pl-nuclia-checkbox-row'; - - var checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.className = 'nuclia-label-checkbox'; - checkbox.value = label; - checkbox.name = 'nuclia_taxonomy_label_map[' + taxonomy + '][terms][' + container.dataset.termId + '][]'; - if (current.indexOf(label) !== -1) { - checkbox.checked = true; - } - - labelWrap.appendChild(checkbox); - labelWrap.appendChild(document.createTextNode(' ' + label)); - container.appendChild(labelWrap); - }); - }); - }) - .catch(function(error) { - console.error('Labelset fetch error:', error); - labelContainers.forEach(function(container) { - container.innerHTML = 'Select a labelset to load labels.'; - }); - }); - } - - /** - * Update fallback labels list for taxonomy - */ - function updateFallbackLabelSelects(taxonomy, labelset) { - var containers = document.querySelectorAll('.nuclia-fallback-labels[data-taxonomy="' + taxonomy + '"]'); - if (!containers.length) { - return; - } - - containers.forEach(function(container) { - container.innerHTML = 'Loading labels...'; - }); - - if (!labelset) { - containers.forEach(function(container) { - container.innerHTML = 'Select a labelset to load labels.'; - }); - return; - } - - var formData = new FormData(); - formData.append('action', 'nuclia_get_labelset_labels'); - formData.append('labelset', labelset); - formData.append('nonce', getLabelsNonce()); - - fetch(ajaxurl, { - method: 'POST', - credentials: 'same-origin', - body: formData - }) - .then(function(response) { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(function(response) { - var labels = (response && response.success && response.data && response.data.labels) ? response.data.labels : []; - containers.forEach(function(container) { - var current = []; - var currentInputs = container.querySelectorAll('input[type="checkbox"]:checked'); - currentInputs.forEach(function(input) { - current.push(input.value); - }); - - container.innerHTML = ''; - if (!labels.length) { - container.innerHTML = 'No labels available.'; - return; - } - - labels.forEach(function(label) { - var labelWrap = document.createElement('label'); - labelWrap.className = 'pl-nuclia-checkbox-row'; - - var checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.className = 'nuclia-fallback-label-checkbox'; - checkbox.value = label; - checkbox.name = 'nuclia_taxonomy_label_map[' + taxonomy + '][fallback][labels][]'; - if (current.indexOf(label) !== -1) { - checkbox.checked = true; - } - - labelWrap.appendChild(checkbox); - labelWrap.appendChild(document.createTextNode(' ' + label)); - container.appendChild(labelWrap); - }); - }); - }) - .catch(function(error) { - console.error('Fallback labelset fetch error:', error); - containers.forEach(function(container) { - container.innerHTML = 'Select a labelset to load labels.'; - }); - }); - } - - /** - * Handle clear synced files button click - */ - function handleClearSyncedClick(e) { - e.preventDefault(); - var clickedButton = e.currentTarget; - - if (!confirm('This will clear all synced file mappings. All posts will need to be re-synced. Continue?')) { - return; - } - - var spinner = clickedButton.querySelector('.spinner'); - var buttonText = clickedButton.querySelector('.nuclia-clear-synced-text'); - - clickedButton.disabled = true; - if (spinner) { - spinner.classList.add('is-active'); - } - if (buttonText) { - buttonText.textContent = 'Clearing...'; - } - - var nonce = clickedButton.dataset.nonce || ''; - - var formData = new FormData(); - formData.append('action', 'nuclia_clear_synced_files'); - formData.append('nonce', nonce); - - fetch(ajaxurl, { - method: 'POST', - credentials: 'same-origin', - body: formData - }) - .then(function(response) { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(function(response) { - if (response.success) { - if (buttonText) { - buttonText.textContent = 'Cleared!'; - } - // Refresh page to show updated counts - setTimeout(function() { - location.reload(); - }, 1500); - } else { - if (buttonText) { - buttonText.textContent = 'Error'; - } - clickedButton.disabled = false; - console.error('Clear synced files failed:', response); - if (response.data && response.data.message) { - alert('Error: ' + response.data.message); - } - } - }) - .catch(function(error) { - if (buttonText) { - buttonText.textContent = 'Error'; - } - clickedButton.disabled = false; - console.error('Clear synced files error:', error); - alert('An error occurred while clearing synced files.'); - }) - .finally(function() { - if (spinner) { - spinner.classList.remove('is-active'); - } - }); - } - - /** - * Get the nonce value - */ - function getNonce() { - return (typeof nucliaReindex !== 'undefined' ? nucliaReindex.nonce : ''); - } - - /** - * Get nonce for labelset fetch - */ - function getLabelsNonce() { - return (typeof nucliaReindex !== 'undefined' ? nucliaReindex.labelsNonce : ''); - } - - /** - * Handle copy button click for code blocks - */ - function handleCopyButtonClick(e) { - var button = e.currentTarget; - var textToCopy = ''; - - // Check for data-copy-text attribute (direct text) - if (button.dataset.copyText) { - textToCopy = button.dataset.copyText; - } - // Check for data-copy-target attribute (copy from element) - else if (button.dataset.copyTarget) { - var targetEl = document.getElementById(button.dataset.copyTarget); - if (targetEl) { - textToCopy = targetEl.textContent.trim(); - } - } - - if (!textToCopy) { - return; - } - - var originalText = button.textContent; - navigator.clipboard.writeText(textToCopy).then(function() { - button.textContent = __('copied', 'Copied!'); - setTimeout(function() { - button.textContent = originalText; - }, 2000); - }).catch(function() { - button.textContent = __('copyFailed', 'Copy failed'); - setTimeout(function() { - button.textContent = originalText; - }, 2000); - }); - } - - /** - * Handle reprocess labels button click - */ - function handleReprocessLabelsClick(e) { - e.preventDefault(); - var clickedButton = e.currentTarget; - - var syncedCount = document.getElementById('nuclia-synced-count'); - var count = syncedCount ? parseInt(syncedCount.textContent, 10) : 0; - - var confirmMsg = __('confirmReprocess', 'This will update labels for %d synced resource(s) with the current taxonomy mapping. Continue?'); - if (!confirm(confirmMsg.replace('%d', count))) { - return; - } - - clickedButton.disabled = true; - clickedButton.textContent = 'Scheduling...'; - - var formData = new FormData(); - formData.append('action', 'nuclia_reprocess_labels'); - formData.append('nonce', getLabelsNonce()); - - fetch(ajaxurl, { - method: 'POST', - credentials: 'same-origin', - body: formData - }) - .then(function(response) { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(function(response) { - if (response.success) { - clickedButton.textContent = 'Scheduled!'; - if (response.data && response.data.message) { - alert(response.data.message); - } - startReprocessStatusPolling(); - // Refresh after a short delay to show updated status - setTimeout(function() { - location.reload(); - }, 2000); - } else { - clickedButton.textContent = 'Error'; - clickedButton.disabled = false; - console.error('Reprocess failed:', response); - if (response.data && response.data.message) { - alert('Error: ' + response.data.message); - } - } - }) - .catch(function(error) { - clickedButton.textContent = 'Error'; - clickedButton.disabled = false; - console.error('Reprocess error:', error); - }); - } - - /** - * Handle cancel reprocess button click - */ - function handleCancelReprocessClick(e) { - e.preventDefault(); - var clickedButton = e.currentTarget; - - if (!confirm('Are you sure you want to cancel the label reprocessing?')) { - return; - } - - clickedButton.disabled = true; - clickedButton.textContent = 'Cancelling...'; - - var formData = new FormData(); - formData.append('action', 'nuclia_cancel_reprocess_labels'); - formData.append('nonce', getLabelsNonce()); - - fetch(ajaxurl, { - method: 'POST', - credentials: 'same-origin', - body: formData - }) - .then(function(response) { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(function(response) { - if (response.success) { - stopReprocessStatusPolling(); - // Refresh page to show updated state - location.reload(); - } else { - clickedButton.textContent = 'Error'; - clickedButton.disabled = false; - console.error('Cancel reprocess failed:', response); - } - }) - .catch(function(error) { - clickedButton.textContent = 'Error'; - clickedButton.disabled = false; - console.error('Cancel reprocess error:', error); - }); - } - - /** - * Start polling for reprocess status updates - */ - function startReprocessStatusPolling() { - if (isReprocessPolling) { - return; - } - isReprocessPolling = true; - pollReprocessStatus(); - reprocessPollInterval = setInterval(pollReprocessStatus, 5000); // Poll every 5 seconds - } - - /** - * Stop polling for reprocess status updates - */ - function stopReprocessStatusPolling() { - isReprocessPolling = false; - if (reprocessPollInterval) { - clearInterval(reprocessPollInterval); - reprocessPollInterval = null; - } - } - - /** - * Poll for current reprocess status - */ - function pollReprocessStatus() { - var formData = new FormData(); - formData.append('action', 'nuclia_get_reprocess_status'); - formData.append('nonce', getLabelsNonce()); - - fetch(ajaxurl, { - method: 'POST', - credentials: 'same-origin', - body: formData - }) - .then(function(response) { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(function(response) { - if (response.success && response.data && response.data.reprocessStatus) { - updateReprocessUIFromStatus(response.data.reprocessStatus); - - // Stop polling if no more pending or running jobs - if (!response.data.reprocessStatus.is_active) { - stopReprocessStatusPolling(); - // Refresh the page to show the reprocess button again - setTimeout(function() { - location.reload(); - }, 1500); - } - } - }) - .catch(function(error) { - console.error('Reprocess status poll error:', error); - }); - } - - /** - * Update reprocess UI elements based on status - */ - function updateReprocessUIFromStatus(status) { - var pendingEl = document.getElementById('nuclia-reprocess-pending'); - var runningEl = document.getElementById('nuclia-reprocess-running'); - var failedEl = document.getElementById('nuclia-reprocess-failed'); - - if (pendingEl) { - pendingEl.textContent = status.pending + ' pending'; - } - if (runningEl) { - runningEl.textContent = status.running + ' running'; - } - if (failedEl && status.failed > 0) { - failedEl.textContent = status.failed + ' failed'; - failedEl.style.display = 'inline'; - } - } - -})(); diff --git a/includes/admin/partials/index.php b/includes/admin/partials/index.php deleted file mode 100644 index e71af0e..0000000 --- a/includes/admin/partials/index.php +++ /dev/null @@ -1 +0,0 @@ - - - - -
-

- -   -

-
- slug ); - submit_button(); - ?> -
-
diff --git a/includes/class-nuclia-api.php b/includes/class-nuclia-api.php deleted file mode 100644 index 2a6b571..0000000 --- a/includes/class-nuclia-api.php +++ /dev/null @@ -1,737 +0,0 @@ -settings = $settings; - $this->endpoint = sprintf( 'https://%1s.rag.progress.cloud/api/v1/kb/%2s/', - $this->settings->get_zone(), - $this->settings->get_kbid() - ); - } - - public function upsert_index( int $post_id, string $rid, string|null $seqid ): void { - global $wpdb; - $wpdb->replace( "{$wpdb->prefix}agentic_rag_for_wp", [ - 'post_id' => $post_id, - 'nuclia_rid' => $rid, - 'nuclia_seqid' => $seqid ?? null - ] ); - } - - public function get_rid( int $post_id ): string | null { - global $wpdb; - $rid = $wpdb->get_var( $wpdb->prepare( "SELECT nuclia_rid FROM {$wpdb->prefix}agentic_rag_for_wp WHERE post_id = %d", $post_id ) ); - return $rid; - } - - public function delete_index( int $post_id ): void { - global $wpdb; - $wpdb->delete( "{$wpdb->prefix}agentic_rag_for_wp", [ 'post_id' => $post_id ] ); - } - - /** - * Clear all indexed posts from the sync mapping table. - * - * @since 1.3.0 - * - * @return int|false Number of rows affected or false on error. - */ - public function clear_all_indexed(): int|false { - global $wpdb; - $result = $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}agentic_rag_for_wp" ); - return $result; - } - - /** - * Get all indexed posts with their Nuclia resource IDs. - * - * @since 1.4.0 - * - * @return array<\stdClass> Array of objects with post_id and nuclia_rid properties. - */ - public function get_all_indexed_posts(): array { - global $wpdb; - return $wpdb->get_results( - $wpdb->prepare( - "SELECT post_id, nuclia_rid FROM {$wpdb->prefix}agentic_rag_for_wp WHERE nuclia_rid IS NOT NULL AND nuclia_rid != %s", - '' - ) - ); - } - - /** - * Update only the labels/classifications for a resource. - * - * This sends a PATCH request with only the usermetadata.classifications field, - * which updates labels without triggering file reprocessing. - * - * @since 1.4.0 - * - * @param int $post_id The post ID. - * @param string $rid The Nuclia resource UUID. - * @param WP_Post $post The post object. - * - * @return array{success: bool, code: int, message: string} Result array with success status, response code, and message. - */ - public function update_resource_labels( int $post_id, string $rid, WP_Post $post ): array { - $uri = "{$this->endpoint}resource/{$rid}"; - - // Reuse existing method to build classifications - $classifications = $this->build_taxonomy_classifications( $post ); - - $body = [ - 'usermetadata' => [ - 'classifications' => $classifications, - ], - ]; - - $args = [ - 'method' => 'PATCH', - 'headers' => [ - 'Content-type' => 'application/json', - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $this->settings->get_token(), - ], - 'body' => json_encode( $body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), - ]; - - nuclia_log( "Updating labels for post {$post_id}, rid: {$rid}" ); - nuclia_log( "URI: {$uri}" ); - - $response = wp_remote_request( $uri, $args ); - $response_code = wp_remote_retrieve_response_code( $response ); - - nuclia_log( "Response code: {$response_code}" ); - - if ( is_wp_error( $response ) ) { - $error_message = $response->get_error_message(); - nuclia_error_log( "Failed to update labels for post {$post_id}: " . $error_message ); - return [ - 'success' => false, - 'code' => 0, - 'message' => $error_message, - ]; - } - - if ( $response_code !== 200 ) { - $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); - $error_message = $api_response['message'] ?? $api_response['detail'] ?? "HTTP {$response_code}"; - nuclia_error_log( "Failed to update labels for post {$post_id}, code: {$response_code}, response: " . print_r( $api_response, true ) ); - return [ - 'success' => false, - 'code' => $response_code, - 'message' => $error_message, - ]; - } - - nuclia_log( "Successfully updated labels for post {$post_id}" ); - return [ - 'success' => true, - 'code' => 200, - 'message' => 'Labels updated successfully', - ]; - } - - /** - * Prepare NucliaDB resource body - * @param WP_Post $post - * @return bool|string - */ - public function prepare_nuclia_resource_body( WP_Post $post ): string { - $body = [ - 'title' => html_entity_decode( wp_strip_all_tags( $post->post_title ), ENT_QUOTES, "UTF-8" ), - 'slug' => (string)$post->ID, - 'metadata' => [ - 'language' => get_bloginfo("language") - ], - 'origin' => [ - 'url' => get_permalink( $post ), - ], - 'created' => gmdate('Y-m-d', strtotime( $post->post_date_gmt )).'T'.gmdate('H:i:s', strtotime( $post->post_date_gmt )).'Z' - ]; - - $taxonomy_label_map = $this->settings->get_taxonomy_label_map(); - $classifications = []; - if ( ! empty( $taxonomy_label_map ) ) { - $classifications = $this->build_taxonomy_classifications( $post ); - } - - if ( ! empty( $classifications ) ) { - $body['usermetadata'] = [ - 'classifications' => $classifications, - ]; - } - - // for attachments - if ( $post->post_type == 'attachment' ) : - // $file = get_attached_file( $post->ID ); - // $filename = esc_html( wp_basename( $file ) ); - $mime_type = get_post_mime_type( $post->ID ); - $body = [ - ...$body, - 'icon' => $mime_type, - ]; - - // other post types - else : - $body = [ - ...$body, - 'icon' => 'text/html', - 'texts' => [ - 'text-1' => [ - 'body' => apply_filters('the_content', $post->post_content ), - 'format' => 'HTML', - ] - ] - ]; - - endif; - - return json_encode( $body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); - } - - /** - * Get available Nuclia labelsets (cached). - * - * @since 1.2.0 - * - * @return array - */ - public function get_labelsets(): array { - $cache = $this->settings->get_labelsets_cache(); - $cached_labelsets = $cache['labelsets'] ?? []; - $fetched_at = (int) ( $cache['fetched_at'] ?? 0 ); - $ttl = defined( 'HOUR_IN_SECONDS' ) ? 6 * HOUR_IN_SECONDS : 21600; - - if ( ! empty( $cached_labelsets ) && $fetched_at > 0 && ( time() - $fetched_at ) < $ttl ) { - return $cached_labelsets; - } - - if ( empty( $this->settings->get_zone() ) || empty( $this->settings->get_kbid() ) || empty( $this->settings->get_token() ) ) { - return $cached_labelsets; - } - - $uri = "{$this->endpoint}labelsets"; - $args = [ - 'method' => 'GET', - 'headers' => [ - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $this->settings->get_token(), - ], - ]; - - $response = wp_remote_get( $uri, $args ); - if ( is_wp_error( $response ) ) { - nuclia_error_log( 'Failed to fetch labelsets: ' . $response->get_error_message() ); - return $cached_labelsets; - } - - $response_code = wp_remote_retrieve_response_code( $response ); - if ( $response_code !== 200 ) { - nuclia_error_log( 'Failed to fetch labelsets, response code: ' . $response_code ); - return $cached_labelsets; - } - - $data = json_decode( wp_remote_retrieve_body( $response ), true ); - $normalized = $this->normalize_labelsets_response( $data ); - $labelsets = $normalized['labelsets']; - $labels_map = $normalized['labels']; - if ( ! empty( $labelsets ) ) { - if ( ! empty( $labels_map ) ) { - $this->settings->set_labelsets_cache_with_labels( $labelsets, $labels_map ); - } else { - $this->settings->set_labelsets_cache( $labelsets ); - } - return $labelsets; - } - - return $cached_labelsets; - } - - /** - * Get labels for a labelset (cached). - * - * @since 1.2.0 - * - * @param string $labelset - * @return array - */ - public function get_labelset_labels( string $labelset ): array { - $labelset = trim( $labelset ); - if ( $labelset === '' ) { - return []; - } - - $cache = $this->settings->get_labelsets_cache(); - $fetched_at = (int) ( $cache['fetched_at'] ?? 0 ); - $ttl = defined( 'HOUR_IN_SECONDS' ) ? 6 * HOUR_IN_SECONDS : 21600; - $cached_labels = $this->settings->get_labelset_labels_cache( $labelset ); - - if ( ! empty( $cached_labels ) && $fetched_at > 0 && ( time() - $fetched_at ) < $ttl ) { - return $cached_labels; - } - - if ( empty( $this->settings->get_zone() ) || empty( $this->settings->get_kbid() ) || empty( $this->settings->get_token() ) ) { - return $cached_labels; - } - - $labels = $this->fetch_labelset_labels( $labelset ); - if ( ! empty( $labels ) ) { - $this->settings->set_labelset_labels_cache( $labelset, $labels ); - return $labels; - } - - return $cached_labels; - } - - /** - * Normalize labelset list from API response. - * - * @since 1.2.0 - * - * @param mixed $data - * @return array - */ - private function normalize_labelsets_response( mixed $data ): array { - if ( ! is_array( $data ) ) { - return [ - 'labelsets' => [], - 'labels' => [], - ]; - } - - $labelsets = []; - $labels_map = []; - $payload = $data['labelsets'] ?? $data; - - if ( is_array( $payload ) ) { - $is_assoc = array_keys( $payload ) !== range( 0, count( $payload ) - 1 ); - if ( $is_assoc ) { - $labelsets = array_keys( $payload ); - foreach ( $payload as $labelset => $entry ) { - $labels = $this->normalize_labelset_labels( $entry ); - if ( ! empty( $labels ) ) { - $labels_map[ (string) $labelset ] = $labels; - } - } - } else { - foreach ( $payload as $entry ) { - if ( is_string( $entry ) ) { - $labelsets[] = $entry; - } elseif ( is_array( $entry ) ) { - if ( isset( $entry['labelset'] ) ) { - $labelset_name = (string) $entry['labelset']; - $labelsets[] = $labelset_name; - $labels = $this->normalize_labelset_labels( $entry ); - if ( ! empty( $labels ) ) { - $labels_map[ $labelset_name ] = $labels; - } - } elseif ( isset( $entry['name'] ) ) { - $labelsets[] = (string) $entry['name']; - } elseif ( isset( $entry['id'] ) ) { - $labelsets[] = (string) $entry['id']; - } - } - } - } - } - - $labelsets = array_filter( array_map( 'sanitize_text_field', $labelsets ) ); - return [ - 'labelsets' => array_values( array_unique( $labelsets ) ), - 'labels' => $labels_map, - ]; - } - - /** - * Fetch labels for a labelset from the API. - * - * @since 1.2.0 - * - * @param string $labelset - * @return array - */ - private function fetch_labelset_labels( string $labelset ): array { - $labelset = rawurlencode( $labelset ); - $candidates = [ - "{$this->endpoint}labelsets/{$labelset}", - "{$this->endpoint}labelset/{$labelset}", - "{$this->endpoint}labelsets/{$labelset}/labels", - "{$this->endpoint}labelset/{$labelset}/labels", - ]; - - foreach ( $candidates as $uri ) { - $args = [ - 'method' => 'GET', - 'headers' => [ - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $this->settings->get_token(), - ], - ]; - - $response = wp_remote_get( $uri, $args ); - if ( is_wp_error( $response ) ) { - continue; - } - - $response_code = wp_remote_retrieve_response_code( $response ); - if ( $response_code !== 200 ) { - continue; - } - - $data = json_decode( wp_remote_retrieve_body( $response ), true ); - $labels = $this->normalize_labelset_labels( $data ); - if ( ! empty( $labels ) ) { - return $labels; - } - } - - return []; - } - - /** - * Normalize label list for a labelset response. - * - * @since 1.2.0 - * - * @param mixed $data - * @return array - */ - private function normalize_labelset_labels( mixed $data ): array { - if ( ! is_array( $data ) ) { - return []; - } - - $labels = []; - $payload = $data['labels'] ?? $data['labelset'] ?? $data; - - if ( is_array( $payload ) ) { - $is_assoc = array_keys( $payload ) !== range( 0, count( $payload ) - 1 ); - if ( $is_assoc ) { - $labels = array_keys( $payload ); - } else { - foreach ( $payload as $entry ) { - if ( is_string( $entry ) ) { - $labels[] = $entry; - } elseif ( is_array( $entry ) ) { - if ( isset( $entry['title'] ) ) { - $labels[] = (string) $entry['title']; - } elseif ( isset( $entry['text'] ) ) { - $labels[] = (string) $entry['text']; - } elseif ( isset( $entry['uri'] ) ) { - $labels[] = (string) $entry['uri']; - } elseif ( isset( $entry['related'] ) && is_string( $entry['related'] ) ) { - $labels[] = $entry['related']; - } elseif ( isset( $entry['label'] ) ) { - $labels[] = (string) $entry['label']; - } elseif ( isset( $entry['name'] ) ) { - $labels[] = (string) $entry['name']; - } elseif ( isset( $entry['id'] ) ) { - $labels[] = (string) $entry['id']; - } - } - } - } - } - - $labels = array_filter( array_map( 'sanitize_text_field', $labels ) ); - return array_values( array_unique( $labels ) ); - } - - /** - * Build classifications based on taxonomy mapping. - * - * @since 1.2.0 - * - * @param WP_Post $post - * - * @return array - */ - private function build_taxonomy_classifications( WP_Post $post ): array { - $taxonomy_label_map = $this->settings->get_taxonomy_label_map(); - if ( empty( $taxonomy_label_map ) ) { - return []; - } - - $classifications = []; - - foreach ( $taxonomy_label_map as $taxonomy => $config ) { - if ( ! taxonomy_exists( $taxonomy ) || ! is_array( $config ) ) { - continue; - } - - $labelset = isset( $config['labelset'] ) ? trim( (string) $config['labelset'] ) : ''; - $term_map = is_array( $config['terms'] ?? null ) ? $config['terms'] : []; - $fallback = is_array( $config['fallback'] ?? null ) ? $config['fallback'] : []; - $fallback_labelset = isset( $fallback['labelset'] ) ? trim( (string) $fallback['labelset'] ) : ''; - $fallback_labels = is_array( $fallback['labels'] ?? null ) ? $fallback['labels'] : []; - - if ( $labelset === '' || empty( $term_map ) ) { - $term_map = []; - } - - if ( empty( $term_map ) && $fallback_labelset === '' ) { - continue; - } - - $term_ids = wp_get_post_terms( $post->ID, $taxonomy, [ 'fields' => 'ids' ] ); - if ( is_wp_error( $term_ids ) ) { - continue; - } - - if ( empty( $term_ids ) ) { - if ( $fallback_labelset !== '' && ! empty( $fallback_labels ) ) { - foreach ( $fallback_labels as $label ) { - $label = trim( (string) $label ); - if ( $label === '' ) { - continue; - } - $classifications[] = [ - 'labelset' => $fallback_labelset, - 'label' => $label - ]; - } - } - } else { - foreach ( $term_ids as $term_id ) { - $term_id = (int) $term_id; - if ( empty( $term_map[ $term_id ] ) ) { - continue; - } - - $labels = $term_map[ $term_id ]; - if ( ! is_array( $labels ) ) { - $labels = $labels !== '' ? [ (string) $labels ] : []; - } - - foreach ( $labels as $label ) { - $label = trim( (string) $label ); - if ( $label === '' ) { - continue; - } - - $classifications[] = [ - 'labelset' => $labelset, - 'label' => $label - ]; - } - } - } - } - - $unique = []; - foreach ( $classifications as $classification ) { - $key = $classification['labelset'] . '|' . $classification['label']; - $unique[ $key ] = $classification; - } - - return array_values( $unique ); - } - - /** - * Create a resource - * - * @since 1.0.0 - * - * @param int $post_id ID of the post. - * - * @param string $body Content to send for indexation. - */ - public function create_resource( int $post_id, WP_Post $post ): void { - - $uri = "{$this->endpoint}resources"; - - $args = [ - 'method' => 'POST', - 'headers' => [ - 'Content-type' => 'application/json', - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' .$this->settings->get_token() - ], - 'body' => $this->prepare_nuclia_resource_body( $post ) - ]; - - nuclia_log("endpoint : {$uri}"); - - $response = wp_remote_request( $uri, $args ); - $response_code = wp_remote_retrieve_response_code( $response ); // int or empty string - - nuclia_log("code : {$response_code}"); - - if ( !is_wp_error( $response ) ) { - $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); - // successfull response - if ( $response_code === 201 ) { - $rid = $api_response['uuid']; - $seqid = $api_response['seqid']; - - // Only upload file for attachments - if ( $post->post_type === 'attachment' ) { - $file = get_attached_file( $post->ID ); - - // Verify file exists before attempting upload - if ( $file && file_exists( $file ) ) { - $uri = "{$this->endpoint}resource/{$rid}/file/file/upload"; - nuclia_log("uri : {$uri}"); - $filename = esc_html( wp_basename( $file ) ); - $args = [ - 'method' => 'POST', - 'headers' => [ - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' .$this->settings->get_token(), - 'Content-Type' => get_post_mime_type( $post->ID ) ?? 'application/octet-stream', - 'x-filename' => $filename, - 'x-md5' => md5_file( $file ), - ], - 'body' => file_get_contents( $file ) - ]; - $response = wp_remote_request( $uri, $args ); - $response_code = wp_remote_retrieve_response_code( $response ); // int or empty string - nuclia_log("code : {$response_code}"); - if ( !is_wp_error( $response ) ) { - $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); - if ( $response_code === 200 ) { - $seqid = $api_response['seqid']; - } - } - } else { - nuclia_error_log("File not found for attachment {$post_id}: {$file}"); - } - } - - // Save the index entry for both attachments and regular posts - $this->upsert_index( $post_id, $rid, $seqid ); - nuclia_log("nuclia success : ".print_r($api_response, true) ); - } - // Validation error - else { - nuclia_error_log("nuclia error : ".print_r($api_response, true) ); - }; - } else { - nuclia_error_log("connection error: ".print_r($response,true) ); - }; - - } - - /** - * Modify a resource - * - * @since 1.0.0 - * - * @param int $post_id ID of the post. - * - * @param string $rid Resource ID in nucliaDB - * @param string $body The content to index. - */ - public function modify_resource( int $post_id, string $rid, WP_Post $post ): void { - - $uri = "{$this->endpoint}resource/{$rid}"; - $args = [ - 'method' => 'PATCH', - 'headers' => [ - 'Content-type' => 'application/json', - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' .$this->settings->get_token() - ], - 'body' => $this->prepare_nuclia_resource_body( $post ) - ]; - - nuclia_log( $uri ); - nuclia_log( print_r( $args, true ) ); - - $response = wp_remote_request( $uri, $args ); - $response_code = wp_remote_retrieve_response_code( $response ); // int or empty string - nuclia_log("code : {$response_code}"); - if ( !is_wp_error( $response ) ) { - $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); - // successfull response - if ( $response_code === 200 ) { - $seqid = $api_response['seqid']; - $this->upsert_index( $post_id, $rid, $seqid ); - nuclia_log("nuclia success : ".print_r($api_response, true)); - } - // Validation error - else { - nuclia_error_log("nuclia error : ".print_r($api_response, true)); - }; - } else { - nuclia_error_log("connection error: ".print_r($response,true)); - }; - } - - /** - * Delete a resource - * - * @since 1.0.0 - * - * @param int $post_id ID of the post. - * - * @param string $rid Resource ID in nucliaDB - */ - public function delete_resource( int $post_id, string $rid ): void { - - $uri = "{$this->endpoint}resource/{$rid}"; - - $args = [ - 'method' => 'DELETE', - 'headers' => [ - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' .$this->settings->get_token() - ], - 'body' => '' - ]; - - nuclia_log( $uri ); - nuclia_log( print_r( $args, true ) ); - - $response = wp_remote_request( $uri, $args ); - $response_code = wp_remote_retrieve_response_code( $response ); // int or empty string - nuclia_log("code : {$response_code}"); - - if ( !is_wp_error( $response ) ) { - $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); - // successfull response - if ( $response_code === 204 ) { - $this->delete_index( $post_id ); - nuclia_log("nuclia success : ".print_r($api_response, true)); - } - // Validation error - else { - nuclia_error_log("nuclia error : ".print_r($api_response, true)); - - }; - } else { - nuclia_error_log("connection error: ".print_r($response,true) ); - }; - } -} diff --git a/includes/class-nuclia-background-processor.php b/includes/class-nuclia-background-processor.php deleted file mode 100644 index 72da0ce..0000000 --- a/includes/class-nuclia-background-processor.php +++ /dev/null @@ -1,440 +0,0 @@ -plugin = $plugin; - } - - /** - * Register hooks for Action Scheduler. - * - * @since 1.1.0 - */ - public function register_hooks(): void { - // Hook for processing individual posts - add_action( self::HOOK_PROCESS_SINGLE, [ $this, 'process_single_post' ], 10, 2 ); - - // Hook for scheduling batches - add_action( self::HOOK_SCHEDULE_BATCH, [ $this, 'schedule_post_type' ], 10, 1 ); - - // Hook for full reindex trigger - add_action( 'nuclia_schedule_full_reindex', [ $this, 'schedule_full_reindex' ] ); - } - - /** - * Schedule indexing for all enabled post types. - * - * @since 1.1.0 - */ - public function schedule_full_reindex(): void { - nuclia_log( 'Scheduling full reindex for all post types' ); - $indexable_post_types = $this->plugin->get_indexable_post_types(); - - foreach ( array_keys( $indexable_post_types ) as $post_type ) { - // Schedule batch scheduling for each post type with a slight delay between them - $this->schedule_post_type( $post_type ); - } - - nuclia_log( 'Scheduled full reindex for all post types: ' . implode( ', ', array_keys( $indexable_post_types ) ) ); - } - - /** - * Schedule indexing for a specific post type. - * - * @since 1.1.0 - * - * @param string $post_type The post type to schedule indexing for. - */ - public function schedule_post_type( string $post_type ): void { - if ( ! post_type_exists( $post_type ) ) { - nuclia_log( "Post type {$post_type} does not exist, skipping." ); - return; - } - - // Get unindexed posts for this post type - $unindexed_posts = $this->get_unindexed_posts( $post_type ); - - if ( empty( $unindexed_posts ) ) { - nuclia_log( "No unindexed posts found for {$post_type}." ); - return; - } - - $scheduled_count = 0; - - foreach ( $unindexed_posts as $post_id ) { - // Check if this post is already scheduled - if ( $this->is_post_scheduled( $post_id, $post_type ) ) { - continue; - } - - // Schedule the post for processing - as_schedule_single_action( - time() + ( $scheduled_count * 2 ), // Stagger by 2 seconds to avoid rate limiting - self::HOOK_PROCESS_SINGLE, - [ - 'post_id' => $post_id, - 'post_type' => $post_type, - ], - self::GROUP - ); - - $scheduled_count++; - } - - nuclia_log( "Scheduled {$scheduled_count} posts for indexing (post type: {$post_type})." ); - } - - /** - * Process a single post for indexing. - * - * This is the callback for Action Scheduler. - * - * @since 1.1.0 - * - * @param int $post_id The post ID to index. - * @param string $post_type The post type. - * - * @throws Exception If indexing fails and should be retried. - */ - public function process_single_post( int $post_id, string $post_type ): void { - nuclia_log( "Processing post {$post_id} (type: {$post_type}) via Action Scheduler." ); - - // Check if API is still reachable - if ( ! $this->plugin->get_settings()->get_api_is_reachable() ) { - nuclia_log( 'API not reachable, skipping post processing.' ); - throw new Exception( 'Nuclia API not reachable. Will retry later.' ); - } - - $post = get_post( $post_id ); - - if ( ! $post ) { - nuclia_log( "Post {$post_id} not found, skipping." ); - return; - } - - // Check if post type is still enabled - $indexable_post_types = $this->plugin->get_indexable_post_types(); - if ( ! array_key_exists( $post_type, $indexable_post_types ) ) { - nuclia_log( "Post type {$post_type} is no longer enabled for indexing, skipping." ); - return; - } - - // Check if already indexed - $rid = $this->plugin->get_api()->get_rid( $post_id ); - if ( $rid ) { - nuclia_log( "Post {$post_id} is already indexed (rid: {$rid}), skipping." ); - return; - } - - try { - if ( $post_type === 'attachment' ) { - // Hack: attachments have 'inherit' status - $post->post_status = 'publish'; - $this->plugin->get_api()->create_resource( $post_id, $post ); - } else { - // Check post status - if ( $post->post_status !== 'publish' || $post->post_password ) { - nuclia_log( "Post {$post_id} is not public or is password protected, skipping." ); - return; - } - - $this->plugin->get_api()->create_resource( $post_id, $post ); - } - - nuclia_log( "Successfully indexed post {$post_id}." ); - } catch ( Exception $e ) { - nuclia_error_log( "Failed to index post {$post_id}: " . $e->getMessage() ); - // Re-throw to trigger Action Scheduler retry - throw $e; - } - } - - /** - * Get unindexed posts for a specific post type. - * - * @since 1.1.0 - * - * @param string $post_type The post type to query. - * - * @return array Array of post IDs that are not yet indexed. - */ - private function get_unindexed_posts( string $post_type ): array { - global $wpdb; - - $post_status = ( $post_type !== 'attachment' ) ? 'publish' : 'inherit'; - $limit = 500; // Process in batches to avoid memory issues - $offset = 0; - $all_posts = []; - - do { - $results = $wpdb->get_results( - $wpdb->prepare( - "SELECT p.ID FROM {$wpdb->posts} AS p - LEFT JOIN {$wpdb->prefix}agentic_rag_for_wp AS idx ON ( p.ID = idx.post_id ) - WHERE idx.post_id IS NULL - AND p.post_type = %s - AND p.post_status = %s - AND p.post_password = '' - LIMIT %d OFFSET %d", - $post_type, - $post_status, - $limit, - $offset - ) - ); - - if ( ! empty( $wpdb->last_error ) ) { - nuclia_error_log( 'Database error in get_unindexed_posts: ' . $wpdb->last_error ); - break; - } - - $all_posts = array_merge( $all_posts, wp_list_pluck( $results, 'ID' ) ); - $offset += $limit; - } while ( count( $results ) === $limit ); - - return $all_posts; - } - - /** - * Check if a post is already scheduled for processing. - * - * @since 1.1.0 - * - * @param int $post_id The post ID. - * @param string $post_type The post type. - * - * @return bool True if already scheduled. - */ - private function is_post_scheduled( int $post_id, string $post_type ): bool { - return as_has_scheduled_action( - self::HOOK_PROCESS_SINGLE, - [ - 'post_id' => $post_id, - 'post_type' => $post_type, - ], - self::GROUP - ); - } - - /** - * Get the count of pending actions. - * - * @since 1.1.0 - * - * @return int The number of pending actions. - */ - public function get_pending_count(): int { - $count = $this->as_count_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_PENDING - ] - ); - - return $count; - } - - /** - * Get the count of running/in-progress actions. - * - * @since 1.1.0 - * - * @return int The number of running actions. - */ - public function get_running_count(): int { - if ( ! function_exists( 'as_get_scheduled_actions' ) ) { - return 0; - } - - $actions = as_get_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_RUNNING, - ], - 'ids' - ); - - return count( $actions ); - } - - /** - * Get the count of failed actions. - * - * @since 1.1.0 - * - * @return int The number of failed actions. - */ - public function get_failed_count(): int { - if ( ! function_exists( 'as_get_scheduled_actions' ) ) { - return 0; - } - - $actions = as_get_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_FAILED, - ], - 'ids' - ); - - return count( $actions ); - } - - /** - * Cancel all pending indexing actions. - * - * @since 1.1.0 - */ - public function cancel_all(): void { - if ( ! function_exists( 'as_unschedule_all_actions' ) ) { - return; - } - - as_unschedule_all_actions( self::HOOK_PROCESS_SINGLE, null, self::GROUP ); - - nuclia_log( 'Cancelled all pending Nuclia indexing actions.' ); - } - - /** - * Cancel pending actions for a specific post type. - * - * @since 1.1.0 - * - * @param string $post_type The post type to cancel actions for. - */ - public function cancel_post_type( string $post_type ): void { - if ( ! function_exists( 'as_get_scheduled_actions' ) ) { - return; - } - - $actions = as_get_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_PENDING, - ], - 'ids' - ); - - foreach ( $actions as $action_id ) { - $action = ActionScheduler::store()->fetch_action( $action_id ); - $args = $action->get_args(); - - if ( isset( $args['post_type'] ) && $args['post_type'] === $post_type ) { - as_unschedule_action( self::HOOK_PROCESS_SINGLE, $args, self::GROUP ); - } - } - - nuclia_log( "Cancelled pending Nuclia indexing actions for post type: {$post_type}." ); - } - - /** - * Get status information for the admin UI. - * - * @since 1.1.0 - * - * @return array Status information array. - */ - public function get_status(): array { - return [ - 'pending' => $this->get_pending_count(), - 'running' => $this->get_running_count(), - 'failed' => $this->get_failed_count(), - 'is_active' => $this->get_pending_count() > 0 || $this->get_running_count() > 0, - ]; - } - - protected function as_count_scheduled_actions( $args = array() ) { - if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { - return 0; - } - $store = ActionScheduler::store(); - foreach ( array( 'date', 'modified' ) as $key ) { - if ( isset( $args[ $key ] ) ) { - $args[ $key ] = as_get_datetime_object( $args[ $key ] ); - } - } - $count = $store->query_actions( $args, 'count' ); - return $count; - } - - - /** - * Get pending count for a specific post type. - * - * @since 1.1.0 - * - * @param string $post_type The post type. - * - * @return int The number of pending actions for this post type. - */ - public function get_pending_count_for_post_type( string $post_type ): int { - $count = $this->as_count_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_PENDING, - 'partial_args_matching' => 'json', - 'args' => [ - 'post_type' => $post_type - ] - ], - ); - - return $count; - } -} diff --git a/includes/class-nuclia-label-reprocessor.php b/includes/class-nuclia-label-reprocessor.php deleted file mode 100644 index b2e7fb6..0000000 --- a/includes/class-nuclia-label-reprocessor.php +++ /dev/null @@ -1,347 +0,0 @@ -plugin = $plugin; - } - - /** - * Register hooks for Action Scheduler. - * - * @since 1.4.0 - */ - public function register_hooks(): void { - add_action( self::HOOK_PROCESS_SINGLE, [ $this, 'process_single_labels' ], 10, 2 ); - } - - /** - * Schedule label reprocessing for all indexed posts. - * - * @since 1.4.0 - * - * @return int Number of posts scheduled for reprocessing. - */ - public function schedule_full_reprocess(): int { - // Check if Action Scheduler is available - if ( ! function_exists( 'as_schedule_single_action' ) ) { - nuclia_error_log( 'Action Scheduler not available. Cannot schedule label reprocessing.' ); - return 0; - } - - // Check if already running - $current_pending = $this->get_pending_count(); - $current_running = $this->get_running_count(); - if ( $current_pending > 0 || $current_running > 0 ) { - nuclia_log( "Reprocessing already in progress ({$current_pending} pending, {$current_running} running)." ); - return 0; - } - - nuclia_log( 'Scheduling full label reprocessing' ); - - $indexed_posts = $this->plugin->get_api()->get_all_indexed_posts(); - - if ( empty( $indexed_posts ) ) { - nuclia_log( 'No indexed posts found for label reprocessing.' ); - return 0; - } - - $total = count( $indexed_posts ); - - // Warn if large batch - if ( $total > self::MAX_BATCH_SIZE ) { - nuclia_log( "Large batch detected ({$total} posts). Processing may take a while." ); - } - - $scheduled_count = 0; - - foreach ( $indexed_posts as $row ) { - $post_id = (int) $row->post_id; - $rid = $row->nuclia_rid; - - if ( empty( $rid ) ) { - continue; - } - - // Check if this post is already scheduled - if ( $this->is_post_scheduled( $post_id ) ) { - continue; - } - - // Schedule the post for label reprocessing - as_schedule_single_action( - time() + ( $scheduled_count * 2 ), // Stagger by 2 seconds to avoid rate limiting - self::HOOK_PROCESS_SINGLE, - [ - 'post_id' => $post_id, - 'rid' => $rid, - ], - self::GROUP - ); - - $scheduled_count++; - } - - nuclia_log( "Scheduled {$scheduled_count} posts for label reprocessing." ); - return $scheduled_count; - } - - /** - * Process a single post's labels. - * - * This is the callback for Action Scheduler. - * - * @since 1.4.0 - * - * @param int $post_id The post ID. - * @param string $rid The Nuclia resource UUID. - * - * @throws Exception If update fails and should be retried. - */ - public function process_single_labels( int $post_id, string $rid ): void { - // Validate input parameters - if ( $post_id <= 0 ) { - nuclia_error_log( "Invalid post_id: {$post_id}. Skipping label reprocessing." ); - return; // Invalid data, don't retry - } - - if ( empty( $rid ) ) { - nuclia_error_log( "Empty RID for post {$post_id}. Skipping label reprocessing." ); - return; // Invalid data, don't retry - } - - // Validate RID format (UUID-like: 36 chars with hyphens) - if ( ! preg_match( '/^[a-f0-9\-]{36}$/i', $rid ) ) { - nuclia_error_log( "Invalid RID format for post {$post_id}: {$rid}. Skipping label reprocessing." ); - return; // Invalid data, don't retry - } - - nuclia_log( "Processing labels for post {$post_id} (rid: {$rid}) via Action Scheduler." ); - - // Check if API is still reachable - if ( ! $this->plugin->get_settings()->get_api_is_reachable() ) { - nuclia_log( 'API not reachable, skipping label reprocessing.' ); - throw new Exception( 'Nuclia API not reachable. Will retry later.' ); - } - - $post = get_post( $post_id ); - - if ( ! $post ) { - nuclia_log( "Post {$post_id} not found, skipping label reprocessing." ); - return; // Post deleted, skip without error - } - - try { - $result = $this->plugin->get_api()->update_resource_labels( $post_id, $rid, $post ); - - if ( ! $result['success'] ) { - throw new Exception( $result['message'] ?? "Failed to update labels for post {$post_id}" ); - } - - nuclia_log( "Successfully updated labels for post {$post_id}." ); - } catch ( Exception $e ) { - nuclia_error_log( "Failed to update labels for post {$post_id}: " . $e->getMessage() ); - // Re-throw to trigger Action Scheduler retry - throw $e; - } - } - - /** - * Check if a post is already scheduled for label reprocessing. - * - * @since 1.4.0 - * - * @param int $post_id The post ID. - * - * @return bool True if already scheduled. - */ - private function is_post_scheduled( int $post_id ): bool { - if ( ! function_exists( 'as_has_scheduled_action' ) ) { - return false; // Safe default - allow scheduling if Action Scheduler not available - } - - return as_has_scheduled_action( - self::HOOK_PROCESS_SINGLE, - [ - 'post_id' => $post_id, - ], - self::GROUP - ); - } - - /** - * Get the count of pending label reprocessing actions. - * - * @since 1.4.0 - * - * @return int The number of pending actions. - */ - public function get_pending_count(): int { - if ( ! function_exists( 'as_get_scheduled_actions' ) ) { - return 0; - } - - $count = $this->as_count_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_PENDING, - ] - ); - - return $count; - } - - /** - * Get the count of running/in-progress label reprocessing actions. - * - * @since 1.4.0 - * - * @return int The number of running actions. - */ - public function get_running_count(): int { - if ( ! function_exists( 'as_get_scheduled_actions' ) ) { - return 0; - } - - $actions = as_get_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_RUNNING, - ], - 'ids' - ); - - return count( $actions ); - } - - /** - * Get the count of failed label reprocessing actions. - * - * @since 1.4.0 - * - * @return int The number of failed actions. - */ - public function get_failed_count(): int { - if ( ! function_exists( 'as_get_scheduled_actions' ) ) { - return 0; - } - - $actions = as_get_scheduled_actions( - [ - 'hook' => self::HOOK_PROCESS_SINGLE, - 'group' => self::GROUP, - 'status' => ActionScheduler_Store::STATUS_FAILED, - ], - 'ids' - ); - - return count( $actions ); - } - - /** - * Cancel all pending label reprocessing actions. - * - * @since 1.4.0 - */ - public function cancel_all(): void { - if ( ! function_exists( 'as_unschedule_all_actions' ) ) { - return; - } - - as_unschedule_all_actions( self::HOOK_PROCESS_SINGLE, null, self::GROUP ); - - nuclia_log( 'Cancelled all pending Nuclia label reprocessing actions.' ); - } - - /** - * Get status information for the admin UI. - * - * @since 1.4.0 - * - * @return array{pending: int, running: int, failed: int, is_active: bool} Status information array. - */ - public function get_status(): array { - return [ - 'pending' => $this->get_pending_count(), - 'running' => $this->get_running_count(), - 'failed' => $this->get_failed_count(), - 'is_active' => $this->get_pending_count() > 0 || $this->get_running_count() > 0, - ]; - } - - /** - * Count scheduled actions using Action Scheduler. - * - * @since 1.4.0 - * - * @param array $args Query arguments. - * @return int - */ - protected function as_count_scheduled_actions( $args = [] ): int { - if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { - return 0; - } - - $store = ActionScheduler::store(); - - foreach ( [ 'date', 'modified' ] as $key ) { - if ( isset( $args[ $key ] ) ) { - $args[ $key ] = as_get_datetime_object( $args[ $key ] ); - } - } - - return $store->query_actions( $args, 'count' ); - } -} diff --git a/includes/class-nuclia-plugin.php b/includes/class-nuclia-plugin.php deleted file mode 100644 index d9b58f4..0000000 --- a/includes/class-nuclia-plugin.php +++ /dev/null @@ -1,421 +0,0 @@ -settings = new Nuclia_Settings(); - $this->api = new Nuclia_API( $this->settings ); - $this->background_processor = new Nuclia_Background_Processor( $this ); - $this->label_reprocessor = new Nuclia_Label_Reprocessor( $this ); - - // Register background processor hooks early (before 'init') - $this->background_processor->register_hooks(); - $this->label_reprocessor->register_hooks(); - - add_action( 'init', [ $this, 'load' ], 20 ); - } - - /** - * Load. - * - * @since 1.0.0 - */ - public function load(): void { - // Load admin or public part of the plugin. - if ( is_admin() ) { - - new Nuclia_Admin_Page_Settings( $this ); - add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); - - if ( $this->settings->get_api_is_reachable() ) { - // post, page and custom post type - add_action( 'save_post', [ $this, 'create_or_modify_nucliadb_resource' ], 10, 2 ); // $post_id, $post - // attachment - add_action( 'add_attachment', [ $this, 'add_or_modify_attachment' ], 10, 1 ); // $post_id - add_action( 'attachment_updated', [ $this, 'add_or_modify_attachment' ], 10, 1 ); // $post_id - // delete - add_action( 'delete_post', [ $this, 'delete_nuclia_resource' ], 10, 2 ); // $post_id, $post - - // ajax call to index all content - add_action( 'wp_ajax_nuclia_re_index', [ $this, 're_index' ] ); - } - } - - } - - /** - * Get the Nuclia_API. - * - * @since 1.0.0 - * - * @return Nuclia_API - */ - public function get_api(): Nuclia_API { - return $this->api; - } - - /** - * Get the Nuclia_Settings. - * - * @since 1.0.0 - * - * @return Nuclia_Settings - */ - public function get_settings(): Nuclia_Settings { - return $this->settings; - } - - /** - * Get the Nuclia_Background_Processor. - * - * @since 1.1.0 - * - * @return Nuclia_Background_Processor - */ - public function get_background_processor(): Nuclia_Background_Processor { - return $this->background_processor; - } - - /** - * Get the Nuclia_Label_Reprocessor. - * - * @since 1.4.0 - * - * @return Nuclia_Label_Reprocessor - */ - public function get_label_reprocessor(): Nuclia_Label_Reprocessor { - return $this->label_reprocessor; - } - - /** - * Get indexable post types. - * - * @since 1.0.0 - * - * @return array post type names indexable. - */ - public function get_indexable_post_types(): array { - return $this->settings->get_indexable_post_types(); - } - - /** After attachment is added or modified. - * - * @since 1.0.0 - * - * @param int $post_id Attachment ID. - */ - - public function add_or_modify_attachment( int $post_id ): void { - - $post = get_post( $post_id ); - - // hack attachments are inherit - $post->post_status = 'publish'; - - $this->create_or_modify_nucliadb_resource( $post_id, $post ); - } - - - /** Create or modify resource. - * - * @since 1.0.0 - * - * @param int $post_id Post ID. - * @param WP_Post $post WP_Post object. - */ - - public function create_or_modify_nucliadb_resource( int $post_id, WP_Post $post ): void { - - // auto-save - if( wp_is_post_autosave($post) ) return; - - nuclia_log( 'ID : '.$post_id ); - nuclia_log( 'type : '.$post->post_type ); - nuclia_log( 'password protected: '.( $post->post_password ? 'yes' : 'no' ) ); - nuclia_log( 'status : '.$post->post_status ); - - // indexable post type - if ( !array_key_exists( $post->post_type, $this->get_indexable_post_types() ) ) return; - - // do not index or delete, if not public or password protected - $dont_index = ( $post->post_password || $post->post_status !== 'publish' ) ? true : false; - if ( $dont_index ) { - return; - } - - // resource id if already indexed - $rid = $this->api->get_rid( $post_id ); - - if ( $rid ) { // post already indexed - nuclia_log( 'Modifying resource' ); - // $body = $this->prepare_nuclia_resource_body( $post ); - $this->api->modify_resource( $post_id, $rid, $post ); - } else { // post not indexed - nuclia_log( 'Creating resource' ); - // $body = $this->prepare_nuclia_resource_body( $post ); - $this->api->create_resource( $post_id, $post ); - }; - } - - /** - * Delete nuclia resource. - * - * @since 1.0.0 - * - * @param int $post_id Post ID. - * @param WP_Post $post WP_Post object. - */ - public function delete_nuclia_resource( int $post_id, WP_Post $post ): void { - $rid = $this->api->get_rid( $post_id ); - if ( $rid ) { - $this->api->delete_resource( $post_id, $rid ); - } - } - - - /** - * Prepare NucliaDB resource body - * - * @param WP_Post $post Post to index into NucliaDb. - * - * @return string Prepared resource body as JSON string - */ - - public function prepare_nuclia_resource_body( WP_Post $post ): string { - $body = [ - 'title' => html_entity_decode( wp_strip_all_tags( $post->post_title ), ENT_QUOTES, "UTF-8" ), - 'slug' => (string)$post->ID, - 'metadata' => [ - 'language' => get_bloginfo("language") - ], - 'origin' => [ - 'url' => get_permalink( $post ), - ], - 'created' => gmdate('Y-m-d', strtotime( $post->post_date_gmt )).'T'.gmdate('H:i:s', strtotime( $post->post_date_gmt )).'Z' - ]; - - // for attachments - // https://docs.nuclia.dev/docs/quick-start/push/#push-a-cloud-based-file - if ( $post->post_type == 'attachment' ) : - $file = get_attached_file( $post->ID ); - $filename = esc_html( wp_basename( $file ) ); - $mime_type = get_post_mime_type( $post->ID ); - $body = [ - ...$body, - 'icon' => $mime_type, - // 'files' => [ - // $post->post_name => [ - // 'file' => [ - // 'filename' => $filename, - // 'content_type' => $mime_type, - // 'payload' => base64_encode( file_get_contents( $file ) ) - // ] - // ] - // ] - ]; - - // other post types - else : - $body = [ - ...$body, - 'icon' => 'text/html', - 'texts' => [ - 'text-1' => [ - 'body' => apply_filters('the_content', $post->post_content ), - 'format' => 'HTML', - ] - ] - ]; - - endif; - - return json_encode( $body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); - } - - /** - * Enqueue scripts. - * - * @since 1.0.0 - */ - public function enqueue_scripts( string $hook = '' ): void { - if ( $hook !== 'toplevel_page_progress-agentic-rag' ) { - return; - } - - $plugin_url = plugin_dir_url(__FILE__) . 'admin/js/reindex-button.js'; - wp_enqueue_script( - 'nuclia-admin-reindex-button', - $plugin_url, - [], - PROGRESS_NUCLIA_VERSION, - true - ); - - // pass nonce to js. - wp_localize_script( - 'nuclia-admin-reindex-button', - 'nucliaReindex', - [ - 'nonce' => wp_create_nonce( 'nuclia_reindex_nonce' ), - 'labelsNonce' => wp_create_nonce( 'nuclia_labels_nonce' ), - 'i18n' => [ - 'confirmReprocess' => __( 'This will update labels for %d synced resource(s) with the current taxonomy mapping. Continue?', 'progress-agentic-rag' ), - 'copied' => __( 'Copied!', 'progress-agentic-rag' ), - 'copyFailed' => __( 'Copy failed', 'progress-agentic-rag' ), - ], - ] - ); - - $taxonomies = get_taxonomies( - [ - 'public' => true, - ], - 'objects' - ); - - $taxonomy_data = []; - foreach ( $taxonomies as $taxonomy ) { - $terms = get_terms( - [ - 'taxonomy' => $taxonomy->name, - 'hide_empty' => false, - ] - ); - - if ( is_wp_error( $terms ) ) { - $terms = []; - } - - $taxonomy_data[ $taxonomy->name ] = [ - 'label' => $taxonomy->labels->name ?? $taxonomy->name, - 'terms' => array_map( - static function ( $term ) { - return [ - 'id' => $term->term_id, - 'name' => $term->name, - ]; - }, - $terms - ), - ]; - } - - $labelsets = $this->api->get_labelsets(); - - wp_localize_script( - 'nuclia-admin-reindex-button', - 'nucliaMappingData', - [ - 'taxonomies' => $taxonomy_data, - 'labelsets' => $labelsets, - ] - ); - } - - /** - * Re index. - * - * @since 1.0.0 - * - * @throws RuntimeException If index ID or page are not provided, or index name does not exist. - * @throws Exception If index ID or page are not provided, or index name does not exist. - */ - public function re_index(): void { - if ( ! current_user_can( 'manage_options' ) ) { - wp_send_json_error( 'Unauthorized', 403 ); - } - - // Verify nonce. - if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'nuclia_reindex_nonce' ) ) { - wp_send_json_error( 'Invalid nonce', 403 ); - } - - $post_type = sanitize_text_field( $_POST['post_type'] ?? '' ); - - try { - if ( empty( $post_type ) || !post_type_exists( $post_type ) ) { - nuclia_log( 'Post type should be provided' ); - throw new RuntimeException( 'Post type should be provided.' ); - } - - $indexable = get_option( "nuclia_indexable_{$post_type}", [] ); - - $total_pages = count( $indexable ); - nuclia_log( "Total pages {$total_pages}" ); - if ( $total_pages ) { - $post_id = array_shift($indexable); - update_option( "nuclia_indexable_{$post_type}", $indexable ); - $total_pages --; - if ( $post_type == 'attachment' ) { - $this->add_or_modify_attachment( $post_id ); - } else { - $post = get_post( $post_id ); - // $body = $this->prepare_nuclia_resource_body( $post ); - $this->api->create_resource( $post_id, $post ); - }; - }; - - $response = [ - 'nbPosts' => $total_pages - ]; - - wp_send_json( $response, 200 ); - } catch ( Exception $exception ) { - wp_send_json( ['error' => $exception->getMessage()], 500 ); - throw $exception; - } - } - -} diff --git a/includes/class-nuclia-settings.php b/includes/class-nuclia-settings.php deleted file mode 100644 index 637ef3e..0000000 --- a/includes/class-nuclia-settings.php +++ /dev/null @@ -1,239 +0,0 @@ - 0, - 'labelsets' => [], - 'labels' => [] - ] ); - add_option( 'nuclia_indexable_post_types', [ - 'post' => 1 , - 'page' => 1 - ]); - } - - /** - * Get the Nuclia Zone. - * - * @since 1.0.0 - * - * @return string - */ - public function get_zone(): string { - return (string) get_option( 'nuclia_zone', '' ); - } - - /** - * Get the Nuclia token. - * - * @since 1.0.0 - * - * @return string - */ - public function get_token(): string { - return (string) get_option( 'nuclia_token', '' ); - - } - - /** - * Get the Nuclia Knowledge Box ID - * - * @since 1.0.0 - * - * @return string - */ - public function get_kbid(): string { - return (string) get_option( 'nuclia_kbid', '' ); - - } - - /** - * Get the Nuclia Account ID. - * - * @since 1.2.0 - * - * @return string - */ - public function get_account_id(): string { - return (string) get_option( 'nuclia_account_id', '' ); - } - - /** - * Get the indexable post types - * - * @since 1.0.0 - * - * @return array - */ - public function get_indexable_post_types(): array { - return (array) get_option( 'nuclia_indexable_post_types', [] ); - } - - /** - * Get taxonomy -> label mapping configuration. - * - * @since 1.2.0 - * - * @return array - */ - public function get_taxonomy_label_map(): array { - $map = (array) get_option( 'nuclia_taxonomy_label_map', [] ); - foreach ( $map as $taxonomy => $config ) { - if ( ! is_array( $config ) ) { - continue; - } - $terms = is_array( $config['terms'] ?? null ) ? $config['terms'] : []; - foreach ( $terms as $term_id => $labels ) { - if ( is_array( $labels ) ) { - continue; - } - if ( is_string( $labels ) && $labels !== '' ) { - $terms[ $term_id ] = [ $labels ]; - continue; - } - unset( $terms[ $term_id ] ); - } - $map[ $taxonomy ]['terms'] = $terms; - } - return $map; - } - - /** - * Get cached Nuclia labelsets. - * - * @since 1.2.0 - * - * @return array - */ - public function get_labelsets_cache(): array { - $cache = get_option( 'nuclia_labelsets_cache', [] ); - return is_array( $cache ) ? $cache : []; - } - - /** - * Set cached Nuclia labelsets. - * - * @since 1.2.0 - * - * @param array $labelsets - */ - public function set_labelsets_cache( array $labelsets ): void { - $cache = $this->get_labelsets_cache(); - $labels = is_array( $cache['labels'] ?? null ) ? $cache['labels'] : []; - - update_option( 'nuclia_labelsets_cache', [ - 'fetched_at' => time(), - 'labelsets' => array_values( $labelsets ), - 'labels' => $labels - ] ); - } - - /** - * Set labelsets cache including labels map. - * - * @since 1.2.0 - * - * @param array $labelsets - * @param array $labels_map - */ - public function set_labelsets_cache_with_labels( array $labelsets, array $labels_map ): void { - $labels = []; - foreach ( $labels_map as $labelset => $labels_list ) { - if ( ! is_string( $labelset ) || ! is_array( $labels_list ) ) { - continue; - } - $labels[ $labelset ] = array_values( $labels_list ); - } - - update_option( 'nuclia_labelsets_cache', [ - 'fetched_at' => time(), - 'labelsets' => array_values( $labelsets ), - 'labels' => $labels, - ] ); - } - - /** - * Get cached labels for a labelset. - * - * @since 1.2.0 - * - * @param string $labelset - * @return array - */ - public function get_labelset_labels_cache( string $labelset ): array { - $cache = $this->get_labelsets_cache(); - $labels = is_array( $cache['labels'] ?? null ) ? $cache['labels'] : []; - return is_array( $labels[ $labelset ] ?? null ) ? $labels[ $labelset ] : []; - } - - /** - * Store cached labels for a labelset. - * - * @since 1.2.0 - * - * @param string $labelset - * @param array $labels - */ - public function set_labelset_labels_cache( string $labelset, array $labels ): void { - $cache = $this->get_labelsets_cache(); - $labelsets = is_array( $cache['labelsets'] ?? null ) ? $cache['labelsets'] : []; - $stored_labels = is_array( $cache['labels'] ?? null ) ? $cache['labels'] : []; - $stored_labels[ $labelset ] = array_values( $labels ); - - update_option( 'nuclia_labelsets_cache', [ - 'fetched_at' => time(), - 'labelsets' => $labelsets, - 'labels' => $stored_labels, - ] ); - } - - /** - * Get the API is reachable option setting. - * - * @since 1.0.0 - * - * @return bool - */ - public function get_api_is_reachable(): bool { - $enabled = get_option( 'nuclia_api_is_reachable', 'no' ); - - return 'yes' === $enabled; - } - - /** - * Set the API is reachable option setting. - * - * @since 1.0.0 - * - * @param bool $flag If the API is reachable or not, 'yes' or 'no'. - */ - public function set_api_is_reachable( bool $flag ): void { - $value = $flag ? 'yes' : 'no'; - update_option( 'nuclia_api_is_reachable', $value ); - } - -} diff --git a/includes/index.php b/includes/index.php deleted file mode 100644 index e71af0e..0000000 --- a/includes/index.php +++ /dev/null @@ -1 +0,0 @@ - - - $CHANGES - - **Added** - **Changed** - **Deprecated** - **Removed** - **Fixed** - **Security** - -change-template: '* $TITLE (PR #$NUMBER)' diff --git a/includes/libraries/action-scheduler/.github/workflows/pr-unit-tests.yml b/includes/libraries/action-scheduler/.github/workflows/pr-unit-tests.yml deleted file mode 100644 index 44571bc..0000000 --- a/includes/libraries/action-scheduler/.github/workflows/pr-unit-tests.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Run unit tests on PR -on: - pull_request -jobs: - test: - name: PHP ${{ matrix.php }} WP ${{ matrix.wp }} MU ${{ matrix.multisite }} DB ${{ matrix.db }} - timeout-minutes: 15 - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - # We test against the earliest and latest PHP versions for each major supported version. - php: [ '7.2', '7.4', '8.0', '8.3' ] - wp: [ '6.5', '6.6', 'latest', 'nightly' ] - multisite: [ '0', '1' ] - db: [ 'mysql:5.6', 'mysql:8.1', 'mariadb:10.4', 'mariadb:10.6'] - exclude: - # WordPress 6.6+ requires PHP 7.2+ - - php: 7.2 - wp: 6.6 - - php: 7.2 - wp: latest - - php: 7.2 - wp: nightly - services: - database: - image: ${{ matrix.db }} - env: - MYSQL_ROOT_PASSWORD: root - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1 - with: - php-version: ${{ matrix.php }} - tools: composer - extensions: mysql - coverage: none - - - name: Tool versions - run: | - php --version - composer --version - - - name: Get composer cache directory - id: composer-cache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies - run: composer install --prefer-dist - - - name: Install Subversion - run: sudo apt-get update && sudo apt-get install -y subversion - - - name: Init DB and WP - run: | - # Use mysql_native_password when using PHP < 7.4 and MySQL >= 8.0 - if [ "$(php -r 'echo version_compare(PHP_VERSION, "7.4", "<");')" -eq 1 ] && [ "${{ matrix.db }}" == "mysql:8.1" ]; then - mysql -uroot -proot -h127.0.0.1 -e "ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; FLUSH PRIVILEGES;" - fi - ./tests/bin/install.sh woo_test root root 127.0.0.1 ${{ matrix.wp }} - - - name: Run tests - run: | - ./vendor/bin/phpunit --version - WP_MULTISITE=${{ matrix.multisite }} ./vendor/bin/phpunit -c ./tests/phpunit.xml.dist - - - name: Code Coverage - run: | - bash <(curl -s https://codecov.io/bash) diff --git a/includes/libraries/action-scheduler/.gitignore b/includes/libraries/action-scheduler/.gitignore deleted file mode 100644 index 6716d1c..0000000 --- a/includes/libraries/action-scheduler/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Operating System files -.DS_Store -Thumbs.db - -# IDE files -.idea -.vscode/* -project.xml -project.properties -.project -.settings* -*.sublime-project -*.sublime-workspace -.sublimelinterrc - -# Project files -node_modules/ -vendor/ - -#PHP Unit -.phpunit.result.cache -phpunit.xml - -# Build files -action-scheduler.zip diff --git a/includes/libraries/action-scheduler/Gruntfile.js b/includes/libraries/action-scheduler/Gruntfile.js deleted file mode 100644 index 4477049..0000000 --- a/includes/libraries/action-scheduler/Gruntfile.js +++ /dev/null @@ -1,57 +0,0 @@ -module.exports = function( grunt ) { - 'use strict'; - - grunt.initConfig({ - // Check textdomain errors. - checktextdomain: { - options:{ - text_domain: 'action-scheduler', - keywords: [ - '__:1,2d', - '_e:1,2d', - '_x:1,2c,3d', - 'esc_html__:1,2d', - 'esc_html_e:1,2d', - 'esc_html_x:1,2c,3d', - 'esc_attr__:1,2d', - 'esc_attr_e:1,2d', - 'esc_attr_x:1,2c,3d', - '_ex:1,2c,3d', - '_n:1,2,4d', - '_nx:1,2,4c,5d', - '_n_noop:1,2,3d', - '_nx_noop:1,2,3c,4d' - ] - }, - files: { - src: [ - '**/*.php', - '!node_modules/**', - '!tests/**', - '!vendor/**', - '!tmp/**' - ], - expand: true - } - }, - - // PHP Code Sniffer. - phpcs: { - options: { - bin: 'vendor/bin/phpcs' - }, - dist: { - src: [ - '**/*.php', // Include all php files. - '!deprecated/**', - '!node_modules/**', - '!vendor/**' - ] - } - } - }); - - // Load NPM tasks to be used here. - grunt.loadNpmTasks( 'grunt-phpcs' ); - grunt.loadNpmTasks( 'grunt-checktextdomain' ); -}; diff --git a/includes/libraries/action-scheduler/codecov.yml b/includes/libraries/action-scheduler/codecov.yml deleted file mode 100644 index fe69f96..0000000 --- a/includes/libraries/action-scheduler/codecov.yml +++ /dev/null @@ -1,13 +0,0 @@ -codecov: - branch: master - -coverage: - ignore: - - tests/.* - - lib/.* - status: - project: false - patch: false - changes: false - -comment: false diff --git a/includes/libraries/action-scheduler/composer.json b/includes/libraries/action-scheduler/composer.json deleted file mode 100644 index 66c7fcc..0000000 --- a/includes/libraries/action-scheduler/composer.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "woocommerce/action-scheduler", - "description": "Action Scheduler for WordPress and WooCommerce", - "homepage": "https://actionscheduler.org/", - "type": "wordpress-plugin", - "license": "GPL-3.0-or-later", - "prefer-stable": true, - "minimum-stability": "dev", - "require": { - "php": ">=7.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5", - "wp-cli/wp-cli": "~2.5.0", - "woocommerce/woocommerce-sniffs": "0.1.0", - "yoast/phpunit-polyfills": "^2.0" - }, - "config": { - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - }, - "platform": { - "php": "7.2" - } - }, - "archive": { - "exclude": [ - "node_modules" - ] - }, - "scripts": { - "test": [ - "./vendor/bin/phpunit tests -c tests/phpunit.xml.dist" - ], - "phpcs": [ - "phpcs -s -p" - ], - "phpcs-pre-commit": [ - "phpcs -s -p -n" - ], - "phpcbf": [ - "phpcbf -p" - ] - }, - "extra": { - "scripts-description": { - "test": "Run unit tests", - "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", - "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" - } - } -} diff --git a/includes/libraries/action-scheduler/composer.lock b/includes/libraries/action-scheduler/composer.lock deleted file mode 100644 index 6abeeb5..0000000 --- a/includes/libraries/action-scheduler/composer.lock +++ /dev/null @@ -1,2390 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "55c846ad8e6e369a91b7de2e9976f685", - "packages": [], - "packages-dev": [ - { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.0", - "source": { - "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev" - }, - "require-dev": { - "composer/composer": "*", - "phpcompatibility/php-compatibility": "^9.0", - "sensiolabs/security-checker": "^4.1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" - }, - "autoload": { - "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" - } - ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", - "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" - ], - "support": { - "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", - "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" - }, - "time": "2020-06-25T14:57:39+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" - } - ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], - "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" - }, - "funding": [ - { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" - } - ], - "time": "2022-12-30T00:15:36+00:00" - }, - { - "name": "mustache/mustache", - "version": "v2.14.2", - "source": { - "type": "git", - "url": "https://github.com/bobthecow/mustache.php.git", - "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb", - "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb", - "shasum": "" - }, - "require": { - "php": ">=5.2.4" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "~1.11", - "phpunit/phpunit": "~3.7|~4.0|~5.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Mustache": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Justin Hileman", - "email": "justin@justinhileman.info", - "homepage": "http://justinhileman.com" - } - ], - "description": "A Mustache implementation in PHP.", - "homepage": "https://github.com/bobthecow/mustache.php", - "keywords": [ - "mustache", - "templating" - ], - "support": { - "issues": "https://github.com/bobthecow/mustache.php/issues", - "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.2" - }, - "time": "2022-08-23T13:07:01+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.13.1", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2025-04-29T12:36:36+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "54750ef60c58e43759730615a392c31c80e23176" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", - "reference": "54750ef60c58e43759730615a392c31c80e23176", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:33:53+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, - { - "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", - "source": { - "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" - }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." - }, - "type": "phpcodesniffer-standard", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Wim Godden", - "homepage": "https://github.com/wimg", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" - } - ], - "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", - "keywords": [ - "compatibility", - "phpcs", - "standards" - ], - "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibility" - }, - "time": "2019-12-27T09:44:58+00:00" - }, - { - "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.3", - "source": { - "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", - "shasum": "" - }, - "require": { - "phpcompatibility/php-compatibility": "^9.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "paragonie/random_compat": "dev-master", - "paragonie/sodium_compat": "dev-master" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." - }, - "type": "phpcodesniffer-standard", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Wim Godden", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "lead" - } - ], - "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", - "homepage": "http://phpcompatibility.com/", - "keywords": [ - "compatibility", - "paragonie", - "phpcs", - "polyfill", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", - "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", - "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" - }, - "funding": [ - { - "url": "https://github.com/PHPCompatibility", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - } - ], - "time": "2024-04-24T21:30:46+00:00" - }, - { - "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "41bef18ba688af638b7310666db28e1ea9158b2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/41bef18ba688af638b7310666db28e1ea9158b2f", - "reference": "41bef18ba688af638b7310666db28e1ea9158b2f", - "shasum": "" - }, - "require": { - "phpcompatibility/php-compatibility": "^9.0", - "phpcompatibility/phpcompatibility-paragonie": "^1.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." - }, - "type": "phpcodesniffer-standard", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Wim Godden", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "lead" - } - ], - "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", - "homepage": "http://phpcompatibility.com/", - "keywords": [ - "compatibility", - "phpcs", - "standards", - "wordpress" - ], - "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" - }, - "time": "2019-08-28T14:22:28+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "7.0.17", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "40a4ed114a4aea5afd6df8d0f0c9cd3033097f66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/40a4ed114a4aea5afd6df8d0f0c9cd3033097f66", - "reference": "40a4ed114a4aea5afd6df8d0f0c9cd3033097f66", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": ">=7.2", - "phpunit/php-file-iterator": "^2.0.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.1.3 || ^4.0", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^4.2.2", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1.3" - }, - "require-dev": { - "phpunit/phpunit": "^8.2.2" - }, - "suggest": { - "ext-xdebug": "^2.7.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.17" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:09:37+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "2.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "69deeb8664f611f156a924154985fbd4911eb36b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/69deeb8664f611f156a924154985fbd4911eb36b", - "reference": "69deeb8664f611f156a924154985fbd4911eb36b", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T13:39:50+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" - }, - "time": "2015-06-21T13:50:34+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "2.1.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a691211e94ff39a34811abd521c31bd5b305b0bb", - "reference": "a691211e94ff39a34811abd521c31bd5b305b0bb", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T13:42:41+00:00" - }, - { - "name": "phpunit/php-token-stream", - "version": "3.1.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "9c1da83261628cb24b6a6df371b6e312b3954768" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768", - "reference": "9c1da83261628cb24b6a6df371b6e312b3954768", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", - "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "abandoned": true, - "time": "2021-07-26T12:15:06+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "8.5.42", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3a68a70824da546d26ac08ca4fced67341f4158f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3a68a70824da546d26ac08ca4fced67341f4158f", - "reference": "3a68a70824da546d26ac08ca4fced67341f4158f", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.5.0", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", - "phar-io/manifest": "^2.0.4", - "phar-io/version": "^3.2.1", - "php": ">=7.2", - "phpunit/php-code-coverage": "^7.0.17", - "phpunit/php-file-iterator": "^2.0.6", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.1.4", - "sebastian/comparator": "^3.0.5", - "sebastian/diff": "^3.0.6", - "sebastian/environment": "^4.2.5", - "sebastian/exporter": "^3.1.6", - "sebastian/global-state": "^3.0.5", - "sebastian/object-enumerator": "^3.0.5", - "sebastian/resource-operations": "^2.0.3", - "sebastian/type": "^1.1.5", - "sebastian/version": "^2.0.1" - }, - "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage", - "phpunit/php-invoker": "To allow enforcing time limits" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "8.5-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.42" - }, - "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" - } - ], - "time": "2025-05-02T06:33:00+00:00" - }, - { - "name": "rmccue/requests", - "version": "v1.8.1", - "source": { - "type": "git", - "url": "https://github.com/WordPress/Requests.git", - "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/WordPress/Requests/zipball/82e6936366eac3af4d836c18b9d8c31028fe4cd5", - "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5", - "shasum": "" - }, - "require": { - "php": ">=5.2" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", - "php-parallel-lint/php-console-highlighter": "^0.5.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcompatibility/php-compatibility": "^9.0", - "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5", - "requests/test-server": "dev-master", - "squizlabs/php_codesniffer": "^3.5", - "wp-coding-standards/wpcs": "^2.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Requests": "library/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "ISC" - ], - "authors": [ - { - "name": "Ryan McCue", - "homepage": "http://ryanmccue.info" - } - ], - "description": "A HTTP library written in PHP, for human beings.", - "homepage": "http://github.com/WordPress/Requests", - "keywords": [ - "curl", - "fsockopen", - "http", - "idna", - "ipv6", - "iri", - "sockets" - ], - "support": { - "issues": "https://github.com/WordPress/Requests/issues", - "source": "https://github.com/WordPress/Requests/tree/v1.8.1" - }, - "time": "2021-06-04T09:56:25+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", - "reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T13:45:45+00:00" - }, - { - "name": "sebastian/comparator", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770", - "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770", - "shasum": "" - }, - "require": { - "php": ">=7.1", - "sebastian/diff": "^3.0", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-09-14T12:31:48+00:00" - }, - { - "name": "sebastian/diff", - "version": "3.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/98ff311ca519c3aa73ccd3de053bdb377171d7b6", - "reference": "98ff311ca519c3aa73ccd3de053bdb377171d7b6", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.5 || ^8.0", - "symfony/process": "^2 || ^3.3 || ^4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/3.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:16:36+00:00" - }, - { - "name": "sebastian/environment", - "version": "4.2.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "56932f6049a0482853056ffd617c91ffcc754205" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/56932f6049a0482853056ffd617c91ffcc754205", - "reference": "56932f6049a0482853056ffd617c91ffcc754205", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.5" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/4.2.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T13:49:59+00:00" - }, - { - "name": "sebastian/exporter", - "version": "3.1.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/1939bc8fd1d39adcfa88c5b35335910869214c56", - "reference": "1939bc8fd1d39adcfa88c5b35335910869214c56", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "http://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:21:38+00:00" - }, - { - "name": "sebastian/global-state", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/91c7c47047a971f02de57ed6f040087ef110c5d9", - "reference": "91c7c47047a971f02de57ed6f040087ef110c5d9", - "shasum": "" - }, - "require": { - "php": ">=7.2", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^8.0" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-02T06:13:16+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "3.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "ac5b293dba925751b808e02923399fb44ff0d541" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/ac5b293dba925751b808e02923399fb44ff0d541", - "reference": "ac5b293dba925751b808e02923399fb44ff0d541", - "shasum": "" - }, - "require": { - "php": ">=7.0", - "sebastian/object-reflector": "^1.1.1", - "sebastian/recursion-context": "^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T13:54:02+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "1.1.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "1d439c229e61f244ff1f211e5c99737f90c67def" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d439c229e61f244ff1f211e5c99737f90c67def", - "reference": "1d439c229e61f244ff1f211e5c99737f90c67def", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T13:56:04+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/9bfd3c6f1f08c026f542032dfb42813544f7d64c", - "reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T14:07:30+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/72a7f7674d053d548003b16ff5a106e7e0e06eee", - "reference": "72a7f7674d053d548003b16ff5a106e7e0e06eee", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T13:59:09+00:00" - }, - { - "name": "sebastian/type", - "version": "1.1.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "18f071c3a29892b037d35e6b20ddf3ea39b42874" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/18f071c3a29892b037d35e6b20ddf3ea39b42874", - "reference": "18f071c3a29892b037d35e6b20ddf3ea39b42874", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/1.1.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2024-03-01T14:04:07+00:00" - }, - { - "name": "sebastian/version", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", - "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/master" - }, - "time": "2016-10-03T07:35:21+00:00" - }, - { - "name": "squizlabs/php_codesniffer", - "version": "3.13.1", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1b71b4dd7e7ef651ac749cea67e513c0c832f4bd", - "reference": "1b71b4dd7e7ef651ac749cea67e513c0c832f4bd", - "shasum": "" - }, - "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" - }, - "bin": [ - "bin/phpcbf", - "bin/phpcs" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Greg Sherwood", - "role": "Former lead" - }, - { - "name": "Juliette Reinders Folmer", - "role": "Current lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "keywords": [ - "phpcs", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", - "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", - "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", - "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" - }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" - } - ], - "time": "2025-06-12T15:04:34+00:00" - }, - { - "name": "symfony/finder", - "version": "v4.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "66bd787edb5e42ff59d3523f623895af05043e4f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/66bd787edb5e42ff59d3523f623895af05043e4f", - "reference": "66bd787edb5e42ff59d3523f623895af05043e4f", - "shasum": "" - }, - "require": { - "php": ">=7.1.3", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v4.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-07-29T07:35:46+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.32.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-01-02T08:10:11+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.3", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2024-03-03T12:36:25+00:00" - }, - { - "name": "woocommerce/woocommerce-sniffs", - "version": "0.1.0", - "source": { - "type": "git", - "url": "https://github.com/woocommerce/woocommerce-sniffs.git", - "reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79", - "reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79", - "shasum": "" - }, - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "0.7.0", - "php": ">=7.0", - "phpcompatibility/phpcompatibility-wp": "2.1.0", - "wp-coding-standards/wpcs": "2.3.0" - }, - "type": "phpcodesniffer-standard", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Claudio Sanches", - "email": "claudio@automattic.com" - } - ], - "description": "WooCommerce sniffs", - "keywords": [ - "phpcs", - "standards", - "woocommerce", - "wordpress" - ], - "support": { - "issues": "https://github.com/woocommerce/woocommerce-sniffs/issues", - "source": "https://github.com/woocommerce/woocommerce-sniffs/tree/master" - }, - "time": "2020-08-06T18:23:45+00:00" - }, - { - "name": "wp-cli/mustangostang-spyc", - "version": "0.6.3", - "source": { - "type": "git", - "url": "https://github.com/wp-cli/spyc.git", - "reference": "6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-cli/spyc/zipball/6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7", - "reference": "6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7", - "shasum": "" - }, - "require": { - "php": ">=5.3.1" - }, - "require-dev": { - "phpunit/phpunit": "4.3.*@dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.5.x-dev" - } - }, - "autoload": { - "files": [ - "includes/functions.php" - ], - "psr-4": { - "Mustangostang\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "mustangostang", - "email": "vlad.andersen@gmail.com" - } - ], - "description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)", - "homepage": "https://github.com/mustangostang/spyc/", - "support": { - "source": "https://github.com/wp-cli/spyc/tree/autoload" - }, - "time": "2017-04-25T11:26:20+00:00" - }, - { - "name": "wp-cli/php-cli-tools", - "version": "v0.11.22", - "source": { - "type": "git", - "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "a6bb94664ca36d0962f9c2ff25591c315a550c51" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/a6bb94664ca36d0962f9c2ff25591c315a550c51", - "reference": "a6bb94664ca36d0962f9c2ff25591c315a550c51", - "shasum": "" - }, - "require": { - "php": ">= 5.3.0" - }, - "require-dev": { - "roave/security-advisories": "dev-latest", - "wp-cli/wp-cli-tests": "^4" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.11.x-dev" - } - }, - "autoload": { - "files": [ - "lib/cli/cli.php" - ], - "psr-0": { - "cli": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Daniel Bachhuber", - "email": "daniel@handbuilt.co", - "role": "Maintainer" - }, - { - "name": "James Logsdon", - "email": "jlogsdon@php.net", - "role": "Developer" - } - ], - "description": "Console utilities for PHP", - "homepage": "http://github.com/wp-cli/php-cli-tools", - "keywords": [ - "cli", - "console" - ], - "support": { - "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.22" - }, - "time": "2023-12-03T19:25:05+00:00" - }, - { - "name": "wp-cli/wp-cli", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/wp-cli/wp-cli.git", - "reference": "0bcf0c54f4d35685211d435e25219cc7acbe6d48" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/wp-cli/wp-cli/zipball/0bcf0c54f4d35685211d435e25219cc7acbe6d48", - "reference": "0bcf0c54f4d35685211d435e25219cc7acbe6d48", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "mustache/mustache": "~2.13", - "php": "^5.6 || ^7.0 || ^8.0", - "rmccue/requests": "^1.8", - "symfony/finder": ">2.7", - "wp-cli/mustangostang-spyc": "^0.6.3", - "wp-cli/php-cli-tools": "~0.11.2" - }, - "require-dev": { - "roave/security-advisories": "dev-master", - "wp-cli/db-command": "^1.3 || ^2", - "wp-cli/entity-command": "^1.2 || ^2", - "wp-cli/extension-command": "^1.1 || ^2", - "wp-cli/package-command": "^1 || ^2", - "wp-cli/wp-cli-tests": "^3.0.7" - }, - "suggest": { - "ext-readline": "Include for a better --prompt implementation", - "ext-zip": "Needed to support extraction of ZIP archives when doing downloads or updates" - }, - "bin": [ - "bin/wp", - "bin/wp.bat" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5.x-dev" - } - }, - "autoload": { - "psr-0": { - "WP_CLI\\": "php/" - }, - "classmap": [ - "php/class-wp-cli.php", - "php/class-wp-cli-command.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "WP-CLI framework", - "homepage": "https://wp-cli.org", - "keywords": [ - "cli", - "wordpress" - ], - "support": { - "docs": "https://make.wordpress.org/cli/handbook/", - "issues": "https://github.com/wp-cli/wp-cli/issues", - "source": "https://github.com/wp-cli/wp-cli" - }, - "time": "2021-05-14T13:44:51+00:00" - }, - { - "name": "wp-coding-standards/wpcs", - "version": "2.3.0", - "source": { - "type": "git", - "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "7da1894633f168fe244afc6de00d141f27517b62" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", - "reference": "7da1894633f168fe244afc6de00d141f27517b62", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.3.1" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", - "phpcompatibility/php-compatibility": "^9.0", - "phpcsstandards/phpcsdevtools": "^1.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." - }, - "type": "phpcodesniffer-standard", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Contributors", - "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", - "keywords": [ - "phpcs", - "standards", - "wordpress" - ], - "support": { - "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", - "source": "https://github.com/WordPress/WordPress-Coding-Standards", - "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" - }, - "time": "2020-05-13T23:57:56+00:00" - }, - { - "name": "yoast/phpunit-polyfills", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "a0e3e9adecaa352697786cb29bb0f2fcc25f43f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/a0e3e9adecaa352697786cb29bb0f2fcc25f43f5", - "reference": "a0e3e9adecaa352697786cb29bb0f2fcc25f43f5", - "shasum": "" - }, - "require": { - "php": ">=5.6", - "phpunit/phpunit": "^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" - }, - "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "yoast/yoastcs": "^3.1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "4.x-dev" - } - }, - "autoload": { - "files": [ - "phpunitpolyfills-autoload.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Team Yoast", - "email": "support@yoast.com", - "homepage": "https://yoast.com" - }, - { - "name": "Contributors", - "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" - } - ], - "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", - "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", - "keywords": [ - "phpunit", - "polyfill", - "testing" - ], - "support": { - "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", - "security": "https://github.com/Yoast/PHPUnit-Polyfills/security/policy", - "source": "https://github.com/Yoast/PHPUnit-Polyfills" - }, - "time": "2025-02-09T18:25:29+00:00" - } - ], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": {}, - "prefer-stable": true, - "prefer-lowest": false, - "platform": { - "php": ">=7.2" - }, - "platform-dev": {}, - "platform-overrides": { - "php": "7.2" - }, - "plugin-api-version": "2.6.0" -} diff --git a/includes/libraries/action-scheduler/docs/CNAME b/includes/libraries/action-scheduler/docs/CNAME deleted file mode 100644 index 3b480b5..0000000 --- a/includes/libraries/action-scheduler/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -actionscheduler.org \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/_config.yml b/includes/libraries/action-scheduler/docs/_config.yml deleted file mode 100644 index fe521d9..0000000 --- a/includes/libraries/action-scheduler/docs/_config.yml +++ /dev/null @@ -1,7 +0,0 @@ -title: Action Scheduler - Job Queue for WordPress -description: A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required. -theme: jekyll-theme-hacker -permalink: /:slug/ -plugins: - - jekyll-seo-tag - - jekyll-sitemap diff --git a/includes/libraries/action-scheduler/docs/_layouts/default.html b/includes/libraries/action-scheduler/docs/_layouts/default.html deleted file mode 100644 index 3559804..0000000 --- a/includes/libraries/action-scheduler/docs/_layouts/default.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - -{% seo %} - - - - -
- - - -
-

Usage | Admin | WP-CLI | Background Processing at Scale | API | FAQ | Version 3.0 -

action-scheduler

-

A scalable, traceable job queue for background processing large queues of tasks in WordPress. Designed for distribution in WordPress plugins - no server access required.

-
-
- -
-
- {{ content }} -
-
- - - - {% if site.google_analytics %} - - {% endif %} - - \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/admin.md b/includes/libraries/action-scheduler/docs/admin.md deleted file mode 100644 index 18769eb..0000000 --- a/includes/libraries/action-scheduler/docs/admin.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -description: Learn how to administer background jobs with the Action Scheduler job queue for WordPress. ---- -# Scheduled Actions Administration Screen - -Action Scheduler has a built in administration screen for monitoring, debugging and manually triggering scheduled actions. - -The administration interface is accessible through both: - -1. **Tools > Scheduled Actions** -1. **WooCommerce > Status > Scheduled Actions**, when WooCommerce is installed. - -Among other tasks, from the admin screen you can: - -* run a pending action -* view the scheduled actions with a specific status, like the all actions which have failed or are in-progress (https://cldup.com/NNTwE88Xl8.png). -* view the log entries for a specific action to find out why it failed. -* sort scheduled actions by hook name, scheduled date, claim ID or group name. - -Still have questions? Check out the [FAQ](/faq). - -![](https://cldup.com/5BA2BNB1sw.png) diff --git a/includes/libraries/action-scheduler/docs/android-chrome-192x192.png b/includes/libraries/action-scheduler/docs/android-chrome-192x192.png deleted file mode 100644 index 2227905..0000000 Binary files a/includes/libraries/action-scheduler/docs/android-chrome-192x192.png and /dev/null differ diff --git a/includes/libraries/action-scheduler/docs/android-chrome-256x256.png b/includes/libraries/action-scheduler/docs/android-chrome-256x256.png deleted file mode 100644 index 5f02ac0..0000000 Binary files a/includes/libraries/action-scheduler/docs/android-chrome-256x256.png and /dev/null differ diff --git a/includes/libraries/action-scheduler/docs/api.md b/includes/libraries/action-scheduler/docs/api.md deleted file mode 100644 index 71822ca..0000000 --- a/includes/libraries/action-scheduler/docs/api.md +++ /dev/null @@ -1,288 +0,0 @@ ---- -description: Reference guide for background processing functions provided by the Action Scheduler job queue for WordPress. ---- -# API Reference - -Action Scheduler provides a range of functions for scheduling hooks to run at some time in the future on one or more occasions. - -To understand the scheduling functions, it can help to think of them as extensions to WordPress' `do_action()` function that add the ability to delay and repeat when the hook will be triggered. - -## WP-Cron APIs vs. Action Scheduler APIs - -The Action Scheduler API functions are designed to mirror the WordPress [WP-Cron API functions](http://codex.wordpress.org/Category:WP-Cron_Functions). - -Functions return similar values and accept similar arguments to their WP-Cron counterparts. The notable differences are: - -* `as_schedule_single_action()` & `as_schedule_recurring_action()` will return the ID of the scheduled action rather than boolean indicating whether the event was scheduled -* `as_schedule_recurring_action()` takes an interval in seconds as the recurring interval rather than an arbitrary string -* `as_schedule_single_action()` & `as_schedule_recurring_action()` can accept a `$group` parameter to group different actions for the one plugin together. -* the `wp_` prefix is substituted with `as_` and the term `event` is replaced with `action` - -## API Function Availability - -As mentioned in the [Usage - Load Order](usage.md#load-order) section, Action Scheduler will initialize itself on the `'init'` hook with priority `1`. While API functions are loaded prior to this and can be called, they should not be called until after `'init'` with priority `1`, because each component, like the data store, has not yet been initialized. - -Do not use Action Scheduler API functions prior to `'init'` hook with priority `1`. Doing so could lead to unexpected results, like data being stored in the incorrect location. To make this easier: - -- Action Scheduler provides `Action_Scheduler::is_initialized()` for use in hooks to confirm that the data stores have been initialized. -- It also provides the `'action_scheduler_init'` action hook. It is safe to call API functions during or after this event has fired (tip: you can take advantage of WordPress's [did_action()](https://developer.wordpress.org/reference/functions/did_action/) function to check this). - -## Function Reference / `as_enqueue_async_action()` - -### Description - -Enqueue an action to run one time, as soon as possible. - -### Usage - -```php -as_enqueue_async_action( $hook, $args, $group, $unique, $priority ); -``` - -### Parameters - -- **$hook** (string)(required) Name of the action hook. -- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group to assign this job to. Default: _''_. -- **$unique** (boolean) Whether the action should be unique. Default: _`false`_. -- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. - -### Return value - -`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`. - -## Function Reference / `as_schedule_single_action()` - -### Description - -Schedule an action to run one time at some defined point in the future. - -### Usage - -```php -as_schedule_single_action( $timestamp, $hook, $args, $group, $unique, $priority ); -``` - -### Parameters - -- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. -- **$hook** (string)(required) Name of the action hook. -- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group to assign this job to. Default: _''_. -- **$unique** (boolean) Whether the action should be unique. Default: _`false`_. -- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255.) - -### Return value - -`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`. - -## Function Reference / `as_schedule_recurring_action()` - -### Description - -Schedule an action to run repeatedly with a specified interval in seconds. - -### Usage - -```php -as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group, $unique, $priority ); -``` - -### Parameters - -- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. -- **$interval_in_seconds** (integer)(required) How long to wait between runs. -- **$hook** (string)(required) Name of the action hook. -- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group to assign this job to. Default: _''_. -- **$unique** (boolean) Whether the action should be unique. Default: _`false`_. -- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. - -### Return value - -`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`. - -## Function Reference / `as_schedule_cron_action()` - -### Description - -Schedule an action that recurs on a cron-like schedule. - -If execution of a cron-like action is delayed, the next attempt will still be scheduled according to the provided cron expression. - -### Usage - -```php -as_schedule_cron_action( $timestamp, $schedule, $hook, $args, $group, $unique, $priority ); -``` - -### Parameters - -- **$timestamp** (integer)(required) The Unix timestamp representing the date you want the action to run. -- **$schedule** (string)(required) $schedule A cron-like schedule string, see http://en.wikipedia.org/wiki/Cron. -- **$hook** (string)(required) Name of the action hook. -- **$args** (array) Arguments to pass to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group to assign this job to. Default: _''_. -- **$unique** (boolean) Whether the action should be unique. Default: _`false`_. -- **$priority** (integer) Lower values take precedence over higher values. Defaults to 10, with acceptable values falling in the range 0-255. - -### Return value - -`(integer)` the action's ID. Zero if there was an error scheduling the action. The error will be sent to `error_log`. - -## Function Reference / `as_unschedule_action()` - -### Description - -Cancel the next occurrence of a scheduled action. - -### Usage - -```php -as_unschedule_action( $hook, $args, $group ); -``` - -### Parameters - -- **$hook** (string)(required) Name of the action hook. -- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group the job is assigned to. Default: _''_. - -### Return value - -`(null)` - -## Function Reference / `as_unschedule_all_actions()` - -### Description - -Cancel all occurrences of a scheduled action. - -### Usage - -```php -as_unschedule_all_actions( $hook, $args, $group ) -``` - -### Parameters - -- **$hook** (string)(required) Name of the action hook. -- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group the job is assigned to. Default: _''_. - -### Return value - -`(string|null)` The scheduled action ID if a scheduled action was found, or null if no matching action found. - -## Function Reference / `as_next_scheduled_action()` - -### Description - -Returns the next timestamp for a scheduled action. - -### Usage - -```php -as_next_scheduled_action( $hook, $args, $group ); -``` - -### Parameters - -- **$hook** (string)(required) Name of the action hook. Default: _none_. -- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group the job is assigned to. Default: _''_. - -### Return value - -`(integer|boolean)` The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action. - -## Function Reference / `as_has_scheduled_action()` - -### Description - -Check if there is a scheduled action in the queue, but more efficiently than as_next_scheduled_action(). It's recommended to use this function when you need to know whether a specific action is currently scheduled. _Available since 3.3.0._ - -### Usage - -```php -as_has_scheduled_action( $hook, $args, $group ); -``` - -### Parameters - -- **$hook** (string)(required) Name of the action hook. Default: _none_. -- **$args** (array) Arguments passed to callbacks when the hook triggers. Default: _`array()`_. -- **$group** (string) The group the job is assigned to. Default: _''_. - -### Return value - -`(boolean)` True if a matching action is pending or in-progress, false otherwise. - -## Function Reference / `as_get_scheduled_actions()` - -### Description - -Find scheduled actions. - -### Usage - -```php -as_get_scheduled_actions( $args, $return_format ); -``` - -### Parameters - -- **$args** (array) Arguments to search and filter results by. Possible arguments, with their default values: - * `'hook' => ''` - the name of the action that will be triggered - * `'args' => NULL` - the args array that will be passed with the action - * `'date' => NULL` - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). - * `'date_compare' => '<=`' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' - * `'modified' => NULL` - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). - * `'modified_compare' => '<='` - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' - * `'group' => ''` - the group the action belongs to - * `'status' => ''` - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING - * `'claimed' => NULL` - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID - * `'per_page' => 5` - Number of results to return - * `'offset' => 0` - * `'orderby' => 'date'` - accepted values are 'hook', 'group', 'modified', or 'date' - * `'order' => 'ASC'` -- **$return_format** (string) The format in which to return the scheduled actions: 'OBJECT', 'ARRAY_A', or 'ids'. Default: _'OBJECT'_. - -### Return value - -`(array)` Array of action rows matching the criteria specified with `$args`. - -## Function Reference / `as_supports()` - -### Description - -Check if a specific feature is supported by the current version of Action Scheduler. - -*Available since 3.9.3.* - -### Usage - -```php -as_supports( $feature ); -``` - -### Parameters - -**$feature** (string) (required) -The feature to check support for. -Currently supported: -- `'ensure_recurring_actions_hook'` — Indicates support for the `action_scheduler_ensure_recurring_actions` hook. - -### Return value - -`(boolean)` -True if the feature is supported, false otherwise. - -### Example - -```php -if ( as_supports( 'ensure_recurring_actions_hook' ) ) { - // Safe to depend on the 'action_scheduler_ensure_recurring_actions' hook. - add_action( 'action_scheduler_ensure_recurring_actions', 'my_plugin_schedule_my_recurring_action' ); -} -``` diff --git a/includes/libraries/action-scheduler/docs/apple-touch-icon.png b/includes/libraries/action-scheduler/docs/apple-touch-icon.png deleted file mode 100644 index 2227905..0000000 Binary files a/includes/libraries/action-scheduler/docs/apple-touch-icon.png and /dev/null differ diff --git a/includes/libraries/action-scheduler/docs/assets/css/style.scss b/includes/libraries/action-scheduler/docs/assets/css/style.scss deleted file mode 100644 index a13de0d..0000000 --- a/includes/libraries/action-scheduler/docs/assets/css/style.scss +++ /dev/null @@ -1,57 +0,0 @@ ---- ---- - -@import "{{ site.theme }}"; - -a { - text-shadow: none; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -header h1 a { - color: #b5e853; -} - -.container { - max-width: 700px; -} - -footer { - margin-top: 6em; - padding: 1.6em 0; - border-top: 1px dashed #b5e853; -} - -.footer-image { - text-align: center; - padding-top: 1em; -} - -.github-corner:hover .octo-arm { - animation:octocat-wave 560ms ease-in-out -} - -@keyframes octocat-wave { - 0%,100%{ - transform:rotate(0) - } - 20%,60%{ - transform:rotate(-25deg) - } - 40%,80%{ - transform:rotate(10deg) - } -} - -@media (max-width:500px){ - .github-corner:hover .octo-arm { - animation:none - } - .github-corner .octo-arm { - animation:octocat-wave 560ms ease-in-out - } -} \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/browserconfig.xml b/includes/libraries/action-scheduler/docs/browserconfig.xml deleted file mode 100644 index f6244e6..0000000 --- a/includes/libraries/action-scheduler/docs/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #151515 - - - diff --git a/includes/libraries/action-scheduler/docs/faq.md b/includes/libraries/action-scheduler/docs/faq.md deleted file mode 100644 index e937032..0000000 --- a/includes/libraries/action-scheduler/docs/faq.md +++ /dev/null @@ -1,129 +0,0 @@ -## FAQ - -### Is it safe to release Action Scheduler in my plugin? Won't its functions conflict with another copy of the library? - -Action Scheduler is designed to be used and released in plugins. It avoids redeclaring public API functions when more than one copy of the library is being loaded by different plugins. It will also load only the most recent version of itself (by checking registered versions after all plugins are loaded on the `'plugins_loaded'` hook). - -To use it in your plugin (or theme), simply require the `action-scheduler/action-scheduler.php` file. Action Scheduler will take care of the rest. __Note:__ Action Scheduler is only loaded from a theme if it is not included in any active plugins. - -### I don't want to use WP-Cron. Does Action Scheduler depend on WP-Cron? - -By default, Action Scheduler is initiated by WP-Cron (and the `'shutdown'` hook on admin requests). However, it has no dependency on the WP-Cron system. You can initiate the Action Scheduler queue in other ways with just one or two lines of code. - -For example, you can start a queue directly by calling: - -```php -ActionScheduler::runner()->run(); -``` - -Or trigger the `'action_scheduler_run_queue'` hook and let Action Scheduler do it for you: - -```php -do_action( 'action_scheduler_run_queue', $context_identifier ); -``` - -Further customization can be done by extending the `ActionScheduler_Abstract_QueueRunner` class to create a custom Queue Runner. For an example of a customized queue runner, see the [`ActionScheduler_WPCLI_QueueRunner`](https://github.com/woocommerce/action-scheduler/blob/trunk/classes/WP_CLI/ActionScheduler_WPCLI_QueueRunner.php), which is used when running WP CLI. - -Want to create some other method for initiating Action Scheduler? [Open a new issue](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it. - -### I don't want to use WP-Cron, ever. Does Action Scheduler replace WP-Cron? - -By default, Action Scheduler is designed to work alongside WP-Cron and not change any of its behaviour. This helps avoid unexpectedly overriding WP-Cron on sites installing your plugin, which may have nothing to do with WP-Cron. - -However, we can understand why you might want to replace WP-Cron completely in environments within your control, especially as it gets you the advantages of Action Scheduler. This should be possible without too much code. - -You could use the `'schedule_event'` hook in WordPress to use Action Scheduler for only newly scheduled WP-Cron jobs and map the `$event` param to Action Scheduler API functions. - -Alternatively, you can use a combination of the `'pre_update_option_cron'` and `'pre_option_cron'` hooks to override all new and previously scheduled WP-Cron jobs (similar to the way [Cavalcade](https://github.com/humanmade/Cavalcade) does it). - -If you'd like to create a plugin to do this automatically and want to share your work with others, [open a new issue to let us know](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it. - -### How does Action Scheduler store its data? - -Action Scheduler 3.0 and newer stores data in custom tables prefixed with `actionscheduler_`. For the list of all tables and their schemas, refer to the `ActionScheduler_StoreSchema` class. - -Prior to Action 3.0, actions were a custom post type, and data was stored in `wp_posts`, `wp_postmeta` and related tables. - -Action Scheduler 3+ migrates data from the custom post type to custom tables. - -### Can I use a different storage scheme? - -Of course! Action Scheduler data storage is completely swappable, and always has been. - -If you choose to, you can actually store them anywhere, like in a remote storage service from Amazon Web Services. - -To implement a custom store: - -1. extend the abstract `ActionScheduler_Store` class, being careful to implement each of its methods -2. attach a callback to `'action_scheduler_store_class'` to tell Action Scheduler your class is the one which should be used to manage storage, e.g. - -``` -function eg_define_custom_store( $existing_storage_class ) { - return 'My_Radical_Action_Scheduler_Store'; -} -add_filter( 'action_scheduler_store_class', 'eg_define_custom_store', 10, 1 ); -``` - -Take a look at the `classes/data-stores/ActionScheduler_DBStore.php` class for an example implementation of `ActionScheduler_Store`. - -If you'd like to create a plugin to do this automatically and release it publicly to help others, [open a new issue to let us know](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it. - -### Can I use a different storage scheme just for logging? - -Of course! Action Scheduler's logger is completely swappable, and always has been. You can also customise where logs are stored, and the storage mechanism. - -To implement a custom logger: - -1. extend the abstract `ActionScheduler_Logger` class, being careful to implement each of its methods -2. attach a callback to `'action_scheduler_logger_class'` to tell Action Scheduler your class is the one which should be used to manage logging, e.g. - -``` -function eg_define_custom_logger( $existing_storage_class ) { - return 'My_Radical_Action_Scheduler_Logger'; -} -add_filter( 'action_scheduler_logger_class', 'eg_define_custom_logger', 10, 1 ); -``` - -Take a look at the `classes/data-stores/ActionScheduler_DBLogger.php` class for an example implementation of `ActionScheduler_Logger`. - -### I want to run Action Scheduler only on a dedicated application server in my cluster. Can I do that? - -Wow, now you're really asking the tough questions. In theory, yes, this is possible. The `ActionScheduler_QueueRunner` class, which is responsible for running queues, is swappable via the `'action_scheduler_queue_runner_class'` filter. - -Because of this, you can effectively customise queue running however you need. Whether that means tweaking minor things, like not using WP-Cron at all to initiate queues by overriding `ActionScheduler_QueueRunner::init()`, or completely changing how and where queues are run, by overriding `ActionScheduler_QueueRunner::run()`. - -### Is Action Scheduler safe to use on my production site? - -Yes, absolutely! Action Scheduler is actively used on tens of thousands of production sites already. Right now it's responsible for scheduling everything from emails to payments. - -In fact, every month, Action Scheduler processes millions of payments as part of the [WooCommerce Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/) extension. - -It requires no setup, and won't override any WordPress APIs (unless you want it to). - -### How does Action Scheduler work on WordPress Multisite? - -Action Scheduler is designed to manage the scheduled actions on a single site. It has no special handling for running queues across multiple sites in a multisite network. That said, because its storage and Queue Runner are completely swappable, it would be possible to write multisite handling classes to use with it. - -If you'd like to create a multisite plugin to do this and release it publicly to help others, [open a new issue to let us know](https://github.com/woocommerce/action-scheduler/issues/new), we'd love to help you with it. - -### How can I change the Action Scheduler User-Agent to better identify its requests? - -Action Scheduler has a filter available named `as_async_request_queue_runner_post_args` which can be used to filter the arguments that are being sent to the `wp_remote_post` call. - -The User-Agent parameter is just one of them and can be adjusted as follows: - -``` -function eg_define_custom_user_agent( $args ) { - $versions = ActionScheduler_Versions::instance(); - $args['user-agent'] = 'Action Scheduler/' . $versions->latest_version(); - - return $args; -} -add_filter( 'as_async_request_queue_runner_post_args', 'eg_define_custom_user_agent', 10, 1 ); -``` - -### My site has past-due actions; what can I do? - -Actions that are past-due have missed their scheduled date; because of [how WP Cron works](https://developer.wordpress.org/plugins/cron/), it is normal to have some past-due actions. - -If there are several past-due actions more than one day old, there may be something wrong with your site. If you are using WooCommerce and need help determining the issue, please [contact us on our forum page](https://wordpress.org/support/plugin/woocommerce/). diff --git a/includes/libraries/action-scheduler/docs/favicon-16x16.png b/includes/libraries/action-scheduler/docs/favicon-16x16.png deleted file mode 100644 index 84f8c02..0000000 Binary files a/includes/libraries/action-scheduler/docs/favicon-16x16.png and /dev/null differ diff --git a/includes/libraries/action-scheduler/docs/favicon-32x32.png b/includes/libraries/action-scheduler/docs/favicon-32x32.png deleted file mode 100644 index cf2f07a..0000000 Binary files a/includes/libraries/action-scheduler/docs/favicon-32x32.png and /dev/null differ diff --git a/includes/libraries/action-scheduler/docs/favicon.ico b/includes/libraries/action-scheduler/docs/favicon.ico deleted file mode 100644 index 05bd033..0000000 Binary files a/includes/libraries/action-scheduler/docs/favicon.ico and /dev/null differ diff --git a/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html b/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html deleted file mode 100644 index f3bf171..0000000 --- a/includes/libraries/action-scheduler/docs/google14ef723abb376cd3.html +++ /dev/null @@ -1 +0,0 @@ -google-site-verification: google14ef723abb376cd3.html \ No newline at end of file diff --git a/includes/libraries/action-scheduler/docs/index.md b/includes/libraries/action-scheduler/docs/index.md deleted file mode 100644 index ae7af33..0000000 --- a/includes/libraries/action-scheduler/docs/index.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Action Scheduler - Background Processing Job Queue for WordPress ---- -## WordPress Job Queue with Background Processing - -Action Scheduler is a library for triggering a WordPress hook to run at some time in the future (or as soon as possible, in the case of an async action). Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occasions. - -Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. - -It just so happens, this functionality also creates a robust job queue for background processing large queues of tasks in WordPress. With the addition of logging and an [administration interface](/admin/), it also provides traceability on your tasks processed in the background. - -### Battle-Tested Background Processing - -Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins. - -It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, in 10 concurrent queues at a rate of over 10,000 actions / hour without negatively impacting normal site operations. - -This is all possible on infrastructure and WordPress sites outside the control of the plugin author. - -Action Scheduler is specifically designed for distribution in WordPress plugins (and themes) - no server access required. If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help. - -### How it Works - -Action Scheduler stores the hook name, arguments and scheduled date for an action that should be triggered at some time in the future. - -The scheduler will attempt to run every minute by attaching itself as a callback to the `'action_scheduler_run_schedule'` hook, which is scheduled using WordPress's built-in [WP-Cron](http://codex.wordpress.org/Function_Reference/wp_cron) system. Once per minute on, it will also check on the `'shutdown'` hook of WP Admin requests whether there are pending actions, and if there are, it will initiate a queue via an async loopback request. - -When triggered, Action Scheduler will check for scheduled actions that have a due date at or before this point in time i.e. actions scheduled to run now or at sometime in the past. Action scheduled to run asynchronously, i.e. not scheduled, have a zero date, meaning they will always be due no matter when the check occurs. - -### Batch Processing Background Jobs - -If there are actions to be processed, Action Scheduler will stake a unique claim for a batch of 25 actions and begin processing that batch. The PHP process spawned to run the batch will then continue processing batches of 25 actions until it uses 90% of available memory or has been processing for 30 seconds. - -At that point, if there are additional actions to process, an asynchronous loopback request will be made to the site to continue processing actions in a new request. - -This process and the loopback requests will continue until all actions are processed. - -### Housekeeping - -Before processing a batch, the scheduler will remove any existing claims on actions which have been sitting in a queue for more than five minutes (or more specifically, 10 times the allowed time limit, which defaults to 30 seconds). - -Action Scheduler will also delete any actions which were completed or canceled more than a month ago. - -If an action runs for more than 5 minutes, Action Scheduler will assume the action has timed out and will mark it as failed. However, if all callbacks attached to the action were to successfully complete sometime after that 5 minute timeout, its status would later be updated to completed. - -### Traceable Background Processing - -Did your background job run? - -Never be left wondering with Action Scheduler's built-in record keeping. - -All events for each action are logged in the `actionscheduler_logs` table and displayed in the [administration interface](/admin/). - -The events logged by default include when an action: - * is created - * starts (including details of how it was run, e.g. via [WP CLI](/wp-cli/) or WP Cron) - * completes - * fails - -If it fails with an error that can be recorded, that error will be recorded in the log and visible in administration interface, making it possible to trace what went wrong at some point in the past on a site you didn't have access to in the past. - -## Credits - -Action Scheduler is developed and maintained by folks at [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/). - -Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome. diff --git a/includes/libraries/action-scheduler/docs/mstile-150x150.png b/includes/libraries/action-scheduler/docs/mstile-150x150.png deleted file mode 100644 index 3a3ed19..0000000 Binary files a/includes/libraries/action-scheduler/docs/mstile-150x150.png and /dev/null differ diff --git a/includes/libraries/action-scheduler/docs/perf.md b/includes/libraries/action-scheduler/docs/perf.md deleted file mode 100644 index f272b95..0000000 --- a/includes/libraries/action-scheduler/docs/perf.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: WordPress Background Processing at Scale - Action Scheduler Job Queue -description: Learn how to do WordPress background processing at scale by tuning the Action Scheduler job queue's default WP Cron runner. ---- -# Background Processing at Scale - -Action Scheduler's default processing is designed to work reliably across all different hosting environments. To achieve that, the default processing thresholds are more conservative than many servers could support. - -Specifically, Action Scheduler will only process actions in a request until: - -* 90% of available memory is used -* processing another 3 actions would exceed 30 seconds of total request time, based on the average processing time for the current batch -* in a single concurrent queue - -While Action Scheduler will initiate an asynchronous loopback request to process additional actions, on sites with very large queues, this can result in slow processing times. - -While using [WP CLI to process queues](/wp-cli/) is the best approach to increasing processing speed, on occasion, that is not a viable option. In these cases, it's also possible to increase the processing thresholds in Action Scheduler to increase the rate at which actions are processed by the default queue runner. - -## Increasing Time Limit - -By default, Action Scheduler will only process actions for a maximum of 30 seconds in each request. This time limit minimizes the risk of a script timeout on unknown hosting environments, some of which enforce 30-second timeouts. - -If you know your host supports longer than this time limit for web requests, you can increase this time limit. This allows more actions to be processed in each request and reduces the lag between processing each queue, greatly speeding up the processing rate of scheduled actions. - -For example, the following snippet will increase the time limit to 2 minutes (120 seconds): - -```php -function eg_increase_time_limit( $time_limit ) { - return 120; -} -add_filter( 'action_scheduler_queue_runner_time_limit', 'eg_increase_time_limit' ); -``` - -Some of the known host time limits are: - -* 60 seconds on WP Engine -* 120 seconds on Pantheon -* 120 seconds on SiteGround - -## Increasing Batch Size - -By default, Action Scheduler will claim a batch of 25 actions. This small batch size is because the default time limit is only 30 seconds; however, if you know your actions are processing very quickly, e.g. taking milliseconds not seconds, or that you have more than 30 seconds available to process each batch, increasing the batch size can slightly improve performance. - -This is because claiming a batch has some overhead, so the less often a batch needs to be claimed, the faster actions can be processed. - -For example, to increase the batch size to 100, we can use the following function: - -```php -function eg_increase_action_scheduler_batch_size( $batch_size ) { - return 100; -} -add_filter( 'action_scheduler_queue_runner_batch_size', 'eg_increase_action_scheduler_batch_size' ); -``` - -## Increasing Concurrent Batches - -By default, Action Scheduler will run only one concurrent batch of actions. This is to prevent consuming a lot of available connections or processes on your web server. - -However, your server may allow a large number of connections, for example, because it has a high value for Apache's `MaxClients` setting or PHP-FPM's `pm.max_children` setting. - -If this is the case, you can use the `'action_scheduler_queue_runner_concurrent_batches'` filter to increase the number of concurrent batches allowed, and therefore speed up large numbers of actions scheduled to be processed simultaneously. - -For example, to increase the allowed number of concurrent queues to 10, we can use the following code: - -```php -function eg_increase_action_scheduler_concurrent_batches( $concurrent_batches ) { - return 10; -} -add_filter( 'action_scheduler_queue_runner_concurrent_batches', 'eg_increase_action_scheduler_concurrent_batches' ); -``` - -> WARNING: because the async queue runner introduced in Action Scheduler 3.0 will continue asynchronous loopback requests to process actions, increasing the number of concurrent batches can substantially increase server load and take down a site. [WP CLI](/wp-cli/) is a better method to achieve higher throughput. - -## Increasing Initialisation Rate of Runners - -By default, Action Scheduler initiates at most one queue runner every time the `'action_scheduler_run_queue'` action is triggered by WP Cron. - -Because this action is only triggered at most once every minute, then there will rarely be more than one queue processing actions even if the concurrent runners are increased. - -To handle larger queues on more powerful servers, it's possible to initiate additional queue runners whenever the `'action_scheduler_run_queue'` action is run. - -That can be done by initiating additional secure requests to our server via loopback requests. - -The code below demonstrates how to create 5 loopback requests each time a queue begins: - -```php -/** - * Trigger 5 additional loopback requests with unique URL params. - */ -function eg_request_additional_runners() { - - // allow self-signed SSL certificates - add_filter( 'https_local_ssl_verify', '__return_false', 100 ); - - for ( $i = 0; $i < 5; $i++ ) { - $response = wp_remote_post( admin_url( 'admin-ajax.php' ), array( - 'method' => 'POST', - 'timeout' => 45, - 'redirection' => 5, - 'httpversion' => '1.0', - 'blocking' => false, - 'headers' => array(), - 'body' => array( - 'action' => 'eg_create_additional_runners', - 'instance' => $i, - 'eg_nonce' => wp_create_nonce( 'eg_additional_runner_' . $i ), - ), - 'cookies' => array(), - ) ); - } -} -add_action( 'action_scheduler_run_queue', 'eg_request_additional_runners', 0 ); - -/** - * Handle requests initiated by eg_request_additional_runners() and start a queue runner if the request is valid. - */ -function eg_create_additional_runners() { - - if ( isset( $_POST['eg_nonce'] ) && isset( $_POST['instance'] ) && wp_verify_nonce( $_POST['eg_nonce'], 'eg_additional_runner_' . $_POST['instance'] ) ) { - ActionScheduler_QueueRunner::instance()->run(); - } - - wp_die(); -} -add_action( 'wp_ajax_nopriv_eg_create_additional_runners', 'eg_create_additional_runners', 0 ); -``` - -> WARNING: because of the processing rate of scheduled actions, this kind of increase can very easily take down a site. Use only on high-powered servers and be sure to test before attempting to use it in production. - -## Cleaning Failed Actions - -By default, Action Scheduler does not automatically delete old failed actions. There are two optional methods of removing these actions: - -- Include the failed status in the list of statuses to purge: -```php -add_filter( 'action_scheduler_default_cleaner_statuses', function( $statuses ) { - $statuses[] = ActionScheduler_Store::STATUS_FAILED; - return $statuses; -} ); -``` -- Use [WP CLI](/wp-cli/): -```shell -// Example -wp action-scheduler clean --status=failed --batch-size=50 --before='90 days ago' --pause=2 -``` - -## High Volume Plugin - -It's not necessary to add all of this code yourself, there is a handy plugin to get access to each of these increases - the [Action Scheduler - High Volume](https://github.com/woocommerce/action-scheduler-high-volume) plugin. diff --git a/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg b/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg deleted file mode 100644 index b67c32b..0000000 --- a/includes/libraries/action-scheduler/docs/safari-pinned-tab.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - diff --git a/includes/libraries/action-scheduler/docs/site.webmanifest b/includes/libraries/action-scheduler/docs/site.webmanifest deleted file mode 100644 index de65106..0000000 --- a/includes/libraries/action-scheduler/docs/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-256x256.png", - "sizes": "256x256", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/includes/libraries/action-scheduler/docs/usage.md b/includes/libraries/action-scheduler/docs/usage.md deleted file mode 100644 index 5af0d0b..0000000 --- a/includes/libraries/action-scheduler/docs/usage.md +++ /dev/null @@ -1,211 +0,0 @@ ---- -description: Learn how to use the Action Scheduler background processing job queue for WordPress in your WordPress plugin. ---- -# Usage - -Using Action Scheduler requires: - -1. installing the library -1. scheduling an action - -## Installation - -There are two ways to install Action Scheduler: - -1. regular WordPress plugin; or -1. a library within your plugin's codebase. - -Note that [Action Scheduler follows an L-2 dependency version policy](https://developer.woocommerce.com/2023/10/24/action-scheduler-to-adopt-l-2-dependency-version-policy/). That is, the library requires at least the "latest minus two" version of WordPress and the PHP minimum version requirement of that WordPress version. - -### Usage as a Plugin - -Action Scheduler includes the necessary file headers to be used as a standard WordPress plugin. - -To install it as a plugin: - -1. Download the .zip archive of the latest [stable release](https://github.com/woocommerce/action-scheduler/releases) -1. Go to the **Plugins > Add New > Upload** administration screen on your WordPress site -1. Select the archive file you just downloaded -1. Click **Install Now** -1. Click **Activate** - -Or clone the Git repository into your site's `wp-content/plugins` folder. - -Using Action Scheduler as a plugin can be handy for developing against newer versions, rather than having to update the subtree in your codebase. **When installed as a plugin, Action Scheduler does not provide any user interfaces for scheduling actions**. The only way to interact with Action Scheduler is via code. - -### Usage as a Library - -To use Action Scheduler as a library: - -1. include the Action Scheduler codebase -1. load the library by including the `action-scheduler.php` file - -Using a [subtree in your plugin, theme or site's Git repository](https://www.atlassian.com/blog/git/alternatives-to-git-submodule-git-subtree) to include Action Scheduler is the recommended method. Composer can also be used. - -To include Action Scheduler as a git subtree: - -#### Step 1. Add the Repository as a Remote - -``` -git remote add -f subtree-action-scheduler https://github.com/woocommerce/action-scheduler.git -``` - -Adding the subtree as a remote allows us to refer to it in short from via the name `subtree-action-scheduler`, instead of the full GitHub URL. - -#### Step 2. Add the Repo as a Subtree - -``` -git subtree add --prefix libraries/action-scheduler subtree-action-scheduler trunk --squash -``` - -This will add the `trunk` branch of Action Scheduler to your repository in the folder `libraries/action-scheduler`. - -You can change the `--prefix` to change where the code is included. Or change the `trunk` branch to a tag, like `2.1.0` to include only a stable version. - -#### Step 3. Update the Subtree - -To update Action Scheduler to a new version, use the commands: - -``` -git fetch subtree-action-scheduler trunk -git subtree pull --prefix libraries/action-scheduler subtree-action-scheduler trunk --squash -``` - -### Loading Action Scheduler - -Regardless of how it is installed, to load Action Scheduler, you only need to include the `action-scheduler.php` file, e.g. - -```php - Action Scheduler -1. Take a screenshot of the action status counts at the top of the page. Example screenshot: https://cld.wthms.co/kwIqv7 - - -#### Stage 2: Install & Activate Action Scheduler 3.0+ as a plugin - -1. The migration will start almost immediately -1. Keep an eye on your error log -1. Report any notices, errors or other issues on GitHub - -#### Stage 3: Verify Migration - -1. Go to Tools > Action Scheduler -1. Take a screenshot of the action status counts at the top of the page. -1. Verify the counts match the status counts taken in Stage 1 (the Completed counts could be higher because actions will have been completed to run the migration). diff --git a/includes/libraries/action-scheduler/docs/wp-cli.md b/includes/libraries/action-scheduler/docs/wp-cli.md deleted file mode 100644 index 63287d7..0000000 --- a/includes/libraries/action-scheduler/docs/wp-cli.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -description: Learn how to do WordPress background processing at scale with WP CLI and the Action Scheduler job queue. ---- -# WP CLI - -Action Scheduler has custom [WP CLI](http://wp-cli.org) commands available for processing actions. - -For large sites, WP CLI is a much better choice for running queues of actions than the default WP Cron runner. These are some common cases where WP CLI is a better option: - -* long-running tasks - Tasks that take a significant amount of time to run -* large queues - A large number of tasks will naturally take a longer time -* other plugins with extensive WP Cron usage - WP Cron's limited resources are spread across more tasks - -With a regular web request, you may have to deal with script timeouts enforced by hosts, or other restraints that make it more challenging to run Action Scheduler tasks. Utilizing WP CLI to run commands directly on the server give you more freedom. This means that you typically don't have the same constraints of a normal web request. - -If you choose to utilize WP CLI exclusively, you can disable the normal WP CLI queue runner by installing the [Action Scheduler - Disable Default Queue Runner](https://github.com/woocommerce/action-scheduler-disable-default-runner) plugin. Note that if you do this, you **must** run Action Scheduler via WP CLI or another method, otherwise no scheduled actions will be processed. - -## Commands - -These are the commands available to use with Action Scheduler: - -* `action-scheduler clean` - - Options: - * `--batch-size` - This is the number of actions per status to clean in a single batch. Default is `20`. - * `--batches` - This is the number of batches to process. Default 0 means that batches will continue to process until there are no more actions to delete. - * `--status` - Process only actions with specific status or statuses. Default is `canceled` and `complete`. Define multiple statuses as a comma separated string (without spaces), e.g. `--status=complete,failed,canceled` - * `--before` - Process only actions with scheduled date older than this. Defaults to 31 days. e.g `--before='7 days ago'`, `--before='02-Feb-2020 20:20:20'` - * `--pause` - The number of seconds to pause between batches. Default no pause. - -* `action-scheduler migrate` - - **Note**: This command is only available while the migration to custom tables is in progress. - -* `action-scheduler run` - - Options: - * `--batch-size` - This is the number of actions to run in a single batch. The default is `100`. - * `--batches` - This is the number of batches to run. Using 0 means that batches will continue running until there are no more actions to run. - * `--hooks` - Process only actions with specific hook or hooks, like `'woocommerce_scheduled_subscription_payment'`. By default, actions with any hook will be processed. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=woocommerce_scheduled_subscription_trial_end,woocommerce_scheduled_subscription_payment,woocommerce_scheduled_subscription_expiration` - * `--group` - Process only actions in a specific group, like `'woocommerce-memberships'`. By default, actions in any group (or no group) will be processed. - * `--exclude-groups` - Ignore actions from the specified group or groups (to specify multiple groups, supply a comma-separated list of slugs). This option is ignored if `--group` is also specified. - * `--force` - By default, Action Scheduler limits the number of concurrent batches that can be run at once to ensure the server does not get overwhelmed. Using the `--force` flag overrides this behavior to force the WP CLI queue to run. - -* `action-scheduler action cancel` -* `action-scheduler action create` -* `action-scheduler action delete` -* `action-scheduler action generate` -* `action-scheduler action get` -* `action-scheduler action list` -* `action-scheduler action next` -* `action-scheduler action run` -* `action-scheduler datastore` -* `action-scheduler runner` -* `action-scheduler status` -* `action-scheduler version` - -The best way to get a full list of commands and their available options is to use WP CLI itself. This can be done by running `wp action-scheduler` to list all Action Scheduler commands, or by including the `--help` flag with any of the individual commands. This will provide all relevant parameters and flags for the command. - -## Cautionary Note on Action Dependencies when using `--group` or `--hooks` Options - -The `--group` and `--hooks` options should be used with caution if you have an implicit dependency between scheduled actions based on their schedule. - -For example, consider two scheduled actions for the same subscription: - -* `scheduled_payment` scheduled for `2015-11-13 00:00:00` and -* `scheduled_expiration` scheduled for `2015-11-13 00:01:00`. - -Under normal conditions, Action Scheduler will ensure the `scheduled_payment` action is run before the `scheduled_expiration` action. Because that's how they are scheduled. - -However, when using the `--hooks` option, the `scheduled_payment` and `scheduled_expiration` actions will be processed in separate queues. As a result, this dependency is not guaranteed. - -For example, consider a site with both: - -* 100,000 `scheduled_payment` actions, scheduled for `2015-11-13 00:00:00` -* 100 `scheduled_expiration` actions, scheduled for `2015-11-13 00:01:00` - -If two queue runners are running alongside each other with each runner dedicated to just one of these hooks, the queue runner handling expiration hooks will complete the processing of the expiration hooks more quickly than the queue runner handling all the payment actions. - -**Because of this, the `--group` and `--hooks` options should be used with caution to avoid processing actions with an implicit dependency based on their schedule in separate queues.** - -## Improving Performance with `--group` or `--hooks` - -Being able to run queues for specific hooks or groups of actions is valuable at scale. Why? Because it means you can restrict the concurrency for similar actions. - -For example, let's say you have 300,000 actions queued up comprised of: - -* 100,000 renewals payments -* 100,000 email notifications -* 100,000 membership status updates - -Action Scheduler's default WP Cron queue runner will process them all together. e.g. when it claims a batch of actions, some may be emails, some membership updates and some renewals. - -When you add concurrency to that, you can end up with issues. For example, if you have 3 queues running, they may all be attempting to process similar actions at the same time, which can lead to querying the same database tables with similar queries. Depending on the code/queries running, this can lead to database locks or other issues. - -If you can batch based on each action's group, then you can improve performance by processing like actions consecutively, but still processing the full set of actions concurrently. - -For example, if one queue is created to process emails, another to process membership updates, and another to process renewal payments, then the same queries won't be run at the same time, and 3 separate queues will be able to run more efficiently. - -The WP CLI runner can achieve this using the `--group` option to create queue runners that focus on a specific group and, optionally, the `--exclude-groups` option to create one or more 'catch-all' queue runners that ignore those groups. diff --git a/includes/libraries/action-scheduler/package-lock.json b/includes/libraries/action-scheduler/package-lock.json deleted file mode 100644 index 38d6adf..0000000 --- a/includes/libraries/action-scheduler/package-lock.json +++ /dev/null @@ -1,4995 +0,0 @@ -{ - "name": "action-scheduler", - "version": "3.9.3", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "action-scheduler", - "version": "3.8.0", - "license": "GPL-3.0+", - "devDependencies": { - "grunt": "1.5.3", - "grunt-checktextdomain": "1.0.1", - "grunt-phpcs": "0.4.0", - "husky": "3.0.9", - "lint-staged": "13.2.1" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.0.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "dev": true, - "dependencies": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.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-escapes/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/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/argparse/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "dev": true - }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "node_modules/colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "dev": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cosmiconfig/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "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": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", - "dev": true - }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha512-z8Nrwhi6wzxNMIbxlrTzuUW6KWuKkogZ/7OdDVq+0+kxn77KUH1nipx8iU6suqkHqc4y6n7a9A8IpmxY/pTjWg==", - "dev": true, - "dependencies": { - "glob": "~5.0.0" - }, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/findup-sync/node_modules/glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", - "dev": true, - "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", - "dev": true, - "dependencies": { - "for-in": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/getobject": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", - "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", - "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", - "dev": true, - "dependencies": { - "dateformat": "~3.0.3", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~0.3.0", - "glob": "~7.1.6", - "grunt-cli": "~1.4.3", - "grunt-known-options": "~2.0.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.4.13", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/grunt-checktextdomain": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/grunt-checktextdomain/-/grunt-checktextdomain-1.0.1.tgz", - "integrity": "sha1-slTQHh3pEwBdTbHFMD2QI7mD4Zs=", - "dev": true, - "dependencies": { - "chalk": "~0.2.1", - "text-table": "~0.2.0" - }, - "engines": { - "node": ">= 0.8.0" - }, - "peerDependencies": { - "grunt": ">=0.4.1" - } - }, - "node_modules/grunt-checktextdomain/node_modules/ansi-styles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", - "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-checktextdomain/node_modules/chalk": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.2.1.tgz", - "integrity": "sha1-dhPhV1FFshOGSD9/SFql/6jL0Qw=", - "dev": true, - "dependencies": { - "ansi-styles": "~0.2.0", - "has-color": "~0.1.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/grunt-known-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", - "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/grunt-legacy-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", - "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", - "dev": true, - "dependencies": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/grunt-legacy-log-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", - "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", - "dev": true, - "dependencies": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-legacy-log-utils/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/grunt-legacy-log-utils/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/grunt-legacy-log-utils/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/grunt-legacy-log-utils/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/grunt-legacy-log-utils/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/grunt-legacy-log-utils/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/grunt-legacy-util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", - "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", - "dev": true, - "dependencies": { - "async": "~3.2.0", - "exit": "~0.1.2", - "getobject": "~1.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.21", - "underscore.string": "~3.3.5", - "which": "~2.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt-legacy-util/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/grunt-phpcs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/grunt-phpcs/-/grunt-phpcs-0.4.0.tgz", - "integrity": "sha1-oI1iX8ZEZeRTsr2T+BCyqB6Uvao=", - "dev": true, - "engines": { - "node": "0.10.x" - } - }, - "node_modules/grunt/node_modules/grunt-cli": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", - "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", - "dev": true, - "dependencies": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~4.0.1", - "v8flags": "~3.2.0" - }, - "bin": { - "grunt": "bin/grunt" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/grunt/node_modules/grunt-cli/node_modules/nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/grunt/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-color": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/husky": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz", - "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "chalk": "^2.4.2", - "ci-info": "^2.0.0", - "cosmiconfig": "^5.2.1", - "execa": "^1.0.0", - "get-stdin": "^7.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^4.2.0", - "please-upgrade-node": "^3.2.0", - "read-pkg": "^5.2.0", - "run-node": "^1.0.0", - "slash": "^3.0.0" - }, - "bin": { - "husky-run": "run.js", - "husky-upgrade": "lib/upgrader/bin.js" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/husky/node_modules/get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/husky/node_modules/parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/husky/node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "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/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", - "dev": true - }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "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-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/liftup": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", - "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", - "dev": true, - "dependencies": { - "extend": "^3.0.2", - "findup-sync": "^4.0.0", - "fined": "^1.2.0", - "flagged-respawn": "^1.0.1", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.1", - "rechoir": "^0.7.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/liftup/node_modules/findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", - "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/liftup/node_modules/resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "node_modules/lint-staged": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.1.tgz", - "integrity": "sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw==", - "dev": true, - "dependencies": { - "chalk": "5.2.0", - "cli-truncate": "^3.1.0", - "commander": "^10.0.0", - "debug": "^4.3.4", - "execa": "^7.0.0", - "lilconfig": "2.1.0", - "listr2": "^5.0.7", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-inspect": "^1.12.3", - "pidtree": "^0.6.0", - "string-argv": "^0.3.1", - "yaml": "^2.2.1" - }, - "bin": { - "lint-staged": "bin/lint-staged.js" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/lint-staged" - } - }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/lint-staged/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/lint-staged/node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/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/lint-staged/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/lint-staged/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/lint-staged/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/listr2": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.8.tgz", - "integrity": "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==", - "dev": true, - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.19", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.8.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, - "node_modules/listr2/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/listr2/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/listr2/node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/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/listr2/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/listr2/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/listr2/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/listr2/node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/listr2/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/listr2/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/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "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/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/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/log-update/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/log-update/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/log-update/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/log-update/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/log-update/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/log-update/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/log-update/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/log-update/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/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "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/minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", - "dev": true, - "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", - "dev": true, - "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "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/opencollective-postinstall": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", - "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", - "dev": true, - "bin": { - "opencollective-postinstall": "index.js" - } - }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "dependencies": { - "semver-compare": "^1.0.0" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "dependencies": { - "resolve": "^1.9.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "dev": true, - "dependencies": { - "path-parse": "^1.0.6" - } - }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "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/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", - "dev": true, - "bin": { - "run-node": "run-node" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/rxjs": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", - "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.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": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, - "node_modules/string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true, - "engines": { - "node": ">=0.6.19" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "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/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": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, - "node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/underscore.string": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", - "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", - "dev": true, - "dependencies": { - "sprintf-js": "^1.1.1", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrap-ansi/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/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "node_modules/yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "dev": true, - "engines": { - "node": ">= 14" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "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, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "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 - } - } - }, - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, - "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } - } - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", - "dev": true - }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "requires": { - "fill-range": "^7.1.1" - } - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", - "dev": true, - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "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, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-truncate": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", - "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", - "dev": true, - "requires": { - "slice-ansi": "^5.0.0", - "string-width": "^5.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", - "dev": true - }, - "commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - } - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", - "dev": true - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true - }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", - "dev": true - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha512-z8Nrwhi6wzxNMIbxlrTzuUW6KWuKkogZ/7OdDVq+0+kxn77KUH1nipx8iU6suqkHqc4y6n7a9A8IpmxY/pTjWg==", - "dev": true, - "requires": { - "glob": "~5.0.0" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - } - }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", - "dev": true - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "getobject": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", - "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", - "dev": true - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "grunt": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", - "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", - "dev": true, - "requires": { - "dateformat": "~3.0.3", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~0.3.0", - "glob": "~7.1.6", - "grunt-cli": "~1.4.3", - "grunt-known-options": "~2.0.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.4.13", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" - }, - "dependencies": { - "grunt-cli": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", - "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", - "dev": true, - "requires": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~4.0.1", - "v8flags": "~3.2.0" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - } - } - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - } - } - }, - "grunt-checktextdomain": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/grunt-checktextdomain/-/grunt-checktextdomain-1.0.1.tgz", - "integrity": "sha1-slTQHh3pEwBdTbHFMD2QI7mD4Zs=", - "dev": true, - "requires": { - "chalk": "~0.2.1", - "text-table": "~0.2.0" - }, - "dependencies": { - "ansi-styles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-0.2.0.tgz", - "integrity": "sha1-NZq0sV3NZLptdHNLcsNjYKmvLBk=", - "dev": true - }, - "chalk": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.2.1.tgz", - "integrity": "sha1-dhPhV1FFshOGSD9/SFql/6jL0Qw=", - "dev": true, - "requires": { - "ansi-styles": "~0.2.0", - "has-color": "~0.1.0" - } - } - } - }, - "grunt-known-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", - "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", - "dev": true - }, - "grunt-legacy-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", - "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", - "dev": true, - "requires": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" - } - }, - "grunt-legacy-log-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", - "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", - "dev": true, - "requires": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" - }, - "dependencies": { - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "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 - }, - "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 - }, - "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, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "grunt-legacy-util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", - "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", - "dev": true, - "requires": { - "async": "~3.2.0", - "exit": "~0.1.2", - "getobject": "~1.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.21", - "underscore.string": "~3.3.5", - "which": "~2.0.2" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "grunt-phpcs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/grunt-phpcs/-/grunt-phpcs-0.4.0.tgz", - "integrity": "sha1-oI1iX8ZEZeRTsr2T+BCyqB6Uvao=", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-color": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", - "dev": true - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true - }, - "husky": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/husky/-/husky-3.0.9.tgz", - "integrity": "sha512-Yolhupm7le2/MqC1VYLk/cNmYxsSsqKkTyBhzQHhPK1jFnC89mmmNVuGtLNabjDI6Aj8UNIr0KpRNuBkiC4+sg==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "ci-info": "^2.0.0", - "cosmiconfig": "^5.2.1", - "execa": "^1.0.0", - "get-stdin": "^7.0.0", - "opencollective-postinstall": "^2.0.2", - "pkg-dir": "^4.2.0", - "please-upgrade-node": "^3.2.0", - "read-pkg": "^5.2.0", - "run-node": "^1.0.0", - "slash": "^3.0.0" - }, - "dependencies": { - "get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", - "dev": true - }, - "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", - "lines-and-columns": "^1.1.6" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - } - } - } - }, - "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, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-core-module": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", - "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "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 - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "liftup": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", - "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", - "dev": true, - "requires": { - "extend": "^3.0.2", - "findup-sync": "^4.0.0", - "fined": "^1.2.0", - "flagged-respawn": "^1.0.1", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.1", - "rechoir": "^0.7.0", - "resolve": "^1.19.0" - }, - "dependencies": { - "findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" - } - }, - "resolve": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", - "dev": true, - "requires": { - "is-core-module": "^2.8.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - } - } - }, - "lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true - }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, - "lint-staged": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.2.1.tgz", - "integrity": "sha512-8gfzinVXoPfga5Dz/ZOn8I2GOhf81Wvs+KwbEXQn/oWZAvCVS2PivrXfVbFJc93zD16uC0neS47RXHIjXKYZQw==", - "dev": true, - "requires": { - "chalk": "5.2.0", - "cli-truncate": "^3.1.0", - "commander": "^10.0.0", - "debug": "^4.3.4", - "execa": "^7.0.0", - "lilconfig": "2.1.0", - "listr2": "^5.0.7", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-inspect": "^1.12.3", - "pidtree": "^0.6.0", - "string-argv": "^0.3.1", - "yaml": "^2.2.1" - }, - "dependencies": { - "chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true - }, - "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, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - } - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true - }, - "mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true - }, - "npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "requires": { - "path-key": "^4.0.0" - }, - "dependencies": { - "path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true - } - } - }, - "onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "requires": { - "mimic-fn": "^4.0.0" - } - }, - "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 - }, - "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, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "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 - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "listr2": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.8.tgz", - "integrity": "sha512-mC73LitKHj9w6v30nLNGPetZIlfpUniNSsxxrbaPcWOjDb92SHPzJPi/t+v1YC/lxKz/AJ9egOjww0qUuFxBpA==", - "dev": true, - "requires": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.19", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.8.0", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "dependencies": { - "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 - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "requires": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - } - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "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 - }, - "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 - }, - "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 - }, - "slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "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, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "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, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "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 - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "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 - }, - "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 - }, - "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 - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "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, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "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, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true - }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "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 - }, - "minimatch": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", - "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true - }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", - "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, - "opencollective-postinstall": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", - "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", - "dev": true - }, - "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 - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", - "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - } - } - }, - "please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "requires": { - "semver-compare": "^1.0.0" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "rechoir": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", - "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", - "dev": true, - "requires": { - "resolve": "^1.9.0" - } - }, - "resolve": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", - "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "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, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", - "dev": true - }, - "rxjs": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", - "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true - }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "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 - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - } - } - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", - "dev": true - }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", - "dev": true - }, - "string-argv": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", - "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "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, - "requires": { - "is-number": "^7.0.0" - } - }, - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true - }, - "underscore.string": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", - "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", - "dev": true, - "requires": { - "sprintf-js": "^1.1.1", - "util-deprecate": "^1.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "v8flags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", - "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "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 - }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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, - "requires": { - "color-name": "~1.1.4" - } - }, - "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 - }, - "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 - }, - "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 - }, - "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, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "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, - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "dev": true - } - } -} diff --git a/includes/libraries/action-scheduler/package.json b/includes/libraries/action-scheduler/package.json deleted file mode 100644 index 81ecf36..0000000 --- a/includes/libraries/action-scheduler/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "action-scheduler", - "title": "Action Scheduler", - "version": "3.9.3", - "homepage": "https://actionscheduler.org/", - "repository": { - "type": "git", - "url": "https://github.com/woocommerce/action-scheduler.git" - }, - "license": "GPL-3.0+", - "main": "Gruntfile.js", - "scripts": { - "build": "composer install --no-dev && npm install --only=prod && composer archive --file=$npm_package_name --format=zip && npm run postarchive", - "postarchive": "rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name", - "check-textdomain": "grunt checktextdomain", - "phpcs": "grunt phpcs" - }, - "woorelease": { - "wp_org_slug": "action-scheduler" - }, - "devDependencies": { - "grunt": "1.5.3", - "grunt-checktextdomain": "1.0.1", - "grunt-phpcs": "0.4.0", - "husky": "3.0.9", - "lint-staged": "13.2.1" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, - "lint-staged": { - "*.php": [ - "php -d display_errors=1 -l", - "composer run-script phpcs-pre-commit" - ] - } -} diff --git a/includes/libraries/action-scheduler/phpcs.xml b/includes/libraries/action-scheduler/phpcs.xml deleted file mode 100644 index 4b72a7f..0000000 --- a/includes/libraries/action-scheduler/phpcs.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - WooCommerce dev PHP_CodeSniffer ruleset. - - - docs/ - */node_modules/* - */vendor/* - - - - - - - - - - - - - - - - classes/* - deprecated/* - lib/* - tests/* - - - classes/* - deprecated/* - lib/* - tests/* - - - - tests/ - - - - classes/* - deprecated/* - lib/* - tests/* - - - - classes/ActionScheduler_wcSystemStatus.php - classes/data-stores/ActionScheduler_wpCommentLogger.php - classes/data-stores/ActionScheduler_wpPostStore.php - classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php - classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php - classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php - classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php - tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php - tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php - tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php - - - - tests/* - - diff --git a/includes/libraries/action-scheduler/tests/ActionScheduler_UnitTestCase.php b/includes/libraries/action-scheduler/tests/ActionScheduler_UnitTestCase.php deleted file mode 100644 index 8b7217a..0000000 --- a/includes/libraries/action-scheduler/tests/ActionScheduler_UnitTestCase.php +++ /dev/null @@ -1,70 +0,0 @@ -createResult(); - } - - $this->existing_timezone = date_default_timezone_get(); - - if ( 'UTC' !== $this->existing_timezone ) { - date_default_timezone_set( 'UTC' ); - $result->run( $this ); - } - - date_default_timezone_set( 'Pacific/Fiji' ); // UTC+12. - $result->run( $this ); - - date_default_timezone_set( 'Pacific/Tahiti' ); // UTC-10: it's a magical place. - $result->run( $this ); - - date_default_timezone_set( $this->existing_timezone ); - - return $result; - } -} diff --git a/includes/libraries/action-scheduler/tests/README.md b/includes/libraries/action-scheduler/tests/README.md deleted file mode 100644 index 79873d9..0000000 --- a/includes/libraries/action-scheduler/tests/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Action Scheduler tests - -To run unit tests: - -1. Make sure that PHPUnit is installed by running: - ``` - $ composer install - ``` - -2. Install WordPress and the WP Unit Test lib using the `install.sh` script: - ``` - $ tests/bin/install.sh [db-host] [wp-version] [skip-database-creation] - ``` - -You may need to quote strings with backslashes to prevent them from being processed by the shell or other programs. - -Then, to run the tests: - ``` - $ composer run test - ``` diff --git a/includes/libraries/action-scheduler/tests/bin/install.sh b/includes/libraries/action-scheduler/tests/bin/install.sh deleted file mode 100755 index ee05775..0000000 --- a/includes/libraries/action-scheduler/tests/bin/install.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/includes/libraries/action-scheduler/tests/bootstrap.php b/includes/libraries/action-scheduler/tests/bootstrap.php deleted file mode 100644 index ba6c31b..0000000 --- a/includes/libraries/action-scheduler/tests/bootstrap.php +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - ./phpunit/migration - - - ./phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php - ./phpunit/jobstore/ActionScheduler_DBStore_Test.php - ./phpunit/jobstore/ActionScheduler_HybridStore_Test.php - ./phpunit/logging/ActionScheduler_DBLogger_Test.php - - - ./phpunit/helpers - ./phpunit/jobs - ./phpunit/lock - ./phpunit/procedural_api - ./phpunit/runner - ./phpunit/schedules - ./phpunit/versioning - ./phpunit/logging/ActionScheduler_wpCommentLogger_Test.php - ./phpunit/jobstore/ActionScheduler_wpPostStore_Test.php - ./phpunit/jobstore/ActionScheduler_RecurringActionScheduler_Test.php - - - - - ignore - - - - - .. - - . - - - - diff --git a/includes/libraries/action-scheduler/tests/phpunit/ActionScheduler_Mock_Async_Request_QueueRunner.php b/includes/libraries/action-scheduler/tests/phpunit/ActionScheduler_Mock_Async_Request_QueueRunner.php deleted file mode 100644 index 35d286c..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/ActionScheduler_Mock_Async_Request_QueueRunner.php +++ /dev/null @@ -1,16 +0,0 @@ -init(); - - // Only apply hooks when in the admin. - $_current_screen = $current_screen; - set_current_screen( 'dashboard' ); - - try { - // Verify that the 'action_scheduler_init' hook is registered with the correct callback - $this->assertTrue( - has_action( 'action_scheduler_init', array( - ActionScheduler_RecurringActionScheduler::class, - 'schedule_recurring_scheduler_hook' - ) ) > 0, - 'The schedule_recurring_scheduler_hook method should be hooked into action_scheduler_init.' - ); - } finally { - // Clean up to avoid affecting any other tests. - $current_screen = $_current_screen; - remove_action( - 'action_scheduler_init', - array( - ActionScheduler_RecurringActionScheduler::class, - 'schedule_recurring_scheduler_hook' - ) - ); - } - } - - /** - * Test that schedule_recurring_scheduler_hook schedules the recurring action when not already scheduled. - */ - public function test_schedule_recurring_scheduler_hook_schedules_action() { - // Ensure no action is scheduled initially - $this->assertFalse( - as_has_scheduled_action( 'action_scheduler_ensure_recurring_actions' ), - 'No recurring action should be scheduled initially.' - ); - - $scheduler = new ActionScheduler_RecurringActionScheduler(); - $scheduler->schedule_recurring_scheduler_hook(); - - $this->assertTrue( - as_has_scheduled_action( 'action_scheduler_ensure_recurring_actions' ), - 'The recurring action should now be scheduled.' - ); - } - - /** - * Test that schedule_recurring_scheduler_hook respects caching and does not schedule actions redundantly. - */ - public function test_schedule_recurring_scheduler_hook__respects_cache() { - // Ensure no action is scheduled initially - $this->assertFalse( - as_has_scheduled_action( 'action_scheduler_ensure_recurring_actions' ), - 'No recurring action should be scheduled initially.' - ); - - // Simulate a transient hit - set_transient( 'as_is_ensure_recurring_actions_scheduled', true, HOUR_IN_SECONDS ); - - // Spy on as_schedule_recurring_action to verify it does NOT get called - $scheduler = new ActionScheduler_RecurringActionScheduler(); - $scheduler->schedule_recurring_scheduler_hook(); - - // Assert that no new action was scheduled due to transient hit - $this->assertFalse( - as_has_scheduled_action( 'action_scheduler_ensure_recurring_actions' ), - 'No new recurring action should be scheduled due to transient hit.' - ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/deprecated/ActionScheduler_UnitTestCase.php b/includes/libraries/action-scheduler/tests/phpunit/deprecated/ActionScheduler_UnitTestCase.php deleted file mode 100644 index b8ce753..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/deprecated/ActionScheduler_UnitTestCase.php +++ /dev/null @@ -1,71 +0,0 @@ -createResult(); - } - - $this->existing_timezone = date_default_timezone_get(); - - if ( 'UTC' !== $this->existing_timezone ) { - date_default_timezone_set( 'UTC' ); - $result->run( $this ); - } - - date_default_timezone_set( 'Pacific/Fiji' ); // UTC+12. - $result->run( $this ); - - date_default_timezone_set( 'Pacific/Tahiti' ); // UTC-10: it's a magical place. - $result->run( $this ); - - date_default_timezone_set( $this->existing_timezone ); - - return $result; - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_Callbacks.php b/includes/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_Callbacks.php deleted file mode 100644 index fe56531..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_Callbacks.php +++ /dev/null @@ -1,34 +0,0 @@ -assertEquals( - '0', - ini_get( 'max_execution_time' ), - 'If the max_execution_time was already zero (unlimited), then it will not be changed.' - ); - - ini_set( 'max_execution_time', 60 ); - ActionScheduler_Compatibility::raise_time_limit( 30 ); - $this->assertEquals( - '60', - ini_get( 'max_execution_time' ), - 'If the max_execution_time was already a higher value than we specify, then it will not be changed.' - ); - - ActionScheduler_Compatibility::raise_time_limit( 200 ); - $this->assertEquals( - '200', - ini_get( 'max_execution_time' ), - 'If the max_execution_time was a lower value than we specify, but was above zero, then it will be updated to the new value.' - ); - - ActionScheduler_Compatibility::raise_time_limit( 0 ); - $this->assertEquals( - '0', - ini_get( 'max_execution_time' ), - 'If the max_execution_time was a positive, non-zero value and we then specify zero (unlimited) as the new value, then it will be updated.' - ); - - // Cleanup. - ini_set( 'max_execution_time', $default_max_execution_time ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_TimezoneHelper_Test.php b/includes/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_TimezoneHelper_Test.php deleted file mode 100644 index 4c46a5e..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/helpers/ActionScheduler_TimezoneHelper_Test.php +++ /dev/null @@ -1,110 +0,0 @@ -getTimezone(); - $this->assertInstanceOf( 'DateTimeZone', $timezone ); - $this->assertEquals( $timezone_string, $timezone->getName() ); - - remove_filter( 'option_timezone_string', $timezone_filter ); - } - - /** - * Get timezone strings. - * - * @return array[] - */ - public function local_timezone_provider() { - return array( - array( 'America/New_York' ), - array( 'Australia/Melbourne' ), - array( 'UTC' ), - ); - } - - /** - * Ensure that most GMT offsets don't return UTC as the timezone. - * - * @dataProvider local_timezone_offsets_provider - * - * @param string $gmt_offset GMT offset. - */ - public function test_local_timezone_offsets( $gmt_offset ) { - $gmt_filter = function ( $gmt ) use ( $gmt_offset ) { - return $gmt_offset; - }; - - $date = new ActionScheduler_DateTime(); - - add_filter( 'option_gmt_offset', $gmt_filter ); - ActionScheduler_TimezoneHelper::set_local_timezone( $date ); - remove_filter( 'option_gmt_offset', $gmt_filter ); - - $offset_in_seconds = $gmt_offset * HOUR_IN_SECONDS; - - $this->assertEquals( $offset_in_seconds, $date->getOffset() ); - $this->assertEquals( $offset_in_seconds, $date->getOffsetTimestamp() - $date->getTimestamp() ); - } - - /** - * Get timezone offsets. - * - * @return array[] - */ - public function local_timezone_offsets_provider() { - return array( - array( '-11' ), - array( '-10.5' ), - array( '-10' ), - array( '-9' ), - array( '-8' ), - array( '-7' ), - array( '-6' ), - array( '-5' ), - array( '-4.5' ), - array( '-4' ), - array( '-3.5' ), - array( '-3' ), - array( '-2' ), - array( '-1' ), - array( '1' ), - array( '1.5' ), - array( '2' ), - array( '3' ), - array( '4' ), - array( '5' ), - array( '5.5' ), - array( '5.75' ), - array( '6' ), - array( '7' ), - array( '8' ), - array( '8.5' ), - array( '9' ), - array( '9.5' ), - array( '10' ), - array( '10.5' ), - array( '11' ), - array( '11.5' ), - array( '12' ), - array( '13' ), - ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_Action_Test.php b/includes/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_Action_Test.php deleted file mode 100644 index 2966525..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_Action_Test.php +++ /dev/null @@ -1,54 +0,0 @@ -assertEquals( $schedule, $action->get_schedule() ); - } - - public function test_null_schedule() { - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ); - $this->assertInstanceOf( 'ActionScheduler_NullSchedule', $action->get_schedule() ); - } - - public function test_set_hook() { - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ); - $this->assertEquals( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, $action->get_hook() ); - } - - public function test_args() { - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ); - $this->assertEmpty( $action->get_args() ); - - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 5, 10, 15 ) ); - $this->assertEqualSets( array( 5, 10, 15 ), $action->get_args() ); - } - - public function test_set_group() { - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), null, 'my_group' ); - $this->assertEquals( 'my_group', $action->get_group() ); - } - - public function test_execute() { - $mock = new MockAction(); - - $random = md5( wp_rand() ); - add_action( $random, array( $mock, 'action' ) ); - - $action = new ActionScheduler_Action( $random, array( $random ) ); - $action->execute(); - - remove_action( $random, array( $mock, 'action' ) ); - - $this->assertEquals( 1, $mock->get_call_count() ); - $events = $mock->get_events(); - $event = reset( $events ); - $this->assertEquals( $random, reset( $event['args'] ) ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_NullAction_Test.php b/includes/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_NullAction_Test.php deleted file mode 100644 index 9e630eb..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/jobs/ActionScheduler_NullAction_Test.php +++ /dev/null @@ -1,15 +0,0 @@ -assertEmpty( $action->get_hook() ); - $this->assertEmpty( $action->get_args() ); - $this->assertNull( $action->get_schedule()->get_date() ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/jobstore/AbstractStoreTest.php b/includes/libraries/action-scheduler/tests/phpunit/jobstore/AbstractStoreTest.php deleted file mode 100644 index fb16ba7..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/jobstore/AbstractStoreTest.php +++ /dev/null @@ -1,163 +0,0 @@ -get_store(); - $action_id = $store->save_action( $action ); - - $this->assertEquals( ActionScheduler_Store::STATUS_PENDING, $store->get_status( $action_id ) ); - - $store->mark_complete( $action_id ); - $this->assertEquals( ActionScheduler_Store::STATUS_COMPLETE, $store->get_status( $action_id ) ); - - $store->mark_failure( $action_id ); - $this->assertEquals( ActionScheduler_Store::STATUS_FAILED, $store->get_status( $action_id ) ); - } - - // Start tests for \ActionScheduler_Store::query_actions(). - - // phpcs:ignore Squiz.Commenting.FunctionComment.WrongStyle - public function test_query_actions_query_type_arg_invalid_option() { - $this->expectException( InvalidArgumentException::class ); - $this->get_store()->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ), 'invalid' ); - } - - public function test_query_actions_query_type_arg_valid_options() { - $store = $this->get_store(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) ); - - $action_id_1 = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule ) ); - $action_id_2 = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule ) ); - - $this->assertEquals( array( $action_id_1, $action_id_2 ), $store->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ) ) ); - $this->assertEquals( 2, $store->query_actions( array( 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ), 'count' ) ); - } - - public function test_query_actions_by_single_status() { - $store = $this->get_store(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) ); - - $this->assertEquals( 0, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_PENDING ), 'count' ) ); - - $action_id_1 = $store->save_action( new ActionScheduler_Action( 'my_hook_1', array( 1 ), $schedule ) ); - $action_id_2 = $store->save_action( new ActionScheduler_Action( 'my_hook_2', array( 1 ), $schedule ) ); - $action_id_3 = $store->save_action( new ActionScheduler_Action( 'my_hook_3', array( 1 ), $schedule ) ); - $store->mark_complete( $action_id_3 ); - - $this->assertEquals( 2, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_PENDING ), 'count' ) ); - $this->assertEquals( 1, $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_COMPLETE ), 'count' ) ); - } - - public function test_query_actions_by_array_status() { - $store = $this->get_store(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) ); - - $this->assertEquals( - 0, - $store->query_actions( - array( - 'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_RUNNING ), - ), - 'count' - ) - ); - - $action_id_1 = $store->save_action( new ActionScheduler_Action( 'my_hook_1', array( 1 ), $schedule ) ); - $action_id_2 = $store->save_action( new ActionScheduler_Action( 'my_hook_2', array( 1 ), $schedule ) ); - $action_id_3 = $store->save_action( new ActionScheduler_Action( 'my_hook_3', array( 1 ), $schedule ) ); - $store->mark_failure( $action_id_3 ); - - $this->assertEquals( - 3, - $store->query_actions( - array( - 'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_FAILED ), - ), - 'count' - ) - ); - $this->assertEquals( - 2, - $store->query_actions( - array( - 'status' => array( ActionScheduler_Store::STATUS_PENDING, ActionScheduler_Store::STATUS_COMPLETE ), - ), - 'count' - ) - ); - } - - // phpcs:ignore Squiz.PHP.CommentedOutCode.Found - // End tests for \ActionScheduler_Store::query_actions(). - - /** - * The `has_pending_actions_due` method should return a boolean value depending on whether there are - * pending actions. - * - * @return void - */ - public function test_has_pending_actions_due() { - $store = $this->get_store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - - for ( $i = - 3; $i <= 3; $i ++ ) { - // Some past actions, some future actions. - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $store->save_action( $action ); - } - $this->assertTrue( $store->has_pending_actions_due() ); - - $runner->run(); - $this->assertFalse( $store->has_pending_actions_due() ); - } - - /** - * The `has_pending_actions_due` method should return false when all pending actions are in the future. - * - * @return void - */ - public function test_has_pending_actions_due_only_future_actions() { - $store = $this->get_store(); - - for ( $i = 1; $i <= 3; $i ++ ) { - // Only future actions. - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $store->save_action( $action ); - } - $this->assertFalse( $store->has_pending_actions_due() ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php b/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php deleted file mode 100644 index 2cee74e..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStoreMigrator_Test.php +++ /dev/null @@ -1,26 +0,0 @@ -save_action( $action, null, $last_attempt_date ); - $action_date = $store->get_date( $action_id ); - - $this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) ); - - $action_id = $store->save_action( $action, $scheduled_date, $last_attempt_date ); - $action_date = $store->get_date( $action_id ); - - $this->assertEquals( $last_attempt_date->format( 'U' ), $action_date->format( 'U' ) ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStore_Test.php b/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStore_Test.php deleted file mode 100644 index a0e759f..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_DBStore_Test.php +++ /dev/null @@ -1,814 +0,0 @@ -test_db_supports_skip_locked() - * - * @var \wpdb - */ - private $original_wpdb; - - public function setUp(): void { - global $wpdb; - - // Delete all actions before each test. - $wpdb->query( "DELETE FROM {$wpdb->actionscheduler_actions}" ); - - parent::setUp(); - } - - /** - * Restore the original wpdb global instance for tests that have replaced it. - * - * @return void - */ - public function tearDown(): void { - global $wpdb; - if ( null !== $this->original_wpdb ) { - $wpdb = $this->original_wpdb; - $this->original_wpdb = null; - } - parent::tearDown(); - } - - /** - * Get data store for tests. - * - * @return ActionScheduler_DBStore - */ - protected function get_store() { - return new ActionScheduler_DBStore(); - } - - public function test_create_action() { - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $store = new ActionScheduler_DBStore(); - $action_id = $store->save_action( $action ); - - $this->assertNotEmpty( $action_id ); - } - - public function test_create_action_with_scheduled_date() { - $time = as_get_datetime_object( strtotime( '-1 week' ) ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), new ActionScheduler_SimpleSchedule( $time ) ); - $store = new ActionScheduler_DBStore(); - $action_id = $store->save_action( $action, $time ); - $action_date = $store->get_date( $action_id ); - - $this->assertEquals( $time->format( 'U' ), $action_date->format( 'U' ) ); - } - - public function test_retrieve_action() { - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $store = new ActionScheduler_DBStore(); - $action_id = $store->save_action( $action ); - - $retrieved = $store->fetch_action( $action_id ); - $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); - $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); - $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) ); - $this->assertEquals( $action->get_group(), $retrieved->get_group() ); - } - - public function test_cancel_action() { - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $store = new ActionScheduler_DBStore(); - $action_id = $store->save_action( $action ); - $store->cancel_action( $action_id ); - - $fetched = $store->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched ); - } - - public function test_cancel_actions_by_hook() { - $store = new ActionScheduler_DBStore(); - $actions = array(); - $hook = 'by_hook_test'; - for ( $day = 1; $day <= 3; $day++ ) { - $delta = sprintf( '+%d day', $day ); - $time = as_get_datetime_object( $delta ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( $hook, array(), $schedule, 'my_group' ); - $actions[] = $store->save_action( $action ); - } - $store->cancel_actions_by_hook( $hook ); - - foreach ( $actions as $action_id ) { - $fetched = $store->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched ); - } - } - - public function test_cancel_actions_by_group() { - $store = new ActionScheduler_DBStore(); - $actions = array(); - $group = 'by_group_test'; - for ( $day = 1; $day <= 3; $day++ ) { - $delta = sprintf( '+%d day', $day ); - $time = as_get_datetime_object( $delta ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group ); - $actions[] = $store->save_action( $action ); - } - $store->cancel_actions_by_group( $group ); - - foreach ( $actions as $action_id ) { - $fetched = $store->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched ); - } - } - - public function test_claim_actions() { - $created_actions = array(); - $store = new ActionScheduler_DBStore(); - for ( $i = 3; $i > - 3; $i -- ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $claim = $store->stake_claim(); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions, 3, 3 ), $claim->get_actions() ); - } - - public function test_claim_actions_order() { - - $store = new ActionScheduler_DBStore(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); - $created_actions = array( - $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ), - $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ), - ); - - $claim = $store->stake_claim(); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - - // Verify uniqueness of action IDs. - $this->assertEquals( 2, count( array_unique( $created_actions ) ) ); - - // Verify the count and order of the actions. - $claimed_actions = $claim->get_actions(); - $this->assertCount( 2, $claimed_actions ); - $this->assertEquals( $created_actions, $claimed_actions ); - - // Verify the reversed order doesn't pass. - $reversed_actions = array_reverse( $created_actions ); - $this->assertNotEquals( $reversed_actions, $claimed_actions ); - } - - public function test_claim_actions_by_hooks() { - $created_actions_by_hook = array(); - $created_actions = array(); - - $store = new ActionScheduler_DBStore(); - $unique_hook_one = 'my_unique_hook_one'; - $unique_hook_two = 'my_unique_hook_two'; - $unique_hooks = array( - $unique_hook_one, - $unique_hook_two, - ); - - for ( $i = 3; $i > - 3; $i -- ) { - foreach ( $unique_hooks as $unique_hook ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( $unique_hook, array( $i ), $schedule, 'my_group' ); - $action_id = $store->save_action( $action ); - - $created_actions[] = $action_id; - $created_actions_by_hook[ $unique_hook ][] = $action_id; - } - } - - $claim = $store->stake_claim( 10, null, $unique_hooks ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 6, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions, 6, 6 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - $claim = $store->stake_claim( 10, null, array( $unique_hook_one ) ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ], 3, 3 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - $claim = $store->stake_claim( 10, null, array( $unique_hook_two ) ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ], 3, 3 ), $claim->get_actions() ); - } - - public function test_claim_actions_by_group() { - $created_actions = array(); - $store = new ActionScheduler_DBStore(); - $unique_group_one = 'my_unique_group_one'; - $unique_group_two = 'my_unique_group_two'; - $unique_groups = array( - $unique_group_one, - $unique_group_two, - ); - - for ( $i = 3; $i > - 3; $i -- ) { - foreach ( $unique_groups as $unique_group ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, $unique_group ); - - $created_actions[ $unique_group ][] = $store->save_action( $action ); - } - } - - $claim = $store->stake_claim( 10, null, array(), $unique_group_one ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 3, 3 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - $claim = $store->stake_claim( 10, null, array(), $unique_group_two ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 3, 3 ), $claim->get_actions() ); - } - - /** - * The DBStore allows one or more groups to be excluded from a claim. - */ - public function test_claim_actions_with_group_exclusions() { - $created_actions = array(); - $store = new ActionScheduler_DBStore(); - $groups = array( 'foo', 'bar', 'baz' ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); - - // Create 6 actions (with 2 in each test group). - foreach ( $groups as $group_slug ) { - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group_slug ); - - $created_actions[ $group_slug ] = array( - $store->save_action( $action ), - $store->save_action( $action ), - ); - } - - // If we exclude group 'foo' (representing 2 actions) the remaining 4 actions from groups 'bar' and 'baz' should still be claimed. - $store->set_claim_filter( 'exclude-groups', 'foo' ); - $claim = $store->stake_claim(); - $this->assertEquals( - array_merge( $created_actions['bar'], $created_actions['baz'] ), - $claim->get_actions(), - 'A single group can successfully be excluded from claims.' - ); - $store->release_claim( $claim ); - - // If we exclude groups 'bar' and 'baz' (representing 4 actions) the remaining 2 actions from group 'foo' should still be claimed. - $store->set_claim_filter( 'exclude-groups', array( 'bar', 'baz' ) ); - $claim = $store->stake_claim(); - $this->assertEquals( - $created_actions['foo'], - $claim->get_actions(), - 'Multiple groups can successfully be excluded from claims.' - ); - $store->release_claim( $claim ); - - // If we include group 'foo' (representing 2 actions) after excluding all groups, the inclusion should 'win'. - $store->set_claim_filter( 'exclude-groups', array( 'foo', 'bar', 'baz' ) ); - $claim = $store->stake_claim( 10, null, array(), 'foo' ); - $this->assertEquals( - $created_actions['foo'], - $claim->get_actions(), - 'Including a specific group takes precedence over group exclusions.' - ); - $store->release_claim( $claim ); - } - - public function test_claim_actions_by_hook_and_group() { - $created_actions_by_hook = array(); - $created_actions = array(); - $store = new ActionScheduler_DBStore(); - - $unique_hook_one = 'my_other_unique_hook_one'; - $unique_hook_two = 'my_other_unique_hook_two'; - $unique_hooks = array( - $unique_hook_one, - $unique_hook_two, - ); - - $unique_group_one = 'my_other_other_unique_group_one'; - $unique_group_two = 'my_other_unique_group_two'; - $unique_groups = array( - $unique_group_one, - $unique_group_two, - ); - - for ( $i = 3; $i > - 3; $i -- ) { - foreach ( $unique_hooks as $unique_hook ) { - foreach ( $unique_groups as $unique_group ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( $unique_hook, array( $i ), $schedule, $unique_group ); - $action_id = $store->save_action( $action ); - - $created_actions[ $unique_group ][] = $action_id; - $created_actions_by_hook[ $unique_hook ][ $unique_group ][] = $action_id; - } - } - } - - /** Test Both Hooks with Each Group */ - - $claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_one ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 6, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions[ $unique_group_one ], 6, 6 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - $claim = $store->stake_claim( 10, null, $unique_hooks, $unique_group_two ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 6, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions[ $unique_group_two ], 6, 6 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - /** Test Just One Hook with Group One */ - - $claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_one ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_one ], 3, 3 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - $claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_one ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_one ], 3, 3 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - /** Test Just One Hook with Group Two */ - - $claim = $store->stake_claim( 10, null, array( $unique_hook_one ), $unique_group_two ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_one ][ $unique_group_two ], 3, 3 ), $claim->get_actions() ); - - $store->release_claim( $claim ); - - $claim = $store->stake_claim( 24, null, array( $unique_hook_two ), $unique_group_two ); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions_by_hook[ $unique_hook_two ][ $unique_group_two ], 3, 3 ), $claim->get_actions() ); - } - - /** - * Confirm that priorities are respected when claiming actions. - * - * @return void - */ - public function test_claim_actions_respecting_priority() { - $store = new ActionScheduler_DBStore(); - - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-2 hours' ) ); - $routine_action_1 = $store->save_action( new ActionScheduler_Action( 'routine_past_due', array(), $schedule, '' ) ); - - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); - $action = new ActionScheduler_Action( 'high_priority_past_due', array(), $schedule, '' ); - $action->set_priority( 5 ); - $priority_action = $store->save_action( $action ); - - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-4 hours' ) ); - $routine_action_2 = $store->save_action( new ActionScheduler_Action( 'routine_past_due', array(), $schedule, '' ) ); - - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '+1 hour' ) ); - $action = new ActionScheduler_Action( 'high_priority_future', array(), $schedule, '' ); - $action->set_priority( 2 ); - $priority_future_action = $store->save_action( $action ); - - $claim = $store->stake_claim(); - $this->assertEquals( - array( $priority_action, $routine_action_2, $routine_action_1 ), - $claim->get_actions(), - 'High priority actions take precedence over older but lower priority actions.' - ); - } - - /** - * The query used to claim actions explicitly ignores future pending actions, but it - * is still possible under unusual conditions (such as if MySQL runs out of temporary - * storage space) for such actions to be returned. - * - * When this happens, we still expect the store to filter them out, otherwise there is - * a risk that actions will be unexpectedly processed ahead of time. - * - * @see https://github.com/woocommerce/action-scheduler/issues/634 - */ - public function test_claim_filters_out_unexpected_future_actions() { - $group = __METHOD__; - $store = new ActionScheduler_DBStore(); - - // Create 4 actions: 2 that are already due (-3hrs and -1hrs) and 2 that are not yet due (+1hr and +3hrs). - for ( $i = -3; $i <= 3; $i += 2 ) { - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $i . ' hours' ) ); - $action_ids[] = $store->save_action( new ActionScheduler_Action( 'test_' . $i, array(), $schedule, $group ) ); - } - - // This callback is used to simulate the unusual conditions whereby MySQL might unexpectedly return future - // actions, contrary to the conditions used by the store object when staking its claim. - $simulate_unexpected_db_behavior = function( $sql ) use ( $action_ids ) { - global $wpdb; - - // Look out for the claim update query, ignore all others. - if ( - 0 !== strpos( $sql, "UPDATE $wpdb->actionscheduler_actions" ) - || ! preg_match( "/claim_id = 0 AND scheduled_date_gmt <= '([0-9:\-\s]{19})'/", $sql, $matches ) - || count( $matches ) !== 2 - ) { - return $sql; - } - - // Now modify the query, forcing it to also return the future actions we created. - return str_replace( $matches[1], as_get_datetime_object( '+4 hours' )->format( 'Y-m-d H:i:s' ), $sql ); - }; - - add_filter( 'query', $simulate_unexpected_db_behavior ); - $claim = $store->stake_claim( 10, null, array(), $group ); - $claimed_actions = $claim->get_actions(); - $this->assertCount( 2, $claimed_actions ); - - // Cleanup. - remove_filter( 'query', $simulate_unexpected_db_behavior ); - $store->release_claim( $claim ); - } - - public function test_duplicate_claim() { - $created_actions = array(); - $store = new ActionScheduler_DBStore(); - for ( $i = 0; $i > - 3; $i -- ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $claim1 = $store->stake_claim(); - $claim2 = $store->stake_claim(); - $this->assertCount( 3, $claim1->get_actions() ); - $this->assertCount( 0, $claim2->get_actions() ); - } - - public function test_release_claim() { - $created_actions = array(); - $store = new ActionScheduler_DBStore(); - for ( $i = 0; $i > - 3; $i -- ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $claim1 = $store->stake_claim(); - - $store->release_claim( $claim1 ); - $this->assertCount( 0, $store->find_actions_by_claim_id( $claim1->get_id() ) ); - - $claim2 = $store->stake_claim(); - $this->assertCount( 3, $claim2->get_actions() ); - $store->release_claim( $claim2 ); - $this->assertCount( 0, $store->find_actions_by_claim_id( $claim1->get_id() ) ); - - } - - public function test_search() { - $created_actions = array(); - $store = new ActionScheduler_DBStore(); - for ( $i = - 3; $i <= 3; $i ++ ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $next_no_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ); - $this->assertEquals( $created_actions[0], $next_no_args ); - - $next_with_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 1 ) ) ); - $this->assertEquals( $created_actions[4], $next_with_args ); - - $non_existent = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 17 ) ) ); - $this->assertNull( $non_existent ); - } - - public function test_search_by_group() { - $store = new ActionScheduler_DBStore(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) ); - - $abc = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'abc' ) ); - $def = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'def' ) ); - $ghi = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'ghi' ) ); - - $this->assertEquals( $abc, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'abc' ) ) ); - $this->assertEquals( $def, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'def' ) ) ); - $this->assertEquals( $ghi, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'ghi' ) ) ); - } - - public function test_get_run_date() { - $time = as_get_datetime_object( '-10 minutes' ); - $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $store = new ActionScheduler_DBStore(); - $action_id = $store->save_action( $action ); - - $this->assertEquals( $time->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) ); - - $action = $store->fetch_action( $action_id ); - $action->execute(); - $now = as_get_datetime_object(); - $store->mark_complete( $action_id ); - - $this->assertEquals( $now->format( 'U' ), $store->get_date( $action_id )->format( 'U' ) ); - - $next = $action->get_schedule()->get_next( $now ); - $new_action_id = $store->save_action( $action, $next ); - - $this->assertEquals( (int) ( $now->format( 'U' ) ) + HOUR_IN_SECONDS, $store->get_date( $new_action_id )->format( 'U' ) ); - } - - /** - * Test creating a unique action. - */ - public function test_create_action_unique() { - $time = as_get_datetime_object(); - $hook = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $store = new ActionScheduler_DBStore(); - $action = new ActionScheduler_Action( $hook, array(), $schedule ); - - $action_id = $store->save_action( $action ); - $this->assertNotEquals( 0, $action_id ); - $action_from_db = $store->fetch_action( $action_id ); - $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) ); - - $action = new ActionScheduler_Action( $hook, array(), $schedule ); - $action_id_duplicate = $store->save_unique_action( $action ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * Test saving unique actions across different groups. Different groups should be saved, same groups shouldn't. - */ - public function test_create_action_unique_with_different_groups() { - $time = as_get_datetime_object(); - $hook = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $store = new ActionScheduler_DBStore(); - $action = new ActionScheduler_Action( $hook, array(), $schedule, 'group1' ); - - $action_id = $store->save_action( $action ); - $action_from_db = $store->fetch_action( $action_id ); - $this->assertNotEquals( 0, $action_id ); - $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) ); - - $action2 = new ActionScheduler_Action( $hook, array(), $schedule, 'group2' ); - $action_id_group2 = $store->save_unique_action( $action2 ); - $this->assertNotEquals( 0, $action_id_group2 ); - $action_2_from_db = $store->fetch_action( $action_id_group2 ); - $this->assertTrue( is_a( $action_2_from_db, ActionScheduler_Action::class ) ); - - $action3 = new ActionScheduler_Action( $hook, array(), $schedule, 'group2' ); - $action_id_group2_double = $store->save_unique_action( $action3 ); - $this->assertEquals( 0, $action_id_group2_double ); - } - - /** - * Test saving a unique action first, and then successfully scheduling a non-unique action. - */ - public function test_create_action_unique_and_then_non_unique() { - $time = as_get_datetime_object(); - $hook = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $store = new ActionScheduler_DBStore(); - $action = new ActionScheduler_Action( $hook, array(), $schedule ); - - $action_id = $store->save_unique_action( $action ); - $this->assertNotEquals( 0, $action_id ); - $action_from_db = $store->fetch_action( $action_id ); - $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) ); - - // Non unique action is scheduled even if the previous one was unique. - $action = new ActionScheduler_Action( $hook, array(), $schedule ); - $action_id_duplicate = $store->save_action( $action ); - $this->assertNotEquals( 0, $action_id_duplicate ); - $action_from_db_duplicate = $store->fetch_action( $action_id_duplicate ); - $this->assertTrue( is_a( $action_from_db_duplicate, ActionScheduler_Action::class ) ); - } - - /** - * Test asserting that action when an action is created with empty args, it matches with actions created with args for uniqueness. - */ - public function test_create_action_unique_with_empty_array() { - $time = as_get_datetime_object(); - $hook = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $store = new ActionScheduler_DBStore(); - $action = new ActionScheduler_Action( $hook, array( 'foo' => 'bar' ), $schedule ); - - $action_id = $store->save_unique_action( $action ); - $this->assertNotEquals( 0, $action_id ); - $action_from_db = $store->fetch_action( $action_id ); - $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) ); - - $action_with_empty_args = new ActionScheduler_Action( $hook, array(), $schedule ); - $action_id_duplicate = $store->save_unique_action( $action_with_empty_args ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * Uniqueness does not check for args, so actions with different args can't be scheduled when unique is true. - */ - public function test_create_action_unique_with_different_args_still_fail() { - $time = as_get_datetime_object(); - $hook = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $store = new ActionScheduler_DBStore(); - $action = new ActionScheduler_Action( $hook, array( 'foo' => 'bar' ), $schedule ); - - $action_id = $store->save_unique_action( $action ); - $this->assertNotEquals( 0, $action_id ); - $action_from_db = $store->fetch_action( $action_id ); - $this->assertTrue( is_a( $action_from_db, ActionScheduler_Action::class ) ); - - $action_with_diff_args = new ActionScheduler_Action( $hook, array( 'foo' => 'bazz' ), $schedule ); - $action_id_duplicate = $store->save_unique_action( $action_with_diff_args ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * When a set of claimed actions are processed, they should be executed in the expected order (by priority, - * then by least number of attempts, then by scheduled date, then finally by action ID). - * - * @return void - */ - public function test_actions_are_processed_in_correct_order() { - global $wpdb; - - $now = time(); - $actual_order = array(); - - // When `foo` actions are processed, record the sequence number they supply. - $watcher = function ( $number ) use ( &$actual_order ) { - $actual_order[] = $number; - }; - - as_schedule_single_action( $now - 10, 'foo', array( 4 ), '', false, 10 ); - as_schedule_single_action( $now - 20, 'foo', array( 3 ), '', false, 10 ); - as_schedule_single_action( $now - 5, 'foo', array( 2 ), '', false, 5 ); - as_schedule_single_action( $now - 20, 'foo', array( 1 ), '', false, 5 ); - $reattempted = as_schedule_single_action( $now - 40, 'foo', array( 7 ), '', false, 20 ); - as_schedule_single_action( $now - 40, 'foo', array( 5 ), '', false, 20 ); - as_schedule_single_action( $now - 40, 'foo', array( 6 ), '', false, 20 ); - - // Modify the `attempt` count on one of our test actions, to change expectations about its execution order. - $wpdb->update( - $wpdb->actionscheduler_actions, - array( 'attempts' => 5 ), - array( 'action_id' => $reattempted ) - ); - - add_action( 'foo', $watcher ); - ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run(); - remove_action( 'foo', $watcher ); - - $this->assertEquals( range( 1, 7 ), $actual_order, 'When a claim is processed, individual actions execute in the expected order.' ); - } - - /** - * When a set of claimed actions are processed, they should be executed in the expected order (by priority, - * then by least number of attempts, then by scheduled date, then finally by action ID). This should be true - * even if actions are scheduled from within other scheduled actions. - * - * This test is a variation of `test_actions_are_processed_in_correct_order`, see discussion in - * https://github.com/woocommerce/action-scheduler/issues/951 to see why this specific nuance is tested. - * - * @return void - */ - public function test_child_actions_are_processed_in_correct_order() { - $time = time() - 10; - $actual_order = array(); - $watcher = function ( $number ) use ( &$actual_order ) { - $actual_order[] = $number; - }; - $parent_action = function () use ( $time ) { - // We generate 20 test actions because this is optimal for reproducing the conditions in the - // linked bug report. With fewer actions, the error condition is less likely to surface. - for ( $i = 1; $i <= 20; $i++ ) { - as_schedule_single_action( $time, 'foo', array( $i ) ); - } - }; - - add_action( 'foo', $watcher ); - add_action( 'parent', $parent_action ); - - as_schedule_single_action( $time, 'parent' ); - ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run(); - - remove_action( 'foo', $watcher ); - add_action( 'parent', $parent_action ); - - $this->assertEquals( range( 1, 20 ), $actual_order, 'Once claimed, scheduled actions are executed in the expected order, including if "child actions" are scheduled from within another action.' ); - } - - /** - * @param bool $expected_result - * @param string $db_server_info - * - * @return void - * - * @dataProvider db_supports_skip_locked_provider - */ - public function test_db_supports_skip_locked( bool $expected_result, string $db_server_info ) { - global $wpdb; - - // Stash the original since we're overwriting it with a partial mock. Self::tear_down() will restore this. - $this->original_wpdb = $wpdb; - - $wpdb = $this->getMockBuilder( get_class( $wpdb ) ) - ->setMethods( [ 'db_server_info' ] ) - ->disableOriginalConstructor() - ->getMock(); - - $wpdb->method( 'db_server_info' )->willReturn( $db_server_info ); - - $reflection = new \ReflectionClass( ActionScheduler_DBStore::class ); - $method = $reflection->getMethod( 'db_supports_skip_locked' ); - $method->setAccessible( true ); - $db_store = new ActionScheduler_DBStore(); - $this->assertSame( $expected_result, $method->invoke( $db_store ) ); - } - - /** - * Data Provider for ::test_db_supports_skip_locked(). - * - * @return array[] - */ - public static function db_supports_skip_locked_provider(): array { - // PHP <= 8.0.15 didn't strip the 5.5.5- prefix for MariaDB. - $maria_db_prefix = PHP_VERSION_ID < 80016 ? '5.5.5-' : ''; - - return array( - 'MySQL 5.6.1 does not support skip locked' => array( - false, - '5.6.1' - ), - 'MySQL 8.0.0 does not support skip locked' => array( - false, - '8.0.0' - ), - 'MySQL 8.0.1 does support skip locked' => array( - true, - '8.0.1' - ), - 'MySQL 8.4.4 does support skip locked' => array( - true, - '8.4.4' - ), - 'MariaDB 10.5.0 does not support skip locked' => array( - false, - $maria_db_prefix . '10.5.0-MariaDB' - ), - 'MariaDB 10.6.0 does support skip locked' => array( - true, - $maria_db_prefix . '10.6.0-MariaDB' - ), - 'MariaDB 11.5.0 does support skip locked' => array( - true, - $maria_db_prefix . '11.5.0-MariaDB' - ), - ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_HybridStore_Test.php b/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_HybridStore_Test.php deleted file mode 100644 index 52dcf2c..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_HybridStore_Test.php +++ /dev/null @@ -1,277 +0,0 @@ -init(); - } - update_option( ActionScheduler_HybridStore::DEMARKATION_OPTION, $this->demarkation_id ); - $hybrid = new ActionScheduler_HybridStore(); - $hybrid->set_autoincrement( '', ActionScheduler_StoreSchema::ACTIONS_TABLE ); - } - - public function tearDown(): void { - parent::tearDown(); - - // reset the autoincrement index. - /** @var \wpdb $wpdb */ - global $wpdb; - $wpdb->query( "TRUNCATE TABLE {$wpdb->actionscheduler_actions}" ); - $wpdb->query( "TRUNCATE TABLE {$wpdb->actionscheduler_logs}" ); - delete_option( ActionScheduler_HybridStore::DEMARKATION_OPTION ); - } - - public function test_actions_are_migrated_on_find() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - $source_logger = new CommentLogger(); - $destination_logger = new ActionScheduler_DBLogger(); - - $config = new Config(); - $config->set_source_store( $source_store ); - $config->set_source_logger( $source_logger ); - $config->set_destination_store( $destination_store ); - $config->set_destination_logger( $destination_logger ); - - $hybrid_store = new ActionScheduler_HybridStore( $config ); - - $time = as_get_datetime_object( '10 minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - $source_id = $source_store->save_action( $action ); - - $found = $hybrid_store->find_action( __FUNCTION__, array() ); - - $this->assertNotEquals( $source_id, $found ); - $this->assertGreaterThanOrEqual( $this->demarkation_id, $found ); - - $found_in_source = $source_store->fetch_action( $source_id ); - $this->assertInstanceOf( NullAction::class, $found_in_source ); - } - - public function test_actions_are_migrated_on_query() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - $source_logger = new CommentLogger(); - $destination_logger = new ActionScheduler_DBLogger(); - - $config = new Config(); - $config->set_source_store( $source_store ); - $config->set_source_logger( $source_logger ); - $config->set_destination_store( $destination_store ); - $config->set_destination_logger( $destination_logger ); - - $hybrid_store = new ActionScheduler_HybridStore( $config ); - - $source_actions = array(); - $destination_actions = array(); - - for ( $i = 0; $i < 10; $i++ ) { - // create in instance in the source store. - $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $source_actions[] = $source_store->save_action( $action ); - - // create an instance in the destination store. - $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $destination_actions[] = $destination_store->save_action( $action ); - } - - $found = $hybrid_store->query_actions( - array( - 'hook' => __FUNCTION__, - 'per_page' => 6, - ) - ); - - $this->assertCount( 6, $found ); - foreach ( $found as $key => $action_id ) { - $this->assertNotContains( $action_id, $source_actions ); - $this->assertGreaterThanOrEqual( $this->demarkation_id, $action_id ); - if ( 0 === $key % 2 ) { // it should have been in the source store. - $this->assertNotContains( $action_id, $destination_actions ); - } else { // it should have already been in the destination store. - $this->assertContains( $action_id, $destination_actions ); - } - } - - // six of the original 10 should have migrated to the new store, - // even though only three were retrieve in the final query. - $found_in_source = $source_store->query_actions( - array( - 'hook' => __FUNCTION__, - 'per_page' => 10, - ) - ); - $this->assertCount( 4, $found_in_source ); - } - - - public function test_actions_are_migrated_on_claim() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - $source_logger = new CommentLogger(); - $destination_logger = new ActionScheduler_DBLogger(); - - $config = new Config(); - $config->set_source_store( $source_store ); - $config->set_source_logger( $source_logger ); - $config->set_destination_store( $destination_store ); - $config->set_destination_logger( $destination_logger ); - - $hybrid_store = new ActionScheduler_HybridStore( $config ); - - $source_actions = array(); - $destination_actions = array(); - - for ( $i = 0; $i < 10; $i++ ) { - // create in instance in the source store. - $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $source_actions[] = $source_store->save_action( $action ); - - // create an instance in the destination store. - $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $destination_actions[] = $destination_store->save_action( $action ); - } - - $claim = $hybrid_store->stake_claim( 6 ); - - $claimed_actions = $claim->get_actions(); - $this->assertCount( 6, $claimed_actions ); - $this->assertCount( 3, array_intersect( $destination_actions, $claimed_actions ) ); - - // six of the original 10 should have migrated to the new store, - // even though only three were retrieve in the final claim. - $found_in_source = $source_store->query_actions( - array( - 'hook' => __FUNCTION__, - 'per_page' => 10, - ) - ); - $this->assertCount( 4, $found_in_source ); - - $this->assertEquals( 0, $source_store->get_claim_count() ); - $this->assertEquals( 1, $destination_store->get_claim_count() ); - $this->assertEquals( 1, $hybrid_store->get_claim_count() ); - - } - - public function test_fetch_respects_demarkation() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - $source_logger = new CommentLogger(); - $destination_logger = new ActionScheduler_DBLogger(); - - $config = new Config(); - $config->set_source_store( $source_store ); - $config->set_source_logger( $source_logger ); - $config->set_destination_store( $destination_store ); - $config->set_destination_logger( $destination_logger ); - - $hybrid_store = new ActionScheduler_HybridStore( $config ); - - $source_actions = array(); - $destination_actions = array(); - - for ( $i = 0; $i < 2; $i++ ) { - // create in instance in the source store. - $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $source_actions[] = $source_store->save_action( $action ); - - // create an instance in the destination store. - $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $destination_actions[] = $destination_store->save_action( $action ); - } - - foreach ( $source_actions as $action_id ) { - $action = $hybrid_store->fetch_action( $action_id ); - $this->assertInstanceOf( ActionScheduler_Action::class, $action ); - $this->assertNotInstanceOf( NullAction::class, $action ); - } - - foreach ( $destination_actions as $action_id ) { - $action = $hybrid_store->fetch_action( $action_id ); - $this->assertInstanceOf( ActionScheduler_Action::class, $action ); - $this->assertNotInstanceOf( NullAction::class, $action ); - } - } - - public function test_mark_complete_respects_demarkation() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - $source_logger = new CommentLogger(); - $destination_logger = new ActionScheduler_DBLogger(); - - $config = new Config(); - $config->set_source_store( $source_store ); - $config->set_source_logger( $source_logger ); - $config->set_destination_store( $destination_store ); - $config->set_destination_logger( $destination_logger ); - - $hybrid_store = new ActionScheduler_HybridStore( $config ); - - $source_actions = array(); - $destination_actions = array(); - - for ( $i = 0; $i < 2; $i++ ) { - // create in instance in the source store. - $time = as_get_datetime_object( ( $i * 10 + 1 ) . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $source_actions[] = $source_store->save_action( $action ); - - // create an instance in the destination store. - $time = as_get_datetime_object( ( $i * 10 + 5 ) . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( __FUNCTION__, array(), $schedule ); - - $destination_actions[] = $destination_store->save_action( $action ); - } - - foreach ( $source_actions as $action_id ) { - $hybrid_store->mark_complete( $action_id ); - $action = $hybrid_store->fetch_action( $action_id ); - $this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action ); - } - - foreach ( $destination_actions as $action_id ) { - $hybrid_store->mark_complete( $action_id ); - $action = $hybrid_store->fetch_action( $action_id ); - $this->assertInstanceOf( ActionScheduler_FinishedAction::class, $action ); - } - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php b/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php deleted file mode 100644 index e11fe94..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/jobstore/ActionScheduler_wpPostStore_Test.php +++ /dev/null @@ -1,474 +0,0 @@ -save_action( $action ); - - $this->assertNotEmpty( $action_id ); - } - - public function test_create_action_with_scheduled_date() { - $time = as_get_datetime_object( strtotime( '-1 week' ) ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), new ActionScheduler_SimpleSchedule( $time ) ); - $store = new ActionScheduler_wpPostStore(); - - $action_id = $store->save_action( $action, $time ); - $action_date = $store->get_date( $action_id ); - - $this->assertEquals( $time->getTimestamp(), $action_date->getTimestamp() ); - } - - public function test_retrieve_action() { - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $store = new ActionScheduler_wpPostStore(); - $action_id = $store->save_action( $action ); - $retrieved = $store->fetch_action( $action_id ); - - $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); - $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); - $this->assertEquals( $action->get_schedule()->get_date()->getTimestamp(), $retrieved->get_schedule()->get_date()->getTimestamp() ); - $this->assertEquals( $action->get_group(), $retrieved->get_group() ); - } - - /** - * @dataProvider provide_bad_args - * - * @param string $content Post content. - */ - public function test_action_bad_args( $content ) { - $store = new ActionScheduler_wpPostStore(); - $post_id = wp_insert_post( - array( - 'post_type' => ActionScheduler_wpPostStore::POST_TYPE, - 'post_status' => ActionScheduler_Store::STATUS_PENDING, - 'post_content' => $content, - ) - ); - - $fetched = $store->fetch_action( $post_id ); - $this->assertInstanceOf( 'ActionScheduler_NullSchedule', $fetched->get_schedule() ); - } - - public function provide_bad_args() { - return array( - array( '{"bad_json":true}}' ), - ); - } - - public function test_cancel_action() { - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $store = new ActionScheduler_wpPostStore(); - $action_id = $store->save_action( $action ); - $store->cancel_action( $action_id ); - - $fetched = $store->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched ); - } - - public function test_cancel_actions_by_hook() { - $store = new ActionScheduler_wpPostStore(); - $actions = array(); - $hook = 'by_hook_test'; - for ( $day = 1; $day <= 3; $day++ ) { - $delta = sprintf( '+%d day', $day ); - $time = as_get_datetime_object( $delta ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( $hook, array(), $schedule, 'my_group' ); - $actions[] = $store->save_action( $action ); - } - $store->cancel_actions_by_hook( $hook ); - - foreach ( $actions as $action_id ) { - $fetched = $store->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched ); - } - } - - public function test_cancel_actions_by_group() { - $store = new ActionScheduler_wpPostStore(); - $actions = array(); - $group = 'by_group_test'; - - for ( $day = 1; $day <= 3; $day++ ) { - $delta = sprintf( '+%d day', $day ); - $time = as_get_datetime_object( $delta ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, $group ); - $actions[] = $store->save_action( $action ); - } - $store->cancel_actions_by_group( $group ); - - foreach ( $actions as $action_id ) { - $fetched = $store->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_CanceledAction', $fetched ); - } - } - - public function test_claim_actions() { - $created_actions = array(); - $store = new ActionScheduler_wpPostStore(); - - for ( $i = 3; $i > -3; $i-- ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $claim = $store->stake_claim(); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - - $this->assertCount( 3, $claim->get_actions() ); - $this->assertEqualSets( array_slice( $created_actions, 3, 3 ), $claim->get_actions() ); - } - - public function test_claim_actions_order() { - $store = new ActionScheduler_wpPostStore(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); - $created_actions = array( - $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ), - $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'my_group' ) ), - ); - - $claim = $store->stake_claim(); - $this->assertInstanceof( 'ActionScheduler_ActionClaim', $claim ); - - // Verify uniqueness of action IDs. - $this->assertEquals( 2, count( array_unique( $created_actions ) ) ); - - // Verify the count and order of the actions. - $claimed_actions = $claim->get_actions(); - $this->assertCount( 2, $claimed_actions ); - $this->assertEquals( $created_actions, $claimed_actions ); - - // Verify the reversed order doesn't pass. - $reversed_actions = array_reverse( $created_actions ); - $this->assertNotEquals( $reversed_actions, $claimed_actions ); - } - - public function test_duplicate_claim() { - $created_actions = array(); - $store = new ActionScheduler_wpPostStore(); - - for ( $i = 0; $i > -3; $i-- ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $claim1 = $store->stake_claim(); - $claim2 = $store->stake_claim(); - $this->assertCount( 3, $claim1->get_actions() ); - $this->assertCount( 0, $claim2->get_actions() ); - } - - public function test_release_claim() { - $created_actions = array(); - $store = new ActionScheduler_wpPostStore(); - - for ( $i = 0; $i > -3; $i-- ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $claim1 = $store->stake_claim(); - - $store->release_claim( $claim1 ); - - $claim2 = $store->stake_claim(); - $this->assertCount( 3, $claim2->get_actions() ); - } - - public function test_search() { - $created_actions = array(); - $store = new ActionScheduler_wpPostStore(); - - for ( $i = -3; $i <= 3; $i++ ) { - $time = as_get_datetime_object( $i . ' hours' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( $i ), $schedule, 'my_group' ); - - $created_actions[] = $store->save_action( $action ); - } - - $next_no_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ); - $this->assertEquals( $created_actions[0], $next_no_args ); - - $next_with_args = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 1 ) ) ); - $this->assertEquals( $created_actions[4], $next_with_args ); - - $non_existent = $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'args' => array( 17 ) ) ); - $this->assertNull( $non_existent ); - } - - public function test_search_by_group() { - $store = new ActionScheduler_wpPostStore(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) ); - - $abc = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'abc' ) ); - $def = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'def' ) ); - $ghi = $store->save_action( new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 1 ), $schedule, 'ghi' ) ); - - $this->assertEquals( $abc, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'abc' ) ) ); - $this->assertEquals( $def, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'def' ) ) ); - $this->assertEquals( $ghi, $store->find_action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array( 'group' => 'ghi' ) ) ); - } - - public function test_post_author() { - $current_user = get_current_user_id(); - - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $store = new ActionScheduler_wpPostStore(); - $action_id = $store->save_action( $action ); - - $post = get_post( $action_id ); - $this->assertEquals( 0, $post->post_author ); - - $new_user = $this->factory->user->create_object( - array( - 'user_login' => __FUNCTION__, - 'user_pass' => md5( wp_rand() ), - ) - ); - - wp_set_current_user( $new_user ); - - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $action_id = $store->save_action( $action ); - $post = get_post( $action_id ); - $this->assertEquals( 0, $post->post_author ); - - wp_set_current_user( $current_user ); - } - - /** - * @issue 13 - */ - public function test_post_status_for_recurring_action() { - $time = as_get_datetime_object( '10 minutes' ); - $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $store = new ActionScheduler_wpPostStore(); - $action_id = $store->save_action( $action ); - - $action = $store->fetch_action( $action_id ); - $action->execute(); - $store->mark_complete( $action_id ); - - $next = $action->get_schedule()->get_next( as_get_datetime_object() ); - $new_action_id = $store->save_action( $action, $next ); - - $this->assertEquals( 'publish', get_post_status( $action_id ) ); - $this->assertEquals( 'pending', get_post_status( $new_action_id ) ); - } - - public function test_get_run_date() { - $time = as_get_datetime_object( '-10 minutes' ); - $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $store = new ActionScheduler_wpPostStore(); - $action_id = $store->save_action( $action ); - - $this->assertEquals( $store->get_date( $action_id )->getTimestamp(), $time->getTimestamp() ); - - $action = $store->fetch_action( $action_id ); - $action->execute(); - $now = as_get_datetime_object(); - $store->mark_complete( $action_id ); - - $this->assertEquals( $store->get_date( $action_id )->getTimestamp(), $now->getTimestamp(), '', 1 ); // allow timestamp to be 1 second off for older versions of PHP. - - $next = $action->get_schedule()->get_next( $now ); - $new_action_id = $store->save_action( $action, $next ); - - $this->assertEquals( (int) ( $now->getTimestamp() ) + HOUR_IN_SECONDS, $store->get_date( $new_action_id )->getTimestamp() ); - } - - public function test_claim_actions_by_hooks() { - $hook1 = __FUNCTION__ . '_hook_1'; - $hook2 = __FUNCTION__ . '_hook_2'; - $store = new ActionScheduler_wpPostStore(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); - - $action1 = $store->save_action( new ActionScheduler_Action( $hook1, array(), $schedule ) ); - $action2 = $store->save_action( new ActionScheduler_Action( $hook2, array(), $schedule ) ); - - // Claiming no hooks should include all actions. - $claim = $store->stake_claim( 10 ); - $this->assertEquals( 2, count( $claim->get_actions() ) ); - $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) ); - $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - - // Claiming a hook should claim only actions with that hook. - $claim = $store->stake_claim( 10, null, array( $hook1 ) ); - $this->assertEquals( 1, count( $claim->get_actions() ) ); - $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - - // Claiming two hooks should claim actions with either of those hooks. - $claim = $store->stake_claim( 10, null, array( $hook1, $hook2 ) ); - $this->assertEquals( 2, count( $claim->get_actions() ) ); - $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) ); - $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - - // Claiming two hooks should claim actions with either of those hooks. - $claim = $store->stake_claim( 10, null, array( __METHOD__ . '_hook_3' ) ); - $this->assertEquals( 0, count( $claim->get_actions() ) ); - $this->assertFalse( in_array( $action1, $claim->get_actions(), true ) ); - $this->assertFalse( in_array( $action2, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - } - - /** - * @issue 121 - */ - public function test_claim_actions_by_group() { - $group1 = md5( wp_rand() ); - $store = new ActionScheduler_wpPostStore(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); - - $action1 = $store->save_action( new ActionScheduler_Action( __METHOD__, array(), $schedule, $group1 ) ); - $action2 = $store->save_action( new ActionScheduler_Action( __METHOD__, array(), $schedule ) ); - - // Claiming no group should include all actions. - $claim = $store->stake_claim( 10 ); - $this->assertEquals( 2, count( $claim->get_actions() ) ); - $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) ); - $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - - // Claiming a group should claim only actions in that group. - $claim = $store->stake_claim( 10, null, array(), $group1 ); - $this->assertEquals( 1, count( $claim->get_actions() ) ); - $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - } - - public function test_claim_actions_by_hook_and_group() { - $hook1 = __FUNCTION__ . '_hook_1'; - $hook2 = __FUNCTION__ . '_hook_2'; - $hook3 = __FUNCTION__ . '_hook_3'; - $group1 = 'group_' . md5( wp_rand() ); - $group2 = 'group_' . md5( wp_rand() ); - $store = new ActionScheduler_wpPostStore(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '-1 hour' ) ); - - $action1 = $store->save_action( new ActionScheduler_Action( $hook1, array(), $schedule, $group1 ) ); - $action2 = $store->save_action( new ActionScheduler_Action( $hook2, array(), $schedule ) ); - $action3 = $store->save_action( new ActionScheduler_Action( $hook3, array(), $schedule, $group2 ) ); - - // Claiming no hooks or group should include all actions. - $claim = $store->stake_claim( 10 ); - $this->assertEquals( 3, count( $claim->get_actions() ) ); - $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) ); - $this->assertTrue( in_array( $action2, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - - // Claiming a group and hook should claim only actions in that group. - $claim = $store->stake_claim( 10, null, array( $hook1 ), $group1 ); - $this->assertEquals( 1, count( $claim->get_actions() ) ); - $this->assertTrue( in_array( $action1, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - - // Claiming a group and hook should claim only actions with that hook in that group. - $claim = $store->stake_claim( 10, null, array( $hook2 ), $group1 ); - $this->assertEquals( 0, count( $claim->get_actions() ) ); - $this->assertFalse( in_array( $action1, $claim->get_actions(), true ) ); - $this->assertFalse( in_array( $action2, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - - // Claiming a group and hook should claim only actions with that hook in that group. - $claim = $store->stake_claim( 10, null, array( $hook1, $hook2 ), $group2 ); - $this->assertEquals( 0, count( $claim->get_actions() ) ); - $this->assertFalse( in_array( $action1, $claim->get_actions(), true ) ); - $this->assertFalse( in_array( $action2, $claim->get_actions(), true ) ); - $store->release_claim( $claim ); - } - - /** - * The query used to claim actions explicitly ignores future pending actions, but it - * is still possible under unusual conditions (such as if MySQL runs out of temporary - * storage space) for such actions to be returned. - * - * When this happens, we still expect the store to filter them out, otherwise there is - * a risk that actions will be unexpectedly processed ahead of time. - * - * @see https://github.com/woocommerce/action-scheduler/issues/634 - */ - public function test_claim_filters_out_unexpected_future_actions() { - $group = __METHOD__; - $store = new ActionScheduler_wpPostStore(); - - // Create 4 actions: 2 that are already due (-3hrs and -1hrs) and 2 that are not yet due (+1hr and +3hrs). - for ( $i = -3; $i <= 3; $i += 2 ) { - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $i . ' hours' ) ); - $action_ids[] = $store->save_action( new ActionScheduler_Action( 'test_' . $i, array(), $schedule, $group ) ); - } - - // This callback is used to simulate the unusual conditions whereby MySQL might unexpectedly return future - // actions, contrary to the conditions used by the store object when staking its claim. - $simulate_unexpected_db_behavior = function( $sql ) use ( $action_ids ) { - global $wpdb; - - $post_type = ActionScheduler_wpPostStore::POST_TYPE; - $pending = ActionScheduler_wpPostStore::STATUS_PENDING; - - // Look out for the claim update query, ignore all others. - if ( - 0 !== strpos( $sql, "UPDATE $wpdb->posts" ) - || 0 !== strpos( $sql, "WHERE post_type = '$post_type' AND post_status = '$pending' AND post_password = ''" ) - || ! preg_match( "/AND post_date_gmt <= '([0-9:\-\s]{19})'/", $sql, $matches ) - || count( $matches ) !== 2 - ) { - return $sql; - } - - // Now modify the query, forcing it to also return the future actions we created. - return str_replace( $matches[1], as_get_datetime_object( '+4 hours' )->format( 'Y-m-d H:i:s' ), $sql ); - }; - - add_filter( 'query', $simulate_unexpected_db_behavior ); - $claim = $store->stake_claim( 10, null, array(), $group ); - $claimed_actions = $claim->get_actions(); - $this->assertCount( 2, $claimed_actions ); - - // Cleanup. - remove_filter( 'query', $simulate_unexpected_db_behavior ); - $store->release_claim( $claim ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/lock/ActionScheduler_OptionLock_Test.php b/includes/libraries/action-scheduler/tests/phpunit/lock/ActionScheduler_OptionLock_Test.php deleted file mode 100644 index 63afa31..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/lock/ActionScheduler_OptionLock_Test.php +++ /dev/null @@ -1,77 +0,0 @@ -assertInstanceOf( 'ActionScheduler_Lock', $lock ); - $this->assertInstanceOf( 'ActionScheduler_OptionLock', $lock ); - } - - public function test_is_locked() { - $lock = ActionScheduler::lock(); - $lock_type = md5( wp_rand() ); - - $this->assertFalse( $lock->is_locked( $lock_type ) ); - - $lock->set( $lock_type ); - $this->assertTrue( $lock->is_locked( $lock_type ) ); - } - - public function test_set() { - $lock = ActionScheduler::lock(); - $lock_type = md5( wp_rand() ); - - $lock->set( $lock_type ); - $this->assertTrue( $lock->is_locked( $lock_type ) ); - } - - public function test_get_expiration() { - $lock = ActionScheduler::lock(); - $lock_type = md5( wp_rand() ); - - $lock->set( $lock_type ); - - $expiration = $lock->get_expiration( $lock_type ); - $current_time = time(); - - $this->assertGreaterThanOrEqual( 0, $expiration ); - $this->assertGreaterThan( $current_time, $expiration ); - $this->assertLessThan( $current_time + MINUTE_IN_SECONDS + 1, $expiration ); - } - - /** - * A call to `ActionScheduler::lock()->set()` should fail if the lock is already held (ie, by another process). - * - * @return void - */ - public function test_lock_resists_race_conditions() { - global $wpdb; - - $lock = ActionScheduler::lock(); - $type = md5( wp_rand() ); - - // Approximate conditions in which a concurrently executing request manages to set (and obtain) the lock - // immediately before the current request can do so. - $simulate_concurrent_claim = function ( $query ) use ( $lock, $type ) { - static $executed = false; - - if ( ! $executed && false !== strpos( $query, 'action_scheduler_lock_' ) && 0 === strpos( $query, 'INSERT INTO' ) ) { - $executed = true; - $lock->set( $type ); - } - - return $query; - }; - - add_filter( 'query', $simulate_concurrent_claim ); - $wpdb->suppress_errors( true ); - $this->assertFalse( $lock->is_locked( $type ), 'Initially, the lock is not held' ); - $this->assertFalse( $lock->set( $type ), 'The lock was not obtained, because another process already claimed it.' ); - $wpdb->suppress_errors( false ); - remove_filter( 'query', $simulate_concurrent_claim ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_DBLogger_Test.php b/includes/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_DBLogger_Test.php deleted file mode 100644 index 27f9b4c..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_DBLogger_Test.php +++ /dev/null @@ -1,134 +0,0 @@ -assertInstanceOf( 'ActionScheduler_Logger', $logger ); - $this->assertInstanceOf( ActionScheduler_DBLogger::class, $logger ); - } - - public function test_add_log_entry() { - $action_id = as_schedule_single_action( time(), __METHOD__ ); - $logger = ActionScheduler::logger(); - $message = 'Logging that something happened'; - $log_id = $logger->log( $action_id, $message ); - $entry = $logger->get_entry( $log_id ); - - $this->assertEquals( $action_id, $entry->get_action_id() ); - $this->assertEquals( $message, $entry->get_message() ); - } - - public function test_storage_logs() { - $action_id = as_schedule_single_action( time(), __METHOD__ ); - $logger = ActionScheduler::logger(); - $logs = $logger->get_logs( $action_id ); - $expected = new ActionScheduler_LogEntry( $action_id, 'action created' ); - $this->assertCount( 1, $logs ); - $this->assertEquals( $expected->get_action_id(), $logs[0]->get_action_id() ); - $this->assertEquals( $expected->get_message(), $logs[0]->get_message() ); - } - - public function test_execution_logs() { - $action_id = as_schedule_single_action( time(), ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ); - $logger = ActionScheduler::logger(); - $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' ); - $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' ); - - $runner = ActionScheduler_Mocker::get_queue_runner(); - $runner->run( 'Unit Tests' ); - - // Expect 3 logs with the correct action ID. - $logs = $logger->get_logs( $action_id ); - $this->assertCount( 3, $logs ); - foreach ( $logs as $log ) { - $this->assertEquals( $action_id, $log->get_action_id() ); - } - - // Expect created, then started, then completed. - $this->assertEquals( 'action created', $logs[0]->get_message() ); - $this->assertEquals( $started->get_message(), $logs[1]->get_message() ); - $this->assertEquals( $finished->get_message(), $logs[2]->get_message() ); - } - - public function test_failed_execution_logs() { - $hook = __METHOD__; - add_action( $hook, array( $this, 'a_hook_callback_that_throws_an_exception' ) ); - $action_id = as_schedule_single_action( time(), $hook ); - $logger = ActionScheduler::logger(); - $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' ); - $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' ); - $failed = new ActionScheduler_LogEntry( $action_id, 'action failed via Unit Tests: Execution failed' ); - - $runner = ActionScheduler_Mocker::get_queue_runner(); - $runner->run( 'Unit Tests' ); - - // Expect 3 logs with the correct action ID. - $logs = $logger->get_logs( $action_id ); - $this->assertCount( 3, $logs ); - foreach ( $logs as $log ) { - $this->assertEquals( $action_id, $log->get_action_id() ); - $this->assertNotEquals( $finished->get_message(), $log->get_message() ); - } - - // Expect created, then started, then failed. - $this->assertEquals( 'action created', $logs[0]->get_message() ); - $this->assertEquals( $started->get_message(), $logs[1]->get_message() ); - $this->assertEquals( $failed->get_message(), $logs[2]->get_message() ); - } - - public function test_fatal_error_log() { - $action_id = as_schedule_single_action( time(), __METHOD__ ); - $logger = ActionScheduler::logger(); - $args = array( - 'type' => E_ERROR, - 'message' => 'Test error', - 'file' => __FILE__, - 'line' => __LINE__, - ); - - do_action( 'action_scheduler_unexpected_shutdown', $action_id, $args ); - - $logs = $logger->get_logs( $action_id ); - $found_log = false; - foreach ( $logs as $l ) { - if ( strpos( $l->get_message(), 'unexpected shutdown' ) === 0 ) { - $found_log = true; - } - } - $this->assertTrue( $found_log, 'Unexpected shutdown log not found' ); - } - - public function test_canceled_action_log() { - $action_id = as_schedule_single_action( time(), __METHOD__ ); - as_unschedule_action( __METHOD__ ); - $logger = ActionScheduler::logger(); - $logs = $logger->get_logs( $action_id ); - $expected = new ActionScheduler_LogEntry( $action_id, 'action canceled' ); - $this->assertEquals( $expected->get_message(), end( $logs )->get_message() ); - } - - public function test_deleted_action_cleanup() { - $time = as_get_datetime_object( '-10 minutes' ); - $schedule = new \ActionScheduler_SimpleSchedule( $time ); - $action = new \ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $store = new ActionScheduler_DBStore(); - $action_id = $store->save_action( $action ); - - $logger = new ActionScheduler_DBLogger(); - $logs = $logger->get_logs( $action_id ); - $this->assertNotEmpty( $logs ); - - $store->delete_action( $action_id ); - $logs = $logger->get_logs( $action_id ); - $this->assertEmpty( $logs ); - } - - public function a_hook_callback_that_throws_an_exception() { - throw new \RuntimeException( 'Execution failed' ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php b/includes/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php deleted file mode 100644 index b5d41db..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/logging/ActionScheduler_wpCommentLogger_Test.php +++ /dev/null @@ -1,241 +0,0 @@ -assertInstanceOf( 'ActionScheduler_Logger', $logger ); - if ( $this->using_comment_logger() ) { - $this->assertInstanceOf( 'ActionScheduler_wpCommentLogger', $logger ); - } else { - $this->assertNotInstanceOf( 'ActionScheduler_wpCommentLogger', $logger ); - } - } - - public function test_add_log_entry() { - $action_id = as_schedule_single_action( time(), 'a hook' ); - $logger = ActionScheduler::logger(); - $message = 'Logging that something happened'; - $log_id = $logger->log( $action_id, $message ); - $entry = $logger->get_entry( $log_id ); - - $this->assertEquals( $action_id, $entry->get_action_id() ); - $this->assertEquals( $message, $entry->get_message() ); - } - - public function test_add_log_datetime() { - $action_id = as_schedule_single_action( time(), 'a hook' ); - $logger = ActionScheduler::logger(); - $message = 'Logging that something happened'; - $date = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); - $log_id = $logger->log( $action_id, $message, $date ); - $entry = $logger->get_entry( $log_id ); - - $this->assertEquals( $action_id, $entry->get_action_id() ); - $this->assertEquals( $message, $entry->get_message() ); - - $date = new ActionScheduler_DateTime( 'now', new DateTimeZone( 'UTC' ) ); - $log_id = $logger->log( $action_id, $message, $date ); - $entry = $logger->get_entry( $log_id ); - - $this->assertEquals( $action_id, $entry->get_action_id() ); - $this->assertEquals( $message, $entry->get_message() ); - } - - public function test_erroneous_entry_id() { - $comment = wp_insert_comment( - array( - 'comment_post_ID' => 1, - 'comment_author' => 'test', - 'comment_content' => 'this is not a log entry', - ) - ); - - $logger = ActionScheduler::logger(); - $entry = $logger->get_entry( $comment ); - - $this->assertEquals( '', $entry->get_action_id() ); - $this->assertEquals( '', $entry->get_message() ); - } - - public function test_storage_comments() { - $action_id = as_schedule_single_action( time(), 'a hook' ); - $logger = ActionScheduler::logger(); - $logs = $logger->get_logs( $action_id ); - $expected = new ActionScheduler_LogEntry( $action_id, 'action created' ); - - // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict - $this->assertTrue( in_array( $this->log_entry_to_array( $expected ), $this->log_entry_to_array( $logs ) ) ); - } - - protected function log_entry_to_array( $logs ) { - if ( $logs instanceof ActionScheduler_LogEntry ) { - return array( - 'action_id' => $logs->get_action_id(), - 'message' => $logs->get_message(), - ); - } - - foreach ( $logs as $id => $log ) { - $logs[ $id ] = array( - 'action_id' => $log->get_action_id(), - 'message' => $log->get_message(), - ); - } - - return $logs; - } - - public function test_execution_comments() { - $action_id = as_schedule_single_action( time(), ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ); - $logger = ActionScheduler::logger(); - $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' ); - $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' ); - - $runner = ActionScheduler_Mocker::get_queue_runner(); - $runner->run( 'Unit Tests' ); - - $logs = $logger->get_logs( $action_id ); - - // phpcs:disable WordPress.PHP.StrictInArray.MissingTrueStrict - $this->assertTrue( in_array( $this->log_entry_to_array( $started ), $this->log_entry_to_array( $logs ) ) ); - $this->assertTrue( in_array( $this->log_entry_to_array( $finished ), $this->log_entry_to_array( $logs ) ) ); - // phpcs:enable - } - - public function test_failed_execution_comments() { - $hook = md5( wp_rand() ); - add_action( $hook, array( $this, 'a_hook_callback_that_throws_an_exception' ) ); - - $action_id = as_schedule_single_action( time(), $hook ); - $logger = ActionScheduler::logger(); - $started = new ActionScheduler_LogEntry( $action_id, 'action started via Unit Tests' ); - $finished = new ActionScheduler_LogEntry( $action_id, 'action complete via Unit Tests' ); - $failed = new ActionScheduler_LogEntry( $action_id, 'action failed via Unit Tests: Execution failed' ); - - $runner = ActionScheduler_Mocker::get_queue_runner(); - $runner->run( 'Unit Tests' ); - - $logs = $logger->get_logs( $action_id ); - - // phpcs:disable WordPress.PHP.StrictInArray.MissingTrueStrict - $this->assertTrue( in_array( $this->log_entry_to_array( $started ), $this->log_entry_to_array( $logs ) ) ); - $this->assertFalse( in_array( $this->log_entry_to_array( $finished ), $this->log_entry_to_array( $logs ) ) ); - $this->assertTrue( in_array( $this->log_entry_to_array( $failed ), $this->log_entry_to_array( $logs ) ) ); - // phpcs:enable - } - - public function test_failed_schedule_next_instance_comments() { - $action_id = wp_rand(); - $logger = ActionScheduler::logger(); - $log_entry = new ActionScheduler_LogEntry( $action_id, 'There was a failure scheduling the next instance of this action: Execution failed' ); - - try { - $this->a_hook_callback_that_throws_an_exception(); - } catch ( Exception $e ) { - do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK ) ); - } - - $logs = $logger->get_logs( $action_id ); - - // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict - $this->assertTrue( in_array( $this->log_entry_to_array( $log_entry ), $this->log_entry_to_array( $logs ) ) ); - } - - public function test_fatal_error_comments() { - $hook = md5( wp_rand() ); - $action_id = as_schedule_single_action( time(), $hook ); - $logger = ActionScheduler::logger(); - $args = array( - 'type' => E_ERROR, - 'message' => 'Test error', - 'file' => __FILE__, - 'line' => __LINE__, - ); - - do_action( 'action_scheduler_unexpected_shutdown', $action_id, $args ); - - $logs = $logger->get_logs( $action_id ); - $found_log = false; - foreach ( $logs as $l ) { - if ( strpos( $l->get_message(), 'unexpected shutdown' ) === 0 ) { - $found_log = true; - } - } - $this->assertTrue( $found_log, 'Unexpected shutdown log not found' ); - } - - public function test_canceled_action_comments() { - $action_id = as_schedule_single_action( time(), 'a hook' ); - as_unschedule_action( 'a hook' ); - - $logger = ActionScheduler::logger(); - $logs = $logger->get_logs( $action_id ); - $expected = new ActionScheduler_LogEntry( $action_id, 'action canceled' ); - - // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict - $this->assertTrue( in_array( $this->log_entry_to_array( $expected ), $this->log_entry_to_array( $logs ) ) ); - } - - public function a_hook_callback_that_throws_an_exception() { - throw new RuntimeException( 'Execution failed' ); - } - - public function test_filtering_of_get_comments() { - if ( ! $this->using_comment_logger() ) { - $this->assertTrue( true ); - return; - } - - $post_id = $this->factory->post->create_object( array( 'post_title' => __FUNCTION__ ) ); - $comment_id = $this->factory->comment->create_object( - array( - 'comment_post_ID' => $post_id, - 'comment_author' => __CLASS__, - 'comment_content' => __FUNCTION__, - ) - ); - - // Verify that we're getting the expected comment before we add logging comments. - $comments = get_comments(); - $this->assertCount( 1, $comments ); - $this->assertEquals( $comment_id, $comments[0]->comment_ID ); - - $action_id = as_schedule_single_action( time(), 'a hook' ); - $logger = ActionScheduler::logger(); - $message = 'Logging that something happened'; - $log_id = $logger->log( $action_id, $message ); - - // Verify that logging comments are excluded from general comment queries. - $comments = get_comments(); - $this->assertCount( 1, $comments ); - $this->assertEquals( $comment_id, $comments[0]->comment_ID ); - - // Verify that logging comments are returned when asking for them specifically. - $comments = get_comments( - array( - 'type' => ActionScheduler_wpCommentLogger::TYPE, - ) - ); - - // Expecting two: one when the action is created, another when we added our custom log. - $this->assertCount( 2, $comments ); - $this->assertContains( $log_id, wp_list_pluck( $comments, 'comment_ID' ) ); - } - - private function using_comment_logger() { - if ( is_null( $this->use_comment_logger ) ) { - $this->use_comment_logger = ! ActionScheduler_DataController::dependencies_met(); - } - - return $this->use_comment_logger; - } -} - diff --git a/includes/libraries/action-scheduler/tests/phpunit/migration/ActionMigrator_Test.php b/includes/libraries/action-scheduler/tests/phpunit/migration/ActionMigrator_Test.php deleted file mode 100644 index bbf716d..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/migration/ActionMigrator_Test.php +++ /dev/null @@ -1,144 +0,0 @@ -init(); - } - } - - public function test_migrate_from_wpPost_to_db() { - $source = new ActionScheduler_wpPostStore(); - $destination = new ActionScheduler_DBStore(); - $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() ); - - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $action_id = $source->save_action( $action ); - - $new_id = $migrator->migrate( $action_id ); - - // ensure we get the same record out of the new store as we stored in the old. - $retrieved = $destination->fetch_action( $new_id ); - $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); - $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); - $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) ); - $this->assertEquals( $action->get_group(), $retrieved->get_group() ); - $this->assertEquals( \ActionScheduler_Store::STATUS_PENDING, $destination->get_status( $new_id ) ); - - // ensure that the record in the old store does not exist. - $old_action = $source->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); - } - - public function test_does_not_migrate_missing_action_from_wpPost_to_db() { - $source = new ActionScheduler_wpPostStore(); - $destination = new ActionScheduler_DBStore(); - $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() ); - - $action_id = wp_rand( 100, 100000 ); - - $new_id = $migrator->migrate( $action_id ); - $this->assertEquals( 0, $new_id ); - - // ensure we get the same record out of the new store as we stored in the old. - $retrieved = $destination->fetch_action( $new_id ); - $this->assertInstanceOf( 'ActionScheduler_NullAction', $retrieved ); - } - - public function test_migrate_completed_action_from_wpPost_to_db() { - $source = new ActionScheduler_wpPostStore(); - $destination = new ActionScheduler_DBStore(); - $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() ); - - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $action_id = $source->save_action( $action ); - $source->mark_complete( $action_id ); - - $new_id = $migrator->migrate( $action_id ); - - // ensure we get the same record out of the new store as we stored in the old. - $retrieved = $destination->fetch_action( $new_id ); - $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); - $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); - $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) ); - $this->assertEquals( $action->get_group(), $retrieved->get_group() ); - $this->assertTrue( $retrieved->is_finished() ); - $this->assertEquals( \ActionScheduler_Store::STATUS_COMPLETE, $destination->get_status( $new_id ) ); - - // ensure that the record in the old store does not exist. - $old_action = $source->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); - } - - public function test_migrate_failed_action_from_wpPost_to_db() { - $source = new ActionScheduler_wpPostStore(); - $destination = new ActionScheduler_DBStore(); - $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() ); - - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $action_id = $source->save_action( $action ); - $source->mark_failure( $action_id ); - - $new_id = $migrator->migrate( $action_id ); - - // ensure we get the same record out of the new store as we stored in the old. - $retrieved = $destination->fetch_action( $new_id ); - $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); - $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); - $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) ); - $this->assertEquals( $action->get_group(), $retrieved->get_group() ); - $this->assertTrue( $retrieved->is_finished() ); - $this->assertEquals( \ActionScheduler_Store::STATUS_FAILED, $destination->get_status( $new_id ) ); - - // ensure that the record in the old store does not exist. - $old_action = $source->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); - } - - public function test_migrate_canceled_action_from_wpPost_to_db() { - $source = new ActionScheduler_wpPostStore(); - $destination = new ActionScheduler_DBStore(); - $migrator = new ActionMigrator( $source, $destination, $this->get_log_migrator() ); - - $time = as_get_datetime_object(); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule, 'my_group' ); - $action_id = $source->save_action( $action ); - $source->cancel_action( $action_id ); - - $new_id = $migrator->migrate( $action_id ); - - // ensure we get the same record out of the new store as we stored in the old. - $retrieved = $destination->fetch_action( $new_id ); - $this->assertEquals( $action->get_hook(), $retrieved->get_hook() ); - $this->assertEqualSets( $action->get_args(), $retrieved->get_args() ); - $this->assertEquals( $action->get_schedule()->get_date()->format( 'U' ), $retrieved->get_schedule()->get_date()->format( 'U' ) ); - $this->assertEquals( $action->get_group(), $retrieved->get_group() ); - $this->assertTrue( $retrieved->is_finished() ); - $this->assertEquals( \ActionScheduler_Store::STATUS_CANCELED, $destination->get_status( $new_id ) ); - - // ensure that the record in the old store does not exist. - $old_action = $source->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_NullAction', $old_action ); - } - - private function get_log_migrator() { - return new LogMigrator( \ActionScheduler::logger(), new ActionScheduler_DBLogger() ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/migration/BatchFetcher_Test.php b/includes/libraries/action-scheduler/tests/phpunit/migration/BatchFetcher_Test.php deleted file mode 100644 index 3877f0b..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/migration/BatchFetcher_Test.php +++ /dev/null @@ -1,75 +0,0 @@ -init(); - } - } - - public function test_nothing_to_migrate() { - $store = new PostStore(); - $batch_fetcher = new BatchFetcher( $store ); - - $actions = $batch_fetcher->fetch(); - $this->assertEmpty( $actions ); - } - - public function test_get_due_before_future() { - $store = new PostStore(); - $due = array(); - $future = array(); - - for ( $i = 0; $i < 5; $i ++ ) { - $time = as_get_datetime_object( $i + 1 . ' minutes' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $future[] = $store->save_action( $action ); - - $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $due[] = $store->save_action( $action ); - } - - $batch_fetcher = new BatchFetcher( $store ); - - $actions = $batch_fetcher->fetch(); - - $this->assertEqualSets( $due, $actions ); - } - - public function test_get_future_before_complete() { - $store = new PostStore(); - $future = array(); - $complete = array(); - - for ( $i = 0; $i < 5; $i ++ ) { - $time = as_get_datetime_object( $i + 1 . ' minutes' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $future[] = $store->save_action( $action ); - - $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_FinishedAction( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $complete[] = $store->save_action( $action ); - } - - $batch_fetcher = new BatchFetcher( $store ); - - $actions = $batch_fetcher->fetch(); - - $this->assertEqualSets( $future, $actions ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/migration/Config_Test.php b/includes/libraries/action-scheduler/tests/phpunit/migration/Config_Test.php deleted file mode 100644 index 6ca1e74..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/migration/Config_Test.php +++ /dev/null @@ -1,33 +0,0 @@ -expectException( \RuntimeException::class ); - $config->get_source_store(); - } - - public function test_source_logger_required() { - $config = new Config(); - $this->expectException( \RuntimeException::class ); - $config->get_source_logger(); - } - - public function test_destination_store_required() { - $config = new Config(); - $this->expectException( \RuntimeException::class ); - $config->get_destination_store(); - } - - public function test_destination_logger_required() { - $config = new Config(); - $this->expectException( \RuntimeException::class ); - $config->get_destination_logger(); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/migration/Controller_Test.php b/includes/libraries/action-scheduler/tests/phpunit/migration/Controller_Test.php deleted file mode 100644 index b35001c..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/migration/Controller_Test.php +++ /dev/null @@ -1,76 +0,0 @@ -schedule_migration(); - - $this->assertTrue( - as_next_scheduled_action( Scheduler::HOOK ) > 0, - 'Confirm that the Migration Controller scheduled the migration.' - ); - - as_unschedule_action( Scheduler::HOOK ); - } - - /** - * Test to ensure that if an essential table is missing, the Migration - * Controller will not schedule a migration. - * - * @see https://github.com/woocommerce/action-scheduler/issues/653 - */ - public function test_migration_not_scheduled_if_tables_are_missing() { - as_unschedule_action( Scheduler::HOOK ); - $this->rename_claims_table(); - Controller::instance()->schedule_migration(); - - $this->assertFalse( - as_next_scheduled_action( Scheduler::HOOK ), - 'When required tables are missing, the migration will not be scheduled.' - ); - - $this->restore_claims_table_name(); - } - - /** - * Rename the claims table, so that it cannot be used by the library. - */ - private function rename_claims_table() { - global $wpdb; - $normal_table_name = $wpdb->prefix . Schema::CLAIMS_TABLE; - $modified_table_name = $normal_table_name . 'x'; - - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( "RENAME TABLE {$normal_table_name} TO {$modified_table_name}" ); - } - - /** - * Restore the expected name of the claims table, so that it can be used by the library - * and any further tests. - */ - private function restore_claims_table_name() { - global $wpdb; - $normal_table_name = $wpdb->prefix . Schema::CLAIMS_TABLE; - $modified_table_name = $normal_table_name . 'x'; - - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared - $wpdb->query( "RENAME TABLE {$modified_table_name} TO {$normal_table_name}" ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/migration/LogMigrator_Test.php b/includes/libraries/action-scheduler/tests/phpunit/migration/LogMigrator_Test.php deleted file mode 100644 index 62d7d96..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/migration/LogMigrator_Test.php +++ /dev/null @@ -1,52 +0,0 @@ -init(); - } - } - - public function test_migrate_from_wpComment_to_db() { - $source = new ActionScheduler_wpCommentLogger(); - $destination = new ActionScheduler_DBLogger(); - $migrator = new LogMigrator( $source, $destination ); - $source_action_id = wp_rand( 10, 10000 ); - $destination_action_id = wp_rand( 10, 10000 ); - - $logs = array(); - for ( $i = 0; $i < 3; $i++ ) { - for ( $j = 0; $j < 5; $j++ ) { - $logs[ $i ][ $j ] = md5( wp_rand() ); - if ( 1 === $i ) { - $source->log( $source_action_id, $logs[ $i ][ $j ] ); - } - } - } - - $migrator->migrate( $source_action_id, $destination_action_id ); - - $migrated = $destination->get_logs( $destination_action_id ); - $this->assertEqualSets( - $logs[1], - array_map( - function( $log ) { - return $log->get_message(); - }, - $migrated - ) - ); - - // no API for deleting logs, so we leave them for manual cleanup later. - $this->assertCount( 5, $source->get_logs( $source_action_id ) ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/migration/Runner_Test.php b/includes/libraries/action-scheduler/tests/phpunit/migration/Runner_Test.php deleted file mode 100644 index 0869c7e..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/migration/Runner_Test.php +++ /dev/null @@ -1,95 +0,0 @@ -init(); - } - } - - public function test_migrate_batches() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - $source_logger = new CommentLogger(); - $destination_logger = new ActionScheduler_DBLogger(); - - $config = new Config(); - $config->set_source_store( $source_store ); - $config->set_source_logger( $source_logger ); - $config->set_destination_store( $destination_store ); - $config->set_destination_logger( $destination_logger ); - - $runner = new Runner( $config ); - - $due = array(); - $future = array(); - $complete = array(); - - for ( $i = 0; $i < 5; $i ++ ) { - $time = as_get_datetime_object( $i + 1 . ' minutes' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $future[] = $source_store->save_action( $action ); - - $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $due[] = $source_store->save_action( $action ); - - $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_FinishedAction( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $complete[] = $source_store->save_action( $action ); - } - - $created = $source_store->query_actions( array( 'per_page' => 0 ) ); - $this->assertCount( 15, $created ); - - $runner->run( 10 ); - - $args = array( - 'per_page' => 0, - 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, - ); - - // due actions should migrate in the first batch. - $migrated = $destination_store->query_actions( $args ); - $this->assertCount( 5, $migrated ); - - $remaining = $source_store->query_actions( $args ); - $this->assertCount( 10, $remaining ); - - $runner->run( 10 ); - - // pending actions should migrate in the second batch. - $migrated = $destination_store->query_actions( $args ); - $this->assertCount( 10, $migrated ); - - $remaining = $source_store->query_actions( $args ); - $this->assertCount( 5, $remaining ); - - $runner->run( 10 ); - - // completed actions should migrate in the third batch. - $migrated = $destination_store->query_actions( $args ); - $this->assertCount( 15, $migrated ); - - $remaining = $source_store->query_actions( $args ); - $this->assertCount( 0, $remaining ); - - } - -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/migration/Scheduler_Test.php b/includes/libraries/action-scheduler/tests/phpunit/migration/Scheduler_Test.php deleted file mode 100644 index e839132..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/migration/Scheduler_Test.php +++ /dev/null @@ -1,138 +0,0 @@ -init(); - } - } - - public function test_migration_is_complete() { - ActionScheduler_DataController::mark_migration_complete(); - $this->assertTrue( ActionScheduler_DataController::is_migration_complete() ); - } - - public function test_migration_is_not_complete() { - $this->assertFalse( ActionScheduler_DataController::is_migration_complete() ); - update_option( ActionScheduler_DataController::STATUS_FLAG, 'something_random' ); - $this->assertFalse( ActionScheduler_DataController::is_migration_complete() ); - } - - public function test_migration_is_scheduled() { - // Clear the any existing migration hooks that have already been setup. - as_unschedule_all_actions( Scheduler::HOOK ); - - $scheduler = new Scheduler(); - $this->assertFalse( - $scheduler->is_migration_scheduled(), - 'Migration is not automatically scheduled when a new ' . Scheduler::class . ' instance is created.' - ); - - $scheduler->schedule_migration(); - $this->assertTrue( - $scheduler->is_migration_scheduled(), - 'Migration is scheduled only after schedule_migration() has been called.' - ); - } - - public function test_scheduler_runs_migration() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - - $return_5 = function () { - return 5; - }; - add_filter( 'action_scheduler/migration_batch_size', $return_5 ); - - // Make sure successive migration actions are delayed so all actions aren't migrated at once on separate hooks. - $return_60 = function () { - return 60; - }; - add_filter( 'action_scheduler/migration_interval', $return_60 ); - - for ( $i = 0; $i < 10; $i++ ) { - $time = as_get_datetime_object( $i + 1 . ' minutes' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $future[] = $source_store->save_action( $action ); - - $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $due[] = $source_store->save_action( $action ); - } - - $this->assertCount( 20, $source_store->query_actions( array( 'per_page' => 0 ) ) ); - - $scheduler = new Scheduler(); - $scheduler->unschedule_migration(); - $scheduler->schedule_migration( time() - 1 ); - - $queue_runner = ActionScheduler_Mocker::get_queue_runner( $destination_store ); - $queue_runner->run(); - - // 5 actions should have moved from the source store when the queue runner triggered the migration action. - $args = array( - 'per_page' => 0, - 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, - ); - $this->assertCount( 15, $source_store->query_actions( $args ) ); - - remove_filter( 'action_scheduler/migration_batch_size', $return_5 ); - remove_filter( 'action_scheduler/migration_interval', $return_60 ); - } - - public function test_scheduler_marks_itself_complete() { - $source_store = new PostStore(); - $destination_store = new ActionScheduler_DBStore(); - - for ( $i = 0; $i < 5; $i ++ ) { - $time = as_get_datetime_object( $i + 1 . ' minutes ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $action = new ActionScheduler_Action( ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, array(), $schedule ); - $due[] = $source_store->save_action( $action ); - } - - $this->assertCount( 5, $source_store->query_actions( array( 'per_page' => 0 ) ) ); - - $scheduler = new Scheduler(); - $scheduler->unschedule_migration(); - $scheduler->schedule_migration( time() - 1 ); - - $queue_runner = ActionScheduler_Mocker::get_queue_runner( $destination_store ); - $queue_runner->run(); - - // All actions should have moved from the source store when the queue runner triggered the migration action. - $args = array( - 'per_page' => 0, - 'hook' => ActionScheduler_Callbacks::HOOK_WITH_CALLBACK, - ); - $this->assertCount( 0, $source_store->query_actions( $args ) ); - - // schedule another so we can get it to run immediately. - $scheduler->unschedule_migration(); - $scheduler->schedule_migration( time() - 1 ); - - // run again so it knows that there's nothing left to process. - $queue_runner->run(); - - $scheduler->unhook(); - - // ensure the flag is set marking migration as complete. - $this->assertTrue( ActionScheduler_DataController::is_migration_complete() ); - - // ensure that another instance has not been scheduled. - $this->assertFalse( $scheduler->is_migration_scheduled() ); - - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/procedural_api/procedural_api_Test.php b/includes/libraries/action-scheduler/tests/phpunit/procedural_api/procedural_api_Test.php deleted file mode 100644 index 405df09..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/procedural_api/procedural_api_Test.php +++ /dev/null @@ -1,498 +0,0 @@ -fetch_action( $action_id ); - $this->assertEquals( $time, $action->get_schedule()->get_date()->getTimestamp() ); - $this->assertEquals( $hook, $action->get_hook() ); - } - - public function test_recurring_action() { - $time = time(); - $hook = md5( wp_rand() ); - $action_id = as_schedule_recurring_action( $time, HOUR_IN_SECONDS, $hook ); - - $store = ActionScheduler::store(); - $action = $store->fetch_action( $action_id ); - $this->assertEquals( $time, $action->get_schedule()->get_date()->getTimestamp() ); - $this->assertEquals( $time + HOUR_IN_SECONDS + 2, $action->get_schedule()->get_next( as_get_datetime_object( $time + 2 ) )->getTimestamp() ); - $this->assertEquals( $hook, $action->get_hook() ); - } - - /** - * Test that we reject attempts to register a recurring action with an invalid interval. This guards against - * 'runaway' recurring actions that are created accidentally and treated as having a zero-second interval. - * - * @return void - */ - public function test_recurring_actions_reject_invalid_intervals() { - $this->assertGreaterThan( - 0, - as_schedule_recurring_action( time(), 1, 'foo' ), - 'When an integer is provided as the interval, a recurring action is successfully created.' - ); - - $this->assertGreaterThan( - 0, - as_schedule_recurring_action( time(), '10', 'foo' ), - 'When an integer-like string is provided as the interval, a recurring action is successfully created.' - ); - - $this->assertGreaterThan( - 0, - as_schedule_recurring_action( time(), 100.0, 'foo' ), - 'When an integer-value as a double is provided as the interval, a recurring action is successfully created.' - ); - - $this->setExpectedIncorrectUsage( 'as_schedule_recurring_action' ); - $this->assertEquals( - 0, - as_schedule_recurring_action( time(), 'nonsense', 'foo' ), - 'When a non-numeric string is provided as the interval, a recurring action is not created and a doing-it-wrong notice is emitted.' - ); - - $this->setExpectedIncorrectUsage( 'as_schedule_recurring_action' ); - $this->assertEquals( - 0, - as_schedule_recurring_action( time(), 123.456, 'foo' ), - 'When a non-integer double is provided as the interval, a recurring action is not created and a doing-it-wrong notice is emitted.' - ); - } - - public function test_cron_schedule() { - $time = as_get_datetime_object( '2014-01-01' ); - $hook = md5( wp_rand() ); - $action_id = as_schedule_cron_action( $time->getTimestamp(), '0 0 10 10 *', $hook ); - - $store = ActionScheduler::store(); - $action = $store->fetch_action( $action_id ); - $expected_date = as_get_datetime_object( '2014-10-10' ); - $this->assertEquals( $expected_date->getTimestamp(), $action->get_schedule()->get_date()->getTimestamp() ); - $this->assertEquals( $hook, $action->get_hook() ); - - $expected_date = as_get_datetime_object( '2015-10-10' ); - $this->assertEquals( $expected_date->getTimestamp(), $action->get_schedule()->get_next( as_get_datetime_object( '2015-01-02' ) )->getTimestamp() ); - } - - public function test_get_next() { - $time = as_get_datetime_object( 'tomorrow' ); - $hook = md5( wp_rand() ); - as_schedule_recurring_action( $time->getTimestamp(), HOUR_IN_SECONDS, $hook ); - - $next = as_next_scheduled_action( $hook ); - - $this->assertEquals( $time->getTimestamp(), $next ); - } - - public function test_get_next_async() { - $hook = md5( wp_rand() ); - $action_id = as_enqueue_async_action( $hook ); - - $next = as_next_scheduled_action( $hook ); - - $this->assertTrue( $next ); - - $store = ActionScheduler::store(); - - // Completed async actions should still return false. - $store->mark_complete( $action_id ); - $next = as_next_scheduled_action( $hook ); - $this->assertFalse( $next ); - - // Failed async actions should still return false. - $store->mark_failure( $action_id ); - $next = as_next_scheduled_action( $hook ); - $this->assertFalse( $next ); - - // Cancelled async actions should still return false. - $store->cancel_action( $action_id ); - $next = as_next_scheduled_action( $hook ); - $this->assertFalse( $next ); - } - - public function provider_time_hook_args_group() { - $time = time() + 60 * 2; - $hook = md5( wp_rand() ); - $args = array( wp_rand(), wp_rand() ); - $group = 'test_group'; - - return array( - - // Test with no args or group. - array( - 'time' => $time, - 'hook' => $hook, - 'args' => array(), - 'group' => '', - ), - - // Test with args but no group. - array( - 'time' => $time, - 'hook' => $hook, - 'args' => $args, - 'group' => '', - ), - - // Test with group but no args. - array( - 'time' => $time, - 'hook' => $hook, - 'args' => array(), - 'group' => $group, - ), - - // Test with args & group. - array( - 'time' => $time, - 'hook' => $hook, - 'args' => $args, - 'group' => $group, - ), - ); - } - - /** - * @dataProvider provider_time_hook_args_group - */ - public function test_unschedule( $time, $hook, $args, $group ) { - - $action_id_unscheduled = as_schedule_single_action( $time, $hook, $args, $group ); - $action_scheduled_time = $time + 1; - $action_id_scheduled = as_schedule_single_action( $action_scheduled_time, $hook, $args, $group ); - - as_unschedule_action( $hook, $args, $group ); - - $next = as_next_scheduled_action( $hook, $args, $group ); - $this->assertEquals( $action_scheduled_time, $next ); - - $store = ActionScheduler::store(); - $unscheduled_action = $store->fetch_action( $action_id_unscheduled ); - - // Make sure the next scheduled action is unscheduled. - $this->assertEquals( $hook, $unscheduled_action->get_hook() ); - $this->assertEquals( as_get_datetime_object( $time ), $unscheduled_action->get_schedule()->get_date() ); - $this->assertEquals( ActionScheduler_Store::STATUS_CANCELED, $store->get_status( $action_id_unscheduled ) ); - $this->assertNull( $unscheduled_action->get_schedule()->get_next( as_get_datetime_object() ) ); - - // Make sure other scheduled actions are not unscheduled. - $this->assertEquals( ActionScheduler_Store::STATUS_PENDING, $store->get_status( $action_id_scheduled ) ); - $scheduled_action = $store->fetch_action( $action_id_scheduled ); - - $this->assertEquals( $hook, $scheduled_action->get_hook() ); - $this->assertEquals( $action_scheduled_time, $scheduled_action->get_schedule()->get_date()->getTimestamp() ); - } - - /** - * @dataProvider provider_time_hook_args_group - */ - public function test_unschedule_all( $time, $hook, $args, $group ) { - - $hook = md5( $hook ); - $action_ids = array(); - - for ( $i = 0; $i < 3; $i++ ) { - $action_ids[] = as_schedule_single_action( $time, $hook, $args, $group ); - } - - as_unschedule_all_actions( $hook, $args, $group ); - - $next = as_next_scheduled_action( $hook ); - $this->assertFalse( $next ); - - $after = as_get_datetime_object( $time ); - $after->modify( '+1 minute' ); - - $store = ActionScheduler::store(); - - foreach ( $action_ids as $action_id ) { - $action = $store->fetch_action( $action_id ); - - $this->assertEquals( $hook, $action->get_hook() ); - $this->assertEquals( as_get_datetime_object( $time ), $action->get_schedule()->get_date() ); - $this->assertEquals( ActionScheduler_Store::STATUS_CANCELED, $store->get_status( $action_id ) ); - $this->assertNull( $action->get_schedule()->get_next( $after ) ); - } - } - - public function test_as_get_datetime_object_default() { - - $utc_now = new ActionScheduler_DateTime( 'now', new DateTimeZone( 'UTC' ) ); - $as_now = as_get_datetime_object(); - - // Don't want to use 'U' as timestamps will always be in UTC. - $this->assertEquals( $utc_now->format( 'Y-m-d H:i:s' ), $as_now->format( 'Y-m-d H:i:s' ) ); - } - - public function test_as_get_datetime_object_relative() { - - $utc_tomorrow = new ActionScheduler_DateTime( 'tomorrow', new DateTimeZone( 'UTC' ) ); - $as_tomorrow = as_get_datetime_object( 'tomorrow' ); - - $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) ); - - $utc_tomorrow = new ActionScheduler_DateTime( 'yesterday', new DateTimeZone( 'UTC' ) ); - $as_tomorrow = as_get_datetime_object( 'yesterday' ); - - $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) ); - } - - public function test_as_get_datetime_object_fixed() { - - $utc_tomorrow = new ActionScheduler_DateTime( '29 February 2016', new DateTimeZone( 'UTC' ) ); - $as_tomorrow = as_get_datetime_object( '29 February 2016' ); - - $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) ); - - $utc_tomorrow = new ActionScheduler_DateTime( '1st January 2024', new DateTimeZone( 'UTC' ) ); - $as_tomorrow = as_get_datetime_object( '1st January 2024' ); - - $this->assertEquals( $utc_tomorrow->format( 'Y-m-d H:i:s' ), $as_tomorrow->format( 'Y-m-d H:i:s' ) ); - } - - public function test_as_get_datetime_object_timezone() { - - $timezone_au = 'Australia/Brisbane'; - $timezone_default = date_default_timezone_get(); - - // phpcs:ignore - date_default_timezone_set( $timezone_au ); - - $au_now = new ActionScheduler_DateTime( 'now' ); - $as_now = as_get_datetime_object(); - - // Make sure they're for the same time. - $this->assertEquals( $au_now->getTimestamp(), $as_now->getTimestamp() ); - - // But not in the same timezone, as $as_now should be using UTC. - $this->assertNotEquals( $au_now->format( 'Y-m-d H:i:s' ), $as_now->format( 'Y-m-d H:i:s' ) ); - - $au_now = new ActionScheduler_DateTime( 'now' ); - $as_au_now = as_get_datetime_object(); - - $this->assertEquals( $au_now->getTimestamp(), $as_now->getTimestamp(), '', 2 ); - - // But not in the same timezone, as $as_now should be using UTC. - $this->assertNotEquals( $au_now->format( 'Y-m-d H:i:s' ), $as_now->format( 'Y-m-d H:i:s' ) ); - - // phpcs:ignore - date_default_timezone_set( $timezone_default ); - } - - public function test_as_get_datetime_object_type() { - $f = 'Y-m-d H:i:s'; - $now = as_get_datetime_object(); - $this->assertInstanceOf( 'ActionScheduler_DateTime', $now ); - - $datetime = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); - $as_datetime = as_get_datetime_object( $datetime ); - $this->assertEquals( $datetime->format( $f ), $as_datetime->format( $f ) ); - } - - public function test_as_has_scheduled_action() { - $store = ActionScheduler::store(); - - $time = as_get_datetime_object( 'tomorrow' ); - $action_id = as_schedule_single_action( $time->getTimestamp(), 'hook_1' ); - - $this->assertTrue( as_has_scheduled_action( 'hook_1' ) ); - $this->assertFalse( as_has_scheduled_action( 'hook_2' ) ); - - // Go to in-progress. - $store->log_execution( $action_id ); - $this->assertTrue( as_has_scheduled_action( 'hook_1' ) ); - - // Go to complete. - $store->mark_complete( $action_id ); - $this->assertFalse( as_has_scheduled_action( 'hook_1' ) ); - - // Go to failed. - $store->mark_failure( $action_id ); - $this->assertFalse( as_has_scheduled_action( 'hook_1' ) ); - } - - public function test_as_has_scheduled_action_with_args() { - as_schedule_single_action( time(), 'hook_1', array( 'a' ) ); - - $this->assertTrue( as_has_scheduled_action( 'hook_1' ) ); - $this->assertFalse( as_has_scheduled_action( 'hook_1', array( 'b' ) ) ); - - // Test for any args. - $this->assertTrue( as_has_scheduled_action( 'hook_1', array( 'a' ) ) ); - } - - public function test_as_has_scheduled_action_with_group() { - as_schedule_single_action( time(), 'hook_1', array(), 'group_1' ); - - $this->assertTrue( as_has_scheduled_action( 'hook_1', null, 'group_1' ) ); - $this->assertTrue( as_has_scheduled_action( 'hook_1', array(), 'group_1' ) ); - } - // phpcs:enable - - /** - * Test as_enqueue_async_action with unique param. - */ - public function test_as_enqueue_async_action_unique() { - $this->set_action_scheduler_store( new ActionScheduler_DBStore() ); - - $action_id = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertValidAction( $action_id ); - - $action_id_duplicate = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * Test enqueuing a unique action using the hybrid store. - * This is using a best-effort approach, so it's possible that the action will be enqueued even if it's not unique. - */ - public function test_as_enqueue_async_action_unique_hybrid_best_effort() { - $this->set_action_scheduler_store( new ActionScheduler_HybridStore() ); - - $action_id = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertValidAction( $action_id ); - - $action_id_duplicate = as_enqueue_async_action( 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * Test as_schedule_single_action with unique param. - */ - public function test_as_schedule_single_action_unique() { - $this->set_action_scheduler_store( new ActionScheduler_DBStore() ); - - $action_id = as_schedule_single_action( time(), 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertValidAction( $action_id ); - - $action_id_duplicate = as_schedule_single_action( time(), 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * Test as_schedule_recurring_action with unique param. - */ - public function test_as_schedule_recurring_action_unique() { - $this->set_action_scheduler_store( new ActionScheduler_DBStore() ); - - $action_id = as_schedule_recurring_action( time(), MINUTE_IN_SECONDS, 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertValidAction( $action_id ); - - $action_id_duplicate = as_schedule_recurring_action( time(), MINUTE_IN_SECONDS, 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * Test as_schedule_cron with unique param. - */ - public function test_as_schedule_cron_action() { - $this->set_action_scheduler_store( new ActionScheduler_DBStore() ); - - $action_id = as_schedule_cron_action( time(), '0 0 * * *', 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertValidAction( $action_id ); - - $action_id_duplicate = as_schedule_cron_action( time(), '0 0 * * *', 'hook_1', array( 'a' ), 'dummy', true ); - $this->assertEquals( 0, $action_id_duplicate ); - } - - /** - * Test recovering from an incorrect database schema when scheduling a single action. - */ - public function test_as_recover_from_incorrect_schema() { - // custom error reporting so we can test for errors sent to error_log. - global $wpdb; - $wpdb->suppress_errors( true ); - $error_capture = tmpfile(); - $actual_error_log = ini_set( 'error_log', stream_get_meta_data( $error_capture )['uri'] ); // phpcs:ignore WordPress.PHP.IniSet.Risky - - // we need a hybrid store so that dropping the priority column will cause an exception. - $this->set_action_scheduler_store( new ActionScheduler_HybridStore() ); - $this->assertEquals( 'ActionScheduler_HybridStore', get_class( ActionScheduler::store() ) ); - - // drop the priority column from the actions table. - $wpdb->query( "ALTER TABLE {$wpdb->actionscheduler_actions} DROP COLUMN priority" ); - - // try to schedule a single action. - $action_id = as_schedule_single_action( time(), 'hook_17', array( 'a', 'b' ), 'dummytest', true ); - - // ensure that no exception was thrown and zero was returned. - $this->assertEquals( 0, $action_id ); - - // try to schedule an async action. - $action_id = as_enqueue_async_action( 'hook_18', array( 'a', 'b' ), 'dummytest', true ); - // ensure that no exception was thrown and zero was returned. - $this->assertEquals( 0, $action_id ); - - // try to schedule a recurring action. - $action_id = as_schedule_recurring_action( time(), MINUTE_IN_SECONDS, 'hook_19', array( 'a', 'b' ), 'dummytest', true ); - // ensure that no exception was thrown and zero was returned. - $this->assertEquals( 0, $action_id ); - - // try to schedule a cron action. - $action_id = as_schedule_cron_action( time(), '0 0 * * *', 'hook_20', array( 'a', 'b' ), 'dummytest', true ); - // ensure that no exception was thrown and zero was returned. - $this->assertEquals( 0, $action_id ); - - // ensure that all four errors were logged to error_log. - $logged_errors = stream_get_contents( $error_capture ); - $this->assertContains( 'Caught exception while enqueuing action "hook_17": Error saving action', $logged_errors ); - $this->assertContains( 'Caught exception while enqueuing action "hook_18": Error saving action', $logged_errors ); - $this->assertContains( 'Caught exception while enqueuing action "hook_19": Error saving action', $logged_errors ); - $this->assertContains( 'Caught exception while enqueuing action "hook_20": Error saving action', $logged_errors ); - $this->assertContains( "Unknown column 'priority' in ", $logged_errors ); - - // recreate the priority column. - $wpdb->query( "ALTER TABLE {$wpdb->actionscheduler_actions} ADD COLUMN priority tinyint(10) UNSIGNED NOT NULL DEFAULT 10" ); - // restore error logging. - $wpdb->suppress_errors( false ); - ini_set( 'error_log', $actual_error_log ); // phpcs:ignore WordPress.PHP.IniSet.Risky - } - - /** - * Test that as_supports returns true for supported features. - */ - public function test_as_supports_for_supported_feature() { - $this->assertTrue( as_supports( 'ensure_recurring_actions_hook' ) ); - } - - /** - * Test that as_supports returns false for unsupported features. - */ - public function test_as_supports_for_unsupported_feature() { - $this->assertFalse( as_supports( 'non_existent_feature' ) ); - } - - /** - * Helper method to set actions scheduler store. - * - * @param ActionScheduler_Store $store Store instance to set. - */ - private function set_action_scheduler_store( $store ) { - $store_factory_setter = function() use ( $store ) { - self::$store = $store; - }; - $binded_store_factory_setter = Closure::bind( $store_factory_setter, null, ActionScheduler_Store::class ); - $binded_store_factory_setter(); - } - - /** - * Helper method to assert valid action. - * - * @param int $action_id Action ID to assert. - */ - private function assertValidAction( $action_id ) { - $this->assertNotEquals( 0, $action_id ); - $action = ActionScheduler::store()->fetch_action( $action_id ); - $this->assertInstanceOf( 'ActionScheduler_Action', $action ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php b/includes/libraries/action-scheduler/tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php deleted file mode 100644 index 483f73f..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/procedural_api/wc_get_scheduled_actions_Test.php +++ /dev/null @@ -1,131 +0,0 @@ - */ - private $hooks = array(); - - /** @var array */ - private $args = array(); - - /** @var array */ - private $groups = array(); - - public function setUp(): void { - parent::setUp(); - - $store = ActionScheduler::store(); - - for ( $i = 0; $i < 10; $i++ ) { - $this->hooks[ $i ] = md5( wp_rand() ); - $this->args[ $i ] = md5( wp_rand() ); - $this->groups[ $i ] = md5( wp_rand() ); - } - - for ( $i = 0; $i < 10; $i++ ) { - for ( $j = 0; $j < 10; $j++ ) { - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( $j - 3 . 'days' ) ); - $group = $this->groups[ ( $i + $j ) % 10 ]; - $action = new ActionScheduler_Action( $this->hooks[ $i ], array( $this->args[ $j ] ), $schedule, $group ); - $store->save_action( $action ); - } - } - } - - public function test_date_queries() { - $actions = as_get_scheduled_actions( - array( - 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ), - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 30, $actions ); - - $actions = as_get_scheduled_actions( - array( - 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ), - 'date_compare' => '>=', - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 70, $actions ); - } - - public function test_hook_queries() { - $actions = as_get_scheduled_actions( - array( - 'hook' => $this->hooks[2], - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 10, $actions ); - - $actions = as_get_scheduled_actions( - array( - 'hook' => $this->hooks[2], - 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ), - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 3, $actions ); - } - - public function test_args_queries() { - $actions = as_get_scheduled_actions( - array( - 'args' => array( $this->args[5] ), - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 10, $actions ); - - $actions = as_get_scheduled_actions( - array( - 'args' => array( $this->args[5] ), - 'hook' => $this->hooks[3], - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 1, $actions ); - - $actions = as_get_scheduled_actions( - array( - 'args' => array( $this->args[5] ), - 'hook' => $this->hooks[3], - 'date' => as_get_datetime_object( gmdate( 'Y-m-d 00:00:00' ) ), - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 0, $actions ); - } - - public function test_group_queries() { - $actions = as_get_scheduled_actions( - array( - 'group' => $this->groups[1], - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 10, $actions ); - - $actions = as_get_scheduled_actions( - array( - 'group' => $this->groups[1], - 'hook' => $this->hooks[9], - 'per_page' => -1, - ), - 'ids' - ); - $this->assertCount( 1, $actions ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueCleaner_Test.php b/includes/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueCleaner_Test.php deleted file mode 100644 index e137200..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueCleaner_Test.php +++ /dev/null @@ -1,173 +0,0 @@ -save_action( $action ); - } - - $runner->run(); - - add_filter( 'action_scheduler_retention_period', '__return_zero' ); // delete any finished job. - $cleaner = new ActionScheduler_QueueCleaner( $store ); - $cleaned = $cleaner->delete_old_actions(); - remove_filter( 'action_scheduler_retention_period', '__return_zero' ); - - $this->assertIsArray( $cleaned, 'ActionScheduler_QueueCleaner::delete_old_actions() returns an array.' ); - $this->assertCount( 5, $cleaned, 'ActionScheduler_QueueCleaner::delete_old_actions() deleted the expected number of actions.' ); - - foreach ( $created_actions as $action_id ) { - $action = $store->fetch_action( $action_id ); - $this->assertFalse( $action->is_finished() ); // it's a NullAction. - } - } - - public function test_invalid_retention_period_filter_hook() { - // Supplying a non-integer such as null would break under 3.5.4 and earlier. - add_filter( 'action_scheduler_retention_period', '__return_null' ); - $cleaner = new ActionScheduler_QueueCleaner( ActionScheduler::store() ); - - $this->setExpectedIncorrectUsage( 'ActionScheduler_QueueCleaner::delete_old_actions' ); - $result = $cleaner->delete_old_actions(); - remove_filter( 'action_scheduler_retention_period', '__return_zero' ); - - $this->assertIsArray( - $result, - 'ActionScheduler_QueueCleaner::delete_old_actions() can be invoked without a fatal error, even if the retention period was invalid.' - ); - - $this->assertCount( - 0, - $result, - 'ActionScheduler_QueueCleaner::delete_old_actions() will not delete any actions if the retention period was invalid.' - ); - } - - public function test_delete_canceled_actions() { - $store = ActionScheduler::store(); - - $random = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) ); - - $created_actions = array(); - for ( $i = 0; $i < 5; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $action_id = $store->save_action( $action ); - $store->cancel_action( $action_id ); - $created_actions[] = $action_id; - } - - // track the actions that are deleted. - $mock_action = new MockAction(); - add_action( 'action_scheduler_deleted_action', array( $mock_action, 'action' ), 10, 1 ); - add_filter( 'action_scheduler_retention_period', '__return_zero' ); // delete any finished job. - - $cleaner = new ActionScheduler_QueueCleaner( $store ); - $cleaner->delete_old_actions(); - - remove_filter( 'action_scheduler_retention_period', '__return_zero' ); - remove_action( 'action_scheduler_deleted_action', array( $mock_action, 'action' ), 10 ); - - $deleted_actions = array(); - foreach ( $mock_action->get_args() as $action ) { - $deleted_actions[] = reset( $action ); - } - - $this->assertEqualSets( $created_actions, $deleted_actions ); - } - - public function test_do_not_delete_recent_actions() { - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $random = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) ); - - $created_actions = array(); - for ( $i = 0; $i < 5; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $created_actions[] = $store->save_action( $action ); - } - - $runner->run(); - - $cleaner = new ActionScheduler_QueueCleaner( $store ); - $cleaner->delete_old_actions(); - - foreach ( $created_actions as $action_id ) { - $action = $store->fetch_action( $action_id ); - $this->assertTrue( $action->is_finished() ); // It's a FinishedAction. - } - } - - public function test_reset_unrun_actions() { - $store = ActionScheduler::store(); - - $random = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) ); - - $created_actions = array(); - for ( $i = 0; $i < 5; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $created_actions[] = $store->save_action( $action ); - } - - $store->stake_claim( 10 ); - - // don't actually process the jobs, to simulate a request that timed out. - - add_filter( 'action_scheduler_timeout_period', '__return_zero' ); // delete any finished job. - $cleaner = new ActionScheduler_QueueCleaner( $store ); - $cleaner->reset_timeouts(); - - remove_filter( 'action_scheduler_timeout_period', '__return_zero' ); - - $claim = $store->stake_claim( 10 ); - $this->assertEqualSets( $created_actions, $claim->get_actions() ); - } - - public function test_do_not_reset_failed_action() { - $store = ActionScheduler::store(); - $random = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) ); - - $created_actions = array(); - for ( $i = 0; $i < 5; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $created_actions[] = $store->save_action( $action ); - } - - $claim = $store->stake_claim( 10 ); - foreach ( $claim->get_actions() as $action_id ) { - // simulate the first action interrupted by an uncatchable fatal error. - $store->log_execution( $action_id ); - break; - } - - add_filter( 'action_scheduler_timeout_period', '__return_zero' ); // delete any finished job. - $cleaner = new ActionScheduler_QueueCleaner( $store ); - $cleaner->reset_timeouts(); - remove_filter( 'action_scheduler_timeout_period', '__return_zero' ); - - $new_claim = $store->stake_claim( 10 ); - $this->assertCount( 4, $new_claim->get_actions() ); - - add_filter( 'action_scheduler_failure_period', '__return_zero' ); - $cleaner->mark_failures(); - remove_filter( 'action_scheduler_failure_period', '__return_zero' ); - - $failed = $store->query_actions( array( 'status' => ActionScheduler_Store::STATUS_FAILED ) ); - $this->assertEquals( $created_actions[0], $failed[0] ); - $this->assertCount( 1, $failed ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueRunner_Test.php b/includes/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueRunner_Test.php deleted file mode 100644 index 46e850c..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/runner/ActionScheduler_QueueRunner_Test.php +++ /dev/null @@ -1,613 +0,0 @@ -run(); - - $this->assertEquals( 0, $actions_run ); - } - - public function test_run() { - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $mock = new MockAction(); - $random = md5( wp_rand() ); - - add_action( $random, array( $mock, 'action' ) ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) ); - - for ( $i = 0; $i < 5; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $store->save_action( $action ); - } - - $actions_run = $runner->run(); - - remove_action( $random, array( $mock, 'action' ) ); - - $this->assertEquals( 5, $mock->get_call_count() ); - $this->assertEquals( 5, $actions_run ); - } - - public function test_run_with_future_actions() { - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $mock = new MockAction(); - $random = md5( wp_rand() ); - - add_action( $random, array( $mock, 'action' ) ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) ); - - for ( $i = 0; $i < 3; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $store->save_action( $action ); - } - - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( 'tomorrow' ) ); - for ( $i = 0; $i < 3; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $store->save_action( $action ); - } - - $actions_run = $runner->run(); - - remove_action( $random, array( $mock, 'action' ) ); - - $this->assertEquals( 3, $mock->get_call_count() ); - $this->assertEquals( 3, $actions_run ); - } - - /** - * When an action is processed, it is set to "in-progress" (running) status immediately before the - * callback is invoked. If this fails (which could be because it is already in progress) then the - * action should be skipped. - * - * @return void - */ - public function test_run_with_action_that_is_already_in_progress() { - $store = ActionScheduler::store(); - $hook = uniqid(); - $callback = function () {}; - $count = 0; - $actions = array(); - $completed = array(); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '1 day ago' ) ); - - for ( $i = 0; $i < 3; $i++ ) { - $actions[] = $store->save_action( new ActionScheduler_Action( $hook, array( $hook ), $schedule ) ); - } - - /** - * This function "sabotages" the next action by prematurely setting its status to "in-progress", simulating - * an edge case where a concurrent process runs the action. - */ - $saboteur = function () use ( &$count, $store, $actions ) { - if ( 0 === $count++ ) { - $store->log_execution( $actions[1] ); - } - }; - - /** - * @param int $action_id The ID of the recently completed action. - * - * @return void - */ - $spy = function ( $action_id ) use ( &$completed ) { - $completed[] = $action_id; - }; - - add_action( 'action_scheduler_begin_execute', $saboteur ); - add_action( 'action_scheduler_completed_action', $spy ); - add_action( $hook, $callback ); - - $actions_attempted = ActionScheduler_Mocker::get_queue_runner( $store )->run(); - - remove_action( 'action_scheduler_begin_execute', $saboteur ); - remove_action( 'action_scheduler_completed_action', $spy ); - remove_action( $hook, $callback ); - - $this->assertEquals( 3, $actions_attempted, 'The queue runner attempted to process all 3 actions.' ); - $this->assertEquals( array( $actions[0], $actions[2] ), $completed, 'Only two of the three actions were completed (one was skipped, because it was processed by a concurrent request).' ); - } - - public function test_completed_action_status() { - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $random = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '12 hours ago' ) ); - $action = new ActionScheduler_Action( $random, array(), $schedule ); - $action_id = $store->save_action( $action ); - - $runner->run(); - - $finished_action = $store->fetch_action( $action_id ); - - $this->assertTrue( $finished_action->is_finished() ); - } - - public function test_next_instance_of_cron_action() { - // Create an action with daily Cron expression (i.e. midnight each day). - $random = md5( wp_rand() ); - $action_id = ActionScheduler::factory()->cron( $random, array(), null, '0 0 * * *' ); - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - - // Make sure the 1st instance of the action is scheduled to occur tomorrow. - $date = as_get_datetime_object( 'tomorrow' ); - $date->modify( '-1 minute' ); - $claim = $store->stake_claim( 10, $date ); - $this->assertCount( 0, $claim->get_actions() ); - - $store->release_claim( $claim ); - - $date->modify( '+1 minute' ); - - $claim = $store->stake_claim( 10, $date ); - $actions = $claim->get_actions(); - $this->assertCount( 1, $actions ); - - $fetched_action_id = reset( $actions ); - $fetched_action = $store->fetch_action( $fetched_action_id ); - - $this->assertEquals( $fetched_action_id, $action_id ); - $this->assertEquals( $random, $fetched_action->get_hook() ); - $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 ); - - $store->release_claim( $claim ); - - // Make sure the 2nd instance of the cron action is scheduled to occur tomorrow still. - $runner->process_action( $action_id ); - - $claim = $store->stake_claim( 10, $date ); - $actions = $claim->get_actions(); - $this->assertCount( 1, $actions ); - - $fetched_action_id = reset( $actions ); - $fetched_action = $store->fetch_action( $fetched_action_id ); - - $this->assertNotEquals( $fetched_action_id, $action_id ); - $this->assertEquals( $random, $fetched_action->get_hook() ); - $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 ); - } - - public function test_next_instance_of_interval_action() { - $random = md5( wp_rand() ); - $date = as_get_datetime_object( '12 hours ago' ); - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - - // Create an action to recur every 24 hours, with the first instance scheduled to run 12 hours ago. - $action_id = ActionScheduler::factory()->create( - array( - 'type' => 'recurring', - 'hook' => $random, - 'when' => $date->getTimestamp(), - 'pattern' => DAY_IN_SECONDS, - 'priority' => 2, - ) - ); - - // Make sure the 1st instance of the action is scheduled to occur 12 hours ago. - $claim = $store->stake_claim( 10, $date ); - $actions = $claim->get_actions(); - $this->assertCount( 1, $actions ); - - $fetched_action_id = reset( $actions ); - $fetched_action = $store->fetch_action( $fetched_action_id ); - - $this->assertEquals( $fetched_action_id, $action_id ); - $this->assertEquals( $random, $fetched_action->get_hook() ); - $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 ); - - $store->release_claim( $claim ); - - // Make sure after the queue is run, the 2nd instance of the action is scheduled to occur in 24 hours. - $runner->run(); - - $date = as_get_datetime_object( '+1 day' ); - $claim = $store->stake_claim( 10, $date ); - $actions = $claim->get_actions(); - $this->assertCount( 1, $actions ); - - $fetched_action_id = reset( $actions ); - $fetched_action = $store->fetch_action( $fetched_action_id ); - - $this->assertNotEquals( $fetched_action_id, $action_id ); - $this->assertEquals( $random, $fetched_action->get_hook() ); - $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 ); - $this->assertEquals( 2, $fetched_action->get_priority(), 'The replacement action should inherit the same priority as the original action.' ); - $store->release_claim( $claim ); - - // Make sure the 3rd instance of the cron action is scheduled for 24 hours from now, as the action was run early, ahead of schedule. - $runner->process_action( $fetched_action_id ); - $date = as_get_datetime_object( '+1 day' ); - - $claim = $store->stake_claim( 10, $date ); - $actions = $claim->get_actions(); - $this->assertCount( 1, $actions ); - - $fetched_action_id = reset( $actions ); - $fetched_action = $store->fetch_action( $fetched_action_id ); - - $this->assertNotEquals( $fetched_action_id, $action_id ); - $this->assertEquals( $random, $fetched_action->get_hook() ); - $this->assertEquals( $date->getTimestamp(), $fetched_action->get_schedule()->get_date()->getTimestamp(), '', 1 ); - } - - /** - * As soon as one recurring action has been executed its replacement will be scheduled. - * - * This is true even if the current action fails. This makes sense, since a failure may be temporary in nature. - * However, if the same recurring action consistently fails then it is likely that there is a problem and we should - * stop creating new instances. This test outlines the expected behavior in this regard. - * - * @return void - */ - public function test_failing_recurring_actions_are_not_rescheduled_when_threshold_met() { - $store = ActionScheduler_Store::instance(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $created_actions = array(); - - // Create 4 failed actions (one below the threshold of what counts as 'consistently failing'). - for ( $i = 0; $i < 3; $i++ ) { - // We give each action a unique set of args, this illustrates that in the context of determining consistent - // failure we care only about the hook and not other properties of the action. - $args = array( 'unique-' . $i => hash( 'md5', $i ) ); - $hook = 'will-fail'; - $date = as_get_datetime_object( 12 - $i . ' hours ago' ); - $action_id = as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, $hook, $args ); - $store->mark_failure( $action_id ); - $created_actions[] = $action_id; - } - - // Now create a further action using the same hook, that is also destined to fail. - $date = as_get_datetime_object( '6 hours ago' ); - $pending_action_id = as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, $hook, $args ); - $created_actions[] = $pending_action_id; - - // Process the queue! - $runner->run(); - - $pending_actions = $store->query_actions( - array( - 'hook' => $hook, - 'args' => $args, - 'status' => ActionScheduler_Store::STATUS_PENDING, - ) - ); - - $new_pending_action_id = current( $pending_actions ); - - // We now have 5 instances of the same recurring action. 4 have already failed, one is pending. - $this->assertCount( 1, $pending_actions, 'If the threshold for consistent failure has not been met, a replacement action should have been scheduled.' ); - $this->assertNotContains( $new_pending_action_id, $created_actions, 'Confirm that the replacement action is new, and not one of those we created manually earlier in the test.' ); - - // Process the pending action (we do this directly instead of via `$runner->run()` because it won't actually - // become due for another hour). - $runner->process_action( $new_pending_action_id ); - $pending_actions = $store->query_actions( - array( - 'hook' => $hook, - 'args' => $args, - 'status' => ActionScheduler_Store::STATUS_PENDING, - ) - ); - - // Now 5 instances of the same recurring action have all failed, therefore the threshold for consistent failure - // has been met and, this time, a new action should *not* have been scheduled. - $this->assertCount( 0, $pending_actions, 'The failure threshold (5 consecutive fails for recurring actions with the same signature) having been met, no further actions were scheduled.' ); - } - - /** - * If a recurring action continually fails, it will not be re-scheduled. However, a hook makes it possible to - * exempt specific actions from this behavior (without impacting other unrelated recurring actions). - * - * @see self::test_failing_recurring_actions_are_not_rescheduled_when_threshold_met() - * @return void - */ - public function test_exceptions_can_be_made_for_failing_recurring_actions() { - $store = ActionScheduler_Store::instance(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $observed = 0; - - // Create 2 sets of 5 actions that have already past and have already failed (five being the threshold of what - // counts as 'consistently failing'). - for ( $i = 0; $i < 4; $i++ ) { - $date = as_get_datetime_object( 12 - $i . ' hours ago' ); - $store->mark_failure( as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'foo' ) ); - $store->mark_failure( as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'bar' ) ); - } - - // Add one more action (pending and past-due) to each set. - $date = as_get_datetime_object( '6 hours ago' ); - as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'foo' ); - as_schedule_recurring_action( $date->getTimestamp(), HOUR_IN_SECONDS, 'bar' ); - - // Define a filter function that allows scheduled actions for hook 'foo' to still be rescheduled, despite its - // history of consistent failure. - $filter = function( $is_failing, $action ) use ( &$observed ) { - $observed++; - return 'foo' === $action->get_hook() ? false : $is_failing; - }; - - // Process the queue with our consistent-failure filter function in place. - add_filter( 'action_scheduler_recurring_action_is_consistently_failing', $filter, 10, 2 ); - $runner->run(); - - // Check how many (if any) of our test actions were re-scheduled. - $pending_foo_actions = $store->query_actions( - array( - 'hook' => 'foo', - 'status' => ActionScheduler_Store::STATUS_PENDING, - ) - ); - $pending_bar_actions = $store->query_actions( - array( - 'hook' => 'bar', - 'status' => ActionScheduler_Store::STATUS_PENDING, - ) - ); - - // Expectations... - $this->assertCount( 1, $pending_foo_actions, 'We expect a new instance of action "foo" will have been scheduled.' ); - $this->assertCount( 0, $pending_bar_actions, 'We expect no further instances of action "bar" will have been scheduled.' ); - $this->assertEquals( 2, $observed, 'We expect our callback to have been invoked twice, once in relation to each test action.' ); - - // Clean-up... - remove_filter( 'action_scheduler_recurring_action_is_consistently_failing', $filter, 10, 2 ); - } - - public function test_hooked_into_wp_cron() { - $next = wp_next_scheduled( ActionScheduler_QueueRunner::WP_CRON_HOOK, array( 'WP Cron' ) ); - $this->assertNotEmpty( $next ); - } - - public function test_batch_count_limit() { - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $mock = new MockAction(); - $random = md5( wp_rand() ); - - add_action( $random, array( $mock, 'action' ) ); - $schedule = new ActionScheduler_SimpleSchedule( new ActionScheduler_DateTime( '1 day ago' ) ); - - for ( $i = 0; $i < 2; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $store->save_action( $action ); - } - - $claim = $store->stake_claim(); - - $actions_run = $runner->run(); - - $this->assertEquals( 0, $mock->get_call_count() ); - $this->assertEquals( 0, $actions_run ); - - $store->release_claim( $claim ); - - $actions_run = $runner->run(); - - $this->assertEquals( 2, $mock->get_call_count() ); - $this->assertEquals( 2, $actions_run ); - - remove_action( $random, array( $mock, 'action' ) ); - } - - public function test_changing_batch_count_limit() { - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $random = md5( wp_rand() ); - $schedule = new ActionScheduler_SimpleSchedule( new ActionScheduler_DateTime( '1 day ago' ) ); - - for ( $i = 0; $i < 30; $i++ ) { - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $store->save_action( $action ); - } - - $claims = array(); - - for ( $i = 0; $i < 5; $i++ ) { - $claims[] = $store->stake_claim( 5 ); - } - - $mock1 = new MockAction(); - add_action( $random, array( $mock1, 'action' ) ); - $actions_run = $runner->run(); - remove_action( $random, array( $mock1, 'action' ) ); - - $this->assertEquals( 0, $mock1->get_call_count() ); - $this->assertEquals( 0, $actions_run ); - - add_filter( 'action_scheduler_queue_runner_concurrent_batches', array( $this, 'return_6' ) ); - - $mock2 = new MockAction(); - add_action( $random, array( $mock2, 'action' ) ); - $actions_run = $runner->run(); - remove_action( $random, array( $mock2, 'action' ) ); - - $this->assertEquals( 5, $mock2->get_call_count() ); - $this->assertEquals( 5, $actions_run ); - - remove_filter( 'action_scheduler_queue_runner_concurrent_batches', array( $this, 'return_6' ) ); - - for ( $i = 0; $i < 5; $i++ ) { // to make up for the actions we just processed. - $action = new ActionScheduler_Action( $random, array( $random ), $schedule ); - $store->save_action( $action ); - } - - $mock3 = new MockAction(); - add_action( $random, array( $mock3, 'action' ) ); - $actions_run = $runner->run(); - remove_action( $random, array( $mock3, 'action' ) ); - - $this->assertEquals( 0, $mock3->get_call_count() ); - $this->assertEquals( 0, $actions_run ); - - remove_filter( 'action_scheduler_queue_runner_concurrent_batches', array( $this, 'return_6' ) ); - } - - public function return_6() { - return 6; - } - - public function test_store_fetch_action_failure_schedule_next_instance() { - $random = md5( wp_rand() ); - $schedule = new ActionScheduler_IntervalSchedule( as_get_datetime_object( '12 hours ago' ), DAY_IN_SECONDS ); - $action = new ActionScheduler_Action( $random, array(), $schedule ); - $action_id = ActionScheduler::store()->save_action( $action ); - - // Set up a mock store that will throw an exception when fetching actions. - $store = $this - ->getMockBuilder( 'ActionScheduler_wpPostStore' ) - ->setMethods( array( 'fetch_action' ) ) - ->getMock(); - $store - ->method( 'fetch_action' ) - ->with( array( $action_id ) ) - ->will( $this->throwException( new Exception() ) ); - - // Set up a mock queue runner to verify that schedule_next_instance() - // isn't called for an undefined $action. - $runner = $this - ->getMockBuilder( 'ActionScheduler_QueueRunner' ) - ->setConstructorArgs( array( $store ) ) - ->setMethods( array( 'schedule_next_instance' ) ) - ->getMock(); - $runner - ->expects( $this->never() ) - ->method( 'schedule_next_instance' ); - - $runner->run(); - - // Set up a mock store that will throw an exception when fetching actions. - $store2 = $this - ->getMockBuilder( 'ActionScheduler_wpPostStore' ) - ->setMethods( array( 'fetch_action' ) ) - ->getMock(); - $store2 - ->method( 'fetch_action' ) - ->with( array( $action_id ) ) - ->willReturn( null ); - - // Set up a mock queue runner to verify that schedule_next_instance() - // isn't called for an undefined $action. - $runner2 = $this - ->getMockBuilder( 'ActionScheduler_QueueRunner' ) - ->setConstructorArgs( array( $store ) ) - ->setMethods( array( 'schedule_next_instance' ) ) - ->getMock(); - $runner2 - ->expects( $this->never() ) - ->method( 'schedule_next_instance' ); - - $runner2->run(); - } - - /** - * Checks that actions are processed in the correct order. Specifically, that past-due actions are not - * penalized in favor of newer async actions. - * - * @return void - */ - public function test_order_in_which_actions_are_processed() { - /** @var ActionScheduler_Store $store */ - $store = ActionScheduler::store(); - $runner = ActionScheduler_Mocker::get_queue_runner( $store ); - $execution_order = array(); - $past_due_action = as_schedule_single_action( time() - HOUR_IN_SECONDS, __METHOD__, array( 'execute' => 'first' ) ); - $async_action = as_enqueue_async_action( __METHOD__, array( 'execute' => 'second' ) ); - - $monitor = function ( $order ) use ( &$execution_order ) { - $execution_order[] = $order; - }; - - add_action( __METHOD__, $monitor ); - $runner->run(); - remove_action( __METHOD__, $monitor ); - - $this->assertEquals( - array( - 'first', - 'second', - ), - $execution_order - ); - } - - /** - * Tests the ability of the queue runner to accommodate a range of error conditions (raised recoverable errors - * under PHP 5.6, thrown errors under PHP 7.0 upwards, and exceptions under all supported versions). - * - * @return void - */ - public function test_recoverable_errors_do_not_break_queue_runner() { - $executed = 0; - as_enqueue_async_action( 'foo' ); - as_enqueue_async_action( 'bar' ); - as_enqueue_async_action( 'baz' ); - as_enqueue_async_action( 'foobar' ); - - /** - * Trigger a custom user error. - * - * @return void - */ - $foo = function () use ( &$executed ) { - $executed++; - trigger_error( 'Trouble.', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error - }; - - /** - * Throw an exception. - * - * @throws Exception Intentionally raised for testing purposes. - * - * @return void - */ - $bar = function () use ( &$executed ) { - $executed++; - throw new Exception( 'More trouble.' ); - }; - - /** - * Trigger a recoverable fatal error. Under PHP 5.6 the error will be raised, and under PHP 7.0 and higher the - * error will be thrown (different mechanisms are needed to support this difference). - * - * @throws Throwable Intentionally raised for testing purposes. - * - * @return void - */ - $baz = function () use ( &$executed ) { - $executed++; - (string) (object) array(); - }; - - /** - * A problem-free callback. - * - * @return void - */ - $foobar = function () use ( &$executed ) { - $executed++; - }; - - add_action( 'foo', $foo ); - add_action( 'bar', $bar ); - add_action( 'baz', $baz ); - add_action( 'foobar', $foobar ); - - ActionScheduler_Mocker::get_queue_runner( ActionScheduler::store() )->run(); - $this->assertEquals( 4, $executed, 'All enqueued actions ran as expected despite errors and exceptions being raised by the first actions in the set.' ); - - remove_action( 'foo', $foo ); - remove_action( 'bar', $bar ); - remove_action( 'baz', $baz ); - remove_action( 'foobar', $foobar ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_CronSchedule_Test.php b/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_CronSchedule_Test.php deleted file mode 100644 index 0900514..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_CronSchedule_Test.php +++ /dev/null @@ -1,75 +0,0 @@ -modify( '-1 hour' ); - $schedule = new ActionScheduler_CronSchedule( $start, $cron ); - $this->assertEquals( $time, $schedule->get_date() ); - $this->assertEquals( $start, $schedule->get_first_date() ); - - // Test delaying for a future start date. - $start->modify( '+1 week' ); - $time->modify( '+1 week' ); - - $schedule = new ActionScheduler_CronSchedule( $start, $cron ); - $this->assertEquals( $time, $schedule->get_date() ); - $this->assertEquals( $start, $schedule->get_first_date() ); - } - - public function test_creation_with_first_date() { - $time = as_get_datetime_object( 'tomorrow' ); - $cron = CronExpression::factory( '@daily' ); - $start = clone $time; - $start->modify( '-1 hour' ); - $schedule = new ActionScheduler_CronSchedule( $start, $cron ); - $this->assertEquals( $time, $schedule->get_date() ); - $this->assertEquals( $start, $schedule->get_first_date() ); - - // Test delaying for a future start date. - $first = clone $time; - $first->modify( '-1 day' ); - $start->modify( '+1 week' ); - $time->modify( '+1 week' ); - - $schedule = new ActionScheduler_CronSchedule( $start, $cron, $first ); - $this->assertEquals( $time, $schedule->get_date() ); - $this->assertEquals( $first, $schedule->get_first_date() ); - } - - public function test_next() { - $time = as_get_datetime_object( '2013-06-14' ); - $cron = CronExpression::factory( '@daily' ); - $schedule = new ActionScheduler_CronSchedule( $time, $cron ); - $this->assertEquals( as_get_datetime_object( 'tomorrow' ), $schedule->get_next( as_get_datetime_object() ) ); - } - - public function test_is_recurring() { - $schedule = new ActionScheduler_CronSchedule( as_get_datetime_object( '2013-06-14' ), CronExpression::factory( '@daily' ) ); - $this->assertTrue( $schedule->is_recurring() ); - } - - public function test_cron_format() { - $time = as_get_datetime_object( '2014-01-01' ); - $cron = CronExpression::factory( '0 0 10 10 *' ); - $schedule = new ActionScheduler_CronSchedule( $time, $cron ); - $this->assertEquals( as_get_datetime_object( '2014-10-10' ), $schedule->get_date() ); - - $cron = CronExpression::factory( '0 0 L 1/2 *' ); - $schedule = new ActionScheduler_CronSchedule( $time, $cron ); - $this->assertEquals( as_get_datetime_object( '2014-01-31' ), $schedule->get_date() ); - $this->assertEquals( as_get_datetime_object( '2014-07-31' ), $schedule->get_next( as_get_datetime_object( '2014-06-01' ) ) ); - $this->assertEquals( as_get_datetime_object( '2028-11-30' ), $schedule->get_next( as_get_datetime_object( '2028-11-01' ) ) ); - - $cron = CronExpression::factory( '30 14 * * MON#3 *' ); - $schedule = new ActionScheduler_CronSchedule( $time, $cron ); - $this->assertEquals( as_get_datetime_object( '2014-01-20 14:30:00' ), $schedule->get_date() ); - $this->assertEquals( as_get_datetime_object( '2014-05-19 14:30:00' ), $schedule->get_next( as_get_datetime_object( '2014-05-01' ) ) ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_IntervalSchedule_Test.php b/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_IntervalSchedule_Test.php deleted file mode 100644 index a40052a..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_IntervalSchedule_Test.php +++ /dev/null @@ -1,37 +0,0 @@ -assertEquals( $time, $schedule->get_date() ); - $this->assertEquals( $time, $schedule->get_first_date() ); - } - - public function test_creation_with_first_date() { - $first = as_get_datetime_object(); - $time = as_get_datetime_object( '+12 hours' ); - $schedule = new ActionScheduler_IntervalSchedule( $time, HOUR_IN_SECONDS, $first ); - $this->assertEquals( $time, $schedule->get_date() ); - $this->assertEquals( $first, $schedule->get_first_date() ); - } - - public function test_next() { - $now = time(); - $start = $now - 30; - $schedule = new ActionScheduler_IntervalSchedule( as_get_datetime_object( "@$start" ), MINUTE_IN_SECONDS ); - $this->assertEquals( $start, $schedule->get_date()->getTimestamp() ); - $this->assertEquals( $now + MINUTE_IN_SECONDS, $schedule->get_next( as_get_datetime_object() )->getTimestamp() ); - $this->assertEquals( $start, $schedule->get_next( as_get_datetime_object( "@$start" ) )->getTimestamp() ); - } - - public function test_is_recurring() { - $start = time() - 30; - $schedule = new ActionScheduler_IntervalSchedule( as_get_datetime_object( "@$start" ), MINUTE_IN_SECONDS ); - $this->assertTrue( $schedule->is_recurring() ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_NullSchedule_Test.php b/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_NullSchedule_Test.php deleted file mode 100644 index 4e10da2..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_NullSchedule_Test.php +++ /dev/null @@ -1,17 +0,0 @@ -assertNull( $schedule->get_date() ); - } - - public function test_is_recurring() { - $schedule = new ActionScheduler_NullSchedule(); - $this->assertFalse( $schedule->is_recurring() ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_SimpleSchedule_Test.php b/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_SimpleSchedule_Test.php deleted file mode 100644 index d017673..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/schedules/ActionScheduler_SimpleSchedule_Test.php +++ /dev/null @@ -1,36 +0,0 @@ -assertEquals( $time, $schedule->get_date() ); - } - - public function test_past_date() { - $time = as_get_datetime_object( '-1 day' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $this->assertEquals( $time, $schedule->get_date() ); - } - - public function test_future_date() { - $time = as_get_datetime_object( '+1 day' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $this->assertEquals( $time, $schedule->get_date() ); - } - - public function test_grace_period_for_next() { - $time = as_get_datetime_object( '3 seconds ago' ); - $schedule = new ActionScheduler_SimpleSchedule( $time ); - $this->assertEquals( $time, $schedule->get_date() ); - } - - public function test_is_recurring() { - $schedule = new ActionScheduler_SimpleSchedule( as_get_datetime_object( '+1 day' ) ); - $this->assertFalse( $schedule->is_recurring() ); - } -} diff --git a/includes/libraries/action-scheduler/tests/phpunit/versioning/ActionScheduler_Versions_Test.php b/includes/libraries/action-scheduler/tests/phpunit/versioning/ActionScheduler_Versions_Test.php deleted file mode 100644 index 7eeee35..0000000 --- a/includes/libraries/action-scheduler/tests/phpunit/versioning/ActionScheduler_Versions_Test.php +++ /dev/null @@ -1,42 +0,0 @@ -register( '1.0-dev', 'callback_1_dot_0_dev' ); - $versions->register( '1.0', 'callback_1_dot_0' ); - - $registered = $versions->get_versions(); - - $this->assertArrayHasKey( '1.0-dev', $registered ); - $this->assertArrayHasKey( '1.0', $registered ); - $this->assertCount( 2, $registered ); - - $this->assertEquals( 'callback_1_dot_0_dev', $registered['1.0-dev'] ); - } - - public function test_duplicate_version() { - $versions = new ActionScheduler_Versions(); - $versions->register( '1.0', 'callback_1_dot_0_a' ); - $versions->register( '1.0', 'callback_1_dot_0_b' ); - - $registered = $versions->get_versions(); - - $this->assertArrayHasKey( '1.0', $registered ); - $this->assertCount( 1, $registered ); - } - - public function test_latest_version() { - $versions = new ActionScheduler_Versions(); - $this->assertEquals( '__return_null', $versions->latest_version_callback() ); - $versions->register( '1.2', 'callback_1_dot_2' ); - $versions->register( '1.3', 'callback_1_dot_3' ); - $versions->register( '1.0', 'callback_1_dot_0' ); - - $this->assertEquals( '1.3', $versions->latest_version() ); - $this->assertEquals( 'callback_1_dot_3', $versions->latest_version_callback() ); - } -} diff --git a/includes/nuclia-proxy-rest.php b/includes/nuclia-proxy-rest.php deleted file mode 100644 index f76b3b5..0000000 --- a/includes/nuclia-proxy-rest.php +++ /dev/null @@ -1,1333 +0,0 @@ - 0 ) { - @ob_end_clean(); - } - - // Disable compression and output buffering - @ini_set( 'zlib.output_compression', 'Off' ); - @ini_set( 'output_buffering', 'Off' ); - @ini_set( 'implicit_flush', 'On' ); - } -}, -1 ); - -/** - * Return a comma-separated list of headers allowed for CORS preflight. - * - * @return string - */ -function nuclia_proxy_cors_allow_headers_value(): string { - return implode( - ', ', - [ - 'Content-Type', - 'Authorization', - 'X-Requested-With', - 'x-synchronous', - 'X-Synchronous', - 'Nuclia-Learning-Id', - 'x-ndb-client', - 'X-NDB-Client', - 'Range', - 'If-Range', - ] - ); -} - -/** - * Emit CORS headers for proxy routes. - */ -function nuclia_proxy_send_cors_headers(): void { - if ( headers_sent() ) { - return; - } - - $origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? sanitize_text_field( (string) $_SERVER['HTTP_ORIGIN'] ) : ''; - if ( $origin !== '' ) { - header( 'Access-Control-Allow-Origin: ' . $origin ); - header( 'Vary: Origin', false ); - } else { - header( 'Access-Control-Allow-Origin: *' ); - } - - header( 'Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS' ); - header( 'Access-Control-Allow-Headers: ' . nuclia_proxy_cors_allow_headers_value() ); - header( 'Access-Control-Expose-Headers: X-Nuclia-Upstream-Status, X-Nuclia-Upstream-Content-Type, X-Nuclia-Upstream-URL, Nuclia-Learning-Id, X-NUCLIA-TRACE-ID' ); -} - -/** - * Emit strict preflight CORS headers for the Nuclia REST proxy route. - */ -function nuclia_proxy_send_rest_preflight_cors_headers(): void { - if ( headers_sent() ) { - return; - } - - $origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? sanitize_text_field( (string) $_SERVER['HTTP_ORIGIN'] ) : ''; - if ( $origin !== '' ) { - header( 'Access-Control-Allow-Origin: ' . $origin, true ); - } else { - header( 'Access-Control-Allow-Origin: *', true ); - } - - header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS', true ); - header( 'Access-Control-Allow-Headers: ' . nuclia_proxy_cors_allow_headers_value(), true ); - header( 'Access-Control-Expose-Headers: X-Nuclia-Upstream-Status, X-Nuclia-Upstream-Content-Type, X-Nuclia-Upstream-URL, Nuclia-Learning-Id, X-NUCLIA-TRACE-ID', true ); - header( 'Vary: Origin', true ); - header( 'Access-Control-Max-Age: 86400', true ); -} - -/** - * Override default WordPress REST CORS headers for Nuclia proxy preflight. - * - * @param bool $served Whether the request has already been served. - * @param WP_HTTP_Response $result Result to send to the client. - * @param WP_REST_Request $request Request used to generate the response. - * @param WP_REST_Server $server Server instance. - * @return bool - */ -function nuclia_proxy_force_rest_preflight_cors_headers( $served, $result, WP_REST_Request $request, WP_REST_Server $server ): bool { - $route = (string) $request->get_route(); - $method = strtoupper( (string) $request->get_method() ); - - if ( $method === 'OPTIONS' && str_starts_with( $route, '/progress-agentic-rag/v1/nuclia' ) ) { - nuclia_proxy_send_rest_preflight_cors_headers(); - } - - return $served; -} - -/** - * Return the path-only proxy URL for the Nuclia widget (no query string). - * The widget appends path and ?eph-token=... to this URL; it must not contain "?". - * - * @param string $zone Nuclia zone slug (e.g. aws-us-east-2-1). - * @return string - */ -function nuclia_proxy_url( string $zone ): string { - $zone = sanitize_title( $zone ); - if ( $zone === '' ) { - return ''; - } - return home_url( 'nuclia-proxy/' . $zone ); -} - -/** - * Register rewrite rule for path-only proxy: /nuclia-proxy/{zone}/{path} - */ -function nuclia_proxy_add_rewrite_rules(): void { - add_rewrite_rule( - '^nuclia-proxy/([a-z0-9-]+)/?(.*)?$', - 'index.php?nuclia_proxy=1&nuclia_proxy_zone=$matches[1]&nuclia_proxy_path=$matches[2]', - 'top' - ); -} - -/** - * @param array $vars - * @return array - */ -function nuclia_proxy_query_vars( array $vars ): array { - $vars[] = 'nuclia_proxy'; - $vars[] = 'nuclia_proxy_zone'; - $vars[] = 'nuclia_proxy_path'; - return $vars; -} - -/** - * Handle requests to /nuclia-proxy/{zone}/{path} and run the proxy. - */ -function nuclia_proxy_path_handler(): void { - $zone = ''; - $path = ''; - - if ( get_query_var( 'nuclia_proxy', '' ) === '1' ) { - $zone = get_query_var( 'nuclia_proxy_zone', '' ); - $path = get_query_var( 'nuclia_proxy_path', '' ); - } else { - // Plain permalinks or no rewrite: parse REQUEST_URI (e.g. /nuclia-proxy/zone/v1/kb/.../field). - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : ''; - $path_part = parse_url( $request_uri, PHP_URL_PATH ); - if ( is_string( $path_part ) && str_starts_with( $path_part, '/nuclia-proxy/' ) ) { - $suffix = substr( $path_part, strlen( '/nuclia-proxy/' ) ); - $parts = explode( '/', $suffix, 2 ); - $zone = $parts[0] ?? ''; - $path = isset( $parts[1] ) ? $parts[1] : ''; - } - } - - if ( $zone === '' ) { - return; - } - - $zone = sanitize_title( $zone ); - $path = is_string( $path ) ? ltrim( $path, '/' ) : ''; - nuclia_proxy_send_cors_headers(); - - $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) : 'GET'; - $query_params = isset( $_GET ) && is_array( $_GET ) ? $_GET : []; - $raw_query_string = nuclia_proxy_get_client_query_string(); - $body = ( $method !== 'GET' && $method !== 'HEAD' ) ? file_get_contents( 'php://input' ) : ''; - $content_type = isset( $_SERVER['CONTENT_TYPE'] ) ? sanitize_text_field( (string) $_SERVER['CONTENT_TYPE'] ) : ''; - $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( (string) $_SERVER['HTTP_ACCEPT'] ) : ''; - $passthrough_headers = nuclia_proxy_extract_passthrough_headers_from_server(); - - $result = nuclia_proxy_execute( $zone, $path, $method, $query_params, $content_type, $accept, $body, $raw_query_string, $passthrough_headers ); - - if ( isset( $result['error'] ) ) { - status_header( $result['status'] ); - echo wp_json_encode( [ 'code' => $result['error']['code'], 'message' => $result['error']['message'] ] ); - exit; - } - - nuclia_proxy_binary_response_prepare_output(); - - status_header( $result['status'] ); - $out = nuclia_proxy_prepare_proxy_response_for_output( $result['body'], $result['headers'] ); - foreach ( $out['headers'] as $name => $value ) { - header( (string) $name . ': ' . (string) $value ); - } - if ( $result['stream_file'] !== '' && file_exists( $result['stream_file'] ) ) { - readfile( $result['stream_file'] ); - @unlink( $result['stream_file'] ); - } else { - echo $out['body']; - } - exit; -} - -/** - * Extract a strict allow-list of headers we proxy to Nuclia. - * - * @return array - */ -function nuclia_proxy_extract_passthrough_headers_from_server(): array { - $allowed = [ - 'x-synchronous', - 'x-show-consumption', - 'x-ndb-client', - 'range', - 'if-range', - ]; - - $headers = []; - foreach ( $allowed as $header ) { - $server_key = 'HTTP_' . strtoupper( str_replace( '-', '_', $header ) ); - if ( isset( $_SERVER[ $server_key ] ) ) { - $value = sanitize_text_field( (string) $_SERVER[ $server_key ] ); - if ( $value !== '' ) { - $headers[ $header ] = $value; - } - } - } - - return $headers; -} - -/** - * Extract a strict allow-list of headers from REST requests. - * - * @param WP_REST_Request $request Request object. - * @return array - */ -function nuclia_proxy_extract_passthrough_headers_from_request( WP_REST_Request $request ): array { - $allowed = [ - 'x-synchronous', - 'x-show-consumption', - 'x-ndb-client', - 'range', - 'if-range', - ]; - - $headers = []; - foreach ( $allowed as $header ) { - $value = $request->get_header( $header ); - if ( is_string( $value ) && $value !== '' ) { - $headers[ $header ] = sanitize_text_field( $value ); - } - } - - return $headers; -} - -/** - * Determine whether a string header value should be treated as true. - * - * @param string $value Header value. - * @return bool - */ -function nuclia_proxy_is_truthy_header( string $value ): bool { - $value = strtolower( trim( $value ) ); - return in_array( $value, [ '1', 'true', 'yes', 'on' ], true ); -} - -/** - * Return true when an /ask request should be streamed (incremental ndjson). - * - * @param string $method HTTP method. - * @param string $normalized_path Normalized API path. - * @param array $passthrough_headers Forwarded request headers. - * @return bool - */ -function nuclia_proxy_should_stream_ask( string $method, string $normalized_path, array $passthrough_headers ): bool { - if ( $method !== 'POST' ) { - return false; - } - if ( ! preg_match( '#/ask/?$#', $normalized_path ) ) { - return false; - } - - $synchronous = (string) ( $passthrough_headers['x-synchronous'] ?? '' ); - return ! nuclia_proxy_is_truthy_header( $synchronous ); -} - -function nuclia_register_proxy_rest_routes(): void { - register_rest_route( - 'progress-agentic-rag/v1', - '/nuclia/(?P[a-z0-9-]+)(?:/(?P.*))?', - [ - [ - 'methods' => [ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS' ], - 'callback' => 'nuclia_proxy_rest_handler', - 'permission_callback' => '__return_true', - 'args' => [ - 'zone' => [ - 'validate_callback' => static function ( $value ): bool { - return is_string( $value ) && $value !== ''; - }, - ], - 'path' => [ - 'default' => '', - ], - ], - ], - ] - ); -} - -/** - * Stream a response directly using cURL. - * Passes chunks directly to browser for true incremental delivery. - * - * This function directly outputs headers and content, then exits. - * It does NOT return - control never passes back to the caller. - * - * @param string $remote_url Full remote URL with query params. - * @param string $method HTTP method. - * @param string $token Nuclia service account token (empty if using eph-token). - * @param array $request_headers Extra headers to send upstream. - * @param string $request_body Request body for non-GET/HEAD calls. - * @return never - */ -function nuclia_proxy_stream_with_curl( string $remote_url, string $method, string $token, array $request_headers = [], string $request_body = '' ): void { - nuclia_proxy_binary_response_prepare_output(); - $ch = curl_init( $remote_url ); - $method = strtoupper( $method ); - nuclia_proxy_send_cors_headers(); - - // Track if we have processed headers yet - $headers_processed = false; - - // Capture HTTP status code from status line - $http_status = 200; - - // Capture response headers to forward to client - $response_headers = []; - - /** - * CURLOPT_HEADERFUNCTION callback - receives response headers before body. - */ - $header_callback = function( $ch, $header_line ) use ( &$response_headers, &$http_status ) { - $len = strlen( $header_line ); - - // Upstream may respond over HTTP/2, so status parsing must support both - // HTTP/1.x and HTTP/2 status lines. - if ( preg_match( '#^HTTP/(?:\d(?:\.\d+)?)\s+(\d+)#', $header_line, $matches ) ) { - // Upstream can emit multiple header blocks (redirects/intermediate responses). - // Reset captured headers on each new status line so final status/headers win. - $response_headers = []; - $http_status = (int) $matches[1]; - return $len; - } - - // Skip empty lines and continuation markers - if ( $len <= 2 ) { - return $len; - } - - // Parse header name:value - $parts = explode( ':', $header_line, 2 ); - if ( count( $parts ) === 2 ) { - $name = trim( $parts[0] ); - $value = trim( $parts[1] ); - if ( $name !== '' && $value !== '' ) { - $response_headers[ $name ] = $value; - } - } - - return $len; - }; - - // Build cURL headers - $curl_headers = [ - 'Accept-Encoding: identity', // No compression for streaming - ]; - if ( $token !== '' ) { - $curl_headers[] = 'X-NUCLIA-SERVICEACCOUNT: Bearer ' . $token; - } - foreach ( $request_headers as $name => $value ) { - if ( ! is_string( $name ) || ! is_string( $value ) || $value === '' ) { - continue; - } - if ( strtolower( $name ) === 'accept-encoding' ) { - continue; - } - if ( nuclia_proxy_is_upstream_auth_header_blocked( $name ) ) { - continue; - } - $curl_headers[] = $name . ': ' . $value; - } - - // Headers to skip when forwarding to client - $skip_headers = [ - 'connection', - 'keep-alive', - 'proxy-authenticate', - 'proxy-authorization', - 'te', - 'trailer', - 'transfer-encoding', - 'upgrade', - 'content-encoding', - ]; - - /** - * CURLOPT_WRITEFUNCTION callback - receives body data chunks. - */ - $write_callback = function( $ch, $data ) use ( &$response_headers, &$http_status, &$headers_processed, $skip_headers, $remote_url ) { - $chunk_len = strlen( $data ); - - // On first chunk, send headers BEFORE any body output - if ( ! $headers_processed ) { - $headers_processed = true; - - // Status must be fixed before any body bytes are echoed/flushed, or clients - // may keep the default 200 even when upstream returned an error status. - http_response_code( $http_status ); - status_header( $http_status ); - header( 'X-Accel-Buffering: no' ); - // Debug visibility headers: expose upstream metadata without changing payload - // shape consumed by the widget/client JSON contract. - header( 'X-Nuclia-Upstream-Status: ' . (string) $http_status ); - $upstream_content_type = (string) ( $response_headers['Content-Type'] ?? $response_headers['content-type'] ?? '' ); - if ( $upstream_content_type !== '' ) { - header( 'X-Nuclia-Upstream-Content-Type: ' . $upstream_content_type ); - } - header( 'X-Nuclia-Upstream-URL: ' . nuclia_proxy_upstream_url_debug_header( $remote_url ) ); - foreach ( $response_headers as $name => $value ) { - $lower = strtolower( $name ); - if ( in_array( $lower, $skip_headers, true ) ) { - continue; - } - header( $name . ': ' . $value, false ); - } - } - - // Stream chunk directly to browser - echo $data; - - if ( function_exists( 'ob_flush' ) && ob_get_level() > 0 ) { - @ob_flush(); - } - if ( function_exists( 'flush' ) ) { - flush(); - } - - return $chunk_len; - }; - - // Configure cURL - curl_setopt_array( $ch, [ - CURLOPT_RETURNTRANSFER => false, // Don't return response, use callbacks - CURLOPT_HEADER => false, // Don't include headers in output - CURLOPT_CUSTOMREQUEST => $method, - CURLOPT_FOLLOWLOCATION => true, // Follow redirects - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 60, - CURLOPT_WRITEFUNCTION => $write_callback, - CURLOPT_HEADERFUNCTION => $header_callback, - CURLOPT_HTTPHEADER => $curl_headers, - ] ); - if ( $method === 'HEAD' ) { - curl_setopt( $ch, CURLOPT_NOBODY, true ); - } - if ( $request_body !== '' && $method !== 'GET' && $method !== 'HEAD' ) { - curl_setopt( $ch, CURLOPT_POSTFIELDS, $request_body ); - } - - // Execute request - curl_exec( $ch ); - $error = curl_error( $ch ); - curl_close( $ch ); - - // Handle cURL errors before sending any success headers. - if ( $error !== '' ) { - nuclia_proxy_send_cors_headers(); - status_header( 502 ); - header( 'Content-Type: application/json' ); - echo wp_json_encode( [ 'error' => 'cURL error: ' . $error ] ); - exit; - } - - // Some responses can be valid but contain no body (e.g. HEAD or 204). - if ( ! $headers_processed ) { - http_response_code( $http_status ); - status_header( $http_status ); - header( 'X-Accel-Buffering: no' ); - // Debug visibility headers: expose upstream metadata without changing payload - // shape consumed by the widget/client JSON contract. - header( 'X-Nuclia-Upstream-Status: ' . (string) $http_status ); - $upstream_content_type = (string) ( $response_headers['Content-Type'] ?? $response_headers['content-type'] ?? '' ); - if ( $upstream_content_type !== '' ) { - header( 'X-Nuclia-Upstream-Content-Type: ' . $upstream_content_type ); - } - header( 'X-Nuclia-Upstream-URL: ' . nuclia_proxy_upstream_url_debug_header( $remote_url ) ); - foreach ( $response_headers as $name => $value ) { - $lower = strtolower( $name ); - if ( in_array( $lower, $skip_headers, true ) ) { - continue; - } - header( $name . ': ' . $value, false ); - } - } - - // Exit to prevent WordPress from sending additional output - exit; -} - -/** - * Safe value for debug response headers (no CRLF, bounded length). - * - * @param string $value Raw value. - * @return string - */ -function nuclia_proxy_debug_header_value( string $value ): string { - $value = str_replace( [ "\r", "\n" ], '', $value ); - return substr( $value, 0, 2048 ); -} - -/** - * Redact sensitive query values for debug headers (never expose eph-token to the browser). - * - * @param string $url Full upstream URL. - * @return string - */ -function nuclia_proxy_redact_sensitive_query_params_for_debug( string $url ): string { - if ( $url === '' ) { - return ''; - } - return (string) preg_replace( '/(?<=[?&])eph-token=[^&]*/i', 'eph-token=REDACTED', $url ); -} - -/** - * Safe value for X-Nuclia-Upstream-URL (redacted + length bound). - * - * @param string $remote_url Upstream URL actually requested. - * @return string - */ -function nuclia_proxy_upstream_url_debug_header( string $remote_url ): string { - return nuclia_proxy_debug_header_value( nuclia_proxy_redact_sensitive_query_params_for_debug( $remote_url ) ); -} - -/** - * Silence PHP warnings/notices for binary passthrough and drop output buffers so no stray bytes precede the file. - */ -function nuclia_proxy_binary_response_prepare_output(): void { - @error_reporting( 0 ); - @ini_set( 'display_errors', '0' ); - @ini_set( 'display_startup_errors', '0' ); - @ini_set( 'log_errors', '0' ); - while ( ob_get_level() > 0 ) { - @ob_end_clean(); - } -} - -/** - * If $s is whitespace-stripped base64 for a PDF, return decoded bytes; otherwise null. - */ -function nuclia_proxy_try_decode_pdf_base64_string( string $s ): ?string { - $packed = preg_replace( '/\s+/', '', $s ); - if ( $packed === null || strlen( $packed ) < 100 ) { - return null; - } - if ( ! preg_match( '/^[A-Za-z0-9+\/]+=*$/', $packed ) ) { - return null; - } - $raw = base64_decode( $packed, true ); - if ( $raw === false || $raw === '' ) { - return null; - } - if ( strncmp( $raw, '%PDF', 4 ) !== 0 ) { - return null; - } - return $raw; -} - -/** - * Turn JSON-wrapped or base64-wrapped PDF payloads into raw PDF bytes when detectable. - * - * @return array{body: string, content_type: ?string} content_type set when body was transformed. - */ -function nuclia_proxy_maybe_decode_binary_response_body( string $body, string $content_type_header ): array { - $trim = ltrim( $body ); - if ( $trim === '' ) { - return [ 'body' => $body, 'content_type' => null ]; - } - // JSON envelope with a base64 PDF field. - if ( $trim[0] === '{' ) { - $data = json_decode( $body, true ); - if ( is_array( $data ) ) { - foreach ( [ 'data', 'content', 'body', 'file', 'blob', 'payload', 'base64' ] as $key ) { - if ( ! isset( $data[ $key ] ) || ! is_string( $data[ $key ] ) || $data[ $key ] === '' ) { - continue; - } - $candidate = nuclia_proxy_try_decode_pdf_base64_string( $data[ $key ] ); - if ( $candidate !== null ) { - return [ 'body' => $candidate, 'content_type' => 'application/pdf' ]; - } - } - } - } - // Whole body is base64 for a PDF (often mislabeled as json/text/octet-stream). - if ( ! str_starts_with( $trim, '%PDF' ) ) { - $candidate = nuclia_proxy_try_decode_pdf_base64_string( $body ); - if ( $candidate !== null ) { - $ct_lower = strtolower( $content_type_header ); - if ( str_contains( $ct_lower, 'json' ) - || str_contains( $ct_lower, 'text/' ) - || str_contains( $ct_lower, 'octet-stream' ) - || $content_type_header === '' ) { - return [ 'body' => $candidate, 'content_type' => 'application/pdf' ]; - } - } - } - return [ 'body' => $body, 'content_type' => null ]; -} - -/** - * @param array $headers - * @return array{headers: array, body: string} - */ -function nuclia_proxy_prepare_proxy_response_for_output( string $body, array $headers ): array { - $out_headers = $headers; - $ct = (string) ( $out_headers['Content-Type'] ?? $out_headers['content-type'] ?? '' ); - $processed = nuclia_proxy_maybe_decode_binary_response_body( $body, $ct ); - if ( $processed['content_type'] !== null && $processed['content_type'] !== '' ) { - $out_headers['Content-Type'] = $processed['content_type']; - unset( $out_headers['content-type'], $out_headers['Content-Length'], $out_headers['content-length'] ); - return [ 'headers' => $out_headers, 'body' => $processed['body'] ]; - } - return [ 'headers' => $out_headers, 'body' => $processed['body'] ]; -} - -/** - * Buffered GET for binary/download: single raw body pass, optional base64→PDF decode, no incremental PHP warnings. - * - * @param string $remote_url Full upstream URL. - * @param string $token Service token (empty when ephemeral-only). - * @param array $binary_headers Extra request headers (Accept, Range, …). - * @return never - */ -function nuclia_proxy_serve_binary_get_with_curl( string $remote_url, string $token, array $binary_headers ): void { - nuclia_proxy_binary_response_prepare_output(); - nuclia_proxy_send_cors_headers(); - - $max_bytes = 52428800; - if ( defined( 'NUCLIA_PROXY_MAX_BUFFERED_DOWNLOAD_BYTES' ) ) { - $v = constant( 'NUCLIA_PROXY_MAX_BUFFERED_DOWNLOAD_BYTES' ); - if ( is_int( $v ) && $v > 0 ) { - $max_bytes = $v; - } - } - - $ch = curl_init( $remote_url ); - $curl_headers = [ 'Accept-Encoding: identity' ]; - if ( $token !== '' ) { - $curl_headers[] = 'X-NUCLIA-SERVICEACCOUNT: Bearer ' . $token; - } - foreach ( $binary_headers as $name => $value ) { - if ( ! is_string( $name ) || ! is_string( $value ) || $value === '' ) { - continue; - } - if ( strtolower( $name ) === 'accept-encoding' ) { - continue; - } - if ( nuclia_proxy_is_upstream_auth_header_blocked( $name ) ) { - continue; - } - $curl_headers[] = $name . ': ' . $value; - } - - curl_setopt_array( $ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 120, - CURLOPT_HTTPHEADER => $curl_headers, - ] ); - - $raw = curl_exec( $ch ); - $errno = curl_errno( $ch ); - $header_size = (int) curl_getinfo( $ch, CURLINFO_HEADER_SIZE ); - curl_close( $ch ); - - if ( $errno !== 0 || ! is_string( $raw ) ) { - nuclia_proxy_send_cors_headers(); - status_header( 502 ); - header( 'Content-Type: application/json' ); - echo wp_json_encode( [ 'error' => 'Upstream request failed' ] ); - exit; - } - - $header_block = substr( $raw, 0, $header_size ); - $body = substr( $raw, $header_size ); - $http_status = 200; - $response_headers = []; - foreach ( explode( "\r\n", $header_block ) as $line ) { - if ( preg_match( '#^HTTP/(?:\d(?:\.\d+)?)\s+(\d+)#', $line, $m ) ) { - $http_status = (int) $m[1]; - $response_headers = []; - continue; - } - if ( strpos( $line, ':' ) === false ) { - continue; - } - list( $hn, $hv ) = explode( ':', $line, 2 ); - $hn = trim( $hn ); - $hv = trim( $hv ); - if ( $hn !== '' && $hv !== '' ) { - $response_headers[ $hn ] = $hv; - } - } - - if ( strlen( $body ) > $max_bytes ) { - nuclia_proxy_send_cors_headers(); - status_header( 502 ); - header( 'Content-Type: application/json' ); - echo wp_json_encode( [ 'error' => 'Response too large for proxy buffer' ] ); - exit; - } - - $upstream_ct = (string) ( $response_headers['Content-Type'] ?? $response_headers['content-type'] ?? '' ); - $decoded = nuclia_proxy_maybe_decode_binary_response_body( $body, $upstream_ct ); - $out_body = $decoded['body']; - if ( $decoded['content_type'] !== null && $decoded['content_type'] !== '' ) { - $response_headers['Content-Type'] = $decoded['content_type']; - unset( $response_headers['content-type'], $response_headers['Content-Length'], $response_headers['content-length'] ); - } - - nuclia_proxy_send_cors_headers(); - http_response_code( $http_status ); - status_header( $http_status ); - header( 'X-Accel-Buffering: no' ); - header( 'X-Nuclia-Upstream-Status: ' . (string) $http_status ); - if ( $upstream_ct !== '' ) { - header( 'X-Nuclia-Upstream-Content-Type: ' . $upstream_ct ); - } - header( 'X-Nuclia-Upstream-URL: ' . nuclia_proxy_upstream_url_debug_header( $remote_url ) ); - - $skip_headers = [ - 'connection', - 'keep-alive', - 'proxy-authenticate', - 'proxy-authorization', - 'te', - 'trailer', - 'transfer-encoding', - 'upgrade', - 'content-encoding', - ]; - foreach ( $response_headers as $name => $value ) { - $lower = strtolower( $name ); - if ( in_array( $lower, $skip_headers, true ) ) { - continue; - } - header( $name . ': ' . $value, false ); - } - if ( ! isset( $response_headers['Content-Length'] ) && ! isset( $response_headers['content-length'] ) && $out_body !== '' ) { - header( 'Content-Length: ' . (string) strlen( $out_body ) ); - } - echo $out_body; - exit; -} - -/** - * Raw client query string: prefer QUERY_STRING, then REDIRECT_QUERY_STRING, then REQUEST_URI. - * Some stacks omit or truncate QUERY_STRING; REQUEST_URI is more reliable for repeated params. - * - * @return string Query string without leading '?'. - */ -function nuclia_proxy_get_client_query_string(): string { - if ( isset( $_SERVER['QUERY_STRING'] ) && is_string( $_SERVER['QUERY_STRING'] ) && $_SERVER['QUERY_STRING'] !== '' ) { - return (string) $_SERVER['QUERY_STRING']; - } - if ( isset( $_SERVER['REDIRECT_QUERY_STRING'] ) && is_string( $_SERVER['REDIRECT_QUERY_STRING'] ) && $_SERVER['REDIRECT_QUERY_STRING'] !== '' ) { - return (string) $_SERVER['REDIRECT_QUERY_STRING']; - } - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : ''; - $from_uri = is_string( $request_uri ) ? (string) parse_url( $request_uri, PHP_URL_QUERY ) : ''; - return $from_uri !== null && $from_uri !== '' ? $from_uri : ''; -} - -/** - * Fix mistaken use of "?" between query pairs (e.g. show=value?show=extracted → show=value&show=extracted). - * Unencoded "?" before key= inside the query segment is almost always a broken "&". - * - * @param string $qs Query string (no leading "?"). - * @return string - */ -function nuclia_proxy_normalize_query_delimiters( string $qs ): string { - if ( $qs === '' || ! str_contains( $qs, '?' ) ) { - return $qs; - } - // Repeat until stable so chained mistakes are repaired. - $prev = ''; - while ( $prev !== $qs ) { - $prev = $qs; - $qs = preg_replace( '/\?(?=([a-zA-Z0-9_.-]+)=)/', '&', $qs ) ?? $qs; - } - return $qs; -} - -/** - * Strip proxy-internal params from a raw query string, preserving repeated params. - * - * PHP's $_GET collapses repeated keys (e.g. ?show=value&show=extracted → show=extracted). - * This function works on the raw string so repeated params survive intact. - * - * @param string $raw_qs Raw query string from the client request. - * @return string Cleaned query string (no leading '?'). - */ -function nuclia_proxy_clean_query_string( string $raw_qs ): string { - if ( $raw_qs === '' ) { - return ''; - } - $raw_qs = nuclia_proxy_normalize_query_delimiters( $raw_qs ); - $strip_keys = [ 'rest_route', 'nuclia_proxy', 'nuclia_proxy_zone', 'nuclia_proxy_path' ]; - $parts = explode( '&', $raw_qs ); - $kept = []; - foreach ( $parts as $part ) { - if ( $part === '' ) { - continue; - } - $eq = strpos( $part, '=' ); - $key = $eq !== false ? substr( $part, 0, $eq ) : $part; - $key = urldecode( $key ); - if ( ! in_array( $key, $strip_keys, true ) ) { - $kept[] = $part; - } - } - return implode( '&', $kept ); -} - -/** - * Remove duplicate identical "key=value" segments while preserving intentional repeats - * (e.g. show=value&show=extracted) that differ by value. - * - * @param string $qs Query string without leading '?'. - * @return string - */ -function nuclia_proxy_dedupe_identical_query_pairs( string $qs ): string { - if ( $qs === '' || ! str_contains( $qs, '&' ) ) { - return $qs; - } - $parts = explode( '&', $qs ); - $seen = []; - $out = []; - foreach ( $parts as $part ) { - if ( $part === '' ) { - continue; - } - if ( isset( $seen[ $part ] ) ) { - continue; - } - $seen[ $part ] = true; - $out[] = $part; - } - return implode( '&', $out ); -} - -/** - * True when the merged upstream query string includes eph-token (PHP $_GET may omit it). - */ -function nuclia_proxy_query_string_contains_eph_token( string $cleaned_query_string ): bool { - if ( $cleaned_query_string === '' ) { - return false; - } - return (bool) preg_match( '/(^|&)eph-token=/i', $cleaned_query_string ); -} - -/** - * Signed / ephemeral requests: upstream auth is only eph-token in the query (no service headers). - * - * @param string $normalized_path Normalized Nuclia API path (e.g. api/v1/kb/.../download/field). - * @param string $cleaned_query_string Query forwarded upstream (after WP keys stripped). - * @param array $query_params Parsed query (may omit eph-token if only in raw string). - */ -function nuclia_proxy_is_ephemeral_download_request( string $normalized_path, string $cleaned_query_string, array $query_params ): bool { - if ( nuclia_proxy_query_string_contains_eph_token( $cleaned_query_string ) ) { - return true; - } - $eph = $query_params['eph-token'] ?? null; - if ( is_string( $eph ) && $eph !== '' ) { - return true; - } - if ( is_array( $eph ) ) { - foreach ( $eph as $v ) { - if ( is_string( $v ) && $v !== '' ) { - return true; - } - } - } - return false; -} - -/** - * Client/browser headers that must never be forwarded to Nuclia (prevents mixed auth). - */ -function nuclia_proxy_is_upstream_auth_header_blocked( string $header_name ): bool { - return in_array( strtolower( $header_name ), [ 'authorization', 'x-nuclia-serviceaccount', 'proxy-authorization' ], true ); -} - -/** - * @param array $headers - * @return array - */ -function nuclia_proxy_filter_blocked_upstream_request_headers( array $headers ): array { - $out = []; - foreach ( $headers as $name => $value ) { - if ( ! is_string( $name ) || ! is_string( $value ) || $value === '' ) { - continue; - } - if ( nuclia_proxy_is_upstream_auth_header_blocked( $name ) ) { - continue; - } - $out[ $name ] = $value; - } - return $out; -} - -/** - * Execute the Nuclia proxy request. Shared by REST and path-only handlers. - * - * @param string $zone Nuclia zone. - * @param string $path API path (e.g. v1/kb/.../field). - * @param string $method HTTP method. - * @param array $query_params Parsed query parameters (for eph-token check). - * @param string $content_type Request Content-Type header. - * @param string $accept Request Accept header. - * @param string $body Request body. - * @param string $raw_query_string Raw query string preserving repeated params. - * @param array $passthrough_headers Allow-listed headers forwarded upstream. - * @return array{status: int, headers: array, body: string, stream_file: string, error?: array{code: string, message: string}} - */ -function nuclia_proxy_execute( string $zone, string $path, string $method, array $query_params, string $content_type, string $accept, string $body, string $raw_query_string = '', array $passthrough_headers = [] ): array { - $zone = sanitize_title( $zone ); - if ( $zone === '' ) { - return [ 'status' => 400, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => 'nuclia_proxy_invalid_zone', 'message' => 'Invalid zone' ] ]; - } - - $token = sanitize_text_field( (string) get_option( 'nuclia_token', '' ) ); - if ( $token === '' ) { - return [ 'status' => 500, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => 'nuclia_proxy_missing_token', 'message' => 'Nuclia token is not configured.' ] ]; - } - - $method = strtoupper( $method ); - if ( $method === 'OPTIONS' ) { - return [ 'status' => 200, 'headers' => [], 'body' => '', 'stream_file' => '' ]; - } - - $path_sidecar_qs = ''; - $path_for_norm = $path; - if ( str_contains( $path, '?' ) ) { - $path_parts = explode( '?', $path, 2 ); - $path_for_norm = $path_parts[0]; - $path_sidecar_qs = isset( $path_parts[1] ) ? (string) $path_parts[1] : ''; - } - - $normalized_path = nuclia_proxy_normalize_path( $path_for_norm ); - if ( $normalized_path === null ) { - return [ 'status' => 400, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => 'nuclia_proxy_invalid_path', 'message' => 'Invalid path' ] ]; - } - - $remote_url = sprintf( - 'https://%1$s.rag.progress.cloud/%2$s', - rawurlencode( $zone ), - $normalized_path - ); - - // Use raw query string to preserve repeated params (e.g. ?show=value&show=extracted). - // PHP's $_GET/$_REQUEST collapse repeated keys, losing data the Nuclia API needs. - $cleaned_qs = nuclia_proxy_clean_query_string( $raw_query_string ); - if ( $path_sidecar_qs !== '' ) { - $path_q_clean = nuclia_proxy_clean_query_string( $path_sidecar_qs ); - if ( $path_q_clean !== '' ) { - $cleaned_qs = $cleaned_qs === '' ? $path_q_clean : ( $path_q_clean . '&' . $cleaned_qs ); - } - } - // Path + outer query can both carry eph-token (e.g. rest_route embed + QUERY_STRING); drop identical pairs only. - $cleaned_qs = nuclia_proxy_dedupe_identical_query_pairs( $cleaned_qs ); - if ( $cleaned_qs !== '' ) { - $remote_url .= '?' . $cleaned_qs; - } - - $filtered_passthrough = nuclia_proxy_filter_blocked_upstream_request_headers( $passthrough_headers ); - $ephemeral_upstream = nuclia_proxy_is_ephemeral_download_request( $normalized_path, $cleaned_qs, $query_params ); - $token_for_upstream = $ephemeral_upstream ? '' : $token; - - $binary_upstream_headers = []; - if ( $accept !== '' ) { - $binary_upstream_headers['Accept'] = $accept; - } - if ( isset( $filtered_passthrough['range'] ) && is_string( $filtered_passthrough['range'] ) && $filtered_passthrough['range'] !== '' ) { - $binary_upstream_headers['Range'] = $filtered_passthrough['range']; - } - if ( isset( $filtered_passthrough['if-range'] ) && is_string( $filtered_passthrough['if-range'] ) && $filtered_passthrough['if-range'] !== '' ) { - $binary_upstream_headers['If-Range'] = $filtered_passthrough['if-range']; - } - - // Binary / large GET: cURL streaming avoids wp_remote_request compression + buffering issues. - $should_stream_binary_get = ( $method === 'GET' || $method === 'HEAD' ) && ( - str_contains( $normalized_path, '/download/' ) - || (bool) preg_match( '#/resource/[^/]+/file/#', $normalized_path ) - ); - $is_ask_endpoint = (bool) preg_match( '#/ask/?$#', $normalized_path ); - - // For downloads and resource file payloads, stream via cURL (binary-safe, correct headers). - // Note: stream_with_curl calls exit() and never returns for GET. - if ( $should_stream_binary_get ) { - if ( $method === 'HEAD' ) { - nuclia_proxy_binary_response_prepare_output(); - $curl_headers = [ 'Accept-Encoding: identity' ]; - if ( $accept !== '' ) { - $curl_headers[] = 'Accept: ' . $accept; - } - if ( isset( $filtered_passthrough['range'] ) && is_string( $filtered_passthrough['range'] ) && $filtered_passthrough['range'] !== '' ) { - $curl_headers[] = 'Range: ' . $filtered_passthrough['range']; - } - if ( isset( $filtered_passthrough['if-range'] ) && is_string( $filtered_passthrough['if-range'] ) && $filtered_passthrough['if-range'] !== '' ) { - $curl_headers[] = 'If-Range: ' . $filtered_passthrough['if-range']; - } - if ( $token_for_upstream !== '' ) { - $curl_headers[] = 'X-NUCLIA-SERVICEACCOUNT: Bearer ' . $token_for_upstream; - } - $ch = curl_init( $remote_url ); - curl_setopt_array( $ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => true, - CURLOPT_NOBODY => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 10, - CURLOPT_TIMEOUT => 20, - CURLOPT_HTTPHEADER => $curl_headers, - ] ); - $response = curl_exec( $ch ); - $http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE ); - $header_size = (int) curl_getinfo( $ch, CURLINFO_HEADER_SIZE ); - curl_close( $ch ); - - $headers = []; - if ( is_string( $response ) && $response !== '' ) { - $header_text = substr( $response, 0, $header_size ); - $header_lines = explode( "\r\n", $header_text ); - foreach ( $header_lines as $line ) { - if ( strpos( $line, ':' ) !== false ) { - list( $name, $value ) = explode( ':', $line, 2 ); - $headers[ trim( $name ) ] = trim( $value ); - } - } - } - - status_header( $http_code ); - header( 'X-Nuclia-Upstream-Status: ' . (string) $http_code ); - $upstream_ct = (string) ( $headers['Content-Type'] ?? $headers['content-type'] ?? '' ); - if ( $upstream_ct !== '' ) { - header( 'X-Nuclia-Upstream-Content-Type: ' . $upstream_ct ); - } - header( 'X-Nuclia-Upstream-URL: ' . nuclia_proxy_upstream_url_debug_header( $remote_url ) ); - $skip_headers = [ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-encoding' ]; - foreach ( $headers as $name => $value ) { - $lower = strtolower( $name ); - if ( in_array( $lower, $skip_headers, true ) ) { - continue; - } - header( $name . ': ' . $value ); - } - exit; - } - - nuclia_proxy_serve_binary_get_with_curl( $remote_url, $token_for_upstream, $binary_upstream_headers ); - // Never reaches here due to exit() in stream function - } - - // /ask may return regular JSON or streamed/chunked output. Buffering through - // wp_remote_request() can corrupt or truncate proxy responses, so always use - // the cURL passthrough streaming path for /ask endpoints. - if ( $is_ask_endpoint ) { - $stream_headers = [ - 'Content-Type' => $content_type !== '' ? $content_type : 'application/json', - 'Accept' => ( $accept !== '' && $accept !== '*/*' ) ? $accept : 'application/x-ndjson', - ]; - foreach ( $filtered_passthrough as $name => $value ) { - if ( is_string( $name ) && is_string( $value ) && $value !== '' ) { - $stream_headers[ $name ] = $value; - } - } - nuclia_proxy_stream_with_curl( $remote_url, $method, $token_for_upstream, $stream_headers, $body ); - // Never reaches here due to exit() in stream function - } - - // For non-stream endpoints, use WordPress HTTP API. - $headers = array_filter( - [ - 'X-NUCLIA-SERVICEACCOUNT' => $token_for_upstream !== '' ? 'Bearer ' . $token_for_upstream : null, - 'Content-Type' => $content_type !== '' ? $content_type : null, - 'Accept' => $accept !== '' ? $accept : null, - // Identity avoids br/gzip + header stripping mismatches that corrupt binary bodies. - 'Accept-Encoding' => 'identity', - ] - ); - foreach ( $filtered_passthrough as $name => $value ) { - if ( is_string( $name ) && is_string( $value ) && $value !== '' ) { - $headers[ $name ] = $value; - } - } - - $request_args = [ - 'method' => $method, - 'timeout' => 20, - 'headers' => $headers, - 'redirection' => 5, - ]; - if ( $body !== '' && $method !== 'GET' ) { - $request_args['body'] = $body; - } - - $response = wp_remote_request( $remote_url, $request_args ); - - if ( is_wp_error( $response ) ) { - return [ 'status' => 502, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => $response->get_error_code(), 'message' => $response->get_error_message() ] ]; - } - - $status = wp_remote_retrieve_response_code( $response ); - $payload = wp_remote_retrieve_body( $response ); - $response_headers = wp_remote_retrieve_headers( $response ); - - $out_headers = []; - $skip_headers = [ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-encoding' ]; - foreach ( $response_headers as $name => $value ) { - $lower = strtolower( (string) $name ); - if ( in_array( $lower, $skip_headers, true ) || $value === '' || $value === null ) { - continue; - } - if ( is_array( $value ) ) { - $value = implode( ', ', array_filter( $value, 'is_scalar' ) ); - } - $out_headers[ (string) $name ] = (string) $value; - } - - $out_headers['X-Nuclia-Upstream-Status'] = (string) (int) $status; - $upstream_ct_dbg = $out_headers['Content-Type'] ?? $out_headers['content-type'] ?? ''; - if ( is_string( $upstream_ct_dbg ) && $upstream_ct_dbg !== '' ) { - $out_headers['X-Nuclia-Upstream-Content-Type'] = $upstream_ct_dbg; - } - $out_headers['X-Nuclia-Upstream-URL'] = nuclia_proxy_upstream_url_debug_header( $remote_url ); - - return [ - 'status' => $status, - 'headers' => $out_headers, - 'body' => $payload, - 'stream_file' => '', - ]; -} - -/** - * Proxy Nuclia API requests through WordPress, injecting the service account key. - * - * @param WP_REST_Request $request - * - * @return WP_REST_Response|WP_Error - */ -function nuclia_proxy_rest_handler( WP_REST_Request $request ) { - nuclia_proxy_send_cors_headers(); - if ( strtoupper( (string) $request->get_method() ) === 'OPTIONS' ) { - nuclia_proxy_send_rest_preflight_cors_headers(); - $preflight = new WP_REST_Response( null, 200 ); - $preflight->header( 'Access-Control-Allow-Origin', isset( $_SERVER['HTTP_ORIGIN'] ) ? sanitize_text_field( (string) $_SERVER['HTTP_ORIGIN'] ) : '*' ); - $preflight->header( 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS' ); - $preflight->header( 'Access-Control-Allow-Headers', nuclia_proxy_cors_allow_headers_value() ); - $preflight->header( 'Access-Control-Expose-Headers', 'X-Nuclia-Upstream-Status, X-Nuclia-Upstream-Content-Type, X-Nuclia-Upstream-URL, Nuclia-Learning-Id, X-NUCLIA-TRACE-ID' ); - $preflight->header( 'Vary', 'Origin' ); - $preflight->header( 'Access-Control-Max-Age', '86400' ); - return $preflight; - } - - $zone = sanitize_title( (string) $request['zone'] ); - $path = (string) $request['path']; - - $query_params = $request->get_query_params(); - unset( $query_params['rest_route'] ); - $raw_query_string = nuclia_proxy_get_client_query_string(); - - $result = nuclia_proxy_execute( - $zone, - $path, - $request->get_method(), - $query_params, - $request->get_header( 'content-type' ) ?? '', - $request->get_header( 'accept' ) ?? '', - $request->get_body(), - $raw_query_string, - nuclia_proxy_extract_passthrough_headers_from_request( $request ) - ); - - if ( isset( $result['error'] ) ) { - return new WP_Error( $result['error']['code'], $result['error']['message'], [ 'status' => $result['status'] ] ); - } - - $content_type = (string) ( $result['headers']['Content-Type'] ?? $result['headers']['content-type'] ?? '' ); - $bin = nuclia_proxy_maybe_decode_binary_response_body( $result['body'], $content_type ); - if ( $bin['content_type'] !== null && $bin['content_type'] !== '' ) { - $result['body'] = $bin['body']; - $result['headers']['Content-Type'] = $bin['content_type']; - unset( $result['headers']['content-type'], $result['headers']['Content-Length'], $result['headers']['content-length'] ); - } - - $rest_response = new WP_REST_Response( null, $result['status'] ); - foreach ( $result['headers'] as $name => $value ) { - $rest_response->header( $name, $value ); - } - - $content_type = (string) ( $result['headers']['Content-Type'] ?? $result['headers']['content-type'] ?? '' ); - $is_json = $bin['content_type'] === null && stripos( $content_type, 'application/json' ) !== false; - if ( $is_json && $result['body'] !== '' ) { - $decoded = json_decode( $result['body'], true ); - if ( json_last_error() === JSON_ERROR_NONE ) { - $rest_response->set_data( $decoded ); - return $rest_response; - } - } - - // Non-JSON or streaming: serve via filter so body is output when REST server serves the response. - $proxy_result = $result; - add_filter( - 'rest_pre_serve_request', - static function ( $served, $response, $req, $server ) use ( $rest_response, $proxy_result ) { - if ( $response !== $rest_response ) { - return $served; - } - $out = nuclia_proxy_prepare_proxy_response_for_output( $proxy_result['body'], $proxy_result['headers'] ); - nuclia_proxy_binary_response_prepare_output(); - nuclia_proxy_send_cors_headers(); - if ( ! headers_sent() ) { - status_header( $proxy_result['status'] ); - foreach ( $out['headers'] as $name => $value ) { - header( (string) $name . ': ' . (string) $value ); - } - if ( $proxy_result['stream_file'] !== '' && file_exists( $proxy_result['stream_file'] ) ) { - if ( ! isset( $out['headers']['Content-Length'] ) && ! isset( $out['headers']['content-length'] ) ) { - $size = filesize( $proxy_result['stream_file'] ); - if ( $size !== false ) { - header( 'Content-Length: ' . (string) $size ); - } - } - } - } - if ( $proxy_result['stream_file'] !== '' && file_exists( $proxy_result['stream_file'] ) ) { - readfile( $proxy_result['stream_file'] ); - @unlink( $proxy_result['stream_file'] ); - } else { - echo $out['body']; - } - return true; - }, - 10, - 4 - ); - - return $rest_response; -} - -/** - * Normalize and validate the requested API path. - * - * @param string $path - * - * @return string|null - */ -function nuclia_proxy_normalize_path( string $path ): ?string { - $path = ltrim( $path, '/' ); - - if ( $path === '' ) { - return 'api'; - } - - if ( str_contains( $path, '..' ) ) { - return null; - } - - if ( str_starts_with( $path, 'api/' ) ) { - return $path; - } - - if ( str_starts_with( $path, 'v1/' ) ) { - return 'api/' . $path; - } - - return 'api/' . $path; -} diff --git a/includes/nuclia-proxy-rest.php.bak b/includes/nuclia-proxy-rest.php.bak deleted file mode 100644 index d24f7f2..0000000 --- a/includes/nuclia-proxy-rest.php.bak +++ /dev/null @@ -1,360 +0,0 @@ - $vars - * @return array - */ -function nuclia_proxy_query_vars( array $vars ): array { - $vars[] = 'nuclia_proxy'; - $vars[] = 'nuclia_proxy_zone'; - $vars[] = 'nuclia_proxy_path'; - return $vars; -} - -/** - * Handle requests to /nuclia-proxy/{zone}/{path} and run the proxy. - */ -function nuclia_proxy_path_handler(): void { - $zone = ''; - $path = ''; - - if ( get_query_var( 'nuclia_proxy', '' ) === '1' ) { - $zone = get_query_var( 'nuclia_proxy_zone', '' ); - $path = get_query_var( 'nuclia_proxy_path', '' ); - } else { - // Plain permalinks or no rewrite: parse REQUEST_URI (e.g. /nuclia-proxy/zone/v1/kb/.../field). - $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) $_SERVER['REQUEST_URI'] : ''; - $path_part = parse_url( $request_uri, PHP_URL_PATH ); - if ( is_string( $path_part ) && str_starts_with( $path_part, '/nuclia-proxy/' ) ) { - $suffix = substr( $path_part, strlen( '/nuclia-proxy/' ) ); - $parts = explode( '/', $suffix, 2 ); - $zone = $parts[0] ?? ''; - $path = isset( $parts[1] ) ? $parts[1] : ''; - } - } - - if ( $zone === '' ) { - return; - } - - $zone = sanitize_title( $zone ); - $path = is_string( $path ) ? ltrim( $path, '/' ) : ''; - - $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( (string) $_SERVER['REQUEST_METHOD'] ) : 'GET'; - $query_params = isset( $_GET ) && is_array( $_GET ) ? $_GET : []; - $body = ( $method !== 'GET' && $method !== 'HEAD' ) ? file_get_contents( 'php://input' ) : ''; - $content_type = isset( $_SERVER['CONTENT_TYPE'] ) ? sanitize_text_field( (string) $_SERVER['CONTENT_TYPE'] ) : ''; - $accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( (string) $_SERVER['HTTP_ACCEPT'] ) : ''; - - $result = nuclia_proxy_execute( $zone, $path, $method, $query_params, $content_type, $accept, $body ); - - if ( isset( $result['error'] ) ) { - status_header( $result['status'] ); - echo wp_json_encode( [ 'code' => $result['error']['code'], 'message' => $result['error']['message'] ] ); - exit; - } - - status_header( $result['status'] ); - foreach ( $result['headers'] as $name => $value ) { - header( (string) $name . ': ' . (string) $value ); - } - if ( $result['stream_file'] !== '' && file_exists( $result['stream_file'] ) ) { - readfile( $result['stream_file'] ); - @unlink( $result['stream_file'] ); - } else { - echo $result['body']; - } - exit; -} - -function nuclia_register_proxy_rest_routes(): void { - register_rest_route( - 'progress-agentic-rag/v1', - '/nuclia/(?P[a-z0-9-]+)(?:/(?P.*))?', - [ - [ - 'methods' => [ 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS' ], - 'callback' => 'nuclia_proxy_rest_handler', - 'permission_callback' => '__return_true', - 'args' => [ - 'zone' => [ - 'validate_callback' => static function ( $value ): bool { - return is_string( $value ) && $value !== ''; - }, - ], - 'path' => [ - 'default' => '', - ], - ], - ], - ] - ); -} - -/** - * Execute the Nuclia proxy request. Shared by REST and path-only handlers. - * - * @param string $zone Nuclia zone. - * @param string $path API path (e.g. v1/kb/.../field). - * @param string $method HTTP method. - * @param array $query_params Query parameters (e.g. eph-token). - * @param string $content_type Request Content-Type header. - * @param string $accept Request Accept header. - * @param string $body Request body. - * @return array{status: int, headers: array, body: string, stream_file: string, error?: array{code: string, message: string}} - */ -function nuclia_proxy_execute( string $zone, string $path, string $method, array $query_params, string $content_type, string $accept, string $body ): array { - $zone = sanitize_title( $zone ); - if ( $zone === '' ) { - return [ 'status' => 400, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => 'nuclia_proxy_invalid_zone', 'message' => 'Invalid zone' ] ]; - } - - $token = sanitize_text_field( (string) get_option( 'nuclia_token', '' ) ); - if ( $token === '' ) { - return [ 'status' => 500, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => 'nuclia_proxy_missing_token', 'message' => 'Nuclia token is not configured.' ] ]; - } - - $method = strtoupper( $method ); - if ( $method === 'OPTIONS' ) { - return [ 'status' => 200, 'headers' => [], 'body' => '', 'stream_file' => '' ]; - } - - $normalized_path = nuclia_proxy_normalize_path( $path ); - if ( $normalized_path === null ) { - return [ 'status' => 400, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => 'nuclia_proxy_invalid_path', 'message' => 'Invalid path' ] ]; - } - - $remote_url = sprintf( - 'https://%1$s.rag.progress.cloud/%2$s', - rawurlencode( $zone ), - $normalized_path - ); - - if ( ! empty( $query_params ) ) { - unset( $query_params['rest_route'], $query_params['nuclia_proxy'], $query_params['nuclia_proxy_zone'], $query_params['nuclia_proxy_path'] ); - $remote_url = add_query_arg( $query_params, $remote_url ); - } - - $should_stream = str_contains( $normalized_path, '/download/' ); - $temp_file = ''; - if ( $should_stream ) { - if ( function_exists( 'wp_tempnam' ) ) { - $temp_file = wp_tempnam( 'nuclia-proxy' ); - } else { - $temp_file = tempnam( sys_get_temp_dir(), 'nuclia-proxy-' ); - } - if ( ! $temp_file ) { - $should_stream = false; - } - } - - $headers = array_filter( - [ - 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, - 'Content-Type' => $content_type !== '' ? $content_type : null, - 'Accept' => $accept !== '' ? $accept : null, - // Don't request compression for streaming downloads - WordPress doesn't decompress when streaming to file - // and stripping Content-Encoding header while forwarding gzipped content corrupts PDFs - 'Accept-Encoding' => $should_stream ? 'identity' : 'gzip, deflate, br, zstd' - ] - ); - - $request_args = [ - 'method' => $method, - 'timeout' => $should_stream ? 60 : 20, - 'headers' => $headers, - 'redirection' => $should_stream ? 5 : 0, - ]; - if ( $body !== '' && $method !== 'GET' ) { - $request_args['body'] = $body; - } - if ( $should_stream && $temp_file ) { - $request_args['stream'] = true; - $request_args['filename'] = $temp_file; - } - - // If eph-token is present in query params, remove the Bearer auth token - if ( isset( $query_params['eph-token'] ) ) { - unset( $request_args['headers']['X-NUCLIA-SERVICEACCOUNT'] ); - } - - $response = wp_remote_request( $remote_url, $request_args ); - - if ( is_wp_error( $response ) ) { - return [ 'status' => 502, 'headers' => [], 'body' => '', 'stream_file' => '', 'error' => [ 'code' => $response->get_error_code(), 'message' => $response->get_error_message() ] ]; - } - - $status = wp_remote_retrieve_response_code( $response ); - $payload = $should_stream ? '' : wp_remote_retrieve_body( $response ); - $response_headers = wp_remote_retrieve_headers( $response ); - - $out_headers = []; - $skip_headers = [ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-encoding' ]; - foreach ( $response_headers as $name => $value ) { - $lower = strtolower( (string) $name ); - if ( in_array( $lower, $skip_headers, true ) || $value === '' || $value === null ) { - continue; - } - if ( is_array( $value ) ) { - $value = implode( ', ', array_filter( $value, 'is_scalar' ) ); - } - $out_headers[ (string) $name ] = (string) $value; - } - - return [ - 'status' => $status, - 'headers' => $out_headers, - 'body' => $payload, - 'stream_file' => $should_stream && $temp_file ? $temp_file : '', - ]; -} - -/** - * Proxy Nuclia API requests through WordPress, injecting the service account key. - * - * @param WP_REST_Request $request - * - * @return WP_REST_Response|WP_Error - */ -function nuclia_proxy_rest_handler( WP_REST_Request $request ) { - $zone = sanitize_title( (string) $request['zone'] ); - $path = (string) $request['path']; - - $query_params = $request->get_query_params(); - unset( $query_params['rest_route'] ); - - $result = nuclia_proxy_execute( - $zone, - $path, - $request->get_method(), - $query_params, - $request->get_header( 'content-type' ) ?? '', - $request->get_header( 'accept' ) ?? '', - $request->get_body() - ); - - if ( isset( $result['error'] ) ) { - return new WP_Error( $result['error']['code'], $result['error']['message'], [ 'status' => $result['status'] ] ); - } - - $rest_response = new WP_REST_Response( null, $result['status'] ); - foreach ( $result['headers'] as $name => $value ) { - $rest_response->header( $name, $value ); - } - - $content_type = $result['headers']['Content-Type'] ?? $result['headers']['content-type'] ?? ''; - $is_json = is_string( $content_type ) && stripos( $content_type, 'application/json' ) !== false; - if ( $is_json && $result['body'] !== '' ) { - $decoded = json_decode( $result['body'], true ); - if ( json_last_error() === JSON_ERROR_NONE ) { - $rest_response->set_data( $decoded ); - return $rest_response; - } - } - - // Non-JSON or streaming: serve via filter so body is output when REST server serves the response. - $proxy_result = $result; - add_filter( - 'rest_pre_serve_request', - static function ( $served, $response, $req, $server ) use ( $rest_response, $proxy_result ) { - if ( $response !== $rest_response ) { - return $served; - } - if ( ! headers_sent() ) { - status_header( $proxy_result['status'] ); - foreach ( $proxy_result['headers'] as $name => $value ) { - header( (string) $name . ': ' . (string) $value ); - } - if ( $proxy_result['stream_file'] !== '' && file_exists( $proxy_result['stream_file'] ) ) { - if ( ! isset( $proxy_result['headers']['Content-Length'] ) && ! isset( $proxy_result['headers']['content-length'] ) ) { - $size = filesize( $proxy_result['stream_file'] ); - if ( $size !== false ) { - header( 'Content-Length: ' . (string) $size ); - } - } - } - } - if ( $proxy_result['stream_file'] !== '' && file_exists( $proxy_result['stream_file'] ) ) { - readfile( $proxy_result['stream_file'] ); - @unlink( $proxy_result['stream_file'] ); - } else { - echo $proxy_result['body']; - } - return true; - }, - 10, - 4 - ); - - return $rest_response; -} - -/** - * Normalize and validate the requested API path. - * - * @param string $path - * - * @return string|null - */ -function nuclia_proxy_normalize_path( string $path ): ?string { - $path = ltrim( $path, '/' ); - - if ( $path === '' ) { - return 'api'; - } - - if ( str_contains( $path, '..' ) ) { - return null; - } - - if ( str_starts_with( $path, 'api/' ) ) { - return $path; - } - - if ( str_starts_with( $path, 'v1/' ) ) { - return 'api/' . $path; - } - - return 'api/' . $path; -} diff --git a/nuclia.php b/nuclia.php deleted file mode 100644 index a549da0..0000000 --- a/nuclia.php +++ /dev/null @@ -1,220 +0,0 @@ -

' . esc_html( $notice ) . '

'; - } -} - - -/** - * I18n. - * - * @since 1.0.0 - */ -function nuclia_load_textdomain(): void { - load_plugin_textdomain( - 'progress-agentic-rag', - false, - plugin_basename( dirname( __FILE__ ) ) . '/languages/' - ); -} - -add_action( 'init', 'nuclia_load_textdomain' ); - -/** - * Debug log function - * - * @since 1.0.0 - * - * @var string $notice The notice to log - */ -function nuclia_log( string $notice ): void { - if ( true === WP_DEBUG ) { - error_log("[PROGRESS AGENTIC RAG]: $notice\n" ); - }; -} - -function nuclia_error_log( string $notice ): void { - error_log("[PROGRESS AGENTIC RAG]: $notice\n" ); -} - -// load plugin if requirements are met or display admin notice -if ( nuclia_php_version_check() && nuclia_wp_version_check() ) { - // Load Action Scheduler library for background processing - require_once PROGRESS_NUCLIA_PATH . 'includes/libraries/action-scheduler/action-scheduler.php'; - - require_once PROGRESS_NUCLIA_PATH . 'includes/nuclia-proxy-rest.php'; - require_once PROGRESS_NUCLIA_PATH . 'includes/class-nuclia-api.php'; - require_once PROGRESS_NUCLIA_PATH . 'includes/class-nuclia-settings.php'; - require_once PROGRESS_NUCLIA_PATH . 'includes/class-nuclia-background-processor.php'; - require_once PROGRESS_NUCLIA_PATH . 'includes/class-nuclia-label-reprocessor.php'; - require_once PROGRESS_NUCLIA_PATH . 'includes/class-nuclia-plugin.php'; - if ( is_admin() ) { - require_once PROGRESS_NUCLIA_PATH . 'includes/admin/class-nuclia-admin-page-settings.php'; - } - $nuclia = Nuclia_Plugin_Factory::create(); -} else { - add_action( 'admin_notices', 'nuclia_requirements_error_notice' ); -} - -/** - * Class Nuclia_Plugin_Factory - * - * Responsible for creating a shared instance of the main Nuclia_Plugin object. - * - * @since 1.0.0 - */ -class Nuclia_Plugin_Factory { - - /** - * Create and return a shared instance of the Nuclia_Plugin. - * - * @since 1.0.0 - * - * @return Nuclia_Plugin The shared plugin instance. - */ - public static function create(): Nuclia_Plugin { - - /** - * The static instance to share, else null. - * - * @since 1.0.0 - * - * @var null|Nuclia_Plugin $plugin - */ - static $plugin = null; - - if ( null !== $plugin ) { - return $plugin; - } - - $plugin = new Nuclia_Plugin(); - - return $plugin; - } -} - -function agentic_rag_for_wp_install() { - global $wpdb; - require_once ABSPATH . 'wp-admin/includes/upgrade.php'; - $charset_collate = $wpdb->get_charset_collate(); - $table_name = $wpdb->prefix . 'agentic_rag_for_wp'; - $sql = "CREATE TABLE $table_name ( - id mediumint(9) NOT NULL AUTO_INCREMENT, - post_id mediumint(9) NOT NULL, - nuclia_rid varchar(255) NOT NULL, - nuclia_seqid varchar(255), - PRIMARY KEY (id), - INDEX idx_post_id (post_id), - INDEX idx_nuclia_rid (nuclia_rid) - ) $charset_collate;"; - dbDelta( $sql ); - - // Register path-only proxy rewrite rule and flush so /nuclia-proxy/{zone} works. - if ( function_exists( 'nuclia_proxy_add_rewrite_rules' ) ) { - nuclia_proxy_add_rewrite_rules(); - flush_rewrite_rules(); - } -} - -register_activation_hook( __FILE__, 'agentic_rag_for_wp_install' ); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..38bcab6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,111 @@ +{ + "name": "progress-agentic-rag", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "progress-agentic-rag", + "version": "0.1.0", + "devDependencies": { + "@playwright/test": "1.61.0", + "@types/node": "^24.13.2", + "typescript": "^5.5.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3f978d9 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "progress-agentic-rag", + "version": "0.1.0", + "private": true, + "scripts": { + "test:e2e": "playwright test -c playwright.config.ts" + }, + "devDependencies": { + "@playwright/test": "1.61.0", + "@types/node": "^24.13.2", + "typescript": "^5.5.0" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8d17d9e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,13 @@ + + + + + tests/phpunit + + + + + src + + + diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..de358f0 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/playwright', + timeout: 45000, + expect: { + timeout: 10000 + }, + retries: process.env.CI ? 2 : 0, + workers: 1, + use: { + baseURL: process.env.WP_BASE_URL || 'http://wordpress', + screenshot: 'only-on-failure', + trace: 'on-first-retry', + video: 'retain-on-failure' + }, + projects: [ + { + name: 'chromium-desktop', + use: { + ...devices['Desktop Chrome'] + } + }, + { + name: 'chromium-mobile', + use: { + ...devices['Pixel 5'] + } + } + ] +}); diff --git a/progress-agentic-rag.php b/progress-agentic-rag.php new file mode 100644 index 0000000..1297434 --- /dev/null +++ b/progress-agentic-rag.php @@ -0,0 +1,61 @@ +register(); + } +); diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..aece5d3 --- /dev/null +++ b/readme.txt @@ -0,0 +1,48 @@ +=== Progress Agentic RAG === +Contributors: progresssoftware +Tags: search, ai, knowledge-base, rag, indexing +Requires at least: 6.8 +Tested up to: 7.0 +Stable tag: 0.1.0 +Requires PHP: 8.1 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Index WordPress content into Progress Agentic RAG and power knowledge-base search. + +== Description == + +Progress Agentic RAG connects WordPress content to the Progress Agentic RAG service so site owners can build searchable knowledge bases from selected public content. + +This rebuild is being developed with WordPress.org compatibility, server-side credential handling, background indexing, and automated test coverage as release requirements. + +When the service connection is configured and validated, the plugin embeds the Progress Agentic RAG search widget on the WordPress front page. Widget requests are proxied through WordPress so the Service Access token stays on the server. + +== External Service == + +This plugin connects to the Progress Agentic RAG service. When indexing is enabled, selected public WordPress content and configured taxonomy labels may be sent to Progress Agentic RAG. When the front-page search widget is rendered, visitor search queries and widget API requests are sent to Progress Agentic RAG through the WordPress proxy. The plugin stores service configuration in WordPress options and must keep service access tokens on the server. + +Service details: + +* Service provider: Progress Software +* Service website: https://www.progress.com/ +* Widget script: https://cdn.rag.progress.cloud/nuclia-widget.umd.js +* Terms and privacy: https://www.progress.com/legal + +== Installation == + +1. Upload the plugin files to the `/wp-content/plugins/progress-agentic-rag` directory, or install the plugin through WordPress. +2. Activate the plugin through the Plugins screen. +3. Configure Progress Agentic RAG from the Progress Agentic RAG admin menu. + +== Frequently Asked Questions == + += Does the plugin expose the service token to visitors? = + +No. The production implementation must keep service credentials server-side and use server-side proxy requests where public access is needed. + +== Changelog == + += 0.1.0 = + +* Initial rebuild skeleton. diff --git a/src/Admin/AdminPage.php b/src/Admin/AdminPage.php new file mode 100644 index 0000000..5ca2df1 --- /dev/null +++ b/src/Admin/AdminPage.php @@ -0,0 +1,645 @@ +page_hook = (string) add_menu_page( + __( 'Progress Agentic RAG', 'progress-agentic-rag' ), + __( 'Progress Agentic RAG', 'progress-agentic-rag' ), + 'manage_options', + self::MENU_SLUG, + [ $this, 'render' ], + PROGRESS_AGENTIC_RAG_URL . self::MENU_ICON + ); + } + + public function enqueue_assets( string $hook_suffix ): void { + if ( $hook_suffix !== $this->page_hook ) { + return; + } + + wp_enqueue_style( + 'progress-agentic-rag-admin', + PROGRESS_AGENTIC_RAG_URL . 'assets/css/admin.css', + [], + PROGRESS_AGENTIC_RAG_VERSION + ); + + wp_enqueue_script( + 'progress-agentic-rag-admin', + PROGRESS_AGENTIC_RAG_URL . 'assets/js/admin.js', + [], + PROGRESS_AGENTIC_RAG_VERSION, + true + ); + + wp_localize_script( + 'progress-agentic-rag-admin', + 'progressAgenticRagAdmin', + [ + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'progress_agentic_rag_manual_sync' ), + 'initialSyncStatus' => $this->manual_sync->status(), + 'initialDeleteStatus' => $this->manual_sync->delete_status(), + 'initialBackgroundSyncStatus' => $this->manual_sync->ensure_automatic_sync(), + 'mapping' => $this->mapping_data(), + 'strings' => [ + 'closeDeleteProgress' => __( 'Close delete progress', 'progress-agentic-rag' ), + 'closeSyncProgress' => __( 'Close sync progress', 'progress-agentic-rag' ), + /* translators: %d: synced resource count. */ + 'confirmDelete' => __( 'Delete %d synced resource(s) from Progress Agentic RAG and clear their local sync mappings? This cannot be undone.', 'progress-agentic-rag' ), + /* translators: %d: synced resource count. */ + 'confirmReprocess' => __( 'Update labels for %d synced resource(s) with the current taxonomy mapping?', 'progress-agentic-rag' ), + 'deleteFailed' => __( 'Synced resources could not be deleted.', 'progress-agentic-rag' ), + 'deleteModalLabel' => __( 'Synced resource delete', 'progress-agentic-rag' ), + 'deleteModalTitle' => __( 'Deleting synced resources', 'progress-agentic-rag' ), + 'deleteTotalLabel' => __( 'Total resources', 'progress-agentic-rag' ), + 'deleteDoneLabel' => __( 'Deleted', 'progress-agentic-rag' ), + 'deleteCurrentLabel' => __( 'Current resource', 'progress-agentic-rag' ), + 'deleteRunning' => __( 'Delete in progress', 'progress-agentic-rag' ), + 'deleting' => __( 'Preparing synced resource deletion.', 'progress-agentic-rag' ), + 'failed' => __( 'Manual sync could not start.', 'progress-agentic-rag' ), + 'running' => __( 'Sync in progress', 'progress-agentic-rag' ), + 'syncModalLabel' => __( 'Manual sync', 'progress-agentic-rag' ), + 'syncModalTitle' => __( 'Syncing selected content', 'progress-agentic-rag' ), + 'syncTotalLabel' => __( 'Total entities', 'progress-agentic-rag' ), + 'syncDoneLabel' => __( 'Synced', 'progress-agentic-rag' ), + 'syncCurrentLabel' => __( 'Current entity', 'progress-agentic-rag' ), + 'sync' => __( 'Sync manually', 'progress-agentic-rag' ), + 'reprocessFailed' => __( 'Label reprocessing could not start.', 'progress-agentic-rag' ), + 'starting' => __( 'Preparing manual sync.', 'progress-agentic-rag' ), + 'complete' => __( 'Manual sync complete.', 'progress-agentic-rag' ), + 'mappingSelectLabelset' => __( 'Select a labelset', 'progress-agentic-rag' ), + 'mappingRemove' => __( 'Remove', 'progress-agentic-rag' ), + 'mappingLabelset' => __( 'Labelset', 'progress-agentic-rag' ), + 'mappingTerm' => __( 'Term', 'progress-agentic-rag' ), + 'mappingLabels' => __( 'Progress Agentic RAG labels', 'progress-agentic-rag' ), + 'mappingFallback' => __( 'Fallback labels (when no terms assigned)', 'progress-agentic-rag' ), + 'mappingSelectLabels' => __( 'Select a labelset to load labels.', 'progress-agentic-rag' ), + 'mappingLoadingLabels' => __( 'Loading labels...', 'progress-agentic-rag' ), + 'mappingNoLabels' => __( 'No labels available.', 'progress-agentic-rag' ), + 'mappingLabelsFailed' => __( 'Labels could not be loaded.', 'progress-agentic-rag' ), + 'mappingNoTerms' => __( 'No terms available for this taxonomy.', 'progress-agentic-rag' ), + ], + ] + ); + } + + public function render(): void { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $active_tab = $this->active_tab(); + $connection_status_label = $this->connection_status_label(); + $token_status_label = $this->settings->has_token() ? __( 'Saved', 'progress-agentic-rag' ) : __( 'Missing', 'progress-agentic-rag' ); + + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/page-open.php'; + $this->render_tabs( $active_tab ); + if ( self::TAB_CONNECTION === $active_tab ) { + $this->render_connection_settings(); + } elseif ( self::TAB_TAXONOMY_LABELING === $active_tab ) { + $this->render_taxonomy_labeling(); + } else { + $this->render_indexation(); + } + $this->render_updated_notice(); + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/page-close.php'; + } + + public function save_connection_settings(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to manage Progress Agentic RAG settings.', 'progress-agentic-rag' ) ); + } + + check_admin_referer( 'progress_agentic_rag_save_connection' ); + + $zone = $this->posted_text( SettingsRepository::OPTION_ZONE ); + $kbid = $this->posted_text( SettingsRepository::OPTION_KBID ); + $account_id = $this->posted_text( SettingsRepository::OPTION_ACCOUNT_ID ); + $token = $this->posted_text( SettingsRepository::OPTION_TOKEN ); + + $this->settings->update_connection_settings( $zone, $kbid, $account_id, $token ); + $connected = $this->validate_connection_settings(); + $this->settings->set_api_is_reachable( $connected ); + if ( $connected ) { + $this->manual_sync->ensure_automatic_sync( true ); + } + $this->redirect_to_tab( self::TAB_CONNECTION, $connected ? 'connected' : 'failed' ); + } + + public function save_indexation_settings(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to manage Progress Agentic RAG settings.', 'progress-agentic-rag' ) ); + } + + check_admin_referer( 'progress_agentic_rag_save_indexation' ); + + $allowed_post_types = array_keys( $this->indexable_post_type_objects() ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce checked above; values are sanitized before use. + $raw_post_types = isset( $_POST['indexable_post_types'] ) && is_array( $_POST['indexable_post_types'] ) ? wp_unslash( $_POST['indexable_post_types'] ) : []; + $selected = []; + + foreach ( $raw_post_types as $post_type ) { + $post_type = sanitize_key( $post_type ); + if ( in_array( $post_type, $allowed_post_types, true ) ) { + $selected[] = $post_type; + } + } + + $this->settings->update_indexable_post_types( array_values( array_unique( $selected ) ) ); + $this->manual_sync->ensure_automatic_sync( true ); + $this->redirect_to_tab( self::TAB_INDEXATION ); + } + + public function save_taxonomy_labeling_settings(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to manage Progress Agentic RAG settings.', 'progress-agentic-rag' ) ); + } + + check_admin_referer( 'progress_agentic_rag_save_taxonomy_labeling' ); + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce checked above; SettingsRepository sanitizes the nested map. + $raw_taxonomy_map = isset( $_POST[ SettingsRepository::OPTION_TAXONOMY_LABEL_MAP ] ) && is_array( $_POST[ SettingsRepository::OPTION_TAXONOMY_LABEL_MAP ] ) ? wp_unslash( $_POST[ SettingsRepository::OPTION_TAXONOMY_LABEL_MAP ] ) : []; + $this->settings->update_taxonomy_label_map( $raw_taxonomy_map ); + $this->manual_sync->ensure_automatic_sync( true ); + $this->redirect_to_tab( self::TAB_TAXONOMY_LABELING ); + } + + public function start_manual_sync(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to sync content.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Ajax nonce checked above; values are sanitized before use. + $raw_post_types = isset( $_POST['post_types'] ) && is_array( $_POST['post_types'] ) ? wp_unslash( $_POST['post_types'] ) : []; + $post_types = []; + + foreach ( $raw_post_types as $post_type ) { + $post_types[] = sanitize_key( $post_type ); + } + + $result = $this->manual_sync->start( $post_types ); + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => sanitize_text_field( $result->get_error_message() ) ], 400 ); + } + + wp_send_json_success( $result ); + } + + public function delete_synced_resources_status(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to view synced resource deletion status.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + wp_send_json_success( $this->manual_sync->delete_status() ); + } + + public function manual_sync_status(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to view sync status.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + wp_send_json_success( $this->manual_sync->status() ); + } + + public function background_sync_status(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to view automatic sync status.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + wp_send_json_success( $this->manual_sync->ensure_automatic_sync() ); + } + + public function delete_synced_resources(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to delete synced resources.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + $result = $this->manual_sync->delete_synced_resources(); + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => sanitize_text_field( $result->get_error_message() ) ], 400 ); + } + + wp_send_json_success( $result ); + } + + public function start_label_reprocess(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to reprocess labels.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + $result = $this->manual_sync->start_label_reprocess(); + if ( is_wp_error( $result ) ) { + wp_send_json_error( [ 'message' => sanitize_text_field( $result->get_error_message() ) ], 400 ); + } + + wp_send_json_success( $result ); + } + + public function cancel_label_reprocess(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to cancel label reprocessing.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + wp_send_json_success( $this->manual_sync->cancel_label_reprocess() ); + } + + public function label_reprocess_status(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to view label reprocessing status.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + wp_send_json_success( $this->manual_sync->label_reprocess_status() ); + } + + public function get_labelset_labels(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( [ 'message' => __( 'You do not have permission to load labels.', 'progress-agentic-rag' ) ], 403 ); + } + + check_ajax_referer( 'progress_agentic_rag_manual_sync' ); + + $labelset = isset( $_POST['labelset'] ) ? sanitize_text_field( wp_unslash( $_POST['labelset'] ) ) : ''; + + wp_send_json_success( [ 'labels' => $this->api_client->get_labelset_labels( $labelset ) ] ); + } + + private function active_tab(): string { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab is read-only admin navigation state. + $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : self::TAB_INDEXATION; + + if ( in_array( $tab, [ self::TAB_INDEXATION, self::TAB_TAXONOMY_LABELING, self::TAB_CONNECTION ], true ) ) { + return $tab; + } + + return self::TAB_INDEXATION; + } + + private function render_tabs( string $active_tab ): void { + $tabs = [ + [ + 'key' => self::TAB_INDEXATION, + 'label' => __( 'Indexation', 'progress-agentic-rag' ), + 'url' => $this->tab_url( self::TAB_INDEXATION ), + ], + [ + 'key' => self::TAB_TAXONOMY_LABELING, + 'label' => __( 'Taxonomy labeling', 'progress-agentic-rag' ), + 'url' => $this->tab_url( self::TAB_TAXONOMY_LABELING ), + ], + [ + 'key' => self::TAB_CONNECTION, + 'label' => __( 'Connection settings', 'progress-agentic-rag' ), + 'url' => $this->tab_url( self::TAB_CONNECTION ), + ], + ]; + + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/tabs.php'; + } + + private function render_updated_notice(): void { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Notice flag is read-only redirect state. + if ( ! isset( $_GET['progress-agentic-rag-updated'] ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Connection status is read-only redirect state. + $connection_status = isset( $_GET['connection-status'] ) ? sanitize_key( wp_unslash( $_GET['connection-status'] ) ) : ''; + + if ( 'connected' === $connection_status ) { + $notice_type = 'success'; + $notice_role = 'status'; + $notice_message = __( 'Connection settings saved. Progress Agentic RAG validated the connection successfully.', 'progress-agentic-rag' ); + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/notice.php'; + return; + } + + if ( 'failed' === $connection_status ) { + $notice_type = 'error'; + $notice_role = 'alert'; + $notice_message = __( 'Connection settings saved, but Progress Agentic RAG could not validate the connection. Check the Zone, Knowledge Box ID, and Service token with Writer permissions.', 'progress-agentic-rag' ); + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/notice.php'; + return; + } + + $notice_type = 'success'; + $notice_role = 'status'; + $notice_message = __( 'Settings saved.', 'progress-agentic-rag' ); + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/notice.php'; + } + + private function render_indexation(): void { + $post_types = $this->indexable_post_type_objects(); + $selected_types = $this->settings->get_indexable_post_types(); + $selected_count = count( array_filter( $selected_types ) ); + $connection_status_label = $this->connection_status_label(); + $api_connected = $this->settings->get_api_is_reachable(); + $scheduler_available = $this->manual_sync->scheduler_available(); + $scheduler_status = $this->manual_sync->scheduler_status(); + $background_sync_status = $this->manual_sync->ensure_automatic_sync(); + $manual_sync_status = $this->manual_sync->status(); + $delete_status = $this->manual_sync->delete_status(); + $delete_running = 'running' === ( $delete_status['status'] ?? '' ); + $manual_sync_disabled = 'running' !== ( $manual_sync_status['status'] ?? '' ) && ( ! $api_connected || ! $scheduler_available || $delete_running ); + $manual_sync_button_text = __( 'Sync manually', 'progress-agentic-rag' ); + if ( 'running' === ( $manual_sync_status['status'] ?? '' ) ) { + $manual_sync_button_text = sprintf( + /* translators: 1: processed entity count, 2: total entity count, 3: completion percent. */ + __( 'Sync in progress (%1$d/%2$d, %3$d%%)', 'progress-agentic-rag' ), + (int) ( $manual_sync_status['processed'] ?? 0 ), + (int) ( $manual_sync_status['total'] ?? 0 ), + (int) ( $manual_sync_status['percent'] ?? 0 ) + ); + } + $sync_status = $this->manual_sync->sync_status( array_keys( $post_types ) ); + $label_reprocess_status = $this->manual_sync->label_reprocess_status(); + $delete_button_text = __( 'Delete synced resources', 'progress-agentic-rag' ); + if ( $delete_running ) { + $delete_button_text = sprintf( + /* translators: 1: processed resource count, 2: total resource count, 3: completion percent. */ + __( 'Delete in progress (%1$d/%2$d, %3$d%%)', 'progress-agentic-rag' ), + (int) ( $delete_status['processed'] ?? 0 ), + (int) ( $delete_status['total'] ?? 0 ), + (int) ( $delete_status['percent'] ?? 0 ) + ); + } + $delete_disabled = ! $delete_running && ( ! $api_connected || ! $scheduler_available || empty( $sync_status['total_mapped'] ) ); + + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/indexation.php'; + } + + private function render_taxonomy_labeling(): void { + $post_types = $this->indexable_post_type_objects(); + $api_connected = $this->settings->get_api_is_reachable(); + $scheduler_available = $this->manual_sync->scheduler_available(); + $delete_status = $this->manual_sync->delete_status(); + $delete_running = 'running' === ( $delete_status['status'] ?? '' ); + $sync_status = $this->manual_sync->sync_status( array_keys( $post_types ) ); + $label_reprocess_status = $this->manual_sync->label_reprocess_status(); + $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' ); + $taxonomy_label_map = $this->settings->get_taxonomy_label_map(); + $taxonomy_label_map_option = SettingsRepository::OPTION_TAXONOMY_LABEL_MAP; + $labelsets = $this->api_client->get_labelsets(); + $taxonomy_terms = $this->taxonomy_terms( array_keys( $taxonomies ) ); + $labelset_labels = $this->labelset_labels_for_mapping( $taxonomy_label_map ); + + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/taxonomy-labeling.php'; + } + + private function render_connection_settings(): void { + $zone = $this->settings->get_string( SettingsRepository::OPTION_ZONE ); + $kbid = $this->settings->get_string( SettingsRepository::OPTION_KBID ); + $account_id = $this->settings->get_string( SettingsRepository::OPTION_ACCOUNT_ID ); + $connection_state_label = sprintf( + /* translators: %s: current connection status. */ + __( 'Connection: %s', 'progress-agentic-rag' ), + $this->connection_status_label() + ); + $token_state_label = $this->settings->has_token() ? __( 'Token saved', 'progress-agentic-rag' ) : __( 'Token missing', 'progress-agentic-rag' ); + $zone_option_name = SettingsRepository::OPTION_ZONE; + $kbid_option_name = SettingsRepository::OPTION_KBID; + $account_id_option_name = SettingsRepository::OPTION_ACCOUNT_ID; + $token_option_name = SettingsRepository::OPTION_TOKEN; + + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/connection-settings.php'; + } + + /** + * @return array + */ + private function mapping_data(): array { + $taxonomies = []; + + foreach ( get_taxonomies( [ 'public' => true ], 'objects' ) as $taxonomy_name => $taxonomy ) { + $terms = []; + foreach ( $this->taxonomy_terms( [ $taxonomy_name ] )[ $taxonomy_name ] ?? [] as $term ) { + $terms[] = [ + 'id' => (int) $term->term_id, + 'name' => (string) $term->name, + ]; + } + + $taxonomies[ $taxonomy_name ] = [ + 'label' => isset( $taxonomy->labels->name ) ? (string) $taxonomy->labels->name : (string) $taxonomy_name, + 'terms' => $terms, + ]; + } + + return [ + 'taxonomies' => $taxonomies, + 'labelsets' => $this->api_client->get_labelsets(), + ]; + } + + /** + * @param list $taxonomy_names Taxonomy names. + * + * @return array> + */ + private function taxonomy_terms( array $taxonomy_names ): array { + $taxonomy_terms = []; + + foreach ( $taxonomy_names as $taxonomy_name ) { + $terms = get_terms( + [ + 'taxonomy' => $taxonomy_name, + 'hide_empty' => false, + ] + ); + + $taxonomy_terms[ $taxonomy_name ] = is_wp_error( $terms ) || ! is_array( $terms ) ? [] : array_values( $terms ); + } + + return $taxonomy_terms; + } + + /** + * @param array $mapping Taxonomy label mapping. + * + * @return array> + */ + private function labelset_labels_for_mapping( array $mapping ): array { + $labelsets = []; + + foreach ( $mapping as $config ) { + if ( ! is_array( $config ) ) { + continue; + } + + $labelset = isset( $config['labelset'] ) ? trim( (string) $config['labelset'] ) : ''; + if ( '' !== $labelset ) { + $labelsets[ $labelset ] = true; + } + + $fallback = is_array( $config['fallback'] ?? null ) ? $config['fallback'] : []; + $fallback_labelset = isset( $fallback['labelset'] ) ? trim( (string) $fallback['labelset'] ) : ''; + if ( '' !== $fallback_labelset ) { + $labelsets[ $fallback_labelset ] = true; + } + } + + $labels = []; + foreach ( array_keys( $labelsets ) as $labelset ) { + $labels[ $labelset ] = $this->api_client->get_labelset_labels( $labelset ); + } + + return $labels; + } + + private function connection_status_label(): string { + if ( $this->settings->get_api_is_reachable() ) { + return __( 'Connected', 'progress-agentic-rag' ); + } + + if ( '' === $this->settings->get_string( SettingsRepository::OPTION_ZONE ) || '' === $this->settings->get_string( SettingsRepository::OPTION_KBID ) || ! $this->settings->has_token() ) { + return __( 'Not configured', 'progress-agentic-rag' ); + } + + return __( 'Connection failed', 'progress-agentic-rag' ); + } + + private function validate_connection_settings(): bool { + $zone = $this->settings->get_string( SettingsRepository::OPTION_ZONE ); + $kbid = $this->settings->get_string( SettingsRepository::OPTION_KBID ); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + + if ( '' === $zone || '' === $kbid || '' === $token || ! preg_match( '/^[a-z0-9-]+$/', $zone ) ) { + return false; + } + + $response = wp_remote_post( + sprintf( + 'https://%s.rag.progress.cloud/api/v1/kb/%s/resources', + rawurlencode( $zone ), + rawurlencode( $kbid ) + ), + [ + 'headers' => [ + 'Content-Type' => 'application/json', + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'body' => wp_json_encode( [ 'texts' => 'progress-agentic-rag-validation' ] ), + 'redirection' => 0, + 'timeout' => 10, + ] + ); + + if ( is_wp_error( $response ) ) { + return false; + } + + return 422 === (int) wp_remote_retrieve_response_code( $response ); + } + + private function tab_url( string $tab ): string { + return add_query_arg( + [ + 'page' => self::MENU_SLUG, + 'tab' => $tab, + ], + admin_url( 'admin.php' ) + ); + } + + private function redirect_to_tab( string $tab, string $connection_status = '' ): void { + $args = [ + 'progress-agentic-rag-updated' => 'true', + ]; + + if ( '' !== $connection_status ) { + $args['connection-status'] = $connection_status; + } + + wp_safe_redirect( + add_query_arg( + $args, + $this->tab_url( $tab ) + ) + ); + exit; + } + + private function posted_text( string $key ): string { + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Callers verify the matching admin nonce before reading posted values. + return isset( $_POST[ $key ] ) ? sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) : ''; + } + + /** + * @return array + */ + private function indexable_post_type_objects(): array { + $post_types = get_post_types( [ 'public' => true ], 'objects' ); + + if ( ! isset( $post_types['attachment'] ) ) { + $attachment = get_post_type_object( 'attachment' ); + if ( null !== $attachment ) { + $post_types['attachment'] = $attachment; + } + } + + ksort( $post_types ); + + return $post_types; + } +} diff --git a/src/Api/ApiClient.php b/src/Api/ApiClient.php new file mode 100644 index 0000000..f5e73b5 --- /dev/null +++ b/src/Api/ApiClient.php @@ -0,0 +1,832 @@ +endpoint(); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + + if ( '' === $endpoint || '' === $token ) { + return new WP_Error( 'progress_agentic_rag_missing_connection', __( 'Progress Agentic RAG connection settings are incomplete.', 'progress-agentic-rag' ) ); + } + + $response = wp_remote_request( + $endpoint . 'resources', + [ + 'method' => 'POST', + 'headers' => [ + 'Content-type' => 'application/json', + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'body' => wp_json_encode( $this->resource_body( $post ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), + 'timeout' => 30, + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $response_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 409 === $response_code ) { + $existing = $this->find_resource_by_slug( (string) $post->ID ); + if ( is_wp_error( $existing ) ) { + return $existing; + } + + if ( '' === $existing['rid'] ) { + return new WP_Error( 'progress_agentic_rag_existing_resource_missing', __( 'Progress Agentic RAG reports the resource already exists, but it could not be found by slug.', 'progress-agentic-rag' ) ); + } + + $seqid = $existing['seqid']; + if ( 'attachment' === $post->post_type ) { + $file_result = $this->upload_attachment_file( $endpoint, $token, $existing['rid'], $post ); + if ( is_wp_error( $file_result ) ) { + return $file_result; + } + + if ( '' !== $file_result ) { + $seqid = $file_result; + } + } + + $this->upsert_index( (int) $post->ID, $existing['rid'], $seqid ); + + return true; + } + + if ( 201 !== $response_code ) { + return new WP_Error( 'progress_agentic_rag_resource_failed', $this->response_error_message( $response, __( 'Progress Agentic RAG rejected the resource.', 'progress-agentic-rag' ) ) ); + } + + $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); + $rid = is_array( $api_response ) && isset( $api_response['uuid'] ) ? sanitize_text_field( (string) $api_response['uuid'] ) : ''; + $seqid = is_array( $api_response ) && isset( $api_response['seqid'] ) ? sanitize_text_field( (string) $api_response['seqid'] ) : ''; + + if ( '' === $rid ) { + return new WP_Error( 'progress_agentic_rag_missing_resource_id', __( 'Progress Agentic RAG did not return a resource ID.', 'progress-agentic-rag' ) ); + } + + if ( 'attachment' === $post->post_type ) { + $file_result = $this->upload_attachment_file( $endpoint, $token, $rid, $post ); + if ( is_wp_error( $file_result ) ) { + return $file_result; + } + + if ( '' !== $file_result ) { + $seqid = $file_result; + } + } + + $this->upsert_index( (int) $post->ID, $rid, $seqid ); + + return true; + } + + public function sync_post( WP_Post $post ): bool|WP_Error { + $rid = $this->get_indexed_resource_id( (int) $post->ID ); + + if ( '' === $rid ) { + return $this->index_post( $post ); + } + + $endpoint = $this->endpoint(); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + + if ( '' === $endpoint || '' === $token ) { + return new WP_Error( 'progress_agentic_rag_missing_connection', __( 'Progress Agentic RAG connection settings are incomplete.', 'progress-agentic-rag' ) ); + } + + $response = wp_remote_request( + $endpoint . 'resource/' . rawurlencode( $rid ), + [ + 'method' => 'PATCH', + 'headers' => [ + 'Content-type' => 'application/json', + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'body' => wp_json_encode( $this->resource_body( $post ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), + 'timeout' => 30, + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $response_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 404 === $response_code ) { + $this->delete_index( (int) $post->ID ); + return $this->index_post( $post ); + } + + if ( ! in_array( $response_code, [ 200, 201 ], true ) ) { + return new WP_Error( 'progress_agentic_rag_resource_update_failed', $this->response_error_message( $response, __( 'Progress Agentic RAG rejected the resource update.', 'progress-agentic-rag' ) ) ); + } + + $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); + $seqid = is_array( $api_response ) && isset( $api_response['seqid'] ) ? sanitize_text_field( (string) $api_response['seqid'] ) : ''; + + if ( 'attachment' === $post->post_type ) { + $file_result = $this->upload_attachment_file( $endpoint, $token, $rid, $post ); + if ( is_wp_error( $file_result ) ) { + return $file_result; + } + + if ( '' !== $file_result ) { + $seqid = $file_result; + } + } + + $this->upsert_index( (int) $post->ID, $rid, $seqid ); + + return true; + } + + public function update_resource_labels( WP_Post $post, string $rid ): bool|WP_Error { + $endpoint = $this->endpoint(); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + $rid = sanitize_text_field( $rid ); + + if ( '' === $endpoint || '' === $token || '' === $rid ) { + return new WP_Error( 'progress_agentic_rag_missing_connection', __( 'Progress Agentic RAG connection settings are incomplete.', 'progress-agentic-rag' ) ); + } + + $response = wp_remote_request( + $endpoint . 'resource/' . rawurlencode( $rid ), + [ + 'method' => 'PATCH', + 'headers' => [ + 'Content-type' => 'application/json', + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'body' => wp_json_encode( + [ + 'usermetadata' => [ + 'classifications' => $this->build_taxonomy_classifications( $post ), + ], + ], + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + ), + 'timeout' => 30, + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + return new WP_Error( 'progress_agentic_rag_label_update_failed', $this->response_error_message( $response, __( 'Progress Agentic RAG rejected the label update.', 'progress-agentic-rag' ) ) ); + } + + return true; + } + + /** + * @return list + */ + public function get_labelsets(): array { + $cache = $this->settings->get_labelsets_cache(); + $cached_labelsets = is_array( $cache['labelsets'] ?? null ) ? $cache['labelsets'] : []; + $fetched_at = (int) ( $cache['fetched_at'] ?? 0 ); + $ttl = defined( 'HOUR_IN_SECONDS' ) ? 6 * HOUR_IN_SECONDS : 21600; + + if ( ! empty( $cached_labelsets ) && $fetched_at > 0 && ( time() - $fetched_at ) < $ttl ) { + return $cached_labelsets; + } + + $endpoint = $this->endpoint(); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + if ( '' === $endpoint || '' === $token ) { + return $cached_labelsets; + } + + $response = wp_remote_request( + $endpoint . 'labelsets', + [ + 'method' => 'GET', + 'headers' => [ + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'timeout' => 30, + ] + ); + + if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + return $cached_labelsets; + } + + $normalized = $this->normalize_labelsets_response( json_decode( wp_remote_retrieve_body( $response ), true ) ); + $labelsets = $normalized['labelsets']; + if ( empty( $labelsets ) ) { + return $cached_labelsets; + } + + if ( ! empty( $normalized['labels'] ) ) { + $this->settings->set_labelsets_cache_with_labels( $labelsets, $normalized['labels'] ); + } else { + $this->settings->set_labelsets_cache( $labelsets ); + } + + return $labelsets; + } + + /** + * @return list + */ + public function get_labelset_labels( string $labelset ): array { + $labelset = trim( $labelset ); + if ( '' === $labelset ) { + return []; + } + + $cache = $this->settings->get_labelsets_cache(); + $fetched_at = (int) ( $cache['fetched_at'] ?? 0 ); + $ttl = defined( 'HOUR_IN_SECONDS' ) ? 6 * HOUR_IN_SECONDS : 21600; + $cached_labels = $this->settings->get_labelset_labels_cache( $labelset ); + + if ( ! empty( $cached_labels ) && $fetched_at > 0 && ( time() - $fetched_at ) < $ttl ) { + return $cached_labels; + } + + $labels = $this->fetch_labelset_labels( $labelset ); + if ( ! empty( $labels ) ) { + $this->settings->set_labelset_labels_cache( $labelset, $labels ); + return $labels; + } + + return $cached_labels; + } + + public function delete_resource( int $post_id, string $rid ): bool|WP_Error { + $endpoint = $this->endpoint(); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + $rid = sanitize_text_field( $rid ); + + if ( '' === $endpoint || '' === $token || '' === $rid ) { + return new WP_Error( 'progress_agentic_rag_missing_connection', __( 'Progress Agentic RAG connection settings are incomplete.', 'progress-agentic-rag' ) ); + } + + $response = wp_remote_request( + $endpoint . 'resource/' . rawurlencode( $rid ), + [ + 'method' => 'DELETE', + 'headers' => [ + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'timeout' => 30, + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $response_code = (int) wp_remote_retrieve_response_code( $response ); + if ( ! in_array( $response_code, [ 204, 404 ], true ) ) { + return new WP_Error( 'progress_agentic_rag_delete_failed', $this->response_error_message( $response, __( 'Progress Agentic RAG rejected the resource deletion.', 'progress-agentic-rag' ) ) ); + } + + $this->delete_index( $post_id ); + + return true; + } + + /** + * @return list + */ + public function get_synced_resources(): array { + global $wpdb; + + $table_name = $this->sync_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Reads plugin-owned sync table; values are prepared and the table name is escaped. + $results = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_id, nuclia_rid FROM {$table_name} WHERE nuclia_rid IS NOT NULL AND nuclia_rid != %s", + '' + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + + return is_array( $results ) ? $results : []; + } + + /** + * @param list $post_ids + */ + public function recover_existing_resources( array $post_ids ): int|WP_Error { + $slugs = []; + + foreach ( $post_ids as $post_id ) { + $post_id = (int) $post_id; + if ( $post_id > 0 ) { + $slugs[ (string) $post_id ] = $post_id; + } + } + + if ( empty( $slugs ) ) { + return 0; + } + + $resources = $this->find_resources_by_slugs( array_keys( $slugs ) ); + if ( is_wp_error( $resources ) ) { + return $resources; + } + + $recovered = 0; + foreach ( $resources as $slug => $resource ) { + if ( '' === $resource['rid'] ) { + continue; + } + + $this->upsert_index( $slugs[ $slug ], $resource['rid'], $resource['seqid'] ); + $recovered++; + } + + return $recovered; + } + + private function endpoint(): string { + $zone = $this->settings->get_string( SettingsRepository::OPTION_ZONE ); + $kbid = $this->settings->get_string( SettingsRepository::OPTION_KBID ); + + if ( '' === $zone || '' === $kbid || ! preg_match( '/^[a-z0-9-]+$/', $zone ) ) { + return ''; + } + + return sprintf( + 'https://%s.rag.progress.cloud/api/v1/kb/%s/', + rawurlencode( $zone ), + rawurlencode( $kbid ) + ); + } + + /** + * @return array{rid:string,seqid:string}|WP_Error + */ + private function find_resource_by_slug( string $slug ): array|WP_Error { + if ( '' === $slug ) { + return new WP_Error( 'progress_agentic_rag_missing_connection', __( 'Progress Agentic RAG connection settings are incomplete.', 'progress-agentic-rag' ) ); + } + + $resources = $this->find_resources_by_slugs( [ $slug ] ); + if ( is_wp_error( $resources ) ) { + return $resources; + } + + return $resources[ $slug ] ?? [ + 'rid' => '', + 'seqid' => '', + ]; + } + + /** + * @param list $slugs + * + * @return array|WP_Error + */ + private function find_resources_by_slugs( array $slugs ): array|WP_Error { + $endpoint = $this->endpoint(); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + $lookup = []; + + foreach ( $slugs as $slug ) { + $slug = trim( (string) $slug ); + if ( '' !== $slug ) { + $lookup[ $slug ] = true; + } + } + + if ( empty( $lookup ) ) { + return []; + } + + if ( '' === $endpoint || '' === $token ) { + return new WP_Error( 'progress_agentic_rag_missing_connection', __( 'Progress Agentic RAG connection settings are incomplete.', 'progress-agentic-rag' ) ); + } + + $page_size = 250; + $offset = 0; + $matches = []; + + do { + $response = wp_remote_request( + $endpoint . 'resources?from=' . $offset . '&size=' . $page_size, + [ + 'method' => 'GET', + 'headers' => [ + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'timeout' => 30, + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + return new WP_Error( 'progress_agentic_rag_existing_resource_lookup_failed', $this->response_error_message( $response, __( 'Progress Agentic RAG could not look up the existing resource.', 'progress-agentic-rag' ) ) ); + } + + $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); + $resources = is_array( $api_response['resources'] ?? null ) ? $api_response['resources'] : []; + + foreach ( $resources as $resource ) { + $slug = is_array( $resource ) ? (string) ( $resource['slug'] ?? '' ) : ''; + if ( ! isset( $lookup[ $slug ] ) ) { + continue; + } + + $matches[ $slug ] = [ + 'rid' => sanitize_text_field( (string) ( $resource['id'] ?? $resource['uuid'] ?? '' ) ), + 'seqid' => sanitize_text_field( (string) ( $resource['last_seqid'] ?? $resource['seqid'] ?? '' ) ), + ]; + + if ( count( $matches ) === count( $lookup ) ) { + break 2; + } + } + + $offset += $page_size; + } while ( count( $resources ) === $page_size ); + + return $matches; + } + + /** + * @return array + */ + private function resource_body( WP_Post $post ): array { + $body = [ + 'title' => html_entity_decode( wp_strip_all_tags( $post->post_title ), ENT_QUOTES, 'UTF-8' ), + 'slug' => (string) $post->ID, + 'metadata' => [ + 'language' => get_bloginfo( 'language' ), + ], + 'origin' => [ + 'url' => get_permalink( $post ), + ], + 'created' => gmdate( 'Y-m-d', strtotime( $post->post_date_gmt ) ) . 'T' . gmdate( 'H:i:s', strtotime( $post->post_date_gmt ) ) . 'Z', + ]; + + $classifications = $this->build_taxonomy_classifications( $post ); + if ( ! empty( $classifications ) ) { + $body['usermetadata'] = [ + 'classifications' => $classifications, + ]; + } + + if ( 'attachment' === $post->post_type ) { + $body['icon'] = get_post_mime_type( $post->ID ); + + return $body; + } + + $body['icon'] = 'text/html'; + $body['texts'] = [ + 'text-1' => [ + 'body' => apply_filters( + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core content filters are applied before indexing post content. + 'the_content', + $post->post_content + ), + 'format' => 'HTML', + ], + ]; + + return $body; + } + + /** + * @return list + */ + private function build_taxonomy_classifications( WP_Post $post ): array { + $taxonomy_label_map = $this->settings->get_taxonomy_label_map(); + $classifications = []; + + foreach ( $taxonomy_label_map as $taxonomy => $config ) { + if ( ! is_string( $taxonomy ) || ! taxonomy_exists( $taxonomy ) || ! is_array( $config ) ) { + continue; + } + + $labelset = isset( $config['labelset'] ) ? trim( (string) $config['labelset'] ) : ''; + $term_map = is_array( $config['terms'] ?? null ) ? $config['terms'] : []; + $fallback = is_array( $config['fallback'] ?? null ) ? $config['fallback'] : []; + $fallback_labelset = isset( $fallback['labelset'] ) ? trim( (string) $fallback['labelset'] ) : ''; + $fallback_labels = is_array( $fallback['labels'] ?? null ) ? $fallback['labels'] : []; + $term_ids = wp_get_post_terms( $post->ID, $taxonomy, [ 'fields' => 'ids' ] ); + + if ( is_wp_error( $term_ids ) ) { + continue; + } + + if ( empty( $term_ids ) ) { + foreach ( $fallback_labels as $label ) { + $label = trim( (string) $label ); + if ( '' !== $fallback_labelset && '' !== $label ) { + $classifications[] = [ + 'labelset' => $fallback_labelset, + 'label' => $label, + ]; + } + } + continue; + } + + if ( '' === $labelset || empty( $term_map ) ) { + continue; + } + + foreach ( $term_ids as $term_id ) { + $labels = $term_map[ (int) $term_id ] ?? []; + if ( ! is_array( $labels ) ) { + $labels = '' !== $labels ? [ (string) $labels ] : []; + } + + foreach ( $labels as $label ) { + $label = trim( (string) $label ); + if ( '' === $label ) { + continue; + } + + $classifications[] = [ + 'labelset' => $labelset, + 'label' => $label, + ]; + } + } + } + + $unique = []; + foreach ( $classifications as $classification ) { + $unique[ $classification['labelset'] . '|' . $classification['label'] ] = $classification; + } + + return array_values( $unique ); + } + + /** + * @return array{labelsets:list,labels:array>} + */ + private function normalize_labelsets_response( mixed $data ): array { + if ( ! is_array( $data ) ) { + return [ + 'labelsets' => [], + 'labels' => [], + ]; + } + + $labelsets = []; + $labels_map = []; + $payload = $data['labelsets'] ?? $data; + + if ( is_array( $payload ) ) { + $is_assoc = array_keys( $payload ) !== range( 0, count( $payload ) - 1 ); + if ( $is_assoc ) { + $labelsets = array_keys( $payload ); + foreach ( $payload as $labelset => $entry ) { + $labels = $this->normalize_labelset_labels( $entry ); + if ( ! empty( $labels ) ) { + $labels_map[ (string) $labelset ] = $labels; + } + } + } else { + foreach ( $payload as $entry ) { + if ( is_string( $entry ) ) { + $labelsets[] = $entry; + } elseif ( is_array( $entry ) ) { + if ( isset( $entry['labelset'] ) ) { + $labelset_name = (string) $entry['labelset']; + $labelsets[] = $labelset_name; + $labels = $this->normalize_labelset_labels( $entry ); + if ( ! empty( $labels ) ) { + $labels_map[ $labelset_name ] = $labels; + } + } elseif ( isset( $entry['name'] ) ) { + $labelsets[] = (string) $entry['name']; + } elseif ( isset( $entry['id'] ) ) { + $labelsets[] = (string) $entry['id']; + } + } + } + } + } + + $labelsets = array_filter( array_map( 'sanitize_text_field', $labelsets ) ); + + return [ + 'labelsets' => array_values( array_unique( $labelsets ) ), + 'labels' => $labels_map, + ]; + } + + /** + * @return list + */ + private function fetch_labelset_labels( string $labelset ): array { + $endpoint = $this->endpoint(); + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + if ( '' === $endpoint || '' === $token ) { + return []; + } + + $encoded = rawurlencode( $labelset ); + $candidates = [ + $endpoint . 'labelsets/' . $encoded, + $endpoint . 'labelset/' . $encoded, + $endpoint . 'labelsets/' . $encoded . '/labels', + $endpoint . 'labelset/' . $encoded . '/labels', + ]; + + foreach ( $candidates as $uri ) { + $response = wp_remote_request( + $uri, + [ + 'method' => 'GET', + 'headers' => [ + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + ], + 'timeout' => 30, + ] + ); + + if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + continue; + } + + $labels = $this->normalize_labelset_labels( json_decode( wp_remote_retrieve_body( $response ), true ) ); + if ( ! empty( $labels ) ) { + return $labels; + } + } + + return []; + } + + /** + * @return list + */ + private function normalize_labelset_labels( mixed $data ): array { + if ( ! is_array( $data ) ) { + return []; + } + + $labels = []; + $payload = $data['labels'] ?? $data['labelset'] ?? $data; + + if ( is_array( $payload ) ) { + $is_assoc = array_keys( $payload ) !== range( 0, count( $payload ) - 1 ); + if ( $is_assoc ) { + $labels = array_keys( $payload ); + } else { + foreach ( $payload as $entry ) { + if ( is_string( $entry ) ) { + $labels[] = $entry; + } elseif ( is_array( $entry ) ) { + if ( isset( $entry['title'] ) ) { + $labels[] = (string) $entry['title']; + } elseif ( isset( $entry['text'] ) ) { + $labels[] = (string) $entry['text']; + } elseif ( isset( $entry['uri'] ) ) { + $labels[] = (string) $entry['uri']; + } elseif ( isset( $entry['related'] ) && is_string( $entry['related'] ) ) { + $labels[] = $entry['related']; + } elseif ( isset( $entry['label'] ) ) { + $labels[] = (string) $entry['label']; + } elseif ( isset( $entry['name'] ) ) { + $labels[] = (string) $entry['name']; + } elseif ( isset( $entry['id'] ) ) { + $labels[] = (string) $entry['id']; + } + } + } + } + } + + $labels = array_filter( array_map( 'sanitize_text_field', $labels ) ); + + return array_values( array_unique( $labels ) ); + } + + private function upload_attachment_file( string $endpoint, string $token, string $rid, WP_Post $post ): string|WP_Error { + $file = get_attached_file( $post->ID ); + + if ( ! $file || ! file_exists( $file ) ) { + return ''; + } + + $response = wp_remote_request( + $endpoint . 'resource/' . rawurlencode( $rid ) . '/file/file/upload', + [ + 'method' => 'POST', + 'headers' => [ + 'X-NUCLIA-SERVICEACCOUNT' => 'Bearer ' . $token, + 'Content-Type' => get_post_mime_type( $post->ID ) ?: 'application/octet-stream', + 'x-filename' => sanitize_file_name( wp_basename( $file ) ), + 'x-md5' => md5_file( $file ), + ], + 'body' => file_get_contents( $file ), + 'timeout' => 60, + ] + ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( ! in_array( (int) wp_remote_retrieve_response_code( $response ), [ 200, 201 ], true ) ) { + return new WP_Error( 'progress_agentic_rag_file_upload_failed', $this->response_error_message( $response, __( 'Progress Agentic RAG rejected the attachment file.', 'progress-agentic-rag' ) ) ); + } + + $api_response = json_decode( wp_remote_retrieve_body( $response ), true ); + + return is_array( $api_response ) && isset( $api_response['seqid'] ) ? sanitize_text_field( (string) $api_response['seqid'] ) : ''; + } + + private function get_indexed_resource_id( int $post_id ): string { + global $wpdb; + + $table_name = $this->sync_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Reads plugin-owned sync table; values are prepared and the table name is escaped. + $resource_id = (string) $wpdb->get_var( + $wpdb->prepare( + "SELECT nuclia_rid FROM {$table_name} WHERE post_id = %d", + $post_id + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + + return $resource_id; + } + + private function upsert_index( int $post_id, string $rid, string $seqid ): void { + global $wpdb; + + $table_name = $this->sync_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Writes plugin-owned sync table after upstream indexing succeeds. + $wpdb->delete( $table_name, [ 'post_id' => $post_id ], [ '%d' ] ); + $wpdb->insert( + $table_name, + [ + 'post_id' => $post_id, + 'nuclia_rid' => $rid, + 'nuclia_seqid' => '' !== $seqid ? $seqid : null, + ], + [ + '%d', + '%s', + '%s', + ] + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + } + + private function response_error_message( mixed $response, string $fallback ): string { + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( ! is_array( $data ) ) { + return $fallback; + } + + foreach ( [ 'detail', 'message', 'error' ] as $key ) { + if ( isset( $data[ $key ] ) && is_scalar( $data[ $key ] ) && '' !== (string) $data[ $key ] ) { + return $fallback . ' ' . sanitize_text_field( (string) $data[ $key ] ); + } + } + + return $fallback; + } + + private function delete_index( int $post_id ): void { + global $wpdb; + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Removes one row from the plugin-owned sync table. + $wpdb->delete( $this->sync_table_name(), [ 'post_id' => $post_id ], [ '%d' ] ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + } + + private function sync_table_name(): string { + global $wpdb; + + return esc_sql( $wpdb->prefix . SettingsRepository::SYNC_TABLE_NAME ); + } +} diff --git a/src/Autoloader.php b/src/Autoloader.php new file mode 100644 index 0000000..70596c5 --- /dev/null +++ b/src/Autoloader.php @@ -0,0 +1,31 @@ +should_render() ) { + return; + } + + wp_enqueue_style( + 'progress-agentic-rag-frontend', + PROGRESS_AGENTIC_RAG_URL . 'assets/css/frontend.css', + [], + PROGRESS_AGENTIC_RAG_VERSION + ); + + wp_enqueue_script( + 'progress-agentic-rag-widget', + 'https://cdn.rag.progress.cloud/nuclia-widget.umd.js', + [], + null, + true + ); + } + + public function render(): void { + if ( $this->rendered || ! $this->should_render() ) { + return; + } + + $zone = $this->settings->get_string( SettingsRepository::OPTION_ZONE ); + $kbid = $this->settings->get_string( SettingsRepository::OPTION_KBID ); + $proxy_url = ProxyController::proxy_url( $zone ); + + if ( '' === $proxy_url ) { + return; + } + + $this->rendered = true; + + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/frontend/search-widget.php'; + } + + private function should_render(): bool { + return ! is_admin() + && is_front_page() + && $this->settings->get_api_is_reachable() + && '' !== $this->settings->get_string( SettingsRepository::OPTION_ZONE ) + && '' !== $this->settings->get_string( SettingsRepository::OPTION_KBID ) + && $this->settings->has_token(); + } +} diff --git a/src/Indexing/ManualSync.php b/src/Indexing/ManualSync.php new file mode 100644 index 0000000..8836235 --- /dev/null +++ b/src/Indexing/ManualSync.php @@ -0,0 +1,1246 @@ +settings = $settings; + $this->api_client = $api_client; + $this->scheduler = $scheduler ?? new Scheduler(); + } + + public function register(): void { + add_action( self::HOOK_PROCESS_SINGLE, [ $this, 'process_single_post' ], 10, 3 ); + add_action( self::HOOK_BACKGROUND_SYNC, [ $this, 'process_background_post' ], 10, 3 ); + add_action( self::HOOK_BACKGROUND_DELETE, [ $this, 'process_background_delete' ], 10, 3 ); + add_action( self::HOOK_REPROCESS_LABELS, [ $this, 'process_single_label_reprocess' ], 10, 3 ); + add_action( self::HOOK_DELETE_RESOURCE, [ $this, 'process_single_delete' ], 10, 3 ); + add_action( 'save_post', [ $this, 'schedule_background_post_sync' ], 10, 3 ); + add_action( 'add_attachment', [ $this, 'schedule_background_attachment_sync' ], 10, 1 ); + add_action( 'attachment_updated', [ $this, 'schedule_background_attachment_sync' ], 10, 1 ); + add_action( 'delete_post', [ $this, 'schedule_background_delete' ], 10, 2 ); + } + + /** + * @param list $post_types + * + * @return array|WP_Error + */ + public function start( array $post_types ): array|WP_Error { + $current = $this->state(); + if ( 'running' === ( $current['status'] ?? '' ) ) { + return $this->status(); + } + + if ( 'running' === ( $this->delete_state()['status'] ?? '' ) ) { + return new WP_Error( 'progress_agentic_rag_delete_running', __( 'Wait for synced resource deletion to finish before starting a manual sync.', 'progress-agentic-rag' ) ); + } + + if ( ! $this->scheduler_available() ) { + return new WP_Error( 'progress_agentic_rag_scheduler_missing', __( 'Background scheduling is not available.', 'progress-agentic-rag' ) ); + } + + if ( ! $this->settings->get_api_is_reachable() ) { + return new WP_Error( 'progress_agentic_rag_connection_missing', __( 'Validate the Progress Agentic RAG connection before syncing content.', 'progress-agentic-rag' ) ); + } + + $post_types = $this->sanitize_post_types( $post_types ); + if ( empty( $post_types ) ) { + return new WP_Error( 'progress_agentic_rag_no_post_types', __( 'Select at least one content type to sync.', 'progress-agentic-rag' ) ); + } + + $entities = $this->unindexed_entities( $post_types ); + $recovered = 0; + if ( ! empty( $entities ) ) { + $post_ids = []; + foreach ( $entities as $entity ) { + $post_ids[] = $entity['post_id']; + } + + $recovery = $this->api_client->recover_existing_resources( $post_ids ); + if ( is_wp_error( $recovery ) ) { + return $recovery; + } + + $recovered = $recovery; + if ( $recovered > 0 ) { + $entities = $this->unindexed_entities( $post_types ); + } + } + + $sync_id = wp_generate_uuid4(); + $total = count( $entities ); + $message = 0 === $total ? __( 'Everything selected is already synced.', 'progress-agentic-rag' ) : sprintf( + /* translators: %d is the number of entities queued for manual sync. */ + _n( 'Sync in progress: 0 of %d processed.', 'Sync in progress: 0 of %d processed.', $total, 'progress-agentic-rag' ), + $total + ); + + if ( $recovered > 0 ) { + $message = 0 === $total ? sprintf( + /* translators: %d is the number of upstream resources recovered into the local sync table. */ + _n( 'Recovered %d existing upstream resource. Everything selected is already synced.', 'Recovered %d existing upstream resources. Everything selected is already synced.', $recovered, 'progress-agentic-rag' ), + $recovered + ) : sprintf( + /* translators: 1: recovered upstream resource count, 2: number of entities queued for manual sync. */ + _n( 'Recovered %1$d existing upstream resource. Sync in progress: 0 of %2$d processed.', 'Recovered %1$d existing upstream resources. Sync in progress: 0 of %2$d processed.', $recovered, 'progress-agentic-rag' ), + $recovered, + $total + ); + } + + $state = [ + 'id' => $sync_id, + 'status' => 0 === $total ? 'complete' : 'running', + 'total' => $total, + 'completed' => 0, + 'failed' => 0, + 'recovered' => $recovered, + 'current' => 0 === $total ? __( 'No eligible entities to sync.', 'progress-agentic-rag' ) : __( 'Waiting for the first entity.', 'progress-agentic-rag' ), + 'message' => $message, + 'started_at' => time(), + 'updated_at' => time(), + ]; + + update_option( SettingsRepository::OPTION_MANUAL_SYNC_STATE, $state ); + + foreach ( $entities as $offset => $entity ) { + $this->schedule_single_action( + time() + ( $offset * 2 ), + self::HOOK_PROCESS_SINGLE, + [ + 'post_id' => $entity['post_id'], + 'post_type' => $entity['post_type'], + 'sync_id' => $sync_id, + ], + self::GROUP + ); + } + + return $this->status(); + } + + /** + * @return array + */ + public function status(): array { + $state = $this->state(); + $total = (int) ( $state['total'] ?? 0 ); + $done = (int) ( $state['completed'] ?? 0 ) + (int) ( $state['failed'] ?? 0 ); + $done = min( $total, $done ); + $recovered = (int) ( $state['recovered'] ?? 0 ); + + if ( $total > 0 && $done >= $total && 'running' === ( $state['status'] ?? '' ) ) { + $state['status'] = 'complete'; + $state['message'] = __( 'Manual sync complete.', 'progress-agentic-rag' ); + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_MANUAL_SYNC_STATE, $state ); + } + + $current = (string) ( $state['current'] ?? __( 'Waiting to sync.', 'progress-agentic-rag' ) ); + $message = (string) ( $state['message'] ?? '' ); + + if ( 'running' === ( $state['status'] ?? '' ) ) { + $message = sprintf( + /* translators: 1: processed entity count, 2: total entity count. */ + __( 'Sync in progress: %1$d of %2$d processed.', 'progress-agentic-rag' ), + $done, + $total + ); + + if ( $recovered > 0 ) { + $message = sprintf( + /* translators: 1: recovered upstream resource count, 2: processed entity count, 3: total entity count. */ + _n( 'Recovered %1$d existing upstream resource. Sync in progress: %2$d of %3$d processed.', 'Recovered %1$d existing upstream resources. Sync in progress: %2$d of %3$d processed.', $recovered, 'progress-agentic-rag' ), + $recovered, + $done, + $total + ); + } + + if ( '' === $current || __( 'Queued manual sync.', 'progress-agentic-rag' ) === $current ) { + $current = 0 === $done ? __( 'Waiting for the first entity.', 'progress-agentic-rag' ) : __( 'Waiting for the next entity.', 'progress-agentic-rag' ); + } + } + + return [ + 'id' => (string) ( $state['id'] ?? '' ), + 'status' => (string) ( $state['status'] ?? 'idle' ), + 'total' => $total, + 'completed' => (int) ( $state['completed'] ?? 0 ), + 'failed' => (int) ( $state['failed'] ?? 0 ), + 'recovered' => $recovered, + 'processed' => $done, + 'current' => $current, + 'message' => $message, + 'percent' => $total > 0 ? min( 100, (int) floor( ( $done / $total ) * 100 ) ) : 100, + ]; + } + + /** + * @param list $post_types + * + * @return array + */ + public function sync_status( array $post_types ): array { + $rows = []; + $total_indexable = 0; + $total_synced = 0; + + foreach ( $this->sanitize_post_types( $post_types ) as $post_type ) { + $indexable = $this->count_indexable_entities( $post_type ); + $synced = $this->count_synced_entities( $post_type ); + + $rows[] = [ + 'post_type' => $post_type, + 'label' => $this->post_type_label( $post_type ), + 'indexable' => $indexable, + 'synced' => $synced, + 'remaining' => max( 0, $indexable - $synced ), + ]; + + $total_indexable += $indexable; + $total_synced += $synced; + } + + return [ + 'rows' => $rows, + 'total_indexable' => $total_indexable, + 'total_synced' => $total_synced, + 'total_remaining' => max( 0, $total_indexable - $total_synced ), + 'total_mapped' => $this->count_synced_resources(), + ]; + } + + /** + * @return array + */ + public function background_sync_status(): array { + $state = $this->background_state(); + $total = (int) ( $state['total'] ?? 0 ); + $completed = (int) ( $state['completed'] ?? 0 ); + $failed = (int) ( $state['failed'] ?? 0 ); + $background_id = (string) ( $state['id'] ?? '' ); + if ( '' !== $background_id ) { + $action_completed = $this->count_background_actions_for_id( $this->action_status( 'STATUS_COMPLETE', 'complete' ), $background_id ); + $action_failed = $this->count_background_actions_for_id( $this->action_status( 'STATUS_FAILED', 'failed' ), $background_id ); + $completed = max( $completed, $action_completed ); + $failed = max( $failed, $action_failed ); + if ( $completed !== (int) ( $state['completed'] ?? 0 ) || $failed !== (int) ( $state['failed'] ?? 0 ) ) { + $state['completed'] = $completed; + $state['failed'] = $failed; + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_BACKGROUND_SYNC_STATE, $state ); + } + } + $processed = min( $total, $completed + $failed ); + $pending = $this->count_background_actions( $this->action_status( 'STATUS_PENDING', 'pending' ) ); + $running = $this->count_background_actions( $this->action_status( 'STATUS_RUNNING', 'in-progress' ) ); + $action_failed = $this->count_background_actions( $this->action_status( 'STATUS_FAILED', 'failed' ) ); + + if ( $total > 0 && $processed >= $total && 'running' === ( $state['status'] ?? '' ) ) { + $state['status'] = 'complete'; + $state['message'] = $failed > 0 + ? __( 'Automatic background sync finished with failures.', 'progress-agentic-rag' ) + : __( 'Automatic background sync complete.', 'progress-agentic-rag' ); + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_BACKGROUND_SYNC_STATE, $state ); + } + + if ( 'running' === ( $state['status'] ?? '' ) ) { + $state['message'] = sprintf( + /* translators: 1: processed entity count, 2: total queued entity count. */ + __( 'Automatic sync in progress: %1$d of %2$d processed.', 'progress-agentic-rag' ), + $processed, + $total + ); + } + + return [ + 'id' => (string) ( $state['id'] ?? '' ), + 'status' => (string) ( $state['status'] ?? 'idle' ), + 'total' => $total, + 'completed' => $completed, + 'failed' => $failed, + 'processed' => $processed, + 'pending' => $pending, + 'running' => $running, + 'action_failed' => $action_failed, + 'current' => (string) ( $state['current'] ?? __( 'No automatic sync running.', 'progress-agentic-rag' ) ), + 'message' => (string) ( $state['message'] ?? __( 'No automatic sync running.', 'progress-agentic-rag' ) ), + 'percent' => $total > 0 ? min( 100, (int) floor( ( $processed / $total ) * 100 ) ) : 100, + 'is_active' => 'running' === ( $state['status'] ?? '' ) || $pending > 0 || $running > 0, + ]; + } + + /** + * Queue unsynced selected content for automatic background sync. + * + * @return array + */ + public function ensure_automatic_sync( bool $retry_failed = false ): array { + if ( ! $this->settings->get_api_is_reachable() || ! $this->scheduler_available() ) { + return $this->background_sync_status(); + } + + if ( 'running' === ( $this->state()['status'] ?? '' ) || 'running' === ( $this->delete_state()['status'] ?? '' ) ) { + return $this->background_sync_status(); + } + + $status = $this->background_sync_status(); + if ( ! empty( $status['is_active'] ) ) { + return $status; + } + + $state = $this->background_state(); + if ( ! $retry_failed && 'complete' === ( $state['status'] ?? '' ) && (int) ( $state['failed'] ?? 0 ) > 0 ) { + return $status; + } + + $post_types = $this->sanitize_post_types( array_keys( array_filter( $this->settings->get_indexable_post_types() ) ) ); + $scheduled = $this->schedule_background_entities( $this->unindexed_entities( $post_types ) ); + $status = $this->background_sync_status(); + $status['scheduled'] = $scheduled; + + return $status; + } + + /** + * @return array|WP_Error + */ + public function delete_synced_resources(): array|WP_Error { + $current = $this->delete_state(); + if ( 'running' === ( $current['status'] ?? '' ) ) { + return $this->delete_status(); + } + + if ( 'running' === ( $this->state()['status'] ?? '' ) ) { + return new WP_Error( 'progress_agentic_rag_manual_sync_running', __( 'Wait for manual sync to finish before deleting synced resources.', 'progress-agentic-rag' ) ); + } + + if ( ! $this->scheduler_available() ) { + return new WP_Error( 'progress_agentic_rag_scheduler_missing', __( 'Background scheduling is not available.', 'progress-agentic-rag' ) ); + } + + if ( ! $this->settings->get_api_is_reachable() ) { + return new WP_Error( 'progress_agentic_rag_connection_missing', __( 'Validate the Progress Agentic RAG connection before deleting synced resources.', 'progress-agentic-rag' ) ); + } + + $this->cancel_label_reprocess(); + $this->settings->clear_labels(); + + $resources = $this->api_client->get_synced_resources(); + $total = count( $resources ); + $delete_id = wp_generate_uuid4(); + $state = [ + 'id' => $delete_id, + 'status' => 0 === $total ? 'complete' : 'running', + 'total' => $total, + 'deleted' => 0, + 'failed' => 0, + 'current' => 0 === $total ? __( 'No synced resources to delete.', 'progress-agentic-rag' ) : __( 'Waiting for the first resource.', 'progress-agentic-rag' ), + 'message' => 0 === $total ? __( 'No synced resources to delete. Label settings cleared.', 'progress-agentic-rag' ) : sprintf( + /* translators: %d is the number of resources queued for deletion. */ + _n( 'Delete in progress: 0 of %d processed.', 'Delete in progress: 0 of %d processed.', $total, 'progress-agentic-rag' ), + $total + ), + 'started_at' => time(), + 'updated_at' => time(), + ]; + + update_option( SettingsRepository::OPTION_DELETE_SYNC_STATE, $state ); + + $scheduled = 0; + + foreach ( $resources as $resource ) { + $post_id = isset( $resource->post_id ) ? (int) $resource->post_id : 0; + $rid = isset( $resource->nuclia_rid ) ? (string) $resource->nuclia_rid : ''; + + if ( $post_id <= 0 || '' === $rid ) { + $this->mark_delete_failed( __( 'Missing synced resource.', 'progress-agentic-rag' ) ); + continue; + } + + $this->schedule_single_action( + time() + ( $scheduled * 2 ), + self::HOOK_DELETE_RESOURCE, + [ + 'post_id' => $post_id, + 'rid' => $rid, + 'delete_id' => $delete_id, + ], + self::GROUP_DELETE + ); + + $scheduled++; + } + + return $this->delete_status(); + } + + /** + * @return array + */ + public function delete_status(): array { + $state = $this->delete_state(); + $total = (int) ( $state['total'] ?? 0 ); + $done = (int) ( $state['deleted'] ?? 0 ) + (int) ( $state['failed'] ?? 0 ); + $done = min( $total, $done ); + + if ( $total > 0 && $done >= $total && 'running' === ( $state['status'] ?? '' ) ) { + $state['status'] = 'complete'; + $state['message'] = 0 === (int) ( $state['failed'] ?? 0 ) + ? __( 'Synced resources deleted from Progress Agentic RAG. Local mappings and label settings cleared.', 'progress-agentic-rag' ) + : __( 'Some synced resources could not be deleted. Label settings were cleared; local mappings were cleared only for successful deletions.', 'progress-agentic-rag' ); + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_DELETE_SYNC_STATE, $state ); + } + + $current = (string) ( $state['current'] ?? __( 'Waiting to delete synced resources.', 'progress-agentic-rag' ) ); + $message = (string) ( $state['message'] ?? '' ); + + if ( 'running' === ( $state['status'] ?? '' ) ) { + $message = sprintf( + /* translators: 1: processed resource count, 2: total resource count. */ + __( 'Delete in progress: %1$d of %2$d processed.', 'progress-agentic-rag' ), + $done, + $total + ); + + if ( '' === $current ) { + $current = 0 === $done ? __( 'Waiting for the first resource.', 'progress-agentic-rag' ) : __( 'Waiting for the next resource.', 'progress-agentic-rag' ); + } + } + + return [ + 'id' => (string) ( $state['id'] ?? '' ), + 'status' => (string) ( $state['status'] ?? 'idle' ), + 'total' => $total, + 'deleted' => (int) ( $state['deleted'] ?? 0 ), + 'completed' => (int) ( $state['deleted'] ?? 0 ), + 'failed' => (int) ( $state['failed'] ?? 0 ), + 'processed' => $done, + 'current' => $current, + 'message' => $message, + 'percent' => $total > 0 ? min( 100, (int) floor( ( $done / $total ) * 100 ) ) : 100, + ]; + } + + /** + * @return array|WP_Error + */ + public function start_label_reprocess(): array|WP_Error { + if ( ! $this->scheduler_available() ) { + return new WP_Error( 'progress_agentic_rag_scheduler_missing', __( 'Background scheduling is not available.', 'progress-agentic-rag' ) ); + } + + if ( ! $this->settings->get_api_is_reachable() ) { + return new WP_Error( 'progress_agentic_rag_connection_missing', __( 'Validate the Progress Agentic RAG connection before reprocessing labels.', 'progress-agentic-rag' ) ); + } + + if ( 'running' === ( $this->delete_state()['status'] ?? '' ) ) { + return new WP_Error( 'progress_agentic_rag_delete_running', __( 'Wait for synced resource deletion to finish before reprocessing labels.', 'progress-agentic-rag' ) ); + } + + $status = $this->label_reprocess_status(); + if ( ! empty( $status['is_active'] ) ) { + return new WP_Error( 'progress_agentic_rag_label_reprocess_running', __( 'Label reprocessing is already running.', 'progress-agentic-rag' ) ); + } + + $resources = $this->api_client->get_synced_resources(); + $reprocess_id = wp_generate_uuid4(); + $scheduled = 0; + + foreach ( $resources as $resource ) { + $post_id = isset( $resource->post_id ) ? (int) $resource->post_id : 0; + $rid = isset( $resource->nuclia_rid ) ? (string) $resource->nuclia_rid : ''; + + if ( $post_id <= 0 || '' === $rid ) { + continue; + } + + $this->schedule_single_action( + time() + ( $scheduled * 2 ), + self::HOOK_REPROCESS_LABELS, + [ + 'post_id' => $post_id, + 'rid' => $rid, + 'reprocess_id' => $reprocess_id, + ], + self::GROUP_LABEL_REPROCESSOR + ); + + $scheduled++; + } + + $status = $this->label_reprocess_status(); + $status['scheduled'] = $scheduled; + $status['message'] = sprintf( + /* translators: %d is the number of resources scheduled for label reprocessing. */ + _n( 'Scheduled label update for %d synced resource.', 'Scheduled label updates for %d synced resources.', $scheduled, 'progress-agentic-rag' ), + $scheduled + ); + + return $status; + } + + public function cancel_label_reprocess(): array { + $this->scheduler->unschedule_all_actions( self::HOOK_REPROCESS_LABELS, null, self::GROUP_LABEL_REPROCESSOR ); + + $status = $this->label_reprocess_status(); + $status['message'] = __( 'Label reprocessing cancelled.', 'progress-agentic-rag' ); + + return $status; + } + + /** + * @return array + */ + public function label_reprocess_status(): array { + $pending = $this->count_actions( $this->action_status( 'STATUS_PENDING', 'pending' ) ); + $running = $this->count_actions( $this->action_status( 'STATUS_RUNNING', 'in-progress' ) ); + $failed = $this->count_actions( $this->action_status( 'STATUS_FAILED', 'failed' ) ); + + return [ + 'pending' => $pending, + 'running' => $running, + 'failed' => $failed, + 'synced' => $this->count_synced_resources(), + 'is_active' => $pending > 0 || $running > 0, + ]; + } + + public function process_single_post( int $post_id, string $post_type, string $sync_id ): void { + $state = $this->state(); + if ( $sync_id !== ( $state['id'] ?? '' ) ) { + return; + } + + $post = get_post( $post_id ); + if ( ! $post instanceof WP_Post ) { + $this->mark_failed( __( 'Missing entity.', 'progress-agentic-rag' ) ); + return; + } + + $this->update_current( $this->entity_label( $post ) ); + + if ( ! $this->is_indexable_post( $post, $post_type ) || $this->get_resource_id( $post_id ) ) { + $this->mark_completed(); + return; + } + + $result = $this->api_client->index_post( $post ); + if ( is_wp_error( $result ) ) { + $this->mark_failed( $this->entity_label( $post ) . ': ' . $this->error_message( $result ) ); + return; + } + + $this->mark_completed(); + } + + public function process_single_label_reprocess( int $post_id, string $rid, string $reprocess_id ): void { + if ( $post_id <= 0 || '' === $rid || '' === $reprocess_id ) { + return; + } + + if ( ! $this->settings->get_api_is_reachable() ) { + throw new \RuntimeException( 'Progress Agentic RAG connection is not validated.' ); + } + + $post = get_post( $post_id ); + if ( ! $post instanceof WP_Post ) { + return; + } + + $result = $this->api_client->update_resource_labels( $post, $rid ); + if ( is_wp_error( $result ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized by error_message(). + throw new \RuntimeException( $this->error_message( $result ) ); + } + } + + public function process_single_delete( int $post_id, string $rid, string $delete_id ): void { + $state = $this->delete_state(); + if ( $delete_id !== ( $state['id'] ?? '' ) ) { + return; + } + + $post = get_post( $post_id ); + $label = $post instanceof WP_Post ? $this->entity_label( $post ) : sprintf( + /* translators: %d is the WordPress post ID for a synced resource being deleted. */ + __( 'Synced resource for entity #%d', 'progress-agentic-rag' ), + $post_id + ); + $this->update_delete_current( $label ); + + if ( $post_id <= 0 || '' === $rid ) { + $this->mark_delete_failed( __( 'Missing synced resource.', 'progress-agentic-rag' ) ); + return; + } + + $result = $this->api_client->delete_resource( $post_id, $rid ); + if ( is_wp_error( $result ) ) { + $this->mark_delete_failed( $label . ': ' . $this->error_message( $result ) ); + return; + } + + $this->mark_delete_completed(); + } + + public function schedule_background_post_sync( int $post_id, WP_Post $post, bool $update ): void { + unset( $update ); + + if ( ( function_exists( 'wp_is_post_revision' ) && wp_is_post_revision( $post_id ) ) || ( function_exists( 'wp_is_post_autosave' ) && wp_is_post_autosave( $post_id ) ) ) { + return; + } + + if ( ! $this->settings->get_api_is_reachable() || ! $this->scheduler_available() || ! $this->is_selected_post_type( $post->post_type ) ) { + return; + } + + if ( ! $this->is_indexable_post( $post, $post->post_type ) ) { + $rid = $this->get_resource_id( $post_id ); + if ( '' !== $rid ) { + $background_id = $this->queue_background_sync( $this->entity_label( $post ) ); + $this->schedule_single_action( + time() + 5, + self::HOOK_BACKGROUND_DELETE, + [ + 'post_id' => $post_id, + 'rid' => $rid, + 'background_id' => $background_id, + ], + self::GROUP_BACKGROUND + ); + } + + return; + } + + if ( $this->is_background_sync_scheduled( $post_id, $post->post_type ) ) { + return; + } + + $background_id = $this->queue_background_sync( $this->entity_label( $post ) ); + $this->schedule_single_action( + time() + 5, + self::HOOK_BACKGROUND_SYNC, + [ + 'post_id' => $post_id, + 'post_type' => $post->post_type, + 'background_id' => $background_id, + ], + self::GROUP_BACKGROUND + ); + } + + public function schedule_background_attachment_sync( int $post_id ): void { + $post = get_post( $post_id ); + if ( $post instanceof WP_Post ) { + $this->schedule_background_post_sync( $post_id, $post, true ); + } + } + + public function schedule_background_delete( int $post_id, WP_Post $post ): void { + if ( ! $this->settings->get_api_is_reachable() || ! $this->scheduler_available() ) { + return; + } + + $rid = $this->get_resource_id( $post_id ); + if ( '' === $rid ) { + return; + } + + $background_id = $this->queue_background_sync( $this->entity_label( $post ) ); + $this->schedule_single_action( + time() + 5, + self::HOOK_BACKGROUND_DELETE, + [ + 'post_id' => $post_id, + 'rid' => $rid, + 'background_id' => $background_id, + ], + self::GROUP_BACKGROUND + ); + } + + public function process_background_post( int $post_id, string $post_type, string $background_id = '' ): void { + if ( ! $this->settings->get_api_is_reachable() ) { + $this->mark_background_failed( __( 'Progress Agentic RAG connection is not validated.', 'progress-agentic-rag' ), $background_id ); + throw new \RuntimeException( 'Progress Agentic RAG connection is not validated.' ); + } + + $post = get_post( $post_id ); + if ( ! $post instanceof WP_Post || ! $this->is_selected_post_type( $post_type ) ) { + $this->mark_background_completed( $background_id ); + return; + } + + $this->update_background_current( $this->entity_label( $post ), $background_id ); + + if ( ! $this->is_indexable_post( $post, $post_type ) ) { + $rid = $this->get_resource_id( $post_id ); + if ( '' !== $rid ) { + $result = $this->api_client->delete_resource( $post_id, $rid ); + if ( is_wp_error( $result ) ) { + $this->mark_background_failed( $this->entity_label( $post ) . ': ' . $this->error_message( $result ), $background_id ); + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized by error_message(). + throw new \RuntimeException( $this->error_message( $result ) ); + } + } + + $this->mark_background_completed( $background_id ); + return; + } + + $result = $this->api_client->sync_post( $post ); + if ( is_wp_error( $result ) ) { + $this->mark_background_failed( $this->entity_label( $post ) . ': ' . $this->error_message( $result ), $background_id ); + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized by error_message(). + throw new \RuntimeException( $this->error_message( $result ) ); + } + + $this->mark_background_completed( $background_id ); + } + + public function process_background_delete( int $post_id, string $rid, string $background_id = '' ): void { + if ( $post_id <= 0 || '' === $rid ) { + $this->mark_background_completed( $background_id ); + return; + } + + if ( ! $this->settings->get_api_is_reachable() ) { + $this->mark_background_failed( __( 'Progress Agentic RAG connection is not validated.', 'progress-agentic-rag' ), $background_id ); + throw new \RuntimeException( 'Progress Agentic RAG connection is not validated.' ); + } + + $result = $this->api_client->delete_resource( $post_id, $rid ); + if ( is_wp_error( $result ) ) { + $this->mark_background_failed( $this->error_message( $result ), $background_id ); + // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is sanitized by error_message(). + throw new \RuntimeException( $this->error_message( $result ) ); + } + + $this->mark_background_completed( $background_id ); + } + + /** + * @param list $post_types + * + * @return list + */ + private function unindexed_entities( array $post_types ): array { + global $wpdb; + + $entities = []; + $sync_table_name = $this->sync_table_name(); + + foreach ( $post_types as $post_type ) { + $post_status = 'attachment' === $post_type ? 'inherit' : 'publish'; + $limit = 500; + $offset = 0; + + do { + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Reads plugin-owned sync table; values are prepared and table names are escaped. + $results = $wpdb->get_results( + $wpdb->prepare( + "SELECT p.ID FROM {$wpdb->posts} AS p + LEFT JOIN {$sync_table_name} AS idx ON ( p.ID = idx.post_id ) + WHERE idx.post_id IS NULL + AND p.post_type = %s + AND p.post_status = %s + AND p.post_password = %s + LIMIT %d OFFSET %d", + $post_type, + $post_status, + '', + $limit, + $offset + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + + foreach ( $results as $result ) { + $entities[] = [ + 'post_id' => (int) $result->ID, + 'post_type' => $post_type, + ]; + } + + $offset += $limit; + } while ( count( $results ) === $limit ); + } + + return $entities; + } + + /** + * @param list $entities + */ + private function schedule_background_entities( array $entities ): int { + $scheduled = 0; + + foreach ( $entities as $entity ) { + $post_id = (int) $entity['post_id']; + $post_type = (string) $entity['post_type']; + + if ( $this->is_background_sync_scheduled( $post_id, $post_type ) ) { + continue; + } + + $post = get_post( $post_id ); + $label = $post instanceof WP_Post ? $this->entity_label( $post ) : sprintf( + /* translators: %d is the WordPress post ID for an entity being automatically synced. */ + __( 'Entity #%d', 'progress-agentic-rag' ), + $post_id + ); + $background_id = $this->queue_background_sync( $label ); + + $this->schedule_single_action( + time() + ( $scheduled * 2 ), + self::HOOK_BACKGROUND_SYNC, + [ + 'post_id' => $post_id, + 'post_type' => $post_type, + 'background_id' => $background_id, + ], + self::GROUP_BACKGROUND + ); + + $scheduled++; + } + + return $scheduled; + } + + private function is_background_sync_scheduled( int $post_id, string $post_type ): bool { + foreach ( [ $this->action_status( 'STATUS_PENDING', 'pending' ), $this->action_status( 'STATUS_RUNNING', 'in-progress' ) ] as $status ) { + foreach ( $this->scheduler->scheduled_actions( self::HOOK_BACKGROUND_SYNC, self::GROUP_BACKGROUND, $status ) as $action ) { + $args = $this->scheduled_action_args( $action ); + if ( $post_id === (int) ( $args['post_id'] ?? 0 ) && $post_type === (string) ( $args['post_type'] ?? '' ) ) { + return true; + } + } + } + + return false; + } + + /** + * @return array + */ + private function scheduled_action_args( mixed $action ): array { + if ( is_object( $action ) && method_exists( $action, 'get_args' ) ) { + $args = $action->get_args(); + return is_array( $args ) ? $args : []; + } + + if ( is_object( $action ) && isset( $action->args ) && is_array( $action->args ) ) { + return $action->args; + } + + if ( is_array( $action ) && isset( $action['args'] ) && is_array( $action['args'] ) ) { + return $action['args']; + } + + return []; + } + + private function count_indexable_entities( string $post_type ): int { + global $wpdb; + + $post_status = 'attachment' === $post_type ? 'inherit' : 'publish'; + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Counts eligible public posts for the admin sync summary. + $count = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$wpdb->posts} + WHERE post_type = %s + AND post_status = %s + AND post_password = %s", + $post_type, + $post_status, + '' + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + + return $count; + } + + private function count_synced_entities( string $post_type ): int { + global $wpdb; + + $sync_table_name = $this->sync_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Counts plugin-owned sync rows; values are prepared and table names are escaped. + $count = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$sync_table_name} AS idx + INNER JOIN {$wpdb->posts} AS p ON ( p.ID = idx.post_id ) + WHERE p.post_type = %s + AND idx.nuclia_rid IS NOT NULL + AND idx.nuclia_rid != %s", + $post_type, + '' + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + + return $count; + } + + private function count_synced_resources(): int { + global $wpdb; + + $sync_table_name = $this->sync_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Counts plugin-owned sync rows; values are prepared and the table name is escaped. + $count = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM {$sync_table_name} WHERE nuclia_rid IS NOT NULL AND nuclia_rid != %s", + '' + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + + return $count; + } + + /** + * @param list $post_types + * + * @return list + */ + private function sanitize_post_types( array $post_types ): array { + $allowed = array_keys( $this->indexable_post_type_objects() ); + $clean = []; + + foreach ( $post_types as $post_type ) { + $post_type = sanitize_key( $post_type ); + if ( in_array( $post_type, $allowed, true ) ) { + $clean[] = $post_type; + } + } + + return array_values( array_unique( $clean ) ); + } + + private function is_indexable_post( WP_Post $post, string $post_type ): bool { + if ( $post->post_type !== $post_type || '' !== $post->post_password ) { + return false; + } + + if ( 'attachment' === $post_type ) { + return 'inherit' === $post->post_status; + } + + return 'publish' === $post->post_status; + } + + private function is_selected_post_type( string $post_type ): bool { + $selected = $this->settings->get_indexable_post_types(); + + return isset( $selected[ $post_type ] ) && 1 === (int) $selected[ $post_type ]; + } + + private function get_resource_id( int $post_id ): string { + global $wpdb; + + $sync_table_name = $this->sync_table_name(); + + // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Reads plugin-owned sync table; values are prepared and the table name is escaped. + $resource_id = (string) $wpdb->get_var( + $wpdb->prepare( + "SELECT nuclia_rid FROM {$sync_table_name} WHERE post_id = %d", + $post_id + ) + ); + // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter + + return $resource_id; + } + + private function sync_table_name(): string { + global $wpdb; + + return esc_sql( $wpdb->prefix . SettingsRepository::SYNC_TABLE_NAME ); + } + + private function error_message( WP_Error $error ): string { + return sanitize_text_field( $error->get_error_message() ); + } + + private function entity_label( WP_Post $post ): string { + $title = wp_strip_all_tags( get_the_title( $post ) ); + + if ( '' === $title ) { + $title = sprintf( + /* translators: %d is the WordPress post ID for an entity with no title. */ + __( 'Entity #%d', 'progress-agentic-rag' ), + (int) $post->ID + ); + } + + return sprintf( '%s: %s', $this->entity_type_label( $post->post_type ), $title ); + } + + private function update_current( string $current ): void { + $state = $this->state(); + $state['current'] = $current; + $state['updated_at'] = time(); + + update_option( SettingsRepository::OPTION_MANUAL_SYNC_STATE, $state ); + } + + private function mark_completed(): void { + $state = $this->state(); + $state['completed'] = (int) ( $state['completed'] ?? 0 ) + 1; + $state['updated_at'] = time(); + + update_option( SettingsRepository::OPTION_MANUAL_SYNC_STATE, $state ); + } + + private function mark_failed( string $current ): void { + $state = $this->state(); + $state['failed'] = (int) ( $state['failed'] ?? 0 ) + 1; + $state['current'] = $current; + $state['updated_at'] = time(); + + update_option( SettingsRepository::OPTION_MANUAL_SYNC_STATE, $state ); + } + + private function update_delete_current( string $current ): void { + $state = $this->delete_state(); + $state['current'] = $current; + $state['updated_at'] = time(); + + update_option( SettingsRepository::OPTION_DELETE_SYNC_STATE, $state ); + } + + private function mark_delete_completed(): void { + $state = $this->delete_state(); + $state['deleted'] = (int) ( $state['deleted'] ?? 0 ) + 1; + $state['updated_at'] = time(); + + update_option( SettingsRepository::OPTION_DELETE_SYNC_STATE, $state ); + } + + private function mark_delete_failed( string $current ): void { + $state = $this->delete_state(); + $state['failed'] = (int) ( $state['failed'] ?? 0 ) + 1; + $state['current'] = $current; + $state['updated_at'] = time(); + + update_option( SettingsRepository::OPTION_DELETE_SYNC_STATE, $state ); + } + + private function queue_background_sync( string $current ): string { + $state = $this->background_state(); + + if ( 'running' !== ( $state['status'] ?? '' ) ) { + $state = [ + 'id' => wp_generate_uuid4(), + 'status' => 'running', + 'total' => 0, + 'completed' => 0, + 'failed' => 0, + 'current' => $current, + 'message' => __( 'Automatic background sync queued.', 'progress-agentic-rag' ), + 'started_at' => time(), + 'updated_at' => time(), + ]; + } + + $state['total'] = (int) ( $state['total'] ?? 0 ) + 1; + $state['current'] = $current; + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_BACKGROUND_SYNC_STATE, $state ); + + return (string) $state['id']; + } + + private function update_background_current( string $current, string $background_id ): void { + if ( '' === $background_id ) { + return; + } + + $state = $this->background_state(); + if ( $background_id !== (string) ( $state['id'] ?? '' ) ) { + return; + } + + $state['current'] = $current; + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_BACKGROUND_SYNC_STATE, $state ); + } + + private function mark_background_completed( string $background_id ): void { + if ( '' === $background_id ) { + return; + } + + $state = $this->background_state(); + if ( $background_id !== (string) ( $state['id'] ?? '' ) ) { + return; + } + + if ( (int) ( $state['completed'] ?? 0 ) + (int) ( $state['failed'] ?? 0 ) < (int) ( $state['total'] ?? 0 ) ) { + $state['completed'] = (int) ( $state['completed'] ?? 0 ) + 1; + } + + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_BACKGROUND_SYNC_STATE, $state ); + } + + private function mark_background_failed( string $current, string $background_id ): void { + if ( '' === $background_id ) { + return; + } + + $state = $this->background_state(); + if ( $background_id !== (string) ( $state['id'] ?? '' ) ) { + return; + } + + if ( (int) ( $state['completed'] ?? 0 ) + (int) ( $state['failed'] ?? 0 ) < (int) ( $state['total'] ?? 0 ) ) { + $state['failed'] = (int) ( $state['failed'] ?? 0 ) + 1; + } + + $state['current'] = $current; + $state['updated_at'] = time(); + update_option( SettingsRepository::OPTION_BACKGROUND_SYNC_STATE, $state ); + } + + /** + * @return array + */ + private function state(): array { + $state = get_option( SettingsRepository::OPTION_MANUAL_SYNC_STATE, [] ); + + return is_array( $state ) ? $state : []; + } + + private function delete_state(): array { + $state = get_option( SettingsRepository::OPTION_DELETE_SYNC_STATE, [] ); + + return is_array( $state ) ? $state : []; + } + + private function background_state(): array { + $state = get_option( SettingsRepository::OPTION_BACKGROUND_SYNC_STATE, [] ); + + return is_array( $state ) ? $state : []; + } + + public function scheduler_available(): bool { + return $this->scheduler->available(); + } + + /** + * @return array + */ + public function scheduler_status(): array { + return [ + 'available' => $this->scheduler->available(), + 'backend' => $this->scheduler->backend(), + 'label' => $this->scheduler->backend_label(), + 'message' => $this->scheduler->backend_message(), + ]; + } + + private function schedule_single_action( int $timestamp, string $hook, array $args, string $group ): void { + $this->scheduler->schedule_single_action( $timestamp, $hook, $args, $group ); + } + + private function count_actions( string $status ): int { + return $this->scheduler->count_actions( self::HOOK_REPROCESS_LABELS, self::GROUP_LABEL_REPROCESSOR, $status ); + } + + private function count_background_actions( string $status ): int { + return $this->scheduler->count_actions( self::HOOK_BACKGROUND_SYNC, self::GROUP_BACKGROUND, $status ) + + $this->scheduler->count_actions( self::HOOK_BACKGROUND_DELETE, self::GROUP_BACKGROUND, $status ); + } + + private function count_background_actions_for_id( string $status, string $background_id ): int { + $count = 0; + + foreach ( [ self::HOOK_BACKGROUND_SYNC, self::HOOK_BACKGROUND_DELETE ] as $hook ) { + foreach ( $this->scheduler->scheduled_actions( $hook, self::GROUP_BACKGROUND, $status ) as $action ) { + $args = $this->scheduled_action_args( $action ); + if ( $background_id === (string) ( $args['background_id'] ?? '' ) ) { + $count++; + } + } + } + + return $count; + } + + private function action_status( string $constant, string $fallback ): string { + if ( class_exists( '\ActionScheduler_Store' ) && defined( 'ActionScheduler_Store::' . $constant ) ) { + return (string) constant( 'ActionScheduler_Store::' . $constant ); + } + + return $fallback; + } + + private function post_type_label( string $post_type ): string { + $post_type_object = get_post_type_object( $post_type ); + + return null !== $post_type_object && isset( $post_type_object->labels->name ) ? (string) $post_type_object->labels->name : $post_type; + } + + private function entity_type_label( string $post_type ): string { + $post_type_object = get_post_type_object( $post_type ); + if ( null !== $post_type_object && isset( $post_type_object->labels->singular_name ) && '' !== (string) $post_type_object->labels->singular_name ) { + return (string) $post_type_object->labels->singular_name; + } + + return $this->post_type_label( $post_type ); + } + + /** + * @return array + */ + private function indexable_post_type_objects(): array { + $post_types = get_post_types( [ 'public' => true ], 'objects' ); + + if ( ! isset( $post_types['attachment'] ) ) { + $attachment = get_post_type_object( 'attachment' ); + if ( null !== $attachment ) { + $post_types['attachment'] = $attachment; + } + } + + ksort( $post_types ); + + return $post_types; + } +} diff --git a/src/Indexing/Scheduler.php b/src/Indexing/Scheduler.php new file mode 100644 index 0000000..f0db47a --- /dev/null +++ b/src/Indexing/Scheduler.php @@ -0,0 +1,147 @@ +backend(); + } + + /** + * Get a human-readable backend label. + * + * @return string + */ + public function backend_label(): string { + switch ( $this->backend() ) { + case 'action_scheduler': + return __( 'Action Scheduler', 'progress-agentic-rag' ); + default: + return __( 'Unavailable', 'progress-agentic-rag' ); + } + } + + /** + * Get a backend status message for admin UI. + * + * @return string + */ + public function backend_message(): string { + switch ( $this->backend() ) { + case 'action_scheduler': + return __( 'Background jobs are using Action Scheduler.', 'progress-agentic-rag' ); + default: + return __( 'Background jobs are unavailable because Action Scheduler could not be loaded.', 'progress-agentic-rag' ); + } + } + + /** + * Schedule a single background action. + * + * @param int $timestamp Unix timestamp. + * @param string $hook Action hook. + * @param array $args Action arguments. + * @param string $group Action Scheduler group. + * @return bool + */ + public function schedule_single_action( int $timestamp, string $hook, array $args, string $group ): bool { + if ( function_exists( 'as_schedule_single_action' ) ) { + as_schedule_single_action( $timestamp, $hook, $args, $group ); + return true; + } + + return false; + } + + /** + * Unschedule all matching background actions. + * + * @param string $hook Action hook. + * @param ?array $args Action arguments. + * @param string $group Action Scheduler group. + * @return void + */ + public function unschedule_all_actions( string $hook, ?array $args, string $group ): void { + if ( function_exists( 'as_unschedule_all_actions' ) ) { + as_unschedule_all_actions( $hook, $args, $group ); + } + } + + /** + * Count scheduled actions by status. + * + * @param string $hook Action hook. + * @param string $group Action Scheduler group. + * @param string $status Action status. + * @return int + */ + public function count_actions( string $hook, string $group, string $status ): int { + if ( ! function_exists( 'as_get_scheduled_actions' ) ) { + return 0; + } + + return count( + as_get_scheduled_actions( + array( + 'hook' => $hook, + 'group' => $group, + 'status' => $status, + 'per_page' => -1, + ) + ) + ); + } + + /** + * Get scheduled actions by status. + * + * @param string $hook Action hook. + * @param string $group Action Scheduler group. + * @param string $status Action status. + * @return array + */ + public function scheduled_actions( string $hook, string $group, string $status ): array { + if ( ! function_exists( 'as_get_scheduled_actions' ) ) { + return []; + } + + return as_get_scheduled_actions( + array( + 'hook' => $hook, + 'group' => $group, + 'status' => $status, + 'per_page' => -1, + ) + ); + } +} diff --git a/src/Infrastructure/Requirements.php b/src/Infrastructure/Requirements.php new file mode 100644 index 0000000..65200f8 --- /dev/null +++ b/src/Infrastructure/Requirements.php @@ -0,0 +1,58 @@ +=' ); + } + + public static function is_wp_supported(): bool { + $wp_version = (string) ( $GLOBALS['wp_version'] ?? '0' ); + + return version_compare( $wp_version, PROGRESS_AGENTIC_RAG_MIN_WP_VERSION, '>=' ); + } + + public static function render_admin_notice(): void { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $messages = []; + + if ( ! self::is_php_supported() ) { + $messages[] = sprintf( + /* translators: 1: required PHP version, 2: current PHP version. */ + __( 'Progress Agentic RAG requires PHP %1$s or later. Your site is running PHP %2$s.', 'progress-agentic-rag' ), + PROGRESS_AGENTIC_RAG_MIN_PHP_VERSION, + PHP_VERSION + ); + } + + if ( ! self::is_wp_supported() ) { + $messages[] = sprintf( + /* translators: 1: required WordPress version, 2: current WordPress version. */ + __( 'Progress Agentic RAG requires WordPress %1$s or later. Your site is running WordPress %2$s.', 'progress-agentic-rag' ), + PROGRESS_AGENTIC_RAG_MIN_WP_VERSION, + (string) ( $GLOBALS['wp_version'] ?? 'unknown' ) + ); + } + + if ( [] === $messages ) { + return; + } + + require_once PROGRESS_AGENTIC_RAG_PATH . 'templates/admin/requirements-notice.php'; + } +} diff --git a/src/Lifecycle/Activator.php b/src/Lifecycle/Activator.php new file mode 100644 index 0000000..2299e89 --- /dev/null +++ b/src/Lifecycle/Activator.php @@ -0,0 +1,44 @@ +add_defaults(); + + self::create_sync_table(); + ProxyController::add_rewrite_rules(); + flush_rewrite_rules( false ); + } + + private static function create_sync_table(): void { + global $wpdb; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + $table_name = $wpdb->prefix . SettingsRepository::SYNC_TABLE_NAME; + $charset_collate = $wpdb->get_charset_collate(); + $sql = "CREATE TABLE $table_name ( + id mediumint(9) NOT NULL AUTO_INCREMENT, + post_id mediumint(9) NOT NULL, + nuclia_rid varchar(255) NOT NULL, + nuclia_seqid varchar(255), + PRIMARY KEY (id), + INDEX idx_post_id (post_id), + INDEX idx_nuclia_rid (nuclia_rid) + ) $charset_collate;"; + + dbDelta( $sql ); + } +} diff --git a/src/Lifecycle/Deactivator.php b/src/Lifecycle/Deactivator.php new file mode 100644 index 0000000..d54c9e7 --- /dev/null +++ b/src/Lifecycle/Deactivator.php @@ -0,0 +1,16 @@ +settings = new SettingsRepository(); + $api_client = new ApiClient( $this->settings ); + $scheduler = new Scheduler(); + $this->manual_sync = new ManualSync( $this->settings, $api_client, $scheduler ); + $this->admin_page = new AdminPage( $this->settings, $this->manual_sync, $api_client ); + $this->proxy_controller = new ProxyController( $this->settings ); + $this->search_widget = new SearchWidget( $this->settings ); + } + + public static function instance(): self { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + public function register(): void { + $this->proxy_controller->register(); + $this->search_widget->register(); + $this->manual_sync->register(); + $this->admin_page->register(); + } +} diff --git a/src/Proxy/ProxyController.php b/src/Proxy/ProxyController.php new file mode 100644 index 0000000..cabc17e --- /dev/null +++ b/src/Proxy/ProxyController.php @@ -0,0 +1,378 @@ + $vars Registered query vars. + * @return array + */ + public static function query_vars( array $vars ): array { + $vars[] = self::QUERY_VAR_ENABLED; + $vars[] = self::QUERY_VAR_ZONE; + $vars[] = self::QUERY_VAR_PATH; + + return $vars; + } + + public static function proxy_url( string $zone ): string { + $zone = sanitize_title( $zone ); + if ( '' === $zone ) { + return ''; + } + + $path = '' === get_option( 'permalink_structure', '' ) ? 'index.php/nuclia-proxy/' . $zone : 'nuclia-proxy/' . $zone; + + return home_url( $path ); + } + + public function disable_canonical_redirect( mixed $redirect_url, string $request_url ): mixed { + return str_contains( $request_url, '/nuclia-proxy/' ) ? false : $redirect_url; + } + + public function handle_path_request(): void { + $request = $this->path_request(); + if ( '' === $request['zone'] ) { + return; + } + + $this->send_cors_headers(); + + $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( (string) wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET'; + $body = ( 'GET' !== $method && 'HEAD' !== $method ) ? (string) file_get_contents( 'php://input' ) : ''; + + $result = $this->execute( + $request['zone'], + $request['path'], + $method, + $this->client_query_string(), + isset( $_SERVER['CONTENT_TYPE'] ) ? sanitize_text_field( (string) wp_unslash( $_SERVER['CONTENT_TYPE'] ) ) : '', + isset( $_SERVER['HTTP_ACCEPT'] ) ? sanitize_text_field( (string) wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) : '', + $body, + $this->passthrough_headers() + ); + + status_header( $result['status'] ); + foreach ( $result['headers'] as $name => $value ) { + header( (string) $name . ': ' . (string) $value, false ); + } + + echo $result['body']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + exit; + } + + /** + * @param array $passthrough_headers Allow-listed client request headers. + * @return array{status: int, headers: array, body: string} + */ + private function execute( string $zone, string $path, string $method, string $query_string, string $content_type, string $accept, string $body, array $passthrough_headers ): array { + $zone = strtolower( trim( $zone ) ); + if ( '' === $zone || ! preg_match( '/^[a-z0-9-]+$/', $zone ) ) { + return $this->error( 400, 'progress_agentic_rag_proxy_invalid_zone', __( 'Invalid Progress Agentic RAG zone.', 'progress-agentic-rag' ) ); + } + + if ( ! in_array( $method, [ 'GET', 'POST', 'OPTIONS' ], true ) ) { + return $this->error( 405, 'progress_agentic_rag_proxy_method_not_allowed', __( 'Request method is not allowed.', 'progress-agentic-rag' ) ); + } + + if ( 'OPTIONS' === $method ) { + return [ 'status' => 200, 'headers' => [], 'body' => '' ]; + } + + $token = $this->settings->get_string( SettingsRepository::OPTION_TOKEN ); + if ( '' === $token ) { + return $this->error( 500, 'progress_agentic_rag_proxy_missing_token', __( 'Progress Agentic RAG Service token is not configured.', 'progress-agentic-rag' ) ); + } + + $normalized_path = $this->normalize_path( $path ); + if ( null === $normalized_path ) { + return $this->error( 400, 'progress_agentic_rag_proxy_invalid_path', __( 'Invalid Progress Agentic RAG proxy path.', 'progress-agentic-rag' ) ); + } + + $remote_url = sprintf( + 'https://%s.rag.progress.cloud/%s', + rawurlencode( $zone ), + $normalized_path + ); + + $query_string = $this->clean_query_string( $query_string ); + if ( '' !== $query_string ) { + $remote_url .= '?' . $query_string; + } + + $headers = array_filter( + [ + 'X-NUCLIA-SERVICEACCOUNT' => $this->is_ephemeral_request( $query_string ) ? null : 'Bearer ' . $token, + 'Content-Type' => '' !== $content_type ? $content_type : null, + 'Accept' => '' !== $accept ? $accept : null, + 'Accept-Encoding' => 'identity', + ] + ); + + foreach ( $passthrough_headers as $name => $value ) { + $headers[ $name ] = $value; + } + + $args = [ + 'method' => $method, + 'timeout' => 30, + 'redirection' => 5, + 'headers' => $headers, + ]; + + if ( '' !== $body && 'GET' !== $method ) { + $args['body'] = $body; + } + + $response = wp_remote_request( $remote_url, $args ); + if ( is_wp_error( $response ) ) { + return $this->error( 502, 'progress_agentic_rag_proxy_upstream_failed', sanitize_text_field( $response->get_error_message() ) ); + } + + $status = (int) wp_remote_retrieve_response_code( $response ); + + return [ + 'status' => $status, + 'headers' => $this->response_headers( $response, $remote_url, $status ), + 'body' => (string) wp_remote_retrieve_body( $response ), + ]; + } + + /** + * @return array{zone: string, path: string} + */ + private function path_request(): array { + if ( '1' === get_query_var( self::QUERY_VAR_ENABLED, '' ) ) { + $zone = (string) get_query_var( self::QUERY_VAR_ZONE, '' ); + if ( '' !== $zone ) { + return [ + 'zone' => $zone, + 'path' => ltrim( (string) get_query_var( self::QUERY_VAR_PATH, '' ), '/' ), + ]; + } + } + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw URI is parsed, validated, and normalized before use. + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; + $request_path = (string) wp_parse_url( $request_uri, PHP_URL_PATH ); + $home_path = trim( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ), '/' ); + if ( '' !== $home_path && str_starts_with( $request_path, '/' . $home_path . '/' ) ) { + $request_path = substr( $request_path, strlen( $home_path ) + 1 ); + } + if ( str_starts_with( $request_path, '/index.php/' ) ) { + $request_path = substr( $request_path, strlen( '/index.php' ) ); + } + + if ( ! str_starts_with( $request_path, '/nuclia-proxy/' ) ) { + return [ 'zone' => '', 'path' => '' ]; + } + + $suffix = substr( $request_path, strlen( '/nuclia-proxy/' ) ); + $parts = explode( '/', $suffix, 2 ); + + return [ + 'zone' => (string) ( $parts[0] ?? '' ), + 'path' => isset( $parts[1] ) ? ltrim( $parts[1], '/' ) : '', + ]; + } + + private function normalize_path( string $path ): ?string { + $path = ltrim( $path, '/' ); + $decoded_path = rawurldecode( $path ); + + if ( + '' === $path + || str_contains( $path, '..' ) + || str_contains( $decoded_path, '..' ) + || str_contains( $path, '\\' ) + || str_contains( $decoded_path, '\\' ) + || str_contains( $path, "\r" ) + || str_contains( $decoded_path, "\r" ) + || str_contains( $path, "\n" ) + || str_contains( $decoded_path, "\n" ) + ) { + return null; + } + + if ( str_starts_with( $path, 'api/' ) ) { + return $path; + } + + if ( str_starts_with( $path, 'v1/' ) ) { + return 'api/' . $path; + } + + return 'api/' . $path; + } + + private function client_query_string(): string { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw query string is allow-list cleaned before proxying. + $query_string = isset( $_SERVER['QUERY_STRING'] ) ? (string) wp_unslash( $_SERVER['QUERY_STRING'] ) : ''; + + if ( '' === $query_string && isset( $_SERVER['REQUEST_URI'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw URI is parsed and the query is allow-list cleaned before proxying. + $parsed = wp_parse_url( (string) wp_unslash( $_SERVER['REQUEST_URI'] ), PHP_URL_QUERY ); + $query_string = is_string( $parsed ) ? $parsed : ''; + } + + return $query_string; + } + + private function clean_query_string( string $query_string ): string { + if ( '' === $query_string ) { + return ''; + } + + $strip_keys = [ + self::QUERY_VAR_ENABLED, + self::QUERY_VAR_ZONE, + self::QUERY_VAR_PATH, + 'rest_route', + ]; + $parts = []; + + foreach ( explode( '&', $query_string ) as $part ) { + $key = rawurldecode( explode( '=', $part, 2 )[0] ?? '' ); + if ( '' !== $part && ! in_array( $key, $strip_keys, true ) ) { + $parts[] = $part; + } + } + + return implode( '&', $parts ); + } + + /** + * @return array + */ + private function passthrough_headers(): array { + $allowed = [ + 'x-synchronous', + 'x-show-consumption', + 'x-ndb-client', + 'range', + 'if-range', + ]; + $headers = []; + + foreach ( $allowed as $name ) { + $server_key = 'HTTP_' . strtoupper( str_replace( '-', '_', $name ) ); + if ( isset( $_SERVER[ $server_key ] ) ) { + $value = sanitize_text_field( (string) wp_unslash( $_SERVER[ $server_key ] ) ); + if ( '' !== $value ) { + $headers[ $name ] = $value; + } + } + } + + return $headers; + } + + private function send_cors_headers(): void { + if ( headers_sent() ) { + return; + } + + $origin = isset( $_SERVER['HTTP_ORIGIN'] ) ? sanitize_text_field( (string) wp_unslash( $_SERVER['HTTP_ORIGIN'] ) ) : ''; + header( 'Access-Control-Allow-Origin: ' . ( '' !== $origin ? $origin : '*' ) ); + header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' ); + header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, x-synchronous, X-Synchronous, x-ndb-client, X-NDB-Client, Range, If-Range' ); + header( 'Access-Control-Expose-Headers: X-Nuclia-Upstream-Status, X-Nuclia-Upstream-Content-Type, X-Nuclia-Upstream-URL, Nuclia-Learning-Id, X-NUCLIA-TRACE-ID' ); + header( 'Vary: Origin', false ); + } + + private function is_ephemeral_request( string $query_string ): bool { + return (bool) preg_match( '/(^|&)eph-token=/i', $query_string ); + } + + /** + * @param array $response Upstream response. + * @return array + */ + private function response_headers( array $response, string $remote_url, int $status ): array { + $headers = []; + $skip = [ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade', 'content-encoding' ]; + + foreach ( wp_remote_retrieve_headers( $response ) as $name => $value ) { + $name = (string) $name; + if ( is_array( $value ) ) { + $value = implode( ', ', array_filter( $value, 'is_scalar' ) ); + } + $value = str_replace( [ "\r", "\n" ], '', (string) $value ); + + if ( '' === $value || ! preg_match( '/^[A-Za-z0-9-]+$/', $name ) || in_array( strtolower( $name ), $skip, true ) ) { + continue; + } + + $headers[ $name ] = $value; + } + + $headers['X-Nuclia-Upstream-Status'] = (string) $status; + $content_type = $headers['Content-Type'] ?? $headers['content-type'] ?? ''; + if ( '' !== $content_type ) { + $headers['X-Nuclia-Upstream-Content-Type'] = $content_type; + } + $headers['X-Nuclia-Upstream-URL'] = substr( str_replace( [ "\r", "\n" ], '', (string) preg_replace( '/(?<=[?&])eph-token=[^&]*/i', 'eph-token=REDACTED', $remote_url ) ), 0, 2048 ); + + return $headers; + } + + /** + * @return array{status: int, headers: array, body: string} + */ + private function error( int $status, string $code, string $message ): array { + return [ + 'status' => $status, + 'headers' => [ 'Content-Type' => 'application/json; charset=' . get_option( 'blog_charset', 'UTF-8' ) ], + 'body' => (string) wp_json_encode( + [ + 'code' => $code, + 'message' => $message, + ] + ), + ]; + } +} diff --git a/src/Settings/SettingsRepository.php b/src/Settings/SettingsRepository.php new file mode 100644 index 0000000..bb36795 --- /dev/null +++ b/src/Settings/SettingsRepository.php @@ -0,0 +1,335 @@ + + */ + public function defaults(): array { + return [ + self::OPTION_ZONE => '', + self::OPTION_TOKEN => '', + self::OPTION_KBID => '', + self::OPTION_ACCOUNT_ID => '', + self::OPTION_API_IS_REACHABLE => 'no', + self::OPTION_TAXONOMY_LABEL_MAP => [], + self::OPTION_LABELSETS_CACHE => [ + 'fetched_at' => 0, + 'labelsets' => [], + 'labels' => [], + ], + self::OPTION_INDEXABLE_POST_TYPES => [ + 'post' => 1, + 'page' => 1, + ], + self::OPTION_MANUAL_SYNC_STATE => [], + self::OPTION_DELETE_SYNC_STATE => [], + self::OPTION_BACKGROUND_SYNC_STATE => [], + ]; + } + + public function add_defaults(): void { + foreach ( $this->defaults() as $option_name => $default_value ) { + add_option( $option_name, $default_value ); + } + } + + public function get_string( string $option_name ): string { + $defaults = $this->defaults(); + $value = (string) get_option( $option_name, $defaults[ $option_name ] ?? '' ); + + if ( self::OPTION_ZONE === $option_name ) { + return self::normalize_zone( $value ); + } + + if ( self::OPTION_KBID === $option_name ) { + return self::normalize_kbid( $value ); + } + + if ( self::OPTION_TOKEN === $option_name ) { + return self::normalize_token( $value ); + } + + return $value; + } + + public function has_token(): bool { + return '' !== $this->get_string( self::OPTION_TOKEN ); + } + + public function get_api_is_reachable(): bool { + return 'yes' === get_option( self::OPTION_API_IS_REACHABLE, 'no' ); + } + + public function set_api_is_reachable( bool $flag ): void { + update_option( self::OPTION_API_IS_REACHABLE, $flag ? 'yes' : 'no' ); + } + + /** + * @return array + */ + public function get_indexable_post_types(): array { + $value = get_option( self::OPTION_INDEXABLE_POST_TYPES, $this->defaults()[ self::OPTION_INDEXABLE_POST_TYPES ] ); + + return is_array( $value ) ? $value : []; + } + + /** + * @return array + */ + public function get_taxonomy_label_map(): array { + $value = get_option( self::OPTION_TAXONOMY_LABEL_MAP, $this->defaults()[ self::OPTION_TAXONOMY_LABEL_MAP ] ); + + return is_array( $value ) ? $this->sanitize_taxonomy_label_map( $value ) : []; + } + + /** + * @return array + */ + public function get_labelsets_cache(): array { + $value = get_option( self::OPTION_LABELSETS_CACHE, $this->defaults()[ self::OPTION_LABELSETS_CACHE ] ); + + return is_array( $value ) ? $value : $this->defaults()[ self::OPTION_LABELSETS_CACHE ]; + } + + /** + * @param list $labelsets Labelset names. + */ + public function set_labelsets_cache( array $labelsets ): void { + $cache = $this->get_labelsets_cache(); + $labels = is_array( $cache['labels'] ?? null ) ? $cache['labels'] : []; + + update_option( + self::OPTION_LABELSETS_CACHE, + [ + 'fetched_at' => time(), + 'labelsets' => array_values( $labelsets ), + 'labels' => $labels, + ] + ); + } + + /** + * @param list $labelsets Labelset names. + * @param array> $labels_map Labels keyed by labelset. + */ + public function set_labelsets_cache_with_labels( array $labelsets, array $labels_map ): void { + $labels = []; + foreach ( $labels_map as $labelset => $label_list ) { + if ( is_string( $labelset ) && is_array( $label_list ) ) { + $labels[ $labelset ] = array_values( $label_list ); + } + } + + update_option( + self::OPTION_LABELSETS_CACHE, + [ + 'fetched_at' => time(), + 'labelsets' => array_values( $labelsets ), + 'labels' => $labels, + ] + ); + } + + /** + * @return list + */ + public function get_labelset_labels_cache( string $labelset ): array { + $cache = $this->get_labelsets_cache(); + $labels = is_array( $cache['labels'] ?? null ) ? $cache['labels'] : []; + + return is_array( $labels[ $labelset ] ?? null ) ? $labels[ $labelset ] : []; + } + + /** + * @param list $labels Labels for the labelset. + */ + public function set_labelset_labels_cache( string $labelset, array $labels ): void { + $cache = $this->get_labelsets_cache(); + $labelsets = is_array( $cache['labelsets'] ?? null ) ? $cache['labelsets'] : []; + $stored_labels = is_array( $cache['labels'] ?? null ) ? $cache['labels'] : []; + + $stored_labels[ $labelset ] = array_values( $labels ); + + update_option( + self::OPTION_LABELSETS_CACHE, + [ + 'fetched_at' => time(), + 'labelsets' => $labelsets, + 'labels' => $stored_labels, + ] + ); + } + + /** + * @param array $value Raw taxonomy mapping form data. + */ + public function update_taxonomy_label_map( array $value ): void { + update_option( self::OPTION_TAXONOMY_LABEL_MAP, $this->sanitize_taxonomy_label_map( $value ) ); + } + + /** + * @param array $value Raw taxonomy mapping form data. + * + * @return array + */ + public function sanitize_taxonomy_label_map( array $value ): array { + $sanitized = []; + + foreach ( $value as $taxonomy => $config ) { + $taxonomy = sanitize_key( (string) $taxonomy ); + if ( ! taxonomy_exists( $taxonomy ) || ! is_array( $config ) ) { + continue; + } + + $labelset = isset( $config['labelset'] ) ? sanitize_text_field( (string) $config['labelset'] ) : ''; + $terms = is_array( $config['terms'] ?? null ) ? $config['terms'] : []; + $fallback = is_array( $config['fallback'] ?? null ) ? $config['fallback'] : []; + $fallback_labelset = isset( $fallback['labelset'] ) ? sanitize_text_field( (string) $fallback['labelset'] ) : ''; + $fallback_labels = $fallback['labels'] ?? []; + $clean_terms = []; + $clean_fallback_labels = []; + + if ( ! is_array( $fallback_labels ) ) { + $fallback_labels = '' !== $fallback_labels ? [ (string) $fallback_labels ] : []; + } + + foreach ( $terms as $term_id => $labels ) { + $term_id = (int) $term_id; + if ( $term_id <= 0 || ! term_exists( $term_id, $taxonomy ) ) { + continue; + } + + if ( ! is_array( $labels ) ) { + $labels = '' !== $labels ? [ (string) $labels ] : []; + } + + $clean_labels = []; + foreach ( $labels as $label ) { + $label = sanitize_text_field( (string) $label ); + if ( '' !== $label ) { + $clean_labels[] = $label; + } + } + + $clean_labels = array_values( array_unique( $clean_labels ) ); + if ( ! empty( $clean_labels ) ) { + $clean_terms[ $term_id ] = $clean_labels; + } + } + + foreach ( $fallback_labels as $label ) { + $label = sanitize_text_field( (string) $label ); + if ( '' !== $label ) { + $clean_fallback_labels[] = $label; + } + } + + $clean_fallback_labels = array_values( array_unique( $clean_fallback_labels ) ); + $has_term_mapping = '' !== $labelset && ! empty( $clean_terms ); + $has_fallback = '' !== $fallback_labelset && ! empty( $clean_fallback_labels ); + + if ( $has_term_mapping || $has_fallback ) { + $sanitized[ $taxonomy ] = []; + if ( $has_term_mapping ) { + $sanitized[ $taxonomy ]['labelset'] = $labelset; + $sanitized[ $taxonomy ]['terms'] = $clean_terms; + } + if ( $has_fallback ) { + $sanitized[ $taxonomy ]['fallback'] = [ + 'labelset' => $fallback_labelset, + 'labels' => $clean_fallback_labels, + ]; + } + } + } + + return $sanitized; + } + + public function clear_labels(): void { + update_option( self::OPTION_TAXONOMY_LABEL_MAP, $this->defaults()[ self::OPTION_TAXONOMY_LABEL_MAP ] ); + update_option( self::OPTION_LABELSETS_CACHE, $this->defaults()[ self::OPTION_LABELSETS_CACHE ] ); + } + + public function update_connection_settings( string $zone, string $kbid, string $account_id, string $token ): void { + update_option( self::OPTION_ZONE, self::normalize_zone( $zone ) ); + update_option( self::OPTION_KBID, self::normalize_kbid( $kbid ) ); + update_option( self::OPTION_ACCOUNT_ID, $account_id ); + + if ( '' !== $token ) { + update_option( self::OPTION_TOKEN, self::normalize_token( $token ) ); + } + } + + private static function normalize_zone( string $zone ): string { + $zone = strtolower( trim( $zone ) ); + $host = (string) wp_parse_url( $zone, PHP_URL_HOST ); + + if ( '' === $host ) { + $host = (string) strtok( $zone, '/?#' ); + } + + $host = (string) preg_replace( '/:\d+$/', '', $host ); + if ( str_ends_with( $host, '.rag.progress.cloud' ) ) { + $host = substr( $host, 0, -strlen( '.rag.progress.cloud' ) ); + } + + return $host; + } + + private static function normalize_kbid( string $kbid ): string { + $kbid = trim( $kbid ); + + if ( 1 === preg_match( '~/kb/([^/?#]+)~', $kbid, $matches ) ) { + return $matches[1]; + } + + return trim( $kbid, " \t\n\r\0\x0B/" ); + } + + private static function normalize_token( string $token ): string { + $token = trim( $token ); + + if ( 1 === preg_match( '/^Bearer\s+/i', $token ) ) { + $token = trim( (string) preg_replace( '/^Bearer\s+/i', '', $token, 1 ) ); + } + + return $token; + } + + /** + * @param list $post_types Post type names selected for indexing. + */ + public function update_indexable_post_types( array $post_types ): void { + update_option( self::OPTION_INDEXABLE_POST_TYPES, array_fill_keys( $post_types, 1 ) ); + } + + /** + * @return list + */ + public function option_names(): array { + return array_keys( $this->defaults() ); + } +} diff --git a/templates/admin/connection-settings.php b/templates/admin/connection-settings.php new file mode 100644 index 0000000..1890e17 --- /dev/null +++ b/templates/admin/connection-settings.php @@ -0,0 +1,50 @@ + +
+
+
+ +

+
+
+
+ +
+
+ +
+
+
+
+ + +
+ + + + +
+
+ +
+
+
diff --git a/templates/admin/indexation.php b/templates/admin/indexation.php new file mode 100644 index 0000000..7786d71 --- /dev/null +++ b/templates/admin/indexation.php @@ -0,0 +1,140 @@ + +
+
+
+ +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+

+ +
+ +
+ + +
+ $post_type ) : ?> + + +
+
+
+
+ +

+
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+ + + +
+
+
+
+ + diff --git a/templates/admin/manual-sync-modal.php b/templates/admin/manual-sync-modal.php new file mode 100644 index 0000000..f1f935e --- /dev/null +++ b/templates/admin/manual-sync-modal.php @@ -0,0 +1,48 @@ + + diff --git a/templates/admin/notice.php b/templates/admin/notice.php new file mode 100644 index 0000000..43b00b5 --- /dev/null +++ b/templates/admin/notice.php @@ -0,0 +1,12 @@ + +
+

+
diff --git a/templates/admin/page-close.php b/templates/admin/page-close.php new file mode 100644 index 0000000..82a2200 --- /dev/null +++ b/templates/admin/page-close.php @@ -0,0 +1,11 @@ + + + diff --git a/templates/admin/page-open.php b/templates/admin/page-open.php new file mode 100644 index 0000000..f1cbca8 --- /dev/null +++ b/templates/admin/page-open.php @@ -0,0 +1,26 @@ + +
+
+
+
+

+
+
+ + + + + + + + +
+
diff --git a/templates/admin/requirements-notice.php b/templates/admin/requirements-notice.php new file mode 100644 index 0000000..ca154b8 --- /dev/null +++ b/templates/admin/requirements-notice.php @@ -0,0 +1,10 @@ + +

diff --git a/templates/admin/tabs.php b/templates/admin/tabs.php new file mode 100644 index 0000000..ee7ef78 --- /dev/null +++ b/templates/admin/tabs.php @@ -0,0 +1,20 @@ + + diff --git a/templates/admin/taxonomy-labeling.php b/templates/admin/taxonomy-labeling.php new file mode 100644 index 0000000..a87827b --- /dev/null +++ b/templates/admin/taxonomy-labeling.php @@ -0,0 +1,57 @@ + +
+
+
+ +

+
+
+
+ + + +
+
+

+

+

+ + +

+

+ +

+
+
+ + + + + +
+
+
+ +
+
+
diff --git a/templates/admin/taxonomy-mapping.php b/templates/admin/taxonomy-mapping.php new file mode 100644 index 0000000..636cee4 --- /dev/null +++ b/templates/admin/taxonomy-mapping.php @@ -0,0 +1,144 @@ + +
+
+

+

+
+
+ +

+ + +
+ + + +
+ +
+ $config ) : ?> + +
+
+
+

labels->name ); ?>

+ +
+ +
+ + +

+ + + +

+ + + + + + + + + + + term_id ] ?? []; + if ( ! is_array( $term_labels ) ) { + $term_labels = '' !== $term_labels ? [ (string) $term_labels ] : []; + } + ?> + + + + + + +
name ); ?> +
+ + + + + + + +
+
+ + +
+

+ +
+ + + + + + + +
+
+
+ +
+ +
+
+ diff --git a/templates/frontend/search-widget.php b/templates/frontend/search-widget.php new file mode 100644 index 0000000..332d402 --- /dev/null +++ b/templates/frontend/search-widget.php @@ -0,0 +1,19 @@ + +
+ + +
diff --git a/tests/phpunit/AdminPageTest.php b/tests/phpunit/AdminPageTest.php new file mode 100644 index 0000000..988c615 --- /dev/null +++ b/tests/phpunit/AdminPageTest.php @@ -0,0 +1,595 @@ + (object) [ + 'labels' => (object) [ + 'name' => 'Posts', + 'singular_name' => 'Post', + ], + ], + 'page' => (object) [ + 'labels' => (object) [ + 'name' => 'Pages', + 'singular_name' => 'Page', + ], + ], + 'attachment' => (object) [ + 'labels' => (object) [ + 'name' => 'Media', + 'singular_name' => 'Media', + ], + ], + ]; + $GLOBALS['wpdb'] = new ProgressAgenticRagTestWpdb(); + $_GET = []; + $_POST = []; + + update_option( SettingsRepository::OPTION_ZONE, 'europe-1' ); + update_option( SettingsRepository::OPTION_KBID, 'kb-123' ); + update_option( SettingsRepository::OPTION_TOKEN, 'secret-token' ); + update_option( SettingsRepository::OPTION_API_IS_REACHABLE, 'yes' ); + } + + public function test_register_add_menu_and_enqueue_assets_localize_admin_state(): void { + $GLOBALS['progress_agentic_rag_test_taxonomies']['category'] = (object) [ + 'labels' => (object) [ 'name' => 'Categories' ], + ]; + $GLOBALS['progress_agentic_rag_test_terms']['category'] = [ + 7 => (object) [ + 'term_id' => 7, + 'name' => 'Support', + ], + ]; + $admin = $this->admin_page(); + $admin->register(); + + $actions = array_column( $GLOBALS['progress_agentic_rag_test_actions'], 'hook_name' ); + + self::assertContains( 'admin_menu', $actions ); + self::assertContains( 'admin_enqueue_scripts', $actions ); + self::assertContains( 'wp_ajax_progress_agentic_rag_manual_sync_start', $actions ); + self::assertContains( 'wp_ajax_progress_agentic_rag_get_labelset_labels', $actions ); + + $admin->add_menu(); + $admin->enqueue_assets( 'wrong-hook' ); + + self::assertSame( [], $GLOBALS['progress_agentic_rag_test_enqueued_styles'] ); + + $admin->enqueue_assets( $GLOBALS['progress_agentic_rag_test_menu_pages'][0]['hook'] ); + + self::assertSame( 'progress-agentic-rag-admin', $GLOBALS['progress_agentic_rag_test_enqueued_styles'][0]['handle'] ); + self::assertSame( 'progress-agentic-rag-admin', $GLOBALS['progress_agentic_rag_test_enqueued_scripts'][0]['handle'] ); + self::assertSame( 'progressAgenticRagAdmin', $GLOBALS['progress_agentic_rag_test_localized_scripts'][0]['object_name'] ); + self::assertArrayHasKey( 'initialSyncStatus', $GLOBALS['progress_agentic_rag_test_localized_scripts'][0]['l10n'] ); + self::assertArrayHasKey( 'mapping', $GLOBALS['progress_agentic_rag_test_localized_scripts'][0]['l10n'] ); + self::assertSame( 'Support', $GLOBALS['progress_agentic_rag_test_localized_scripts'][0]['l10n']['mapping']['taxonomies']['category']['terms'][0]['name'] ); + self::assertStringNotContainsString( 'secret-token', wp_json_encode( $GLOBALS['progress_agentic_rag_test_localized_scripts'][0]['l10n'] ) ); + } + + public function test_render_connection_tab_does_not_output_saved_token(): void { + $_GET['tab'] = 'connection'; + + ob_start(); + $this->admin_page()->render(); + $output = ob_get_clean(); + + self::assertStringContainsString( 'Connection settings', $output ); + self::assertStringContainsString( 'Token saved', $output ); + self::assertStringContainsString( 'Stored token is never displayed', $output ); + self::assertStringNotContainsString( 'secret-token', $output ); + } + + public function test_render_taxonomy_labeling_tab_and_notices(): void { + $GLOBALS['progress_agentic_rag_test_taxonomies']['category'] = (object) [ + 'labels' => (object) [ 'name' => 'Categories' ], + ]; + $GLOBALS['progress_agentic_rag_test_terms']['category'] = [ + 7 => (object) [ + 'term_id' => 7, + 'name' => 'Support', + ], + ]; + update_option( + SettingsRepository::OPTION_TAXONOMY_LABEL_MAP, + [ + 'category' => [ + 'labelset' => 'Topic', + 'terms' => [ + 7 => [ 'Docs' ], + ], + 'fallback' => [ + 'labelset' => 'Audience', + 'labels' => [ 'General' ], + ], + ], + ] + ); + $GLOBALS['progress_agentic_rag_test_http_responses'] = [ + [ + 'response' => [ 'code' => 200 ], + 'body' => '{"labelsets":["Topic","Audience"]}', + ], + [ + 'response' => [ 'code' => 200 ], + 'body' => '{"labels":["Docs"]}', + ], + [ + 'response' => [ 'code' => 200 ], + 'body' => '{"labels":["General"]}', + ], + ]; + $_GET = [ + 'tab' => 'taxonomy-labeling', + 'progress-agentic-rag-updated' => 'true', + 'connection-status' => 'connected', + ]; + + ob_start(); + $this->admin_page()->render(); + $output = ob_get_clean(); + + self::assertStringContainsString( 'Taxonomy labeling', $output ); + self::assertStringContainsString( 'Categories', $output ); + self::assertStringContainsString( 'Connection settings saved.', $output ); + } + + public function test_render_defaults_to_indexation_for_unknown_tab_and_blocks_unauthorized_user(): void { + $_GET['tab'] = 'unknown'; + + ob_start(); + $this->admin_page()->render(); + $output = ob_get_clean(); + + self::assertStringContainsString( 'Indexation', $output ); + self::assertStringContainsString( 'Sync manually', $output ); + + $GLOBALS['progress_agentic_rag_test_current_user_can'] = false; + ob_start(); + $this->admin_page()->render(); + + self::assertSame( '', ob_get_clean() ); + } + + #[RunInSeparateProcess] + #[PreserveGlobalState( false )] + public function test_render_indexation_running_states_and_notices(): void { + update_option( + SettingsRepository::OPTION_MANUAL_SYNC_STATE, + [ + 'id' => 'sync-running', + 'status' => 'running', + 'total' => 5, + 'completed' => 2, + 'failed' => 0, + ] + ); + update_option( + SettingsRepository::OPTION_DELETE_SYNC_STATE, + [ + 'id' => 'delete-running', + 'status' => 'running', + 'total' => 4, + 'deleted' => 1, + 'failed' => 0, + ] + ); + $_GET = [ + 'progress-agentic-rag-updated' => 'true', + 'connection-status' => 'failed', + ]; + $GLOBALS['progress_agentic_rag_test_current_user_can'] = true; + + ob_start(); + $this->admin_page()->render(); + $output = ob_get_clean(); + + self::assertStringContainsString( 'Sync in progress (2/5, 40%)', $output ); + self::assertStringContainsString( 'Delete in progress (1/4, 25%)', $output ); + self::assertStringContainsString( 'could not validate the connection', $output ); + } + + #[RunInSeparateProcess] + #[PreserveGlobalState( false )] + public function test_render_default_updated_notice(): void { + $_GET = [ + 'progress-agentic-rag-updated' => 'true', + ]; + $GLOBALS['progress_agentic_rag_test_current_user_can'] = true; + + ob_start(); + $this->admin_page()->render(); + + self::assertStringContainsString( 'Settings saved.', ob_get_clean() ); + } + + public function test_admin_helpers_cover_connection_labels_and_mapping_edges(): void { + update_option( SettingsRepository::OPTION_API_IS_REACHABLE, 'no' ); + update_option( SettingsRepository::OPTION_TOKEN, '' ); + + $admin = $this->admin_page(); + $status_ref = new ReflectionMethod( $admin, 'connection_status_label' ); + $status_ref->setAccessible( true ); + + self::assertSame( 'Not configured', $status_ref->invoke( $admin ) ); + + update_option( SettingsRepository::OPTION_TOKEN, 'secret-token' ); + + self::assertSame( 'Connection failed', $status_ref->invoke( $admin ) ); + + $GLOBALS['progress_agentic_rag_test_http_responses'][] = [ + 'response' => [ + 'code' => 200, + ], + 'body' => '{"labels":["General"]}', + ]; + + $labels_ref = new ReflectionMethod( $admin, 'labelset_labels_for_mapping' ); + $labels_ref->setAccessible( true ); + + self::assertSame( + [ + 'Audience' => [ 'General' ], + ], + $labels_ref->invoke( + $admin, + [ + 'bad' => 'skip', + 'category' => [ + 'fallback' => [ + 'labelset' => 'Audience', + ], + ], + ] + ) + ); + } + + public function test_save_connection_settings_normalizes_validates_and_redirects(): void { + $_POST = [ + SettingsRepository::OPTION_ZONE => 'https://europe-1.rag.progress.cloud/api/v1/kb/kb-123', + SettingsRepository::OPTION_KBID => 'https://europe-1.rag.progress.cloud/api/v1/kb/kb-123', + SettingsRepository::OPTION_ACCOUNT_ID => 'account-123', + SettingsRepository::OPTION_TOKEN => 'Bearer new-token', + ]; + + try { + $this->admin_page()->save_connection_settings(); + self::fail( 'Expected redirect.' ); + } catch ( ProgressAgenticRagTestRedirect $redirect ) { + self::assertStringContainsString( 'tab=connection', $redirect->location ); + self::assertStringContainsString( 'connection-status=connected', $redirect->location ); + } + + self::assertSame( 'europe-1', get_option( SettingsRepository::OPTION_ZONE ) ); + self::assertSame( 'kb-123', get_option( SettingsRepository::OPTION_KBID ) ); + self::assertSame( 'new-token', get_option( SettingsRepository::OPTION_TOKEN ) ); + self::assertSame( 'yes', get_option( SettingsRepository::OPTION_API_IS_REACHABLE ) ); + self::assertSame( 'https://europe-1.rag.progress.cloud/api/v1/kb/kb-123/resources', $GLOBALS['progress_agentic_rag_test_http_requests'][0]['url'] ); + self::assertSame( 'Bearer new-token', $GLOBALS['progress_agentic_rag_test_http_requests'][0]['args']['headers']['X-NUCLIA-SERVICEACCOUNT'] ); + } + + public function test_save_handlers_die_for_unauthorized_user(): void { + $GLOBALS['progress_agentic_rag_test_current_user_can'] = false; + + $this->expectException( ProgressAgenticRagTestWpDie::class ); + + $this->admin_page()->save_connection_settings(); + } + + public function test_other_save_handlers_die_for_unauthorized_user(): void { + $GLOBALS['progress_agentic_rag_test_current_user_can'] = false; + + foreach ( [ 'save_indexation_settings', 'save_taxonomy_labeling_settings' ] as $method ) { + try { + $this->admin_page()->{$method}(); + self::fail( 'Expected wp_die.' ); + } catch ( ProgressAgenticRagTestWpDie $exception ) { + self::assertStringContainsString( 'permission', $exception->getMessage() ); + } + } + } + + public function test_save_connection_settings_marks_failed_validation(): void { + $GLOBALS['progress_agentic_rag_test_http_post_responses'][] = [ + 'response' => [ + 'code' => 500, + ], + 'body' => '{}', + ]; + $_POST = [ + SettingsRepository::OPTION_ZONE => 'bad zone', + SettingsRepository::OPTION_KBID => 'kb-123', + SettingsRepository::OPTION_ACCOUNT_ID => '', + SettingsRepository::OPTION_TOKEN => 'new-token', + ]; + + try { + $this->admin_page()->save_connection_settings(); + self::fail( 'Expected redirect.' ); + } catch ( ProgressAgenticRagTestRedirect $redirect ) { + self::assertStringContainsString( 'connection-status=failed', $redirect->location ); + } + + self::assertSame( 'no', get_option( SettingsRepository::OPTION_API_IS_REACHABLE ) ); + + $GLOBALS['progress_agentic_rag_test_http_post_responses'][] = new WP_Error( 'http_failed', 'Network failed' ); + $_POST = [ + SettingsRepository::OPTION_ZONE => 'europe-1', + SettingsRepository::OPTION_KBID => 'kb-123', + SettingsRepository::OPTION_ACCOUNT_ID => '', + SettingsRepository::OPTION_TOKEN => 'new-token', + ]; + + try { + $this->admin_page()->save_connection_settings(); + self::fail( 'Expected redirect.' ); + } catch ( ProgressAgenticRagTestRedirect $redirect ) { + self::assertStringContainsString( 'connection-status=failed', $redirect->location ); + } + } + + public function test_save_indexation_settings_keeps_allowed_unique_post_types(): void { + $_POST = [ + 'indexable_post_types' => [ 'post', 'page', 'bad