diff --git a/.github/workflows/create-buld-url.yml b/.github/workflows/create-buld-url.yml index 4652def..3e58e0a 100644 --- a/.github/workflows/create-buld-url.yml +++ b/.github/workflows/create-buld-url.yml @@ -15,7 +15,7 @@ jobs: git-sha-8: ${{ steps.retrieve-git-sha-8.outputs.sha8 }} steps: - name: Check out source files - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Get Composer Cache Directory id: composer-cache run: | diff --git a/.github/workflows/test-php.yml b/.github/workflows/test-php.yml index 46998ce..eb235eb 100644 --- a/.github/workflows/test-php.yml +++ b/.github/workflows/test-php.yml @@ -11,12 +11,12 @@ jobs: if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name steps: - name: Setup PHP version - uses: shivammathur/setup-php@v1 + uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: simplexml - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Get Composer Cache Directory id: composer-cache run: | @@ -41,10 +41,10 @@ jobs: - name: Setup PHP version uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: simplexml, mysql - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Get Composer Cache Directory id: composer-cache run: | @@ -60,3 +60,49 @@ jobs: run: composer install --prefer-dist --no-progress --no-suggest - name: PHPStan Static Analysis run: composer phpstan + + phpunit: + name: PHPUnit + runs-on: ubuntu-latest + if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306/tcp + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - name: Setup PHP version + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: simplexml, mysql + tools: phpunit-polyfills + - name: Checkout source code + uses: actions/checkout@v2 + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Setup Composer cache + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: Install Subversion + run: sudo apt-get update && sudo apt-get install -y subversion + - name: Install composer + run: composer install --prefer-dist --no-progress --no-suggest + - name: Install npm + run: npm ci + - name: Run build + run: npm run build + - name: Install WordPress Test Suite + run: | + bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:${{ job.services.mysql.ports['3306'] }} + - name: PHPUnit tests + run: composer run-script phpunit diff --git a/artifact/hyve-lite.zip b/artifact/hyve-lite.zip index 40f2c83..fde1785 100644 Binary files a/artifact/hyve-lite.zip and b/artifact/hyve-lite.zip differ diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100644 index 0000000..878881f --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,152 @@ +#!/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]+$ ]]; 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-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/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 + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet 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/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 + +} + +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 + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json index 07af59f..fcc8339 100644 --- a/composer.json +++ b/composer.json @@ -25,21 +25,32 @@ "scripts": { "lint": "phpcs --standard=phpcs.xml", "format": "phpcbf --standard=phpcs.xml", - "phpstan": "phpstan analyse --memory-limit 2G" + "phpstan": "phpstan analyse --memory-limit 2G", + "phpunit": "phpunit" }, "require-dev": { "phpstan/phpstan": "^1.10", "szepeviktor/phpstan-wordpress": "^1.3", "wp-coding-standards/wpcs": "^3.0", "automattic/vipwpcs": "^3.0", - "phpcompatibility/php-compatibility": "^9.3" + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^9.6", + "yoast/phpunit-polyfills": "^2.0" }, "config": { + "platform": { + "php": "8.1" + }, "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "php-http/discovery": true } }, "require": { - "codeinwp/themeisle-sdk": "^3.3" + "codeinwp/themeisle-sdk": "^3.3", + "hkulekci/qdrant": "^0.5.7", + "symfony/http-client": "^6.4", + "nyholm/psr7": "^1.8", + "yethee/tiktoken": "^0.6.0" } } diff --git a/composer.lock b/composer.lock index e2fd4d8..8e6565e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "899cea0899e04b0a7aeb6b06127115f5", + "content-hash": "77e9f3d27f9d721b4a5ca06950f052f3", "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.24", + "version": "3.3.30", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "b9ba5697b150eddacb4aa482b363d9f459f822c5" + "reference": "801a07604a297f02de3067948176f91b4c84bd8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/b9ba5697b150eddacb4aa482b363d9f459f822c5", - "reference": "b9ba5697b150eddacb4aa482b363d9f459f822c5", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/801a07604a297f02de3067948176f91b4c84bd8a", + "reference": "801a07604a297f02de3067948176f91b4c84bd8a", "shasum": "" }, "require-dev": { @@ -42,100 +42,106 @@ ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.24" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.30" }, - "time": "2024-06-25T09:38:29+00:00" - } - ], - "packages-dev": [ + "time": "2024-09-10T23:53:07+00:00" + }, { - "name": "automattic/vipwpcs", - "version": "3.0.1", + "name": "hkulekci/qdrant", + "version": "v0.5.7", "source": { "type": "git", - "url": "https://github.com/Automattic/VIP-Coding-Standards.git", - "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8" + "url": "https://github.com/hkulekci/qdrant-php.git", + "reference": "0a9219682b7ec485f4528a233f3e36a1cb60d2b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/2b1d206d81b74ed999023cffd924f862ff2753c8", - "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8", + "url": "https://api.github.com/repos/hkulekci/qdrant-php/zipball/0a9219682b7ec485f4528a233f3e36a1cb60d2b5", + "reference": "0a9219682b7ec485f4528a233f3e36a1cb60d2b5", "shasum": "" }, "require": { - "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.2.1", - "phpcsstandards/phpcsutils": "^1.0.11", - "sirbrillig/phpcs-variable-analysis": "^2.11.18", - "squizlabs/php_codesniffer": "^3.9.2", - "wp-coding-standards/wpcs": "^3.1.0" + "php": "^8.1", + "php-http/discovery": "^1.19", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0|^2.0", + "psr/log": "^1.0|^2.0|^3.0", + "webmozart/assert": "^1.11" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcompatibility/php-compatibility": "^9", - "phpcsstandards/phpcsdevtools": "^1.0", - "phpunit/phpunit": "^4 || ^5 || ^6 || ^7 || ^8 || ^9" + "mockery/mockery": "^1.5", + "nyholm/psr7": "^1.8@dev", + "php-http/mock-client": "1.x-dev", + "phpunit/php-code-coverage": "^10.1@dev", + "phpunit/phpunit": "^10.0", + "symfony/http-client": "^6.4.3|^7.1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Qdrant\\": "src/" + } }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Contributors", - "homepage": "https://github.com/Automattic/VIP-Coding-Standards/graphs/contributors" + "name": "Haydar Kulekci", + "email": "haydarkulekci@gmail.com" } ], - "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress VIP minimum coding conventions", - "keywords": [ - "phpcs", - "standards", - "static analysis", - "wordpress" - ], + "description": "PHP Client for Qdrant", "support": { - "issues": "https://github.com/Automattic/VIP-Coding-Standards/issues", - "source": "https://github.com/Automattic/VIP-Coding-Standards", - "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki" + "issues": "https://github.com/hkulekci/qdrant-php/issues", + "source": "https://github.com/hkulekci/qdrant-php/tree/v0.5.7" }, - "time": "2024-05-10T20:31:09+00:00" + "time": "2024-09-27T08:52:26+00:00" }, { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.0.0", + "name": "nyholm/psr7", + "version": "1.8.2", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "4be43904336affa5c2f70744a348312336afd0da" + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", - "reference": "4be43904336affa5c2f70744a348312336afd0da", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" }, "require-dev": { - "composer/composer": "*", - "ext-json": "*", - "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.3.1", - "phpcompatibility/php-compatibility": "^9.0", - "yoast/phpunit-polyfills": "^1.0" + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "branch-alias": { + "dev-master": "1.8-dev" + } }, "autoload": { "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "Nyholm\\Psr7\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -144,374 +150,2966 @@ ], "authors": [ { - "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" } ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcbf", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" + "psr-17", + "psr-7" ], "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "source": "https://github.com/PHPCSStandards/composer-installer" + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, - "time": "2023-01-05T11:28:13+00:00" + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" }, { - "name": "php-stubs/wordpress-stubs", - "version": "v6.5.3", + "name": "php-http/discovery", + "version": "1.20.0", "source": { "type": "git", - "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "e611a83292d02055a25f83291a98fadd0c21e092" + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/e611a83292d02055a25f83291a98fadd0c21e092", - "reference": "e611a83292d02055a25f83291a98fadd0c21e092", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", "shasum": "" }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "nikic/php-parser": "^4.13", - "php": "^7.4 || ~8.0.0", - "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "5.3", - "phpstan/phpstan": "^1.10.49", - "phpunit/phpunit": "^9.5", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.11" + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" }, - "suggest": { - "paragonie/sodium_compat": "Pure PHP implementation of libsodium", - "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "WordPress function and class declaration stubs for static analysis.", - "homepage": "https://github.com/php-stubs/wordpress-stubs", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "PHPStan", - "static analysis", - "wordpress" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.5.3" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2024-05-08T02:12:31+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", + "name": "psr/http-client", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.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": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/http-client", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^3.4.1", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.4.12" + }, + "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": "2024-09-20T08:21:33+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "20414d96f391677bf80078aa55baece78b82647d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.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": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.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": "2024-04-18T09:32:20+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "yethee/tiktoken", + "version": "0.6.0", + "source": { + "type": "git", + "url": "https://github.com/yethee/tiktoken-php.git", + "reference": "780b87464b631ad3086660126eb6d4fb19d9fcfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yethee/tiktoken-php/zipball/780b87464b631ad3086660126eb6d4fb19d9fcfe", + "reference": "780b87464b631ad3086660126eb6d4fb19d9fcfe", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/service-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "mikey179/vfsstream": "^1.6.11", + "phpbench/phpbench": "^1.2", + "phpunit/phpunit": "^10.5.20", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "5.25.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Yethee\\Tiktoken\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP version of tiktoken", + "keywords": [ + "bpe", + "decode", + "encode", + "openai", + "tiktoken", + "tokenizer" + ], + "support": { + "issues": "https://github.com/yethee/tiktoken-php/issues", + "source": "https://github.com/yethee/tiktoken-php/tree/0.6.0" + }, + "time": "2024-08-22T12:57:43+00:00" + } + ], + "packages-dev": [ + { + "name": "automattic/vipwpcs", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/Automattic/VIP-Coding-Standards.git", + "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/2b1d206d81b74ed999023cffd924f862ff2753c8", + "reference": "2b1d206d81b74ed999023cffd924f862ff2753c8", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsextra": "^1.2.1", + "phpcsstandards/phpcsutils": "^1.0.11", + "sirbrillig/phpcs-variable-analysis": "^2.11.18", + "squizlabs/php_codesniffer": "^3.9.2", + "wp-coding-standards/wpcs": "^3.1.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9", + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4 || ^5 || ^6 || ^7 || ^8 || ^9" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/Automattic/VIP-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress VIP minimum coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/Automattic/VIP-Coding-Standards/issues", + "source": "https://github.com/Automattic/VIP-Coding-Standards", + "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki" + }, + "time": "2024-05-10T20:31:09+00:00" + }, + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "4be43904336affa5c2f70744a348312336afd0da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/4be43904336affa5c2f70744a348312336afd0da", + "reference": "4be43904336affa5c2f70744a348312336afd0da", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^2.0 || ^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "*", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "time": "2023-01-05T11:28:13+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^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/2.0.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:23:10+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "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.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.3.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + }, + "time": "2024-10-08T18:51:32+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": "php-stubs/wordpress-stubs", + "version": "v6.6.2", + "source": { + "type": "git", + "url": "https://github.com/php-stubs/wordpress-stubs.git", + "reference": "f50fd7ed45894d036e4fef9ab7e5bbbaff6a30cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f50fd7ed45894d036e4fef9ab7e5bbbaff6a30cc", + "reference": "f50fd7ed45894d036e4fef9ab7e5bbbaff6a30cc", + "shasum": "" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "nikic/php-parser": "^4.13", + "php": "^7.4 || ^8.0", + "php-stubs/generator": "^0.8.3", + "phpdocumentor/reflection-docblock": "^5.4.1", + "phpstan/phpstan": "^1.10.49", + "phpunit/phpunit": "^9.5", + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" + }, + "suggest": { + "paragonie/sodium_compat": "Pure PHP implementation of libsodium", + "symfony/polyfill-php80": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "WordPress function and class declaration stubs for static analysis.", + "homepage": "https://github.com/php-stubs/wordpress-stubs", + "keywords": [ + "PHPStan", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/php-stubs/wordpress-stubs/issues", + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.6.2" + }, + "time": "2024-09-30T07:10:48+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": "phpcsstandards/phpcsextra", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.0.9", + "squizlabs/php_codesniffer": "^3.8.0" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2023-12-08T16:49:07+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.0.12", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", + "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcsstandards/phpcsdevcs": "^1.1.6", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + } + ], + "time": "2024-05-20T13:34:27+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.12.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae", + "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-10-06T15:03:59+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.32", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.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 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", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:23:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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", + "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/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "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", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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", + "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/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.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": "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/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.21", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.5.0 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", + "sebastian/version": "^3.0.2" + }, + "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" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "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/9.6.21" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-09-19T10:50:18+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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 for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "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/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-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/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.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": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.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/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.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 for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-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/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "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": "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/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^9.3" }, - "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": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", - "homepage": "https://github.com/wimg", - "role": "lead" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + "name": "Adam Harvey", + "email": "aharvey@php.net" } ], - "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" - ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { - "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", - "source": "https://github.com/PHPCompatibility/PHPCompatibility" + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, - "time": "2019-12-27T09:44:58+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" }, { - "name": "phpcsstandards/phpcsextra", - "version": "1.2.1", + "name": "sebastian/resource-operations", + "version": "3.0.4", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489" + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", - "reference": "11d387c6642b6e4acaf0bd9bf5203b8cca1ec489", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { - "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.0.9", - "squizlabs/php_codesniffer": "^3.8.0" + "php": ">=7.3" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", - "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, - "type": "phpcodesniffer-standard", + "type": "library", "extra": { "branch-alias": { - "dev-stable": "1.x-dev", - "dev-develop": "1.x-dev" + "dev-main": "3.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" } ], - "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", - "keywords": [ - "PHP_CodeSniffer", - "phpcbf", - "phpcodesniffer-standard", - "phpcs", - "standards", - "static analysis" - ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", - "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", - "source": "https://github.com/PHPCSStandards/PHPCSExtra" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" } ], - "time": "2023-12-08T16:49:07+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { - "name": "phpcsstandards/phpcsutils", - "version": "1.0.12", + "name": "sebastian/type", + "version": "3.2.1", "source": { "type": "git", - "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/87b233b00daf83fb70f40c9a28692be017ea7c6c", - "reference": "87b233b00daf83fb70f40c9a28692be017ea7c6c", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.10.0 || 4.0.x-dev@dev" + "php": ">=7.3" }, "require-dev": { - "ext-filter": "*", - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcsstandards/phpcsdevcs": "^1.1.6", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0" + "phpunit/phpunit": "^9.5" }, - "type": "phpcodesniffer-standard", + "type": "library", "extra": { "branch-alias": { - "dev-stable": "1.x-dev", - "dev-develop": "1.x-dev" + "dev-master": "3.2-dev" } }, "autoload": { "classmap": [ - "PHPCSUtils/" + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" } ], - "description": "A suite of utility functions for use with PHP_CodeSniffer", - "homepage": "https://phpcsutils.com/", - "keywords": [ - "PHP_CodeSniffer", - "phpcbf", - "phpcodesniffer-standard", - "phpcs", - "phpcs3", - "standards", - "static analysis", - "tokens", - "utility" - ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "docs": "https://phpcsutils.com/", - "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", - "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", - "source": "https://github.com/PHPCSStandards/PHPCSUtils" + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", + "url": "https://github.com/sebastianbergmann", "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" } ], - "time": "2024-05-20T13:34:27+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { - "name": "phpstan/phpstan", - "version": "1.11.7", + "name": "sebastian/version", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/52d2bbfdcae7f895915629e4694e9497d0f8e28d", - "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", "shasum": "" }, "require": { - "php": "^7.2|^8.0" - }, - "conflict": { - "phpstan/phpstan-shim": "*" + "php": ">=7.3" }, - "bin": [ - "phpstan", - "phpstan.phar" - ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, "autoload": { - "files": [ - "bootstrap.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], - "description": "PHPStan - PHP Static Analysis Tool", - "keywords": [ - "dev", - "static analysis" + "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": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" }, "funding": [ { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", + "url": "https://github.com/sebastianbergmann", "type": "github" } ], - "time": "2024-07-06T11:17:41+00:00" + "time": "2020-09-28T06:39:44+00:00" }, { "name": "sirbrillig/phpcs-variable-analysis", @@ -573,16 +3171,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.1", + "version": "3.10.3", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877" + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/8f90f7a53ce271935282967f53d0894f8f1ff877", - "reference": "8f90f7a53ce271935282967f53d0894f8f1ff877", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", + "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", "shasum": "" }, "require": { @@ -649,24 +3247,24 @@ "type": "open_collective" } ], - "time": "2024-05-22T21:24:41+00:00" + "time": "2024-09-18T10:38:58+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.30.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -709,7 +3307,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" }, "funding": [ { @@ -725,20 +3323,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v1.3.4", + "version": "v1.3.5", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "891d0767855a32c886a439efae090408cc1fa156" + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/891d0767855a32c886a439efae090408cc1fa156", - "reference": "891d0767855a32c886a439efae090408cc1fa156", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/7f8cfe992faa96b6a33bbd75c7bace98864161e7", + "reference": "7f8cfe992faa96b6a33bbd75c7bace98864161e7", "shasum": "" }, "require": { @@ -753,7 +3351,8 @@ "php-parallel-lint/php-parallel-lint": "^1.1", "phpstan/phpstan-strict-rules": "^1.2", "phpunit/phpunit": "^8.0 || ^9.0", - "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^0.8" + "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.0", + "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, "suggest": { "swissspidy/phpstan-no-private": "Detect usage of internal core functions, classes and methods" @@ -785,9 +3384,59 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.4" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v1.3.5" + }, + "time": "2024-06-28T22:27:19+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" }, - "time": "2024-03-21T16:32:59+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -854,6 +3503,69 @@ } ], "time": "2024-03-25T16:39:00+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "562f449a2ec8ab92fe7b30d94da9622c7b1345fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/562f449a2ec8ab92fe7b30d94da9622c7b1345fe", + "reference": "562f449a2ec8ab92fe7b30d94da9622c7b1345fe", + "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": "3.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": "2024-09-06T22:38:28+00:00" } ], "aliases": [], @@ -863,5 +3575,8 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], + "platform-overrides": { + "php": "8.1" + }, "plugin-api-version": "2.6.0" } diff --git a/hyve-lite.php b/hyve-lite.php index 88cf89d..ff591d4 100644 --- a/hyve-lite.php +++ b/hyve-lite.php @@ -21,6 +21,21 @@ die; } +if ( version_compare( PHP_VERSION, '8.1', '<' ) ) { + add_action( + 'admin_notices', + function () { + ?> +
+

+
+ get_endpoint(); - $routes = array( - 'settings' => array( - array( + $routes = [ + 'settings' => [ + [ 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_settings' ), - ), - array( + 'callback' => [ $this, 'get_settings' ], + ], + [ 'methods' => \WP_REST_Server::CREATABLE, - 'args' => array( - 'data' => array( + 'args' => [ + 'data' => [ 'required' => true, 'type' => 'object', 'validate_callback' => function ( $param ) { return is_array( $param ); }, - ), - ), - 'callback' => array( $this, 'update_settings' ), - ), - ), - 'data' => array( - array( + ], + ], + 'callback' => [ $this, 'update_settings' ], + ], + ], + 'data' => [ + [ 'methods' => \WP_REST_Server::READABLE, - 'args' => array( - 'offset' => array( + 'args' => [ + 'offset' => [ 'required' => false, 'type' => 'integer', 'default' => 0, - ), - 'type' => array( + ], + 'type' => [ 'required' => false, 'type' => 'string', 'default' => 'any', - ), - 'search' => array( + ], + 'search' => [ 'required' => false, 'type' => 'string', - ), - 'status' => array( + ], + 'status' => [ 'required' => false, 'type' => 'string', - ), - ), - 'callback' => array( $this, 'get_data' ), - ), - array( + ], + ], + 'callback' => [ $this, 'get_data' ], + ], + [ 'methods' => \WP_REST_Server::CREATABLE, - 'args' => array( - 'action' => array( + 'args' => [ + 'action' => [ 'required' => false, 'type' => 'string', - ), - 'data' => array( + ], + 'data' => [ 'required' => true, 'type' => 'object', - ), - ), - 'callback' => array( $this, 'add_data' ), - ), - array( + ], + ], + 'callback' => [ $this, 'add_data' ], + ], + [ 'methods' => \WP_REST_Server::DELETABLE, - 'args' => array( - 'id' => array( + 'args' => [ + 'id' => [ 'required' => true, 'type' => 'integer', - ), - ), - 'callback' => array( $this, 'delete_data' ), - ), - ), - 'threads' => array( - array( + ], + ], + 'callback' => [ $this, 'delete_data' ], + ], + ], + 'threads' => [ + [ 'methods' => \WP_REST_Server::READABLE, - 'args' => array( - 'offset' => array( + 'args' => [ + 'offset' => [ 'required' => false, 'type' => 'integer', 'default' => 0, - ), - ), - 'callback' => array( $this, 'get_threads' ), - ), - ), - 'chat' => array( - array( + ], + ], + 'callback' => [ $this, 'get_threads' ], + ], + ], + 'qdrant' => [ + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'qdrant_status' ], + ], + [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'qdrant_deactivate' ], + ], + ], + 'chat' => [ + [ 'methods' => \WP_REST_Server::READABLE, - 'args' => array( - 'run_id' => array( + 'args' => [ + 'run_id' => [ 'required' => true, 'type' => 'string', - ), - 'thread_id' => array( + ], + 'thread_id' => [ 'required' => true, 'type' => 'string', - ), - 'record_id' => array( + ], + 'record_id' => [ 'required' => true, - 'type' => array( + 'type' => [ 'string', 'integer', - ), - ), - 'message' => array( + ], + ], + 'message' => [ 'required' => false, 'type' => 'string', - ), - ), - 'callback' => array( $this, 'get_chat' ), + ], + ], + 'callback' => [ $this, 'get_chat' ], 'permission_callback' => function ( $request ) { $nonce = $request->get_header( 'x_wp_nonce' ); return wp_verify_nonce( $nonce, 'wp_rest' ); }, - ), - array( + ], + [ 'methods' => \WP_REST_Server::CREATABLE, - 'args' => array( - 'message' => array( + 'args' => [ + 'message' => [ 'required' => true, 'type' => 'string', - ), - 'thread_id' => array( + ], + 'thread_id' => [ 'required' => false, 'type' => 'string', - ), - 'record_id' => array( + ], + 'record_id' => [ 'required' => false, - 'type' => array( + 'type' => [ 'string', 'integer', - ), - ), - ), - 'callback' => array( $this, 'send_chat' ), + ], + ], + ], + 'callback' => [ $this, 'send_chat' ], 'permission_callback' => function ( $request ) { $nonce = $request->get_header( 'x_wp_nonce' ); return wp_verify_nonce( $nonce, 'wp_rest' ); }, - ), - ), - ); + ], + ], + ]; foreach ( $routes as $route => $args ) { foreach ( $args as $key => $arg ) { @@ -215,7 +249,7 @@ public function get_settings() { public function update_settings( $request ) { $data = $request->get_param( 'data' ); $settings = Main::get_settings(); - $updated = array(); + $updated = []; foreach ( $data as $key => $datum ) { if ( ! array_key_exists( $key, $settings ) || $settings[ $key ] === $datum ) { @@ -226,15 +260,21 @@ public function update_settings( $request ) { } if ( empty( $updated ) ) { - return rest_ensure_response( array( 'error' => __( 'No settings to update.', 'hyve-lite' ) ) ); + return rest_ensure_response( [ 'error' => __( 'No settings to update.', 'hyve-lite' ) ] ); } $validation = apply_filters( 'hyve_settings_validation', - array( + [ 'api_key' => function ( $value ) { return is_string( $value ); }, + 'qdrant_api_key' => function ( $value ) { + return is_string( $value ); + }, + 'qdrant_endpoint' => function ( $value ) { + return is_string( $value ); + }, 'chat_enabled' => function ( $value ) { return is_bool( $value ); }, @@ -244,6 +284,9 @@ public function update_settings( $request ) { 'default_message' => function ( $value ) { return is_string( $value ); }, + 'chat_model' => function ( $value ) { + return is_string( $value ); + }, 'temperature' => function ( $value ) { return is_numeric( $value ); }, @@ -259,16 +302,16 @@ function ( $carry, $item ) { true ); }, - ) + ] ); foreach ( $updated as $key => $value ) { if ( ! $validation[ $key ]( $value ) ) { return rest_ensure_response( - array( + [ // translators: %s: option key. 'error' => sprintf( __( 'Invalid value: %s', 'hyve-lite' ), $key ), - ) + ] ); } } @@ -281,13 +324,22 @@ function ( $carry, $item ) { $valid_api = $openai->setup_assistant(); if ( is_wp_error( $valid_api ) ) { - return rest_ensure_response( array( 'error' => $this->get_error_message( $valid_api ) ) ); + return rest_ensure_response( [ 'error' => $this->get_error_message( $valid_api ) ] ); } $settings['assistant_id'] = $valid_api; } } + if ( ( isset( $updated['qdrant_api_key'] ) && ! empty( $updated['qdrant_api_key'] ) ) || ( isset( $updated['qdrant_endpoint'] ) && ! empty( $updated['qdrant_endpoint'] ) ) ) { + $qdrant = new Qdrant_API( $data['qdrant_api_key'], $data['qdrant_endpoint'] ); + $init = $qdrant->init(); + + if ( is_wp_error( $init ) ) { + return rest_ensure_response( [ 'error' => $this->get_error_message( $init ) ] ); + } + } + update_option( 'hyve_settings', $settings ); return rest_ensure_response( __( 'Settings updated.', 'hyve-lite' ) ); @@ -301,23 +353,23 @@ function ( $carry, $item ) { * @return \WP_REST_Response */ public function get_data( $request ) { - $args = array( + $args = [ 'post_type' => $request->get_param( 'type' ), 'post_status' => 'publish', 'posts_per_page' => 20, 'fields' => 'ids', 'offset' => $request->get_param( 'offset' ), - 'meta_query' => array( - array( + 'meta_query' => [ + [ 'key' => '_hyve_added', 'compare' => 'NOT EXISTS', - ), - array( + ], + [ 'key' => '_hyve_moderation_failed', 'compare' => 'NOT EXISTS', - ), - ), - ); + ], + ], + ]; $search = $request->get_param( 'search' ); @@ -328,62 +380,62 @@ public function get_data( $request ) { $status = $request->get_param( 'status' ); if ( 'included' === $status ) { - $args['meta_query'] = array( + $args['meta_query'] = [ 'relation' => 'AND', - array( + [ 'key' => '_hyve_added', 'value' => '1', 'compare' => '=', - ), - array( + ], + [ 'key' => '_hyve_moderation_failed', 'compare' => 'NOT EXISTS', - ), - ); + ], + ]; } if ( 'pending' === $status ) { - $args['meta_query'] = array( + $args['meta_query'] = [ 'relation' => 'AND', - array( + [ 'key' => '_hyve_needs_update', 'value' => '1', 'compare' => '=', - ), - array( + ], + [ 'key' => '_hyve_moderation_failed', 'compare' => 'NOT EXISTS', - ), - ); + ], + ]; } if ( 'moderation' === $status ) { - $args['meta_query'] = array( - array( + $args['meta_query'] = [ + [ 'key' => '_hyve_moderation_failed', 'value' => '1', 'compare' => '=', - ), - ); + ], + ]; } $query = new \WP_Query( $args ); - $posts_data = array(); + $posts_data = []; if ( $query->have_posts() ) { foreach ( $query->posts as $post_id ) { - $post_data = array( + $post_data = [ 'ID' => $post_id, 'title' => get_the_title( $post_id ), 'content' => apply_filters( 'the_content', get_post_field( 'post_content', $post_id ) ), - ); + ]; if ( 'moderation' === $status ) { $review = get_post_meta( $post_id, '_hyve_moderation_review', true ); if ( ! is_array( $review ) || empty( $review ) ) { - $review = array(); + $review = []; } $post_data['review'] = $review; @@ -393,11 +445,11 @@ public function get_data( $request ) { } } - $posts = array( + $posts = [ 'posts' => $posts_data, 'more' => $query->found_posts > 20, 'totalChunks' => $this->table->get_count(), - ); + ]; return rest_ensure_response( $posts ); } @@ -408,43 +460,57 @@ public function get_data( $request ) { * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response + * @throws \Exception If Qdrant API fails. */ public function add_data( $request ) { - $data = $request->get_param( 'data' ); - $content = apply_filters( 'the_content', get_post_field( 'post_content', $data['post_id'] ) ); - $chunks = str_split( $content, 2000 ); - - $moderation = $this->moderate( $chunks, $data['post_id'] ); + $data = $request->get_param( 'data' ); + $post_id = $data['ID']; + $data = Tokenizer::tokenize( $data ); + $chunks = array_column( $data, 'post_content' ); + $moderation = OpenAI::instance()->moderate_chunks( $chunks, $post_id ); if ( is_wp_error( $moderation ) ) { - return rest_ensure_response( array( 'error' => $this->get_error_message( $moderation ) ) ); + return rest_ensure_response( [ 'error' => $this->get_error_message( $moderation ) ] ); } if ( true !== $moderation && 'override' !== $request->get_param( 'action' ) ) { - update_post_meta( $data['post_id'], '_hyve_moderation_failed', 1 ); - update_post_meta( $data['post_id'], '_hyve_moderation_review', $moderation ); + update_post_meta( $post_id, '_hyve_moderation_failed', 1 ); + update_post_meta( $post_id, '_hyve_moderation_review', $moderation ); return rest_ensure_response( - array( + [ 'error' => __( 'The content failed moderation policies.', 'hyve-lite' ), 'code' => 'content_failed_moderation', 'review' => $moderation, - ) + ] ); } if ( 'update' === $request->get_param( 'action' ) ) { - $this->table->delete_by_post_id( $data['post_id'] ); - delete_post_meta( $data['post_id'], '_hyve_needs_update' ); - } + if ( Qdrant_API::is_active() ) { + try { + $delete_result = Qdrant_API::instance()->delete_point( $post_id ); - $this->table->insert( $data ); + if ( ! $delete_result ) { + throw new \Exception( __( 'Failed to delete point in Qdrant.', 'hyve-lite' ) ); + } + } catch ( \Exception $e ) { + return rest_ensure_response( [ 'error' => $e->getMessage() ] ); + } + } + + $this->table->delete_by_post_id( $post_id ); + delete_post_meta( $post_id, '_hyve_needs_update' ); + } - update_post_meta( $data['post_id'], '_hyve_added', 1 ); - delete_post_meta( $data['post_id'], '_hyve_moderation_failed' ); - delete_post_meta( $data['post_id'], '_hyve_moderation_review' ); + foreach ( $data as $datum ) { + $id = $this->table->insert( $datum ); + $this->table->process_post( $id ); + } - $this->table->process_posts(); + update_post_meta( $post_id, '_hyve_added', 1 ); + delete_post_meta( $post_id, '_hyve_moderation_failed' ); + delete_post_meta( $post_id, '_hyve_moderation_review' ); return rest_ensure_response( true ); } @@ -455,10 +521,23 @@ public function add_data( $request ) { * @param \WP_REST_Request $request Request object. * * @return \WP_REST_Response + * @throws \Exception If Qdrant API fails. */ public function delete_data( $request ) { $id = $request->get_param( 'id' ); + if ( Qdrant_API::is_active() ) { + try { + $delete_result = Qdrant_API::instance()->delete_point( $id ); + + if ( ! $delete_result ) { + throw new \Exception( __( 'Failed to delete point in Qdrant.', 'hyve-lite' ) ); + } + } catch ( \Exception $e ) { + return rest_ensure_response( [ 'error' => $e->getMessage() ] ); + } + } + $this->table->delete_by_post_id( $id ); delete_post_meta( $id, '_hyve_added' ); @@ -478,40 +557,91 @@ public function delete_data( $request ) { public function get_threads( $request ) { $pages = apply_filters( 'hyve_threads_per_page', 3 ); - $args = array( + $args = [ 'post_type' => 'hyve_threads', 'post_status' => 'publish', 'posts_per_page' => $pages, 'fields' => 'ids', 'offset' => $request->get_param( 'offset' ), - ); + ]; $query = new \WP_Query( $args ); - $posts_data = array(); + $posts_data = []; if ( $query->have_posts() ) { foreach ( $query->posts as $post_id ) { - $post_data = array( + $post_data = [ 'ID' => $post_id, 'title' => get_the_title( $post_id ), 'date' => get_the_date( 'd/m/Y g:i A', $post_id ), 'thread' => get_post_meta( $post_id, '_hyve_thread_data', true ), 'thread_id' => get_post_meta( $post_id, '_hyve_thread_id', true ), - ); + ]; $posts_data[] = $post_data; } } - $posts = array( + $posts = [ 'posts' => $posts_data, 'more' => $query->found_posts > $pages, - ); + ]; return rest_ensure_response( $posts ); } + /** + * Qdrant status. + * + * @return \WP_REST_Response + */ + public function qdrant_status() { + return rest_ensure_response( + [ + 'status' => Qdrant_API::is_active(), + 'migration' => Qdrant_API::instance()->migration_status(), + ] + ); + } + + /** + * Qdrant deactivate. + * + * @return \WP_REST_Response + * @throws \Exception If Qdrant API fails. + */ + public function qdrant_deactivate() { + $settings = Main::get_settings(); + + try { + $deactivated = Qdrant_API::instance()->disconnect(); + + if ( ! $deactivated ) { + throw new \Exception( __( 'Failed to deactivate Qdrant.', 'hyve-lite' ) ); + } + } catch ( \Exception $e ) { + return rest_ensure_response( [ 'error' => $e->getMessage() ] ); + } + + $over_limit = $this->table->get_posts_over_limit(); + + if ( ! empty( $over_limit ) ) { + wp_schedule_single_event( time(), 'hyve_delete_posts', [ $over_limit ] ); + } + + $this->table->update_storage( 'WordPress', 'Qdrant' ); + + $settings['qdrant_api_key'] = ''; + $settings['qdrant_endpoint'] = ''; + + update_option( 'hyve_settings', $settings ); + update_option( 'hyve_qdrant_status', 'inactive' ); + delete_option( 'hyve_qdrant_migration' ); + + return rest_ensure_response( __( 'Qdrant deactivated.', 'hyve-lite' ) ); + } + /** * Get chat. * @@ -525,22 +655,22 @@ public function get_chat( $request ) { $query = $request->get_param( 'message' ); $record_id = $request->get_param( 'record_id' ); - $openai = new OpenAI(); + $openai = OpenAI::instance(); $status = $openai->get_status( $run_id, $thread_id ); if ( is_wp_error( $status ) ) { - return rest_ensure_response( array( 'error' => $this->get_error_message( $status ) ) ); + return rest_ensure_response( [ 'error' => $this->get_error_message( $status ) ] ); } if ( 'completed' !== $status ) { - return rest_ensure_response( array( 'status' => $status ) ); + return rest_ensure_response( [ 'status' => $status ] ); } $messages = $openai->get_messages( $thread_id ); if ( is_wp_error( $messages ) ) { - return rest_ensure_response( array( 'error' => $this->get_error_message( $messages ) ) ); + return rest_ensure_response( [ 'error' => $this->get_error_message( $messages ) ] ); } $messages = array_filter( @@ -555,7 +685,7 @@ function ( $message ) use ( $run_id ) { $message = json_decode( $message, true ); if ( json_last_error() !== JSON_ERROR_NONE ) { - return rest_ensure_response( array( 'error' => __( 'No messages found.', 'hyve-lite' ) ) ); + return rest_ensure_response( [ 'error' => __( 'No messages found.', 'hyve-lite' ) ] ); } $settings = Main::get_settings(); @@ -565,77 +695,67 @@ function ( $message ) use ( $run_id ) { do_action( 'hyve_chat_response', $run_id, $thread_id, $query, $record_id, $message, $response ); return rest_ensure_response( - array( + [ 'status' => $status, 'success' => isset( $message['success'] ) ? $message['success'] : false, 'message' => $response, - ) + ] ); } /** - * Send chat. + * Get Similarity. * - * @param \WP_REST_Request $request Request object. + * @param array $message_vector Message vector. * - * @return \WP_REST_Response + * @return array Posts. */ - public function send_chat( $request ) { - $message = $request->get_param( 'message' ); - $record_id = $request->get_param( 'record_id' ); - $moderation = $this->moderate( $message ); + public function get_similarity( $message_vector ) { + if ( Qdrant_API::is_active() ) { + $scored_points = Qdrant_API::instance()->search( $message_vector ); - if ( true !== $moderation ) { - return rest_ensure_response( array( 'error' => __( 'Message was flagged.', 'hyve-lite' ) ) ); - } - - $openai = new OpenAI(); - $message_vector = $openai->create_embeddings( $message ); - $message_vector = reset( $message_vector ); - $message_vector = $message_vector->embedding; + if ( is_wp_error( $scored_points ) ) { + return []; + } - if ( is_wp_error( $message_vector ) ) { - return rest_ensure_response( array( 'error' => __( 'No embeddings found.', 'hyve-lite' ) ) ); + return $scored_points; } - $hash = md5( strtolower( $message ) ); - set_transient( 'hyve_message_' . $hash, $message_vector, MINUTE_IN_SECONDS ); - $posts = $this->table->get_by_status( 'processed' ); - $embeddings_with_cosine_distance_sorted = array_map( + $scored_points = array_map( function ( $row ) use ( $message_vector ) { $embeddings = json_decode( $row->embeddings, true ); if ( ! is_array( $embeddings ) ) { - return array( + return [ 'post_id' => $row->post_id, - 'distance' => 0, + 'score' => 0, 'token_count' => $row->token_count, 'post_title' => $row->post_title, 'post_content' => $row->post_content, - ); + ]; } - $distance = Cosine_Similarity::calculate( $message_vector, $embeddings ); + $score = Cosine_Similarity::calculate( $message_vector, $embeddings ); - return array( + return [ 'post_id' => $row->post_id, - 'distance' => $distance, + 'score' => $score, 'token_count' => $row->token_count, 'post_title' => $row->post_title, 'post_content' => $row->post_content, - ); + ]; }, $posts ); usort( - $embeddings_with_cosine_distance_sorted, + $scored_points, function ( $a, $b ) { - if ( $a['distance'] < $b['distance'] ) { + if ( $a['score'] < $b['score'] ) { return 1; - } elseif ( $a['distance'] > $b['distance'] ) { + } elseif ( $a['score'] > $b['score'] ) { return -1; } else { return 0; @@ -643,10 +763,40 @@ function ( $a, $b ) { } ); - $embeddings_with_cosine_distance_sorted = array_filter( - $embeddings_with_cosine_distance_sorted, + return $scored_points; + } + + /** + * Send chat. + * + * @param \WP_REST_Request $request Request object. + * + * @return \WP_REST_Response + */ + public function send_chat( $request ) { + $message = $request->get_param( 'message' ); + $record_id = $request->get_param( 'record_id' ); + $moderation = OpenAI::instance()->moderate_chunks( $message ); + + if ( true !== $moderation ) { + return rest_ensure_response( [ 'error' => __( 'Message was flagged.', 'hyve-lite' ) ] ); + } + + $openai = OpenAI::instance(); + $message_vector = $openai->create_embeddings( $message ); + $message_vector = reset( $message_vector ); + $message_vector = $message_vector->embedding; + + if ( is_wp_error( $message_vector ) ) { + return rest_ensure_response( [ 'error' => __( 'No embeddings found.', 'hyve-lite' ) ] ); + } + + $scored_points = $this->get_similarity( $message_vector ); + + $scored_points = array_filter( + $scored_points, function ( $row ) { - return $row['distance'] > 0.4; + return $row['score'] > 0.4; } ); @@ -654,7 +804,7 @@ function ( $row ) { $curr_tokens_length = 0; $article_context = ''; - foreach ( $embeddings_with_cosine_distance_sorted as $row ) { + foreach ( $scored_points as $row ) { $curr_tokens_length += $row['token_count']; if ( $curr_tokens_length < $max_tokens_length ) { $article_context .= "\n ===START POST=== " . $row['post_title'] . ' - ' . $row['post_content'] . ' ===END POST==='; @@ -668,20 +818,20 @@ function ( $row ) { } if ( is_wp_error( $thread_id ) ) { - return rest_ensure_response( array( 'error' => $this->get_error_message( $thread_id ) ) ); + return rest_ensure_response( [ 'error' => $this->get_error_message( $thread_id ) ] ); } $query_run = $openai->create_run( - array( - array( + [ + [ 'role' => 'user', 'content' => 'START QUESTION: ' . $message . ' :END QUESTION', - ), - array( + ], + [ 'role' => 'user', 'content' => 'START CONTEXT: ' . $article_context . ' :END CONTEXT', - ), - ), + ], + ], $thread_id ); @@ -690,25 +840,25 @@ function ( $row ) { $thread_id = $openai->create_thread(); if ( is_wp_error( $thread_id ) ) { - return rest_ensure_response( array( 'error' => $this->get_error_message( $thread_id ) ) ); + return rest_ensure_response( [ 'error' => $this->get_error_message( $thread_id ) ] ); } $query_run = $openai->create_run( - array( - array( + [ + [ 'role' => 'user', 'content' => 'Question: ' . $message, - ), - array( + ], + [ 'role' => 'user', 'content' => 'Context: ' . $article_context, - ), - ), + ], + ], $thread_id ); if ( is_wp_error( $query_run ) ) { - return rest_ensure_response( array( 'error' => $this->get_error_message( $query_run ) ) ); + return rest_ensure_response( [ 'error' => $this->get_error_message( $query_run ) ] ); } } } @@ -716,11 +866,12 @@ function ( $row ) { $record_id = apply_filters( 'hyve_chat_request', $thread_id, $record_id, $message ); return rest_ensure_response( - array( + [ 'thread_id' => $thread_id, 'query_run' => $query_run, 'record_id' => $record_id ? $record_id : null, - ) + 'content' => $article_context, + ] ); } } diff --git a/inc/BaseAPI.php b/inc/BaseAPI.php index 32b1c64..3100c2d 100644 --- a/inc/BaseAPI.php +++ b/inc/BaseAPI.php @@ -41,18 +41,18 @@ class BaseAPI { * * @var array */ - private $errors = array(); + private $errors = []; /** * Constructor. */ public function __construct() { - $this->table = new DB_Table(); + $this->table = DB_Table::instance(); - $this->errors = array( + $this->errors = [ 'invalid_api_key' => __( 'Incorrect API key provided.', 'hyve-lite' ), 'missing_scope' => __( ' You have insufficient permissions for this operation.', 'hyve-lite' ), - ); + ]; } /** @@ -78,83 +78,4 @@ public function get_error_message( $error ) { public function get_endpoint() { return $this->namespace . '/' . $this->version; } - - /** - * Moderate data. - * - * @param array|string $chunks Data to moderate. - * @param int $id Post ID. - * - * @return true|array|\WP_Error - */ - public function moderate( $chunks, $id = null ) { - if ( $id ) { - $moderated = get_transient( 'hyve_moderate_post_' . $id ); - - if ( false !== $moderated ) { - return is_array( $moderated ) ? $moderated : true; - } - } - - $openai = new OpenAI(); - $results = array(); - $return = true; - $settings = Main::get_settings(); - $moderation_threshold = $settings['moderation_threshold']; - - if ( ! is_array( $chunks ) ) { - $chunks = array( $chunks ); - } - - foreach ( $chunks as $chunk ) { - $moderation = $openai->moderate( $chunk ); - - if ( is_wp_error( $moderation ) ) { - return $moderation; - } - - if ( true !== $moderation && is_object( $moderation ) ) { - $results[] = $moderation; - } - } - - if ( ! empty( $results ) ) { - $flagged = array(); - - foreach ( $results as $result ) { - $categories = $result->categories; - - foreach ( $categories as $category => $flag ) { - if ( ! $flag ) { - continue; - } - - if ( ! isset( $moderation_threshold[ $category ] ) || $result->category_scores->$category < ( $moderation_threshold[ $category ] / 100 ) ) { - continue; - } - - if ( ! isset( $flagged[ $category ] ) ) { - $flagged[ $category ] = $result->category_scores->$category; - continue; - } - - if ( $result->category_scores->$category > $flagged[ $category ] ) { - $flagged[ $category ] = $result->category_scores->$category; - } - } - } - - if ( empty( $flagged ) ) { - $return = true; - } else { - $return = $flagged; - } - } - - if ( $id ) { - set_transient( 'hyve_moderate_post_' . $id, $return, MINUTE_IN_SECONDS ); - } - - return $return; - } } diff --git a/inc/Block.php b/inc/Block.php index ee38a3d..dfed15a 100644 --- a/inc/Block.php +++ b/inc/Block.php @@ -15,8 +15,8 @@ class Block { * Constructor. */ public function __construct() { - add_action( 'init', array( $this, 'register_block' ) ); - add_shortcode( 'hyve', array( $this, 'render_shortcode' ) ); + add_action( 'init', [ $this, 'register_block' ] ); + add_shortcode( 'hyve', [ $this, 'render_shortcode' ] ); } /** diff --git a/inc/DB_Table.php b/inc/DB_Table.php index c79e9b9..158eda3 100644 --- a/inc/DB_Table.php +++ b/inc/DB_Table.php @@ -8,6 +8,7 @@ namespace ThemeIsle\HyveLite; use ThemeIsle\HyveLite\OpenAI; +use ThemeIsle\HyveLite\Qdrant_API; /** * Class DB_Table @@ -28,7 +29,7 @@ class DB_Table { * @since 1.2.0 * @var string */ - public $version = '1.0.0'; + public $version = '1.1.0'; /** * Cache prefix. @@ -38,6 +39,26 @@ class DB_Table { */ const CACHE_PREFIX = 'hyve-'; + /** + * The single instance of the class. + * + * @var DB_Table + */ + private static $instance = null; + + /** + * Ensures only one instance of the class is loaded. + * + * @return DB_Table An instance of the class. + */ + public static function instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + /** * DB_Table constructor. * @@ -47,11 +68,8 @@ public function __construct() { global $wpdb; $this->table_name = $wpdb->prefix . 'hyve'; - if ( ! wp_next_scheduled( 'hyve_process_posts' ) ) { - wp_schedule_event( time(), 'daily', 'hyve_process_posts' ); - } - - add_action( 'hyve_process_posts', array( $this, 'process_posts' ) ); + add_action( 'hyve_process_post', [ $this, 'process_post' ], 10, 1 ); + add_action( 'hyve_delete_posts', [ $this, 'delete_posts' ], 10, 1 ); if ( ! $this->table_exists() || version_compare( $this->version, get_option( $this->table_name . '_db_version' ), '>' ) ) { $this->create_table(); @@ -78,6 +96,7 @@ public function create_table() { embeddings longtext NOT NULL, token_count int(11) NOT NULL DEFAULT 0, post_status VARCHAR(255) NOT NULL DEFAULT "scheduled", + storage VARCHAR(255) NOT NULL DEFAULT "WordPress", PRIMARY KEY (id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;'; @@ -106,7 +125,7 @@ public function table_exists() { * @return array */ public function get_columns() { - return array( + return [ 'date' => '%s', 'modified' => '%s', 'post_id' => '%s', @@ -115,7 +134,8 @@ public function get_columns() { 'embeddings' => '%s', 'token_count' => '%d', 'post_status' => '%s', - ); + 'storage' => '%s', + ]; } /** @@ -126,7 +146,7 @@ public function get_columns() { * @return array */ public function get_column_defaults() { - return array( + return [ 'date' => gmdate( 'Y-m-d H:i:s' ), 'modified' => gmdate( 'Y-m-d H:i:s' ), 'post_id' => '', @@ -135,7 +155,33 @@ public function get_column_defaults() { 'embeddings' => '', 'token_count' => 0, 'post_status' => 'scheduled', - ); + 'storage' => 'WordPress', + ]; + } + + /** + * Get a row by ID. + * + * @since 1.3.0 + * + * @param int $id The row ID. + * + * @return object + */ + public function get( $id ) { + global $wpdb; + + $cache = $this->get_cache( 'entry_' . $id ); + + if ( false !== $cache ) { + return $cache; + } + + $result = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $this->table_name, $id ) ); + + $this->set_cache( 'entry_' . $id, $result ); + + return $result; } /** @@ -182,12 +228,12 @@ public function update( $id, $data ) { $data = array_intersect_key( $data, $column_formats ); - $wpdb->update( $this->table_name, $data, array( 'id' => $id ), $column_formats, array( '%d' ) ); + $rows_affected = $wpdb->update( $this->table_name, $data, [ 'id' => $id ], $column_formats, [ '%d' ] ); $this->delete_cache( 'entry_' . $id ); $this->delete_cache( 'entries_processed' ); - return $wpdb->rows_affected; + return $rows_affected; } /** @@ -202,14 +248,13 @@ public function update( $id, $data ) { public function delete_by_post_id( $post_id ) { global $wpdb; - $wpdb->delete( $this->table_name, array( 'post_id' => $post_id ), array( '%d' ) ); + $rows_affected = $wpdb->delete( $this->table_name, [ 'post_id' => $post_id ], [ '%d' ] ); - $this->delete_cache( 'entry_post_' . $post_id ); $this->delete_cache( 'entries' ); $this->delete_cache( 'entries_processed' ); $this->delete_cache( 'entries_count' ); - return $wpdb->rows_affected; + return $rows_affected; } /** @@ -240,37 +285,149 @@ public function get_by_status( $status, $limit = 500 ) { return $results; } + /** + * Get all rows by storage. + * + * @since 1.3.0 + * + * @param string $storage The storage. + * @param int $limit The limit. + * + * @return array + */ + public function get_by_storage( $storage, $limit = 100 ) { + global $wpdb; + $results = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE storage = %s LIMIT %d', $this->table_name, $storage, $limit ) ); + return $results; + } + + /** + * Update storage of all rows. + * + * @since 1.3.0 + * + * @param string $to The storage. + * @param string $from The storage. + * + * @return int + */ + public function update_storage( $to, $from ) { + global $wpdb; + $wpdb->update( $this->table_name, [ 'storage' => $to ], [ 'storage' => $from ], [ '%s' ], [ '%s' ] ); + $this->delete_cache( 'entries' ); + $this->delete_cache( 'entries_processed' ); + return $wpdb->rows_affected; + } + + /** + * Get Posts over limit. + * + * @since 1.3.0 + * + * @return array + */ + public function get_posts_over_limit() { + $limit = apply_filters( 'hyve_chunks_limit', 500 ); + + global $wpdb; + $posts = $wpdb->get_results( $wpdb->prepare( 'SELECT post_id FROM %i ORDER BY id DESC LIMIT %d, %d', $this->table_name, $limit, $this->get_count() ) ); + + if ( ! $posts ) { + return []; + } + + $posts = wp_list_pluck( $posts, 'post_id' ); + $posts = array_unique( $posts ); + + return $posts; + } + /** * Process posts. * * @since 1.2.0 * + * @param int $id The post ID. + * * @return void */ - public function process_posts() { - $posts = $this->get_by_status( 'scheduled' ); - - foreach ( $posts as $post ) { - $id = $post->id; - $content = $post->post_content; - $openai = new OpenAI(); - $embeddings = $openai->create_embeddings( $content ); - $embeddings = reset( $embeddings ); - $embeddings = $embeddings->embedding; - - if ( is_wp_error( $embeddings ) || ! $embeddings ) { - continue; + public function process_post( $id ) { + $post = $this->get( $id ); + $content = $post->post_content; + $openai = OpenAI::instance(); + $stripped = wp_strip_all_tags( $content ); + $embeddings = $openai->create_embeddings( $stripped ); + + if ( is_wp_error( $embeddings ) || ! $embeddings ) { + wp_schedule_single_event( time() + 60, 'hyve_process_post', [ $id ] ); + return; + } + + $embeddings = reset( $embeddings ); + $embeddings = $embeddings->embedding; + $storage = 'WordPress'; + + if ( Qdrant_API::is_active() ) { + try { + $success = Qdrant_API::instance()->add_point( + $embeddings, + [ + 'post_id' => $post->post_id, + 'post_title' => $post->post_title, + 'post_content' => $post->post_content, + 'token_count' => $post->token_count, + 'website_url' => get_site_url(), + ] + ); + + $storage = 'Qdrant'; + } catch ( \Exception $e ) { + $success = new \WP_Error( 'qdrant_error', $e->getMessage() ); } - $embeddings = wp_json_encode( $embeddings ); + if ( is_wp_error( $success ) ) { + wp_schedule_single_event( time() + 60, 'hyve_process_post', [ $id ] ); + return; + } + } + + $embeddings = wp_json_encode( $embeddings ); + + $this->update( + $id, + [ + 'embeddings' => $embeddings, + 'post_status' => 'processed', + 'storage' => $storage, + ] + ); + } + + /** + * Delete posts. + * + * @since 1.3.0 + * + * @param array $posts The posts. + * + * @return void + */ + public function delete_posts( $posts = [] ) { + $twenty = array_slice( $posts, 0, 20 ); + + foreach ( $twenty as $id ) { + $this->delete_by_post_id( $id ); + + delete_post_meta( $id, '_hyve_added' ); + delete_post_meta( $id, '_hyve_needs_update' ); + delete_post_meta( $id, '_hyve_moderation_failed' ); + delete_post_meta( $id, '_hyve_moderation_review' ); + } + + $has_more = count( $posts ) > 20; - $this->update( - $id, - array( - 'embeddings' => $embeddings, - 'post_status' => 'processed', - ) - ); + if ( $has_more ) { + wp_schedule_single_event( time() + 10, 'hyve_delete_posts', [ array_slice( $posts, 20 ) ] ); } } @@ -316,7 +473,7 @@ private function get_cache( $key ) { return false; } - $entries = array(); + $entries = []; for ( $i = 0; $i < $total; $i++ ) { $chunk_key = $key . '_' . $i; diff --git a/inc/Logger.php b/inc/Logger.php new file mode 100644 index 0000000..7816884 --- /dev/null +++ b/inc/Logger.php @@ -0,0 +1,72 @@ + 'hyve', + 'site' => get_site_url(), + 'license' => $license, + 'data' => $event, + ]; + } + + $args = [ + 'headers' => [ + 'Content-Type' => 'application/json', + ], + 'body' => wp_json_encode( $payload ), + ]; + + wp_remote_post( self::TRACK_URL, $args ); + } finally { + return; + } + } + + /** + * Check if the user has consented to tracking. + * + * @return bool + */ + public static function has_consent() { + return 'yes' === get_option( 'hyve_lite_logger_flag', false ) || defined( 'HYVE_BASEFILE' ); + } +} diff --git a/inc/Main.php b/inc/Main.php index 7507795..cb2b2c1 100644 --- a/inc/Main.php +++ b/inc/Main.php @@ -12,6 +12,8 @@ use ThemeIsle\HyveLite\Cosine_Similarity; use ThemeIsle\HyveLite\Threads; use ThemeIsle\HyveLite\API; +use ThemeIsle\HyveLite\Qdrant_API; +use ThemeIsle\HyveLite\Logger; /** * Class Main @@ -34,19 +36,33 @@ class Main { */ public $api; + /** + * Instace of Qdrant_API class. + * + * @since 1.2.0 + * @var Qdrant_API + */ + public $qdrant; + /** * Main constructor. */ public function __construct() { - $this->table = new DB_Table(); - $this->api = new API(); + $this->table = new DB_Table(); + $this->api = new API(); + $this->qdrant = new Qdrant_API(); new Block(); new Threads(); - add_action( 'admin_menu', array( $this, 'register_menu_page' ) ); - add_action( 'save_post', array( $this, 'update_meta' ) ); - add_action( 'delete_post', array( $this, 'delete_post' ) ); + add_action( 'admin_menu', [ $this, 'register_menu_page' ] ); + add_action( 'save_post', [ $this, 'update_meta' ] ); + add_action( 'delete_post', [ $this, 'delete_post' ] ); + add_action( 'hyve_weekly_stats', [ $this, 'log_stats' ] ); + + if ( Logger::has_consent() && ! wp_next_scheduled( 'hyve_weekly_stats' ) ) { + wp_schedule_event( time(), 'weekly', 'hyve_weekly_stats' ); + } $settings = self::get_settings(); @@ -55,7 +71,7 @@ public function __construct() { isset( $settings['api_key'] ) && isset( $settings['assistant_id'] ) && ! empty( $settings['api_key'] ) && ! empty( $settings['assistant_id'] ) ) { - add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } } @@ -72,12 +88,12 @@ public function register_menu_page() { __( 'Hyve', 'hyve-lite' ), 'manage_options', 'hyve', - array( $this, 'menu_page' ), + [ $this, 'menu_page' ], 'dashicons-format-chat', 99 ); - add_action( "admin_print_scripts-$page_hook_suffix", array( $this, 'enqueue_options_assets' ) ); + add_action( "admin_print_scripts-$page_hook_suffix", [ $this, 'enqueue_options_assets' ] ); } /** @@ -106,7 +122,7 @@ public function enqueue_options_assets() { wp_enqueue_style( 'hyve-styles', HYVE_LITE_URL . 'build/backend/style-index.css', - array( 'wp-components' ), + [ 'wp-components' ], $asset_file['version'] ); @@ -120,14 +136,14 @@ public function enqueue_options_assets() { wp_set_script_translations( 'hyve-lite-scripts', 'hyve-lite' ); - $post_types = get_post_types( array( 'public' => true ), 'objects' ); - $post_types_for_js = array(); + $post_types = get_post_types( [ 'public' => true ], 'objects' ); + $post_types_for_js = []; foreach ( $post_types as $post_type ) { - $post_types_for_js[] = array( + $post_types_for_js[] = [ 'label' => $post_type->labels->name, 'value' => $post_type->name, - ); + ]; } $settings = self::get_settings(); @@ -137,22 +153,20 @@ public function enqueue_options_assets() { 'hyve', apply_filters( 'hyve_options_data', - array( - 'api' => $this->api->get_endpoint(), - 'postTypes' => $post_types_for_js, - 'hasAPIKey' => isset( $settings['api_key'] ) && ! empty( $settings['api_key'] ), - 'chunksLimit' => apply_filters( 'hyve_chunks_limit', 500 ), - 'assets' => array( + [ + 'api' => $this->api->get_endpoint(), + 'postTypes' => $post_types_for_js, + 'hasAPIKey' => isset( $settings['api_key'] ) && ! empty( $settings['api_key'] ), + 'chunksLimit' => apply_filters( 'hyve_chunks_limit', 500 ), + 'isQdrantActive' => Qdrant_API::is_active(), + 'assets' => [ 'images' => HYVE_LITE_URL . 'assets/images/', - ), - 'stats' => array( - 'threads' => Threads::get_thread_count(), - 'messages' => Threads::get_messages_count(), - 'totalChunks' => $this->table->get_count(), - ), - 'docs' => 'https://docs.themeisle.com/article/2009-hyve-documentation', - 'pro' => 'https://themeisle.com/plugins/hyve/', - ) + ], + 'stats' => $this->get_stats(), + 'docs' => 'https://docs.themeisle.com/article/2009-hyve-documentation', + 'qdrant_docs' => 'https://docs.themeisle.com/article/2066-integrate-hyve-with-qdrant', + 'pro' => 'https://themeisle.com/plugins/hyve/', + ] ) ); } @@ -167,14 +181,17 @@ public function enqueue_options_assets() { public static function get_default_settings() { return apply_filters( 'hyve_default_settings', - array( + [ 'api_key' => '', + 'qdrant_api_key' => '', + 'qdrant_endpoint' => '', 'chat_enabled' => true, 'welcome_message' => __( 'Hello! How can I help you today?', 'hyve-lite' ), 'default_message' => __( 'Sorry, I\'m not able to help with that.', 'hyve-lite' ), + 'chat_model' => 'gpt-4o-mini', 'temperature' => 1, 'top_p' => 1, - 'moderation_threshold' => array( + 'moderation_threshold' => [ 'sexual' => 80, 'hate' => 70, 'harassment' => 70, @@ -186,8 +203,8 @@ public static function get_default_settings() { 'self-harm/instructions' => 50, 'harassment/threatening' => 60, 'violence' => 70, - ), - ) + ], + ] ); } @@ -199,7 +216,7 @@ public static function get_default_settings() { * @return array */ public static function get_settings() { - $settings = get_option( 'hyve_settings', array() ); + $settings = get_option( 'hyve_settings', [] ); return wp_parse_args( $settings, self::get_default_settings() ); } @@ -216,7 +233,7 @@ public function enqueue_assets() { wp_register_style( 'hyve-styles', HYVE_LITE_URL . 'build/frontend/style-index.css', - array(), + [], $asset_file['version'] ); @@ -237,15 +254,15 @@ public function enqueue_assets() { 'hyve', apply_filters( 'hyve_frontend_data', - array( + [ 'api' => $this->api->get_endpoint(), - 'audio' => array( + 'audio' => [ 'click' => HYVE_LITE_URL . 'assets/audio/click.mp3', 'ping' => HYVE_LITE_URL . 'assets/audio/ping.mp3', - ), + ], 'welcome' => $settings['welcome_message'] ?? '', 'isEnabled' => $settings['chat_enabled'], - ) + ] ) ); @@ -268,6 +285,40 @@ public function enqueue_assets() { ); } + /** + * Get stats. + * + * @since 1.3.0 + * + * @return array + */ + public function get_stats() { + return [ + 'threads' => Threads::get_thread_count(), + 'messages' => Threads::get_messages_count(), + 'totalChunks' => $this->table->get_count(), + ]; + } + + /** + * Log stats. + * + * @since 1.3.0 + * + * @return void + */ + public function log_stats() { + Logger::track( + [ + [ + 'feature' => 'system', + 'featureComponent' => 'stats', + 'featureValue' => $this->get_stats(), + ], + ] + ); + } + /** * Update meta. * @@ -300,5 +351,9 @@ public function update_meta( $post_id ) { */ public function delete_post( $post_id ) { $this->table->delete_by_post_id( $post_id ); + + if ( Qdrant_API::is_active() ) { + $this->qdrant->delete_point( $post_id ); + } } } diff --git a/inc/OpenAI.php b/inc/OpenAI.php index 8c03e19..7f7615c 100644 --- a/inc/OpenAI.php +++ b/inc/OpenAI.php @@ -25,7 +25,14 @@ class OpenAI { * * @var string */ - private $prompt_version = '1.1.0'; + private $prompt_version = '1.2.0'; + + /** + * Chat Model. + * + * @var string + */ + private $chat_model = 'gpt-4o-mini'; /** * API Key. @@ -41,6 +48,26 @@ class OpenAI { */ private $assistant_id; + /** + * The single instance of the class. + * + * @var OpenAI + */ + private static $instance = null; + + /** + * Ensures only one instance of the class is loaded. + * + * @return OpenAI An instance of the class. + */ + public static function instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + /** * Constructor. * @@ -50,12 +77,52 @@ public function __construct( $api_key = '' ) { $settings = Main::get_settings(); $this->api_key = ! empty( $api_key ) ? $api_key : ( isset( $settings['api_key'] ) ? $settings['api_key'] : '' ); $this->assistant_id = isset( $settings['assistant_id'] ) ? $settings['assistant_id'] : ''; + $this->chat_model = isset( $settings['chat_model'] ) ? $settings['chat_model'] : $this->chat_model; if ( $this->assistant_id && version_compare( $this->prompt_version, get_option( 'hyve_prompt_version', '1.0.0' ), '>' ) ) { $this->update_assistant(); } } + /** + * Get Assistant Properties. + * + * @return array + */ + public function get_properties() { + $props = [ + 'instructions' => "You are a Support Assistant tasked with providing precise, to-the-point answers based on the context provided for each query, as well as maintaining awareness of previous context for follow-up questions.\r\n\r\nCore Principles:\r\n\r\n1. Context and Question Analysis\r\n- Identify the context given in each message.\r\n- Determine the specific question to be answered based on the current context and previous interactions.\r\n\r\n2. Relevance Check\r\n- Assess if the current context or previous context contains information directly relevant to the question.\r\n- Proceed based on the following scenarios:\r\na) If current context addresses the question: Formulate a response using current context.\r\nb) If current context is empty but previous context is relevant: Use previous context to answer.\r\nc) If the input is a greeting: Respond appropriately.\r\nd) If neither current nor previous context addresses the question: Respond with an empty response and success: false.\r\n\r\n3. Response Formulation\r\n- Use information from the current context primarily. If current context is insufficient, refer to previous context for follow-up questions.\r\n- Include all relevant details, including any code snippets or links if present.\r\n- Avoid including unnecessary information.\r\n- Format the response in HTML using only these allowed tags: h2, h3, p, img, a, pre, strong, em.\r\n\r\n4. Context Reference\r\n- Do not explicitly mention or refer to the context in your answer.\r\n- Provide a straightforward response that directly answers the question.\r\n\r\n5. Response Structure\r\n- Always structure your response as a JSON object with 'response' and 'success' fields.\r\n- The 'response' field should contain the HTML-formatted answer.\r\n- The 'success' field should be a boolean indicating whether the question was successfully answered.\r\n\r\n6. Handling Follow-up Questions\r\n- Maintain awareness of previous context to answer follow-up questions.\r\n- If current context is empty but the question seems to be a follow-up, attempt to answer using previous context.\r\n\r\nExamples:\r\n\r\n1. Initial Question with Full Answer\r\nContext: The price of XYZ product is $99.99 USD.\r\nQuestion: How much does XYZ cost?\r\nResponse:\r\n{\r\n\"response\": \"

The price of XYZ product is $99.99 USD.

\",\r\n\"success\": true\r\n}\r\n\r\n2. Follow-up Question with Empty Current Context\r\nContext: [Empty]\r\nQuestion: What currency is that in?\r\nResponse:\r\n{\r\n\"response\": \"

The price is in USD (United States Dollars).

\",\r\n\"success\": true\r\n}\r\n\r\n3. No Relevant Information in Current or Previous Context\r\nContext: [Empty]\r\nQuestion: Do you offer gift wrapping?\r\nResponse:\r\n{\r\n\"response\": \"\",\r\n\"success\": false\r\n}\r\n\r\n4. Greeting\r\nQuestion: Hello!\r\nResponse:\r\n{\r\n\"response\": \"

Hello! How can I assist you today?

\",\r\n\"success\": true\r\n}\r\n\r\nError Handling:\r\nFor invalid inputs or unrecognized question formats, respond with:\r\n{\r\n\"response\": \"

I apologize, but I couldn't understand your question. Could you please rephrase it?

\",\r\n\"success\": false\r\n}\r\n\r\nHTML Usage Guidelines:\r\n- Use

for main headings and

for subheadings.\r\n- Wrap paragraphs in

tags.\r\n- Use

 for code snippets or formatted text.\r\n- Apply  for bold and  for italic emphasis sparingly.\r\n- Include  only if specific image information is provided in the context.\r\n- Use  for links, ensuring they are relevant and from the provided context.\r\n\r\nRemember:\r\n- Prioritize using the current context for answers.\r\n- For follow-up questions with empty current context, refer to previous context if relevant.\r\n- If information isn't available in current or previous context, indicate this with an empty response and success: false.\r\n- Always strive to provide the most accurate and relevant information based on available context.",
+			'model'        => $this->chat_model,
+		];
+
+		if ( 'gpt-4o-mini' === $this->chat_model ) {
+			$props['response_format'] = [
+				'type'        => 'json_schema',
+				'json_schema' => [
+					'name'   => 'chatbot_response',
+					'strict' => false,
+					'schema' => [
+						'type'                 => 'object',
+						'properties'           => [
+							'response' => [
+								'type'        => 'string',
+								'description' => 'The HTML-formatted response to the user\'s question.',
+							],
+							'success'  => [
+								'type'        => 'boolean',
+								'description' => 'Indicates whether the question was successfully answered from the provided context.',
+							],
+						],
+						'required'             => [ 'success' ],
+						'additionalProperties' => false,
+					],
+				],
+			];
+		}
+
+		return $props;
+	}
+
 	/**
 	 * Setup Assistant.
 	 * 
@@ -83,10 +150,11 @@ public function setup_assistant() {
 	public function create_assistant() {
 		$response = $this->request(
 			'assistants',
-			array(
-				'instructions' => "Assistant Role & Concise Response Guidelines: As a Support Assistant, provide precise, to-the-point answers based exclusively on the previously provided context.\r\n\r\nSET OF PRINCIPLES TO FOLLOW:\r\n\r\n1. **Identify the Context and Question**:\r\n1.1. **START CONTEXT**: Identify the context provided in the message. **: END CONTEXT**\r\n1.2. **START QUESTION**: Identify the question that needs to be answered based on the context.. **: END QUESTION**\r\n\r\n2. **Check the Context for Relevance**:\r\n2.1. Determine if the context contains information directly relevant to the question.\r\n2.2. If the context addresses the user's question, proceed to the next step.\r\n2.3. If the question is a greeting, respond appropriately with the greeting.\r\n2.4. If the context does not address the user's question, respond with: `{\"response\": \"\", \"success\": false}`.\r\n\r\n3. **Formulate the Response**:\r\n3.1. If the context is sufficient, formulate a clear and concise response using only the information provided in the context.\r\n3.2. Ensure the response includes all important details covered in the context, but avoid any extraneous information.\r\n\r\n4. **Avoid Referring to the Context**:\r\n4.1. Do not refer to the context or state that the response is based on the context in your answer.\r\n4.2. Ensure the response is straightforward and directly answers the question.\r\n\r\n5. **Generate the JSON Response**:\r\n5.1. Structure the response according to the following JSON schema:\r\n\r\n\r\n{\r\n  \"\$schema\": \"http:\/\/json-schema.org\/draft-07\/schema#\",\r\n  \"type\": \"object\",\r\n  \"properties\": {\r\n    \"response\": {\r\n      \"type\": \"string\",\r\n      \"description\": \"Contains the response to the question. Do not include it if the answer wasn't available in the context.\"\r\n    },\r\n    \"success\": {\r\n      \"type\": \"boolean\",\r\n      \"description\": \"Indicates whether the question was successfully answered from provided context.\"\r\n    }\r\n  },\r\n  \"required\": [\"success\"]\r\n}\r\n\r\nExample Usage:\r\n\r\nContext: [Provide context here]\r\nQuestion: [Provide question here]\r\n\r\nExpected Behavior:\r\n\r\n- If the question is fully covered by the context, provide a response using the provided JSON schema.\r\n- If the question is not fully covered by the context, respond with: {\"response\": \"\", \"success\": false}.\r\n\r\nExample Responses:\r\n\r\n- Context covers the question: {\"response\": \"Here is the information you requested.\", \"success\": true}\r\n- Context does not cover the question: {\"response\": \"\", \"success\": false}\r\n- Context does not cover the question but is a greeting: {\"response\": \"Hello, what can I help you with?.\", \"success\": true}",
-				'name'         => 'Chatbot by Hyve',
-				'model'        => 'gpt-3.5-turbo-0125',
+			array_merge(
+				$this->get_properties(),
+				[
+					'name' => 'Chatbot by Hyve',
+				]
 			)
 		);
 
@@ -125,15 +193,13 @@ public function update_assistant() {
 		} else {
 			$response = $this->request(
 				'assistants/' . $this->assistant_id,
-				array(
-					'instructions' => "Assistant Role & Concise Response Guidelines: As a Support Assistant, provide precise, to-the-point answers based exclusively on the previously provided context.\r\n\r\nSET OF PRINCIPLES TO FOLLOW:\r\n\r\n1. **Identify the Context and Question**:\r\n1.1. **START CONTEXT**: Identify the context provided in the message. **: END CONTEXT**\r\n1.2. **START QUESTION**: Identify the question that needs to be answered based on the context.. **: END QUESTION**\r\n\r\n2. **Check the Context for Relevance**:\r\n2.1. Determine if the context contains information directly relevant to the question.\r\n2.2. If the context addresses the user's question, proceed to the next step.\r\n2.3. If the question is a greeting, respond appropriately with the greeting.\r\n2.4. If the context does not address the user's question, respond with: `{\"response\": \"\", \"success\": false}`.\r\n\r\n3. **Formulate the Response**:\r\n3.1. If the context is sufficient, formulate a clear and concise response using only the information provided in the context.\r\n3.2. Ensure the response includes all important details covered in the context, but avoid any extraneous information.\r\n\r\n4. **Avoid Referring to the Context**:\r\n4.1. Do not refer to the context or state that the response is based on the context in your answer.\r\n4.2. Ensure the response is straightforward and directly answers the question.\r\n\r\n5. **Generate the JSON Response**:\r\n5.1. Structure the response according to the following JSON schema:\r\n\r\n\r\n{\r\n  \"\$schema\": \"http:\/\/json-schema.org\/draft-07\/schema#\",\r\n  \"type\": \"object\",\r\n  \"properties\": {\r\n    \"response\": {\r\n      \"type\": \"string\",\r\n      \"description\": \"Contains the response to the question. Do not include it if the answer wasn't available in the context.\"\r\n    },\r\n    \"success\": {\r\n      \"type\": \"boolean\",\r\n      \"description\": \"Indicates whether the question was successfully answered from provided context.\"\r\n    }\r\n  },\r\n  \"required\": [\"success\"]\r\n}\r\n\r\nExample Usage:\r\n\r\nContext: [Provide context here]\r\nQuestion: [Provide question here]\r\n\r\nExpected Behavior:\r\n\r\n- If the question is fully covered by the context, provide a response using the provided JSON schema.\r\n- If the question is not fully covered by the context, respond with: {\"response\": \"\", \"success\": false}.\r\n\r\nExample Responses:\r\n\r\n- Context covers the question: {\"response\": \"Here is the information you requested.\", \"success\": true}\r\n- Context does not cover the question: {\"response\": \"\", \"success\": false}\r\n- Context does not cover the question but is a greeting: {\"response\": \"Hello, what can I help you with?.\", \"success\": true}",
-				)
+				$this->get_properties()
 			);
 
 			if ( is_wp_error( $response ) ) {
 				return $response;
 			}
-	
+
 			if ( ! isset( $response->id ) ) {
 				return false;
 			}
@@ -187,10 +253,10 @@ public function retrieve_assistant() {
 	public function create_embeddings( $content, $model = 'text-embedding-3-small' ) {
 		$response = $this->request(
 			'embeddings',
-			array(
+			[
 				'input' => $content,
 				'model' => $model,
-			)
+			]
 		);
 
 		if ( is_wp_error( $response ) ) {
@@ -211,7 +277,7 @@ public function create_embeddings( $content, $model = 'text-embedding-3-small' )
 	 * 
 	 * @return string|\WP_Error
 	 */
-	public function create_thread( $params = array() ) {
+	public function create_thread( $params = [] ) {
 		$response = $this->request(
 			'threads',
 			$params
@@ -240,10 +306,10 @@ public function create_thread( $params = array() ) {
 	public function send_message( $message, $thread, $role = 'assistant' ) {
 		$response = $this->request(
 			'threads/' . $thread . '/messages',
-			array(
+			[
 				'role'    => $role,
 				'content' => $message,
-			)
+			]
 		);
 
 		if ( is_wp_error( $response ) ) {
@@ -270,15 +336,16 @@ public function create_run( $messages, $thread ) {
 
 		$response = $this->request(
 			'threads/' . $thread . '/runs',
-			array(
+			[
 				'assistant_id'        => $this->assistant_id,
 				'additional_messages' => $messages,
+				'model'               => $this->chat_model,
 				'temperature'         => $settings['temperature'],
 				'top_p'               => $settings['top_p'],
-				'response_format'     => array(
+				'response_format'     => [
 					'type' => 'json_object',
-				),
-			)
+				],
+			]
 		);
 
 		if ( is_wp_error( $response ) ) {
@@ -301,7 +368,7 @@ public function create_run( $messages, $thread ) {
 	 * @return string|\WP_Error
 	 */
 	public function get_status( $run_id, $thread ) {
-		$response = $this->request( 'threads/' . $thread . '/runs/' . $run_id, array(), 'GET' );
+		$response = $this->request( 'threads/' . $thread . '/runs/' . $run_id, [], 'GET' );
 
 		if ( is_wp_error( $response ) ) {
 			return $response;
@@ -322,7 +389,7 @@ public function get_status( $run_id, $thread ) {
 	 * @return mixed
 	 */
 	public function get_messages( $thread ) {
-		$response = $this->request( 'threads/' . $thread . '/messages', array(), 'GET' );
+		$response = $this->request( 'threads/' . $thread . '/messages', [], 'GET' );
 
 		if ( is_wp_error( $response ) ) {
 			return $response;
@@ -345,9 +412,9 @@ public function get_messages( $thread ) {
 	public function moderate( $message ) {
 		$response = $this->request(
 			'moderations',
-			array(
+			[
 				'input' => $message,
-			)
+			]
 		);
 
 		if ( is_wp_error( $response ) ) {
@@ -365,6 +432,83 @@ public function moderate( $message ) {
 		return true;
 	}
 
+	/**
+	 * Moderate data.
+	 * 
+	 * @param array|string $chunks Data to moderate.
+	 * @param int          $id     Post ID.
+	 * 
+	 * @return true|array|\WP_Error
+	 */
+	public function moderate_chunks( $chunks, $id = null ) {
+		if ( $id ) {
+			$moderated = get_transient( 'hyve_moderate_post_' . $id );
+
+			if ( false !== $moderated ) {
+				return is_array( $moderated ) ? $moderated : true;
+			}
+		}
+
+		$openai               = self::instance();
+		$results              = [];
+		$return               = true;
+		$settings             = Main::get_settings();
+		$moderation_threshold = $settings['moderation_threshold'];
+
+		if ( ! is_array( $chunks ) ) {
+			$chunks = [ $chunks ];
+		}
+
+		foreach ( $chunks as $chunk ) {
+			$moderation = $openai->moderate( $chunk );
+
+			if ( is_wp_error( $moderation ) ) {
+				return $moderation;
+			}
+
+			if ( true !== $moderation && is_object( $moderation ) ) {
+				$results[] = $moderation;
+			}
+		}
+
+		if ( ! empty( $results ) ) {
+			$flagged = [];
+	
+			foreach ( $results as $result ) {
+				$categories = $result->categories;
+	
+				foreach ( $categories as $category => $flag ) {
+					if ( ! $flag ) {
+						continue;
+					}
+
+					if ( ! isset( $moderation_threshold[ $category ] ) || $result->category_scores->$category < ( $moderation_threshold[ $category ] / 100 ) ) {
+						continue;
+					}
+
+					if ( ! isset( $flagged[ $category ] ) ) {
+						$flagged[ $category ] = $result->category_scores->$category;
+						continue;
+					}
+	
+					if ( $result->category_scores->$category > $flagged[ $category ] ) {
+						$flagged[ $category ] = $result->category_scores->$category;
+					}
+				}
+			}
+
+			if ( ! empty( $flagged ) ) {
+				$return = $flagged;
+			}
+		}
+
+		if ( $id ) {
+			set_transient( 'hyve_moderate_post_' . $id, $return, MINUTE_IN_SECONDS );
+		}
+
+		return $return;
+	}
+
 	/**
 	 * Create Request.
 	 * 
@@ -374,12 +518,12 @@ public function moderate( $message ) {
 	 * 
 	 * @return mixed
 	 */
-	private function request( $endpoint, $params = array(), $method = 'POST' ) {
+	private function request( $endpoint, $params = [], $method = 'POST' ) {
 		if ( ! $this->api_key ) {
-			return (object) array(
+			return (object) [
 				'error'   => true,
 				'message' => 'API key is missing.',
-			);
+			];
 		}
 
 		$body = wp_json_encode( $params );
@@ -389,28 +533,28 @@ private function request( $endpoint, $params = array(), $method = 'POST' ) {
 		if ( 'POST' === $method ) {
 			$response = wp_remote_post(
 				self::$base_url . $endpoint,
-				array(
-					'headers'     => array(
+				[
+					'headers'     => [
 						'Content-Type'  => 'application/json',
 						'Authorization' => 'Bearer ' . $this->api_key,
 						'OpenAI-Beta'   => 'assistants=v2',
-					), 
+					],
 					'body'        => $body,
 					'method'      => 'POST',
 					'data_format' => 'body',
-				) 
+				]
 			);
 		}
 
 		if ( 'GET' === $method ) {
 			$url  = self::$base_url . $endpoint;
-			$args = array(
-				'headers' => array(
+			$args = [
+				'headers' => [
 					'Content-Type'  => 'application/json',
 					'Authorization' => 'Bearer ' . $this->api_key,
 					'OpenAI-Beta'   => 'assistants=v2',
-				),
-			);
+				],
+			];
 
 			if ( function_exists( 'vip_safe_wp_remote_get' ) ) {
 				$response = vip_safe_wp_remote_get( $url, '', 3, 1, 20, $args );
diff --git a/inc/Qdrant_API.php b/inc/Qdrant_API.php
new file mode 100644
index 0000000..6e7d62c
--- /dev/null
+++ b/inc/Qdrant_API.php
@@ -0,0 +1,422 @@
+setApiKey( $api_key );
+
+		$transport    = ( new Builder() )->build( $config );
+		$this->client = new Qdrant( $transport );
+
+		add_action( 'hyve_lite_migrate_data', [ $this, 'migrate_data' ] );
+	}
+
+	/**
+	 * Initialize Qdrant Integration.
+	 * 
+	 * @return bool|\WP_Error
+	 */
+	public function init() {
+		$collection_exists = $this->collection_exists();
+
+		if ( is_wp_error( $collection_exists ) ) {
+			return $collection_exists;
+		}
+
+		if ( ! $collection_exists ) {
+			$create_collection = $this->create_collection();
+
+			if ( is_wp_error( $create_collection ) ) {
+				return $create_collection;
+			}
+		}
+
+		update_option( 'hyve_qdrant_status', 'active' );
+
+		$existing_chunks = DB_Table::instance()->get_count();
+
+		if ( $existing_chunks > 0 ) {
+			update_option(
+				'hyve_qdrant_migration',
+				[
+					'total'       => (int) $existing_chunks,
+					'current'     => 0,
+					'in_progress' => true,
+				] 
+			);
+
+			wp_schedule_single_event( time(), 'hyve_lite_migrate_data' );
+		}
+
+		return true;
+	}
+
+	/**
+	 * Check if collection exists.
+	 * 
+	 * @return bool|\WP_Error
+	 */ 
+	public function collection_exists() {
+		try {
+			$collections = ( new Collections( $this->client ) )->setCollectionName( self::COLLECTION_NAME );
+			$response    = $collections->exists();
+			return $response['result']['exists'];
+		} catch ( \Exception $e ) {
+			if ( 403 === $e->getCode() ) {
+				update_option( 'hyve_qdrant_status', 'inactive' );
+			}
+
+			return new \WP_Error( 'collection_error', $e->getMessage() );
+		}
+	}
+
+	/**
+	 * Create collection.
+	 * 
+	 * @return bool|\WP_Error
+	 */
+	public function create_collection() {
+		try {
+			$collection = new CreateCollection();
+			$collection->addVector( new VectorParams( 1536, VectorParams::DISTANCE_COSINE ), 'embeddings' );
+			$response = $this->client->collections( self::COLLECTION_NAME )->create( $collection );
+			return $response['result'];
+		} catch ( \Exception $e ) {
+			if ( 403 === $e->getCode() ) {
+				update_option( 'hyve_qdrant_status', 'inactive' );
+			}
+
+			return new \WP_Error( 'collection_error', $e->getMessage() );
+		}
+	}
+
+	/**
+	 * Add point to collection.
+	 * 
+	 * @param array $embeddings Embeddings.
+	 * @param array $data       Data.
+	 * 
+	 * @return bool|\WP_Error
+	 */
+	public function add_point( $embeddings, $data ) {
+		try {
+			$points = new PointsStruct();
+
+			$points->addPoint(
+				new PointStruct(
+					self::random_hash(),
+					new VectorStruct( $embeddings, 'embeddings' ),
+					$data
+				)
+			);
+
+			$response = $this->client->collections( self::COLLECTION_NAME )->points()->upsert( $points, [ 'wait' => 'true' ] );
+
+			return 'completed' === $response['result']['status'];
+		} catch ( \Exception $e ) {
+			if ( 403 === $e->getCode() ) {
+				update_option( 'hyve_qdrant_status', 'inactive' );
+			}
+
+			return new \WP_Error( 'collection_error', $e->getMessage() );
+		}
+	}
+
+
+	/**
+	 * Add point to collection.
+	 * 
+	 * @param array $points Points.
+	 * 
+	 * @return bool|\WP_Error
+	 */
+	public function add_points( $points ) {
+		try {
+			$points_struct = new PointsStruct();
+
+			foreach ( $points as $point ) {
+				$points_struct->addPoint(
+					new PointStruct(
+						self::random_hash(),
+						new VectorStruct( $point['embeddings'], 'embeddings' ),
+						$point['data']
+					)
+				);
+			}
+
+			$response = $this->client->collections( self::COLLECTION_NAME )->points()->upsert( $points_struct, [ 'wait' => 'true' ] );
+
+			return 'completed' === $response['result']['status'];
+		} catch ( \Exception $e ) {
+			if ( 403 === $e->getCode() ) {
+				update_option( 'hyve_qdrant_status', 'inactive' );
+			}
+
+			return new \WP_Error( 'collection_error', $e->getMessage() );
+		}
+	}
+
+	/**
+	 * Delete point from collection.
+	 * 
+	 * @param int $id ID.
+	 * 
+	 * @return bool|\WP_Error
+	 */
+	public function delete_point( $id ) {
+		try {
+			$response = $this->client->collections( self::COLLECTION_NAME )->points()->deleteByFilter(
+				( new Filter() )->addMust(
+					new MatchString( 'post_id', (string) $id )
+				)
+			);
+
+			return 'acknowledged' === $response['result']['status'];
+		} catch ( \Exception $e ) {
+			if ( 403 === $e->getCode() ) {
+				update_option( 'hyve_qdrant_status', 'inactive' );
+			}
+
+			return new \WP_Error( 'collection_error', $e->getMessage() );
+		}
+	}
+
+	/**
+	 * Search collection.
+	 * 
+	 * @param array $embeddings Embeddings.
+	 * 
+	 * @return array|\WP_Error
+	 */
+	public function search( $embeddings ) {
+		try {
+			$search = (
+				new SearchRequest(
+					new VectorStruct( $embeddings, 'embeddings' )
+				)
+			)
+			->setLimit( 10 )
+			->setWithPayload( true );
+
+			$response = $this->client->collections( self::COLLECTION_NAME )->points()->search( $search );
+
+			if ( empty( $response['result'] ) ) {
+				return [];
+			}
+
+			$results = $response['result'];
+
+			$payload = array_map(
+				function ( $result ) {
+					$payload          = $result['payload'];
+					$payload['score'] = $result['score'];
+					return $payload;
+				},
+				$results
+			);
+
+			return $payload;
+		} catch ( \Exception $e ) {
+			if ( 403 === $e->getCode() ) {
+				update_option( 'hyve_qdrant_status', 'inactive' );
+			}
+
+			return new \WP_Error( 'collection_error', $e->getMessage() );
+		}
+	}
+
+	/**
+	 * Disconnect Qdrant Integration.
+	 *
+	 * @since 1.3.0
+	 *  
+	 * @return bool|\WP_Error
+	 */
+	public function disconnect() {
+		try {
+			$response = $this->client->collections( self::COLLECTION_NAME )->points()->deleteByFilter(
+				( new Filter() )->addMust(
+					new MatchString( 'website_url', get_site_url() )
+				)
+			);
+
+			return 'acknowledged' === $response['result']['status'];
+		} catch ( \Exception $e ) {
+			return new \WP_Error( 'collection_error', $e->getMessage() );
+		}
+	}
+
+	/**
+	 * Migrate Data to Qdrant.
+	 * 
+	 * @return void
+	 */
+	public function migrate_data() {
+		$db_table = DB_Table::instance();
+		$posts    = $db_table->get_by_storage( 'WordPress' );
+
+		if ( empty( $posts ) ) {
+			return;
+		}
+
+		$points = [];
+
+		foreach ( $posts as $post ) {
+			$points[] = [
+				'embeddings' => json_decode( $post->embeddings, true ),
+				'data'       => [
+					'post_id'      => $post->post_id,
+					'post_title'   => $post->post_title,
+					'post_content' => $post->post_content,
+					'token_count'  => $post->token_count,
+					'website_url'  => get_site_url(),
+				],
+			];
+		}
+
+		$qdrant = self::instance();
+
+		$success = $qdrant->add_points( $points );
+
+		if ( is_wp_error( $success ) ) {
+			return;
+		}
+
+		foreach ( $posts as $post ) {
+			$db_table->update(
+				$post->id,
+				[
+					'storage' => 'Qdrant',
+				] 
+			);
+		}
+
+		$migration_status = get_option( 'hyve_qdrant_migration', [] );
+
+		$migration_status['current'] += count( $posts );
+
+		$current  = $migration_status['current'] ?? 0;
+		$total    = $migration_status['total'] ?? 0;
+		$has_more = $current < $total;
+
+		if ( ! $has_more ) {
+			$migration_status['in_progress'] = false;
+		}
+
+		update_option( 'hyve_qdrant_migration', $migration_status );
+
+		if ( $has_more ) {
+			wp_schedule_single_event( time() + 10, 'hyve_lite_migrate_data' );
+		}
+	}
+
+	/**
+	 * Qdrant Status.
+	 * 
+	 * @since 1.3.0
+	 * 
+	 * @return bool
+	 */
+	public static function is_active() {
+		return 'active' === get_option( 'hyve_qdrant_status', 'inactive' );
+	}
+
+	/**
+	 * Qdrant Migration Status.
+	 * 
+	 * @since 1.3.0
+	 * 
+	 * @return array
+	 */
+	public static function migration_status() {
+		return get_option( 'hyve_qdrant_migration', [] );
+	}
+
+	/**
+	 * Random Hash.
+	 * 
+	 * @since 1.3.0
+	 * 
+	 * @return string
+	 */
+	public static function random_hash() {
+		$data = random_bytes( 16 );
+
+		$data[6] = chr( ( ord( $data[6] ) & 0x0f ) | 0x40 );
+		$data[8] = chr( ( ord( $data[8] ) & 0x3f ) | 0x80 );
+
+		return vsprintf( '%s%s-%s-%s-%s-%s%s%s', str_split( bin2hex( $data ), 4 ) );
+	}
+}
diff --git a/inc/Threads.php b/inc/Threads.php
index 9778dbe..3050220 100644
--- a/inc/Threads.php
+++ b/inc/Threads.php
@@ -15,9 +15,9 @@ class Threads {
 	 * Constructor.
 	 */
 	public function __construct() {
-		add_action( 'init', array( $this, 'register' ) );
-		add_action( 'hyve_chat_response', array( $this, 'record_message' ), 10, 6 );
-		add_filter( 'hyve_chat_request', array( $this, 'record_thread' ), 10, 3 );
+		add_action( 'init', [ $this, 'register' ] );
+		add_action( 'hyve_chat_response', [ $this, 'record_message' ], 10, 6 );
+		add_filter( 'hyve_chat_request', [ $this, 'record_thread' ], 10, 3 );
 	}
 
 	/**
@@ -26,7 +26,7 @@ public function __construct() {
 	 * @return void
 	 */
 	public function register() {
-		$labels = array(
+		$labels = [
 			'name'               => _x( 'Threads', 'post type general name', 'hyve-lite' ),
 			'singular_name'      => _x( 'Thread', 'post type singular name', 'hyve-lite' ),
 			'menu_name'          => _x( 'Threads', 'admin menu', 'hyve-lite' ),
@@ -41,9 +41,9 @@ public function register() {
 			'parent_item_colon'  => __( 'Parent Thread:', 'hyve-lite' ),
 			'not_found'          => __( 'No Threads found.', 'hyve-lite' ),
 			'not_found_in_trash' => __( 'No Threads found in Trash.', 'hyve-lite' ),
-		);
+		];
 
-		$args = array(
+		$args = [
 			'labels'             => $labels,
 			'description'        => __( 'Threads.', 'hyve-lite' ),
 			'public'             => false,
@@ -51,13 +51,13 @@ public function register() {
 			'show_ui'            => false,
 			'show_in_menu'       => false,
 			'query_var'          => false,
-			'rewrite'            => array( 'slug' => 'threads' ),
+			'rewrite'            => [ 'slug' => 'threads' ],
 			'capability_type'    => 'post',
 			'has_archive'        => false,
 			'hierarchical'       => false,
 			'show_in_rest'       => false,
-			'supports'           => array( 'title', 'editor', 'custom-fields', 'comments' ),
-		);
+			'supports'           => [ 'title', 'editor', 'custom-fields', 'comments' ],
+		];
 
 		register_post_type( 'hyve_threads', $args );
 	}
@@ -81,11 +81,11 @@ public function record_message( $run_id, $thread_id, $query, $record_id, $messag
 
 		self::add_message(
 			$record_id,
-			array(
+			[
 				'thread_id' => $thread_id,
 				'sender'    => 'bot',
 				'message'   => $response,
-			)
+			]
 		);
 	}
 
@@ -102,20 +102,20 @@ public function record_thread( $thread_id, $record_id, $message ) {
 		if ( $record_id ) {
 			$record_id = self::add_message(
 				$record_id,
-				array(
+				[
 					'thread_id' => $thread_id,
 					'sender'    => 'user',
 					'message'   => $message,
-				)
+				]
 			);
 		} else {
 			$record_id = self::create_thread(
 				$message,
-				array(
+				[
 					'thread_id' => $thread_id,
 					'sender'    => 'user',
 					'message'   => $message,
-				)
+				]
 			);
 		}
 
@@ -133,21 +133,21 @@ public function record_thread( $thread_id, $record_id, $message ) {
 	 */
 	public static function create_thread( $title, $data ) {
 		$post_id = wp_insert_post(
-			array(
+			[
 				'post_title'   => $title,
 				'post_content' => '',
 				'post_status'  => 'publish',
 				'post_type'    => 'hyve_threads',
-			)
+			]
 		);
 
-		$thread_data = array(
-			array(
+		$thread_data = [
+			[
 				'time'    => time(),
 				'sender'  => $data['sender'],
-				'message' => $data['message'],
-			),
-		);
+				'message' => wp_kses_post( $data['message'] ),
+			],
+		];
 
 		update_post_meta( $post_id, '_hyve_thread_data', $thread_data );
 		update_post_meta( $post_id, '_hyve_thread_count', 1 );
@@ -173,11 +173,11 @@ public static function add_message( $post_id, $data ) {
 
 		$thread_data = get_post_meta( $post_id, '_hyve_thread_data', true );
 
-		$thread_data[] = array(
+		$thread_data[] = [
 			'time'    => time(),
 			'sender'  => $data['sender'],
-			'message' => $data['message'],
-		);
+			'message' => wp_kses_post( $data['message'] ),
+		];
 
 		update_post_meta( $post_id, '_hyve_thread_data', $thread_data );
 		update_post_meta( $post_id, '_hyve_thread_count', count( $thread_data ) );
diff --git a/inc/Tokenizer.php b/inc/Tokenizer.php
new file mode 100644
index 0000000..2f3b939
--- /dev/null
+++ b/inc/Tokenizer.php
@@ -0,0 +1,112 @@
+get( 'cl100k_base' );
+
+		$content = preg_replace( '/<[^>]+>/', '', $post['content'] );
+		$tokens  = $encoder->encode( $content );
+
+		$article = [
+			'post_id'      => $post['ID'] ?? null,
+			'post_title'   => $post['title'],
+			'post_content' => $post['content'],
+			'tokens'       => $tokens,
+		];
+
+		$data = [];
+
+		$chunked_token_size = 1000;
+		$token_length       = count( $tokens );
+
+		if ( $token_length > $chunked_token_size ) {
+			$shortened_sentences = self::create_chunks( $article['post_content'], $chunked_token_size );
+
+			foreach ( $shortened_sentences as $shortened_sentence ) {
+				$chunked_tokens = $encoder->encode( $post['title'] . ' ' . $shortened_sentence );
+
+				$data[] = [
+					'post_id'      => $article['post_id'],
+					'post_title'   => $article['post_title'],
+					'post_content' => $shortened_sentence,
+					'tokens'       => $chunked_tokens,
+					'token_count'  => count( $chunked_tokens ),
+				];
+			}
+		} else {
+			$chunked_tokens = $encoder->encode( $post['title'] . ' ' . $content );
+
+			$data[] = [
+				'post_id'      => $article['post_id'],
+				'post_title'   => $article['post_title'],
+				'post_content' => $article['post_content'],
+				'tokens'       => $chunked_tokens,
+				'token_count'  => count( $chunked_tokens ),
+			];
+		}
+
+		return $data;
+	}
+
+	/**
+	 * Create Chunks.
+	 * 
+	 * @param string $text Text to chunk.
+	 * @param int    $size Chunk size.
+	 * 
+	 * @return array
+	 */
+	public static function create_chunks( $text, $size = 1000 ) {
+		$provider = new EncoderProvider();
+		$encoder  = $provider->get( 'cl100k_base' );
+
+		$sentences = explode( '. ', $text );
+
+		$chunks        = [];
+		$tokens_so_far = 0;
+		$chunk         = [];
+
+		foreach ( $sentences as $sentence ) {
+			$token_length = count( $encoder->encode( ' ' . $sentence ) );
+
+			if ( $tokens_so_far + $token_length > $size ) {
+				$chunks[]      = implode( '. ', $chunk ) . '.';
+				$chunk         = [];
+				$tokens_so_far = 0;
+			}
+
+			if ( $token_length > $size ) {
+				continue;
+			}
+
+			$chunk[]        = $sentence;
+			$tokens_so_far += $token_length + 1;
+		}
+
+		if ( 0 < count( $chunk ) ) {
+			$chunks[] = implode( '. ', $chunk ) . '.';
+		}
+
+		return $chunks;
+	}
+}
diff --git a/package-lock.json b/package-lock.json
index 1bdcbfa..c7a5aa5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,7 +12,6 @@
         "@dqbd/tiktoken": "^1.0.13",
         "@wordpress/icons": "^9.39.0",
         "classnames": "^2.5.1",
-        "js-tiktoken": "^1.0.10",
         "object-hash": "^3.0.0"
       },
       "devDependencies": {
@@ -6714,6 +6713,7 @@
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "dev": true,
       "funding": [
         {
           "type": "github",
@@ -14488,15 +14488,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/js-tiktoken": {
-      "version": "1.0.12",
-      "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.12.tgz",
-      "integrity": "sha512-L7wURW1fH9Qaext0VzaUDpFGVQgjkdE3Dgsy9/+yXyGEpBKnylTd0mU0bfbNkKDlXRb6TEsZkwuflu1B8uQbJQ==",
-      "license": "MIT",
-      "dependencies": {
-        "base64-js": "^1.5.1"
-      }
-    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index c5959d8..8f0d7f0 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,6 @@
     "start:backend": "wp-scripts start --webpack-src-dir=src/backend --output-path=build/backend --output-filename=index.js",
     "start:frontend": "wp-scripts start --webpack-src-dir=src/frontend --output-path=build/frontend --output-filename=frontend.js",
     "start:block": "wp-scripts start --webpack-src-dir=src/block --output-path=build/block --output-filename=index.js",
-    "test:e2e": "wp-scripts test-e2e",
-    "test:unit": "wp-scripts test-unit-js",
     "dist": "bash bin/dist.sh",
     "release": "npx semantic-release"
   },
@@ -50,7 +48,7 @@
     "grunt-wp-readme-to-markdown": "^2.1.0",
     "npm-run-all": "^4.1.5",
     "replace-in-file": "^7.1.0",
-		"semantic-release": "^19.0.5",
+    "semantic-release": "^19.0.5",
     "semantic-release-slack-bot": "^4.0.2",
     "simple-git-hooks": "^2.9.0",
     "tailwindcss": "^3.4.0"
@@ -59,7 +57,6 @@
     "@dqbd/tiktoken": "^1.0.13",
     "@wordpress/icons": "^9.39.0",
     "classnames": "^2.5.1",
-    "js-tiktoken": "^1.0.10",
     "object-hash": "^3.0.0"
   }
 }
diff --git a/phpcs.xml b/phpcs.xml
index ac9c278..d2ee6eb 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -1,6 +1,9 @@
 
 
     ThemeIsle ruleset
+
+    
+
     
 
     
@@ -14,18 +17,18 @@
     
         
         
+	    
     
 
-
-    
-
     
+
     
         
             
         
     
 
+    
 
     
     
@@ -34,7 +37,6 @@
     node_modules/*
     vendor/*
     build/*
-    tests/*
     Gruntfile.js
     dist
     cypress
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..499d774
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,14 @@
+
+    
+        
+            ./tests/php/unit/tests/
+        
+    
+
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
index f5c47be..459922c 100644
--- a/readme.txt
+++ b/readme.txt
@@ -3,7 +3,7 @@ Contributors: themeisle, hardeepasrani
 Tags: automation, support, chat, ai, openai
 Requires at least: 6.2
 Tested up to: 6.6
-Requires PHP: 7.4
+Requires PHP: 8.1
 Stable tag: 1.1.0
 License: GPLv3
 License URI: https://www.gnu.org/licenses/gpl-3.0.en.html
@@ -23,6 +23,7 @@ For AI processing, Hyve relies on the OpenAI API to provide certain features, su
 By upgrading to [Hyve Pro](https://themeisle.com/plugins/hyve/?utm_source=plugin-readme&utm_medium=hyvelite&utm_campaign=profeatures), you unlock enhanced capabilities that enrich your chatbot's performance:
 
 - **Custom Data**: Add custom information to your chatbot’s knowledge base for data that may not be suitable for public display on your website.
+- **Website & Sitemap Crawling**: Use Website URLs and Sitemaps to add data to your knowledge base from external sources. 
 - **FAQ Insights**: Review frequently asked but unanswered questions to improve your bot's knowledge base.
 - **Chat History**: Access full chat history to analyze and enhance user interactions.
 - **Suggested Questions**: Encourage engagement with predefined starter questions.
diff --git a/src/backend/components/ProgressBar.js b/src/backend/components/ProgressBar.js
new file mode 100644
index 0000000..2d3ea6f
--- /dev/null
+++ b/src/backend/components/ProgressBar.js
@@ -0,0 +1,32 @@
+/**
+ * External dependencies.
+ */
+import classnames from 'classnames';
+
+const ProgressBar = ({
+	value,
+	max = 100,
+	className
+}) => {
+	const progress = 0 < max ? Math.round( ( value / max ) * 100 ) : 0;
+
+	const wrapClasses = classnames(
+		'w-full h-5 border rounded-md border-solid bg-white border-light-gray relative overflow-hidden',
+		className
+	);
+
+	return (
+		
+
+ +
+ ); +}; + +export default ProgressBar; diff --git a/src/backend/parts/Home.js b/src/backend/parts/Home.js index be8d2ea..f63cfa8 100644 --- a/src/backend/parts/Home.js +++ b/src/backend/parts/Home.js @@ -10,6 +10,7 @@ import { } from '@wordpress/components'; import { + dispatch, useDispatch, useSelect } from '@wordpress/data'; @@ -20,6 +21,8 @@ import { help } from '@wordpress/icons'; +const { setRoute } = dispatch( 'hyve' ); + const STATUS = [ { label: __( 'Sessions', 'hyve-lite' ), @@ -33,55 +36,19 @@ const STATUS = [ }, { label: __( 'Knowledge Base', 'hyve-lite' ), - value: `${ hyve?.stats?.totalChunks } / ${ hyve?.chunksLimit }`, - description: __( 'Current knowledge base chunks used.', 'hyve-lite' ) + value: Boolean( hyve.isQdrantActive ) ? hyve?.stats?.totalChunks : `${ hyve?.stats?.totalChunks } / ${ hyve?.chunksLimit }`, + description: __( 'Current knowledge base chunks used.', 'hyve-lite' ), + action: { + label: __( 'Need more storage?', 'hyve-lite' ), + action: () => setRoute( 'integrations' ), + condition: ! Boolean( hyve.isQdrantActive ) && 400 < hyve?.stats?.totalChunks + } } ]; -const Home = () => { - const hasAPI = useSelect( ( select ) => select( 'hyve' ).hasAPI() ); - +const Dashboard = () => { const { setRoute } = useDispatch( 'hyve' ); - if ( ! hasAPI ) { - return ( -
- - -
- -
- -

{ __( 'Welcome to Hyve! Designed to seamlessly integrate AI chat into your WordPress site, this plugin allows you to craft a personalized chat experience using your own posts and pages. Enjoy engaging with your website visitors through Hyve!', 'hyve-lite' ) }

- -

{ __( 'To begin using the Hyve plugin, you\'ll need an OpenAI API key. This key enables Hyve to communicate with OpenAI\'s powerful language models, ensuring you get the best possible responses.', 'hyve-lite' ) }

- -
- - - -
-
-
-
- ); - } - const ACTIONS = [ { label: __( 'Knowledge Base', 'hyve-lite' ), @@ -114,7 +81,7 @@ const Home = () => {

- { STATUS.map( ({ label, value, description }) => ( + { STATUS.map( ({ label, value, description, action }) => (
@@ -129,6 +96,12 @@ const Home = () => {
{ description }
+ + { ( action && action?.condition ) && ( +
+ { action?.label } +
+ ) }
@@ -170,4 +143,47 @@ const Home = () => { ); }; +const Home = () => { + const hasAPI = useSelect( ( select ) => select( 'hyve' ).hasAPI() ); + + const { setRoute } = useDispatch( 'hyve' ); + + if ( ! hasAPI ) { + return ( +
+ + +
+
+

{ __( 'Welcome to Hyve! Designed to seamlessly integrate AI chat into your WordPress site, this plugin allows you to craft a personalized chat experience using your own posts and pages. Enjoy engaging with your website visitors through Hyve!', 'hyve-lite' ) }

+ +

{ __( 'To begin using the Hyve plugin, you\'ll need an OpenAI API key. This key enables Hyve to communicate with OpenAI\'s powerful language models, ensuring you get the best possible responses.', 'hyve-lite' ) }

+ +
+ + + +
+
+
+
+ ); + } + + return ; +}; + export default Home; diff --git a/src/backend/parts/Messages.js b/src/backend/parts/Messages.js index f8a6fa3..fb31484 100644 --- a/src/backend/parts/Messages.js +++ b/src/backend/parts/Messages.js @@ -163,9 +163,12 @@ const Messages = () => { return (
-

{ message.message }

+

); @@ -177,7 +180,7 @@ const Messages = () => { key={ index } className="max-w-[75%] min-w-[50%] text-[white] flex flex-col items-end ml-auto my-3.5" > -

{ message.message }

+

{ message.message }

); diff --git a/src/backend/parts/PostsTable.js b/src/backend/parts/PostsTable.js index 639c468..25d2947 100644 --- a/src/backend/parts/PostsTable.js +++ b/src/backend/parts/PostsTable.js @@ -14,6 +14,9 @@ const PostsTable = ({ hasMore, onFetch, onAction, + actionProps = { + variant: 'primary' + }, actionLabel, isBusy, isDisabled = false, @@ -55,7 +58,7 @@ const PostsTable = ({ )} diff --git a/src/backend/parts/data/KnowledgeBase.js b/src/backend/parts/data/KnowledgeBase.js index 194fcf9..73ca698 100644 --- a/src/backend/parts/data/KnowledgeBase.js +++ b/src/backend/parts/data/KnowledgeBase.js @@ -3,77 +3,30 @@ */ import { __ } from '@wordpress/i18n'; -import apiFetch from '@wordpress/api-fetch'; - import { + Button, Panel, PanelRow } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; - -import { - useEffect, - useState -} from '@wordpress/element'; +import { useState } from '@wordpress/element'; -import { addQueryArgs } from '@wordpress/url'; +import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies. */ -import PostsTable from '../PostsTable'; +import { KNOWLEDGE_BASE } from '../../route'; const KnowledgeBase = () => { - const [ posts, setPosts ] = useState([]); - const [ hasMore, setHasMore ] = useState( false ); - const [ isLoading, setLoading ] = useState( true ); - const [ isDeleting, setDeleting ] = useState([]); - - const { createNotice } = useDispatch( 'core/notices' ); - const { setTotalChunks } = useDispatch( 'hyve' ); - - const fetchPosts = async() => { - setLoading( true ); - - const response = await apiFetch({ - path: addQueryArgs( `${ window.hyve.api }/data`, { - offset: posts?.length || 0, - status: 'included' - }) - }); + const [ view, setView ] = useState( null ); - setLoading( false ); - setPosts( posts.concat( response.posts ) ); - setHasMore( response.more ); - setTotalChunks( response?.totalChunks ); - }; + const SOURCES = applyFilters( 'hyve.data', KNOWLEDGE_BASE ); - const onDelete = async( id ) => { - setDeleting([ ...isDeleting, id ]); - - await apiFetch({ - path: addQueryArgs( `${ window.hyve.api }/data`, { - id - }), - method: 'DELETE' - }); - - setPosts( posts.filter( ( post ) => post.ID !== id ) ); - - createNotice( - 'success', - __( 'Post has been removed.', 'hyve-lite' ), - { - type: 'snackbar', - isDismissible: true - } - ); - }; - - useEffect( () => { - fetchPosts(); - }, []); + if ( view ) { + const { component: Component } = SOURCES[view]; + return ; + } return (
@@ -81,18 +34,41 @@ const KnowledgeBase = () => { header={ __( 'Knowledge Base', 'hyve-lite' ) } > -

{ __( 'A list of all the content that has been added to the knowledge base. It\'s the foundation that supports your chat assistant, enabling it to provide accurate and insightful responses.', 'hyve-lite' ) }

- -
- +

{ __( 'A list of all the content that has been added to the Knowledge Base. It\'s the foundation that supports your chat assistant, enabling it to provide accurate and insightful responses.', 'hyve-lite' ) }

+ +
+ { Object.keys( SOURCES ).map( key => { + const { label, description, icon, isPro = false } = SOURCES[key]; + return ( + + ); + })}
diff --git a/src/backend/parts/data/Posts.js b/src/backend/parts/data/Posts.js new file mode 100644 index 0000000..7b9c428 --- /dev/null +++ b/src/backend/parts/data/Posts.js @@ -0,0 +1,132 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import apiFetch from '@wordpress/api-fetch'; + +import { + Button, + Panel, + PanelRow +} from '@wordpress/components'; + +import { useDispatch } from '@wordpress/data'; + +import { + useEffect, + useState +} from '@wordpress/element'; + +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies. + */ +import PostsTable from '../PostsTable'; +import AddData from './AddData'; + +const Posts = ({ setView }) => { + const [ posts, setPosts ] = useState([]); + const [ hasMore, setHasMore ] = useState( false ); + const [ isLoading, setLoading ] = useState( true ); + const [ isDeleting, setDeleting ] = useState([]); + const [ addPost, setAddPost ] = useState( false ); + + const { createNotice } = useDispatch( 'core/notices' ); + const { setTotalChunks } = useDispatch( 'hyve' ); + + const fetchPosts = async() => { + setLoading( true ); + + const response = await apiFetch({ + path: addQueryArgs( `${ window.hyve.api }/data`, { + offset: posts?.length || 0, + status: 'included' + }) + }); + + setLoading( false ); + setPosts( posts.concat( response.posts ) ); + setHasMore( response.more ); + setTotalChunks( response?.totalChunks ); + }; + + const onDelete = async( id ) => { + setDeleting([ ...isDeleting, id ]); + + await apiFetch({ + path: addQueryArgs( `${ window.hyve.api }/data`, { + id + }), + method: 'DELETE' + }); + + setPosts( posts.filter( ( post ) => post.ID !== id ) ); + + createNotice( + 'success', + __( 'Post has been removed.', 'hyve-lite' ), + { + type: 'snackbar', + isDismissible: true + } + ); + }; + + useEffect( () => { + fetchPosts(); + }, []); + + if ( addPost ) { + return ; + } + + return ( +
+ +
+
+ + +

{ __( 'All the content from your WordPress site that has been added to the Knowledge Base.', 'hyve-lite' ) }

+ +
+ +
+ +
+ +
+
+
+
+ ); +}; + +export default Posts; diff --git a/src/backend/parts/data/SitemapCrawler.js b/src/backend/parts/data/SitemapCrawler.js new file mode 100644 index 0000000..affcc70 --- /dev/null +++ b/src/backend/parts/data/SitemapCrawler.js @@ -0,0 +1,114 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { capitalize } from 'lodash'; + +import { + Button, + Panel, + PanelRow +} from '@wordpress/components'; + +/** + * Internal dependencies. + */ +import UpsellContainer from '../UpsellContainer'; + +const posts = { + 1: { + url: 'https://example.com/sitemap.xml', + status: 'queued' + }, + 2: { + url: 'https://example.com/sitemap.xml', + status: 'completed' + }, + 3: { + url: 'https://example.com/sitemap.xml', + status: 'completed' + } +}; + +const SitemapCrawler = ({ setView }) => { + return ( + <> +
+ +
+
+ + +

{ __( 'Use this tool to crawl a website and add its content to the Knowledge Base using the sitemap.', 'hyve-lite' ) }

+ +
+ +
+ +
+ +
+
+ +
+
+
+
{ __( 'Sitemap URL', 'hyve-lite' ) }
+
{ __( 'Status', 'hyve-lite' ) }
+
{ __( 'Action', 'hyve-lite' ) }
+
+
+ +
+ { Object.keys( posts ).map( ( post ) => ( +
+
+ { posts[ post ].url } +
+ +
{ capitalize( posts[ post ].status ) }
+ +
+ +
+
+ ) )} +
+
+
+
+
+
+
+ + ); +}; + +export default SitemapCrawler; diff --git a/src/backend/parts/data/URLCrawler.js b/src/backend/parts/data/URLCrawler.js new file mode 100644 index 0000000..71b7b7e --- /dev/null +++ b/src/backend/parts/data/URLCrawler.js @@ -0,0 +1,88 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { + Button, + TextControl, + Panel, + PanelRow +} from '@wordpress/components'; + +/** + * Internal dependencies. + */ +import PostsTable from '../PostsTable'; +import UpsellContainer from '../UpsellContainer'; + +const posts = Array.from({ length: 5 }, ( _, i ) => ({ id: i + 1, title: `https://example.com/page${i + 1}` }) );; + +const URLCrawler = ({ setView }) => { + return ( + <> +
+ +
+
+ + +

{ __( 'This page allows you to add URLs to the Knowledge Base. You can add URLs to the Knowledge Base by entering the URL in the field below and clicking the "Crawl URL" button.', 'hyve-lite' ) }

+ +
+ +
+
+ {} } + /> + + +
+
+ + {} } + onAction={ () => {} } + isBusy={ [] } + actionProps={{ + variant: 'secondary', + isDestructive: true + }} + actionLabel={ __( 'Delete', 'hyve-lite' ) } + /> +
+
+
+
+
+ + ); +}; + +export default URLCrawler; diff --git a/src/backend/parts/data/Updated.js b/src/backend/parts/data/Updated.js index 5bfc4b5..bcd63d2 100644 --- a/src/backend/parts/data/Updated.js +++ b/src/backend/parts/data/Updated.js @@ -95,7 +95,7 @@ const Updated = () => { header={ __( 'Updated', 'hyve-lite' ) } > -

{ __( 'Here, you\'ll see posts that have been updated since their addition to the knowledge base. This page allows you to review these updates and decide if you want to refresh the knowledge your assistant relies on.', 'hyve-lite' ) }

+

{ __( 'Here, you\'ll see posts that have been updated since their addition to the Knowledge Base. This page allows you to review these updates and decide if you want to refresh the knowledge your assistant relies on.', 'hyve-lite' ) }

{ + const settings = useSelect( ( select ) => select( 'hyve' ).getSettings() ); + const isQdrantActive = useSelect( ( select ) => select( 'hyve' ).isQdrantActive() ); + + const { + setSetting, + setQdrantStatus + } = useDispatch( 'hyve' ); + + const { createNotice } = useDispatch( 'core/notices' ); + + const [ isSaving, setIsSaving ] = useState( false ); + const [ isOpen, setIsOpen ] = useState( false ); + const [ migrationStatus, setMigrationStatus ] = useState([]); + + const getQdrantStatus = async() => { + try { + const response = await apiFetch({ + path: `${ window.hyve.api }/qdrant` + }); + + if ( response.error ) { + throw new Error( response.error ); + } + + setQdrantStatus( Boolean( response.status ) ); + setMigrationStatus( response.migration ); + + if ( response.migration?.in_progress ) { + setTimeout( getQdrantStatus, 10000 ); + } + } catch ( error ) { + createNotice( + 'error', + error, + { + type: 'snackbar', + isDismissible: true + } + ); + } + }; + + useEffect( () => { + getQdrantStatus(); + }, []); + + const onDeactivate = async() => { + setIsSaving( true ); + + try { + const response = await apiFetch({ + path: `${ window.hyve.api }/qdrant`, + method: 'POST' + }); + + if ( response.error ) { + throw new Error( response.error ); + } + + setQdrantStatus( false ); + + createNotice( + 'success', + __( 'Qdrant disconnected.', 'hyve-lite' ), + { + type: 'snackbar', + isDismissible: true + } + ); + } catch ( error ) { + createNotice( + 'error', + error, + { + type: 'snackbar', + isDismissible: true + } + ); + } + + setIsSaving( false ); + }; + + const onSave = async() => { + setIsSaving( true ); + + try { + const response = await apiFetch({ + path: `${ window.hyve.api }/settings`, + method: 'POST', + data: { + data: settings + } + }); + + if ( response.error ) { + throw new Error( response.error ); + } + + await getQdrantStatus(); + + createNotice( + 'success', + __( 'Settings saved.', 'hyve-lite' ), + { + type: 'snackbar', + isDismissible: true + } + ); + } catch ( error ) { + createNotice( + 'error', + error, + { + type: 'snackbar', + isDismissible: true + } + ); + } + + setIsSaving( false ); + }; + + return ( +
+ + { ! isQdrantActive && ( + <> + +

{ __( 'Use Qdrant to increase the Knowledge Base limit of Hyve. By integrating Qdrant, you can manage larger datasets and improve query performance for your website. To integrate Qdrant with your application, you\'ll need an API key and endpoint.', 'hyve-lite' ) }

+

{ __( 'Qdrant offers a free plan that supports thousands of data chunks, making it an excellent choice for most use cases without incurring additional costs.', 'hyve-lite' ) }

+ + + { __( 'Learn more about Qdrant', 'hyve-lite' ) } + +
+ + + setSetting( 'qdrant_api_key', newValue ) } + /> + + + + setSetting( 'qdrant_endpoint', newValue ) } + /> + + + + + + + ) } + + { ( isQdrantActive && Boolean( migrationStatus?.in_progress ) ) && ( + +

{ __( 'Migration in progress. Please wait while we process your data. This may take a couple of minutes.', 'hyve-lite' ) }

+ + +
+ )} + + { ( isQdrantActive && ! Boolean( migrationStatus?.in_progress ) ) && ( + <> + { isOpen && ( + setIsOpen( false ) } + size="medium" + > +

{ __( 'If you proceed, all the data associated with this website will be deleted from Qdrant, and all the posts exceeding the Knowledge Base limit will be removed from the Knowledge Base.', 'hyve-lite' ) }

+ + +
+ ) } + + +

+ + + { __( 'Qdrant is connected and ready to use.', 'hyve-lite' ) } +

+ + +
+ + )} +
+
+ ); +}; + +export default Qdrant; diff --git a/src/backend/parts/settings/Advanced.js b/src/backend/parts/settings/Advanced.js index 5abb366..a5ed7cd 100644 --- a/src/backend/parts/settings/Advanced.js +++ b/src/backend/parts/settings/Advanced.js @@ -23,6 +23,7 @@ import { const Advanced = () => { const settings = useSelect( ( select ) => select( 'hyve' ).getSettings() ); + const { setHasAPI, setSetting diff --git a/src/backend/parts/settings/Assistant.js b/src/backend/parts/settings/Assistant.js index 53954aa..37e6fce 100644 --- a/src/backend/parts/settings/Assistant.js +++ b/src/backend/parts/settings/Assistant.js @@ -9,7 +9,8 @@ import { Button, Panel, PanelRow, - RangeControl + RangeControl, + SelectControl } from '@wordpress/components'; import { @@ -70,6 +71,26 @@ const Assistant = () => { + + setSetting( 'chat_model', newValue ) } + /> + + { ...state, totalChunks: action.totalChunks }; + case 'SET_QDRANT_STATUS': + return { + ...state, + isQdrantActive: action.isQdrantActive + }; default: return state; } diff --git a/src/backend/style.scss b/src/backend/style.scss index 8cc3c42..1049c03 100644 --- a/src/backend/style.scss +++ b/src/backend/style.scss @@ -26,16 +26,56 @@ display: none; } -.hyve-video { - position: relative; - padding-bottom: 56.25%; /* 16:9 */ - height: 0; - - iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } +.hyve-chat-message { + p { + font-size: 13px; + } + + h1, h2, h3, h4, h5, h6 { + font-size: 13px; + margin-bottom: 12px; + } + + ol, ul { + padding-left: 12px; + } + + pre { + background-color: #282c34; + color: #abb2bf; + padding: 1em; + border-radius: 0; + font-family: monospace; + line-height: 1.5; + overflow-x: auto; + white-space: pre; + word-spacing: normal; + margin: 20px -10px; + + * { + color: #abb2bf; + word-break: normal; + word-wrap: normal; + } + + ::selection { + background-color: #3e4451; + } + + ::before { + content: attr(data-content); + } + + ::before:has(+ :matches(function, const, return, let, var)) { + color: #c678dd; + } + } + + code { + background: #d0effb; + border-radius: 5px; + border: none; + padding: 0 3px; + color: #333; + } } diff --git a/src/backend/utils.js b/src/backend/utils.js index 22f222c..0842bc1 100644 --- a/src/backend/utils.js +++ b/src/backend/utils.js @@ -1,8 +1,3 @@ -/** - * External dependencies. - */ -import { getEncoding } from 'js-tiktoken'; - /** * WordPress dependencies. */ @@ -14,89 +9,6 @@ import { dispatch } from '@wordpress/data'; const { createNotice } = dispatch( 'core/notices' ); -const tokenizer = getEncoding( 'cl100k_base' ); - -const createChunks = ( text, maxTokens, tokenizer ) => { - const sentences = text.split( '. ' ); - - const chunks = []; - let tokensSoFar = 0; - let chunk = []; - - for ( let i = 0; i < sentences.length; i++ ) { - const sentence = sentences[i]; - const tokenLength = tokenizer.encode( ' ' + sentence ).length; - - if ( tokensSoFar + tokenLength > maxTokens ) { - chunks.push( chunk.join( '. ' ) + '.' ); - chunk = []; - tokensSoFar = 0; - } - - if ( tokenLength > maxTokens ) { - continue; - } - - chunk.push( sentence ); - tokensSoFar += tokenLength + 1; - } - - if ( 0 < chunk.length ) { - chunks.push( chunk.join( '. ' ) + '.' ); - } - - return chunks; -}; - -export const tokenize = ( post, chunk = true ) => { - let { - ID, - title, - content - } = post; - - content = content.replace( /<[^>]+>/g, '' ); - - const tokens = tokenizer.encode( content ); - - const article = { - 'post_id': ID, - 'post_title': title, - 'post_content': content, - tokens - }; - - let data = []; - - const chunkedTokenSize = 1000; - const tokenLength = article.tokens.length; - - if ( ( tokenLength > chunkedTokenSize ) && chunk ) { - const shortenedSentences = createChunks( article.post_content, chunkedTokenSize, tokenizer ); - - for ( const shortenedSentence of shortenedSentences ) { - const chunkedTokens = tokenizer.encode( article.title + ' ' + shortenedSentence ); - - data.push({ - ...article, - 'post_content': shortenedSentence, - tokens: chunkedTokens, - 'token_count': chunkedTokens.length - }); - } - } else { - const chunkedTokens = tokenizer.encode( article.title + ' ' + article.post_content ); - - data.push({ - ...article, - tokens: chunkedTokens, - 'token_count': chunkedTokens.length - }); - } - - return data; -}; - export const moderationLabels = { 'hate': { label: __( 'Hate Speech', 'hyve-lite' ), @@ -162,52 +74,40 @@ export const onProcessData = async({ onSuccess = () => {}, onError = () => {} }) => { - const chunks = tokenize( post ); - let bailOut = false; + let response; try { - for ( const data of chunks ) { - if ( bailOut ) { - break; - } - - let response = ''; - - if ( 'knowledge' === type ) { - if ( post.ID ) { - response = await apiFetch({ - path: `${ window.hyve.api }/knowledge/${ post.ID }`, - method: 'POST', - data: { - data, - ...params - } - }); - } else { - response = await apiFetch({ - path: `${ window.hyve.api }/knowledge`, - method: 'POST', - data: { - data, - ...params - } - }); + if ( 'knowledge' === type ) { + response = await apiFetch({ + path: post.ID ? `${ window.hyve.api }/knowledge/${ post.ID }` : `${ window.hyve.api }/knowledge`, + method: 'POST', + data: { + data: post, + ...params } - } else { - response = await apiFetch({ - path: `${ window.hyve.api }/data`, - method: 'POST', - data: { - data, - ...params - } - }); - } + }); + } else if ( 'link' === type ) { + response = await apiFetch({ + path: `${ window.hyve.api }/link`, + method: 'POST', + data: { + data: post, + ...params + } + }); + } else { + response = await apiFetch({ + path: `${ window.hyve.api }/data`, + method: 'POST', + data: { + data: post, + ...params + } + }); + } - if ( response.error ) { - bailOut = true; - throw response; - } + if ( response.error ) { + throw response; } createNotice( diff --git a/src/block/render.php b/src/block/render.php index 339f67f..7cd2715 100644 --- a/src/block/render.php +++ b/src/block/render.php @@ -21,9 +21,9 @@ $hyve_id, - ) + ] ) ); ?> diff --git a/src/frontend/App.js b/src/frontend/App.js index d0cf8f8..74a36a5 100644 --- a/src/frontend/App.js +++ b/src/frontend/App.js @@ -65,7 +65,11 @@ class App { add( message, sender, id = null ) { const time = new Date(); - message = this.sanitize( message ); + if ( 'user' === sender ) { + message = this.sanitize( message ); + } + + message = this.addTargetBlank( message ); this.messages.push({ time, message, sender, id }); this.addMessage( time, message, sender, id ); @@ -89,6 +93,29 @@ class App { return tempDiv.innerHTML; } + addTargetBlank( message ) { + const tempDiv = document.createElement( 'div' ); + tempDiv.innerHTML = message; + + const links = tempDiv.querySelectorAll( 'a' ); + + links.forEach( link => { + link.target = '_blank'; + }); + + const images = tempDiv.querySelectorAll( 'img' ); + + images.forEach( image => { + const anchor = document.createElement( 'a' ); + anchor.href = image.src; + anchor.target = '_blank'; + anchor.appendChild( image.cloneNode( true ) ); + image.parentNode.replaceChild( anchor, image ); + }); + + return tempDiv.innerHTML; + } + setThreadID( threadID ) { this.threadID = threadID; } @@ -204,7 +231,7 @@ class App { const date = `${ String( datetime.getDate() ).padStart( 2, 0 ) }/${ String( datetime.getMonth() + 1 ).padStart( 2, 0 ) }/${ datetime.getFullYear() } ${ String( datetime.getHours() ).padStart( 2, 0 ) }:${ String( datetime.getMinutes() ).padStart( 2, 0 ) } ${ 12 <= datetime.getHours() ? 'PM' : 'AM' }`; - let messageHTML = `

${message}

`; + let messageHTML = `
${message}
`; if ( null === id ) { messageHTML += ``; diff --git a/src/frontend/style.scss b/src/frontend/style.scss index 4b5c34d..9b608cd 100644 --- a/src/frontend/style.scss +++ b/src/frontend/style.scss @@ -239,9 +239,8 @@ display: flex; flex-direction: column; - p { + div { font-size: 13px; - display: flex; padding: 10px; border-radius: 5px; width: 100%; @@ -249,6 +248,19 @@ margin: 0; } + p { + font-size: 13px; + } + + h1, h2, h3, h4, h5, h6 { + font-size: 13px; + margin-bottom: 12px; + } + + ol, ul { + padding-left: 12px; + } + time { font-size: 10px; padding: 4px; @@ -274,7 +286,7 @@ color: #000000; } - p { + div { background-color: var( --user_background, #1155cc ); justify-content: flex-end; } @@ -295,9 +307,60 @@ color: #ffffff; } - p { + div { background-color: var( --assistant_background, #ecf1fb ); justify-content: flex-start; + + p { + margin: 0px; + padding: 0px; + } + + pre { + background-color: #282c34; + color: #abb2bf; + padding: 1em; + border-radius: 0; + font-family: monospace; + line-height: 1.5; + overflow-x: auto; + white-space: pre; + word-spacing: normal; + margin: 20px -10px; + + * { + color: #abb2bf; + word-break: normal; + word-wrap: normal; + } + + ::selection { + background-color: #3e4451; + } + + ::before { + content: attr(data-content); + } + + ::before:has(+ :matches(function, const, return, let, var)) { + color: #c678dd; + } + } + + img { + max-width: 100%; + height: auto; + border-radius: 5px; + cursor: pointer; + } + + code { + background: #d0effb; + border-radius: 5px; + border: none; + padding: 0 3px; + color: #333; + } } } diff --git a/tests/php/static-analysis-stubs/hyve-lite.php b/tests/php/static-analysis-stubs/hyve-lite.php index c306f87..d06ec19 100644 --- a/tests/php/static-analysis-stubs/hyve-lite.php +++ b/tests/php/static-analysis-stubs/hyve-lite.php @@ -3,9 +3,11 @@ * Hyve Constants. * * Adds Hyve constants for PHPStan to use. + * + * @package Codeinwp\HyveLite */ define( 'HYVE_LITE_BASEFILE', __FILE__ ); define( 'HYVE_LITE_URL', plugins_url( '/', __FILE__ ) ); -define( 'HYVE_LITE_PATH', dirname( __FILE__ ) ); -define( 'HYVE_LITE_VERSION', '1.0.0' ); \ No newline at end of file +define( 'HYVE_LITE_PATH', __DIR__ ); +define( 'HYVE_LITE_VERSION', '1.0.0' ); diff --git a/tests/php/unit/bootstrap.php b/tests/php/unit/bootstrap.php new file mode 100644 index 0000000..6d16ce8 --- /dev/null +++ b/tests/php/unit/bootstrap.php @@ -0,0 +1,51 @@ +set_role( 'administrator' ); + +wp_update_user( + [ + 'ID' => 1, + 'first_name' => 'Admin', + 'last_name' => 'User', + ] +); + +// Clean DB before tests. +global $wpdb; +$wpdb->query( 'DROP TABLE IF EXISTS ' . $wpdb->prefix . 'hyve' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange diff --git a/tests/php/unit/tests/test-block.php b/tests/php/unit/tests/test-block.php new file mode 100644 index 0000000..5a54c41 --- /dev/null +++ b/tests/php/unit/tests/test-block.php @@ -0,0 +1,33 @@ + 'true' ]; + $result = $block->render_shortcode( $atts ); + $this->assertStringContainsString( 'id="hyve-chat"', $result ); + } + + /** + * Test render_shortcode method without floating attribute. + */ + public function test_render_shortcode_without_floating() { + $block = new Block(); + $atts = []; + $result = $block->render_shortcode( $atts ); + $this->assertStringContainsString( 'id="hyve-inline-chat"', $result ); + } +} diff --git a/tests/php/unit/tests/test-consine-similarity.php b/tests/php/unit/tests/test-consine-similarity.php new file mode 100644 index 0000000..d957780 --- /dev/null +++ b/tests/php/unit/tests/test-consine-similarity.php @@ -0,0 +1,70 @@ +invoke_rrivate_method( 'dot_product', $vector_a, $vector_b ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test the magnitude calculation. + */ + public function test_magnitude() { + $vector = [ 1, 2, 3 ]; + $expected = sqrt( 14 ); // sqrt(1^2 + 2^2 + 3^2). + $result = $this->invoke_rrivate_method( 'magnitude', $vector ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test the cosine similarity calculation. + */ + public function test_calculate() { + $vector_a = [ 1, 2, 3 ]; + $vector_b = [ 4, 5, 6 ]; + $expected = 32.0 / ( sqrt( 14 ) * sqrt( 77 ) ); // dot_product / (magnitude_a * magnitude_b). + $result = Cosine_Similarity::calculate( $vector_a, $vector_b ); + $this->assertEquals( $expected, $result ); + } + + /** + * Test cosine similarity with zero magnitude vector. + */ + public function test_calculate_with_zero_magnitude() { + $vector_a = [ 0, 0, 0 ]; + $vector_b = [ 4, 5, 6 ]; + $expected = 0.0; // Should return 0.0 due to zero magnitude. + $result = Cosine_Similarity::calculate( $vector_a, $vector_b ); + $this->assertEquals( $expected, $result ); + } + + /** + * Helper method to invoke private methods for testing. + * + * @param string $method_name The name of the private method. + * @param mixed ...$args The arguments to pass to the method. + * @return mixed The result of the method call. + */ + private function invoke_rrivate_method( string $method_name, ...$args ) { + $reflection = new \ReflectionClass( Cosine_Similarity::class ); + $method = $reflection->getMethod( $method_name ); + $method->setAccessible( true ); + return $method->invokeArgs( null, $args ); + } +} diff --git a/tests/php/unit/tests/test-db.php b/tests/php/unit/tests/test-db.php new file mode 100644 index 0000000..cb88de3 --- /dev/null +++ b/tests/php/unit/tests/test-db.php @@ -0,0 +1,91 @@ +db_table = new DB_Table(); + } + + /** + * Test insert. + */ + public function test_insert() { + $data = [ + 'post_content' => 'Test content.', + 'post_id' => 42, + ]; + + $insert_id = $this->db_table->insert( $data ); + $this->assertEquals( 1, $insert_id ); + } + + /** + * Test get. + */ + public function test_get() { + $row = $this->db_table->get( 1 ); + + $this->assertIsObject( $row ); + $this->assertEquals( 1, $row->id ); + } + + /** + * Test update. + */ + public function test_update() { + $data = [ + 'post_content' => 'Updated content.', + ]; + + $rows_affected = $this->db_table->update( 1, $data ); + $row = $this->db_table->get( 1 ); + + $this->assertEquals( 1, $rows_affected ); + $this->assertEquals( 'Updated content.', $row->post_content ); + } + + /** + * Test delete. + */ + public function test_delete_by_post_id() { + $rows_affected = $this->db_table->delete_by_post_id( 42 ); + $this->assertEquals( 1, $rows_affected ); + } + + /** + * Test get_posts_over_limit. + */ + public function test_get_posts_over_limit() { + for ( $i = 0; $i < 600; $i++ ) { + $data = [ + 'post_content' => 'Test content.', + 'post_id' => $i, + ]; + + $this->db_table->insert( $data ); + } + + $posts = $this->db_table->get_posts_over_limit(); + $this->assertCount( 100, $posts ); + } +} diff --git a/tests/php/unit/tests/test-tokenizer.php b/tests/php/unit/tests/test-tokenizer.php new file mode 100644 index 0000000..fb4b48d --- /dev/null +++ b/tests/php/unit/tests/test-tokenizer.php @@ -0,0 +1,54 @@ + 1, + 'title' => 'Test Title', + 'content' => 'This is a short content.', + ]; + + $result = Tokenizer::tokenize( $post ); + + $this->assertCount( 1, $result ); + $this->assertEquals( $post['ID'], $result[0]['post_id'] ); + $this->assertEquals( $post['title'], $result[0]['post_title'] ); + $this->assertEquals( $post['content'], $result[0]['post_content'] ); + $this->assertArrayHasKey( 'tokens', $result[0] ); + $this->assertArrayHasKey( 'token_count', $result[0] ); + } + + /** + * Test tokenize method with long content. + */ + public function testTokenizeLongContent() { + $post = [ + 'ID' => 1, + 'title' => 'Test Title', + 'content' => str_repeat( 'This is a long content. ', 1000 ), + ]; + + $result = Tokenizer::tokenize( $post ); + + $this->assertGreaterThan( 1, count( $result ) ); + foreach ( $result as $chunk ) { + $this->assertEquals( $post['ID'], $chunk['post_id'] ); + $this->assertEquals( $post['title'], $chunk['post_title'] ); + $this->assertArrayHasKey( 'tokens', $chunk ); + $this->assertArrayHasKey( 'token_count', $chunk ); + } + } +}